Skip to content

Commit 3513ef7

Browse files
committed
重构
1 parent eef73a3 commit 3513ef7

File tree

3 files changed

+79
-86
lines changed

3 files changed

+79
-86
lines changed

C++/LeetCodet题解(C++版).pdf

-1.21 KB
Binary file not shown.

C++/chapDFS.tex

+78-84
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ \subsubsection{描述}
2323
\subsubsection{分析}
2424
在每一步都可以判断中间结果是否为合法结果,用回溯法。
2525

26-
一个长度为n的字符串,有n+1个地方可以砍断,每个地方可断可不断,前后两个隔板默认已经使用,因此复杂度为$O(2^{n-1})$
26+
一个长度为n的字符串,$n-1$个地方可以砍断,每个地方可断可不断,因此复杂度为$O(2^{n-1})$
2727

2828

2929
\subsubsection{深搜1}
@@ -34,40 +34,40 @@ \subsubsection{深搜1}
3434
public:
3535
vector<vector<string>> partition(string s) {
3636
vector<vector<string>> result;
37-
vector<string> output; // 一个partition方案
38-
DFS(s, 0, 1, output, result);
37+
vector<string> path; // 一个partition方案
38+
dfs(s, path, result, 0, 1);
3939
return result;
4040
}
4141

4242
// s[0, prev-1]之间已经处理,保证是回文串
4343
// prev 表示s[prev-1]与s[prev]之间的空隙位置,start同理
44-
void DFS(string &s, size_t prev, size_t start, vector<string>& output,
45-
vector<vector<string>> &result) {
44+
void dfs(string &s, vector<string>& path,
45+
vector<vector<string>> &result, size_t prev, size_t start) {
4646
if (start == s.size()) { // 最后一个隔板
4747
if (isPalindrome(s, prev, start - 1)) { // 必须使用
48-
output.push_back(s.substr(prev, start - prev));
49-
result.push_back(output);
50-
output.pop_back();
48+
path.push_back(s.substr(prev, start - prev));
49+
result.push_back(path);
50+
path.pop_back();
5151
}
5252
return;
5353
}
5454
// 不断开
55-
DFS(s, prev, start + 1, output, result);
55+
dfs(s, path, result, prev, start + 1);
5656
// 如果[prev, start-1] 是回文,则可以断开,也可以不断开(上一行已经做了)
5757
if (isPalindrome(s, prev, start - 1)) {
58-
// 不断开,if 上一行已经做了
5958
// 断开
60-
output.push_back(s.substr(prev, start - prev));
61-
DFS(s, start, start + 1, output, result);
62-
output.pop_back();
59+
path.push_back(s.substr(prev, start - prev));
60+
dfs(s, path, result, start, start + 1);
61+
path.pop_back();
6362
}
6463
}
6564

66-
bool isPalindrome(string &s, int start, int end) {
67-
while (start < end) {
68-
if (s[start++] != s[end--]) return false;
65+
bool isPalindrome(const string &s, int start, int end) {
66+
while (s[start] == s[end]) {
67+
++start;
68+
--end;
6969
}
70-
return true;
70+
return start >= end;
7171
}
7272
};
7373
\end{Code}
@@ -81,32 +81,31 @@ \subsubsection{深搜2}
8181
public:
8282
vector<vector<string>> partition(string s) {
8383
vector<vector<string>> result;
84-
vector<string> output; // 一个partition方案
85-
DFS(s, 0, output, result);
84+
vector<string> path; // 一个partition方案
85+
DFS(s, path, result, 0);
8686
return result;
8787
}
8888
// 搜索必须以s[start]开头的partition方案
89-
void DFS(string &s, int start, vector<string>& output,
90-
vector<vector<string>> &result) {
89+
void DFS(string &s, vector<string>& path,
90+
vector<vector<string>> &result, int start) {
9191
if (start == s.size()) {
92-
result.push_back(output);
92+
result.push_back(path);
9393
return;
9494
}
9595
for (int i = start; i < s.size(); i++) {
9696
if (isPalindrome(s, start, i)) { // 从i位置砍一刀
97-
output.push_back(s.substr(start, i - start + 1));
98-
DFS(s, i + 1, output, result); // 继续往下砍
99-
output.pop_back(); // 撤销上一个push_back的砍
97+
path.push_back(s.substr(start, i - start + 1));
98+
DFS(s, path, result, i + 1); // 继续往下砍
99+
path.pop_back(); // 撤销上上行
100100
}
101101
}
102102
}
103-
bool isPalindrome(string &s, int start, int end) {
104-
while (start < end) {
105-
if (s[start] != s[end]) return false;
106-
start++;
107-
end--;
103+
bool isPalindrome(const string &s, int start, int end) {
104+
while (s[start] == s[end]) {
105+
++start;
106+
--end;
108107
}
109-
return true;
108+
return start >= end;
110109
}
111110
};
112111
\end{Code}
@@ -452,22 +451,21 @@ \subsubsection{代码}
452451
public:
453452
vector<vector<string> > solveNQueens(int n) {
454453
this->columns = vector<int>(n, 0);
455-
this->principal_diagonals = vector<int>(2 * n, 0);
456-
this->counter_diagonals = vector<int>(2 * n, 0);
454+
this->main_diag = vector<int>(2 * n, 0);
455+
this->anti_diag = vector<int>(2 * n, 0);
457456

458457
vector<vector<string> > result;
459458
vector<int> C(n, 0); // C[i]表示第i行皇后所在的列编号
460-
dfs(0, C, result);
459+
dfs(C, result, 0);
461460
return result;
462461
}
463462
private:
464463
// 这三个变量用于剪枝
465464
vector<int> columns; // 表示已经放置的皇后占据了哪些列
466-
vector<int> principal_diagonals; // 占据了哪些主对角线
467-
vector<int> counter_diagonals; // 占据了哪些副对角线
465+
vector<int> main_diag; // 占据了哪些主对角线
466+
vector<int> anti_diag; // 占据了哪些副对角线
468467

469-
void dfs(int row, vector<int> &C,
470-
vector<vector<string> > &result) {
468+
void dfs(vector<int> &C, vector<vector<string> > &result, int row) {
471469
const int N = C.size();
472470
if (row == N) { // 终止条件,也是收敛条件,意味着找到了一个可行解
473471
vector<string> solution;
@@ -483,20 +481,16 @@ \subsubsection{代码}
483481
}
484482

485483
for (int j = 0; j < N; ++j) { // 扩展状态,一列一列的试
486-
const bool ok = columns[j] == 0 &&
487-
principal_diagonals[row + j] == 0
488-
&& counter_diagonals[row - j + N] == 0;
489-
if (ok) { // 剪枝:如果合法,继续递归
490-
// 执行扩展动作
491-
C[row] = j;
492-
columns[j] = principal_diagonals[row + j] =
493-
counter_diagonals[row - j + N] = 1;
494-
dfs(row + 1, C, result);
495-
// 撤销动作
496-
// C[row] = 0;
497-
columns[j] = principal_diagonals[row + j] =
498-
counter_diagonals[row - j + N] = 0;
499-
}
484+
const bool ok = columns[j] == 0 && main_diag[row + j] == 0 &&
485+
anti_diag[row - j + N] == 0;
486+
if (!ok) continue; // 剪枝:如果合法,继续递归
487+
// 执行扩展动作
488+
C[row] = j;
489+
columns[j] = main_diag[row + j] = anti_diag[row - j + N] = 1;
490+
dfs(C, result, row + 1);
491+
// 撤销动作
492+
// C[row] = 0;
493+
columns[j] = main_diag[row + j] = anti_diag[row - j + N] = 0;
500494
}
501495
}
502496
};
@@ -533,42 +527,41 @@ \subsubsection{代码}
533527
int totalNQueens(int n) {
534528
this->count = 0;
535529
this->columns = vector<int>(n, 0);
536-
this->principal_diagonals = vector<int>(2 * n, 0);
537-
this->counter_diagonals = vector<int>(2 * n, 0);
530+
this->main_diag = vector<int>(2 * n, 0);
531+
this->anti_diag = vector<int>(2 * n, 0);
538532

539533
vector<int> C(n, 0); // C[i]表示第i行皇后所在的列编号
540-
dfs(0, C);
534+
dfs(C, 0);
541535
return this->count;
542536
}
543537
private:
544538
int count; // 解的个数
545539
// 这三个变量用于剪枝
546540
vector<int> columns; // 表示已经放置的皇后占据了哪些列
547-
vector<int> principal_diagonals; // 占据了哪些主对角线
548-
vector<int> counter_diagonals; // 占据了哪些副对角线
541+
vector<int> main_diag; // 占据了哪些主对角线
542+
vector<int> anti_diag; // 占据了哪些副对角线
549543

550-
void dfs(int row, vector<int> &C) {
544+
void dfs(vector<int> &C, int row) {
551545
const int N = C.size();
552546
if (row == N) { // 终止条件,也是收敛条件,意味着找到了一个可行解
553-
this->count++;
547+
++this->count;
554548
return;
555549
}
556550

557551
for (int j = 0; j < N; ++j) { // 扩展状态,一列一列的试
558552
const bool ok = columns[j] == 0 &&
559-
principal_diagonals[row + j] == 0
560-
&& counter_diagonals[row - j + N] == 0;
561-
if (ok) { // 剪枝:如果合法,继续递归
562-
// 执行扩展动作
563-
C[row] = j;
564-
columns[j] = principal_diagonals[row + j] =
565-
counter_diagonals[row - j + N] = 1;
566-
dfs(row + 1, C);
567-
// 撤销动作
568-
// C[row] = 0;
569-
columns[j] = principal_diagonals[row + j] =
570-
counter_diagonals[row - j + N] = 0;
571-
}
553+
main_diag[row + j] == 0 &&
554+
anti_diag[row - j + N] == 0;
555+
if (!ok) continue; // 剪枝:如果合法,继续递归
556+
// 执行扩展动作
557+
C[row] = j;
558+
columns[j] = main_diag[row + j] =
559+
anti_diag[row - j + N] = 1;
560+
dfs(C, row + 1);
561+
// 撤销动作
562+
// C[row] = 0;
563+
columns[j] = main_diag[row + j] =
564+
anti_diag[row - j + N] = 0;
572565
}
573566
}
574567
};
@@ -1036,8 +1029,8 @@ \subsection{思考的步骤}
10361029
\begin{enumerate}
10371030
\item 是求路径条数,还是路径本身(或动作序列)?深搜最常见的三个问题,求可行解的总数,求一个可行解,求所有可行解。
10381031
\begin{enumerate}
1032+
\item 如果是路径条数,则不需要存储路径。
10391033
\item 如果是求路径本身,则要用一个数组\fn{path[]}存储路径。跟宽搜不同,宽搜虽然最终求的也是一条路径,但是需要存储扩展过程中的所有路径,在没找到答案之前所有路径都不能放弃;而深搜,在搜索过程中始终只有一条路径,因此用一个数组就足够了。
1040-
\item 如果是路径条数,则不需要存储路径。
10411034
\end{enumerate}
10421035

10431036
\item 只要求一个解,还是要求所有解?如果只要求一个解,那找到一个就可以返回;如果要求所有解,找到了一个后,还要继续扩展,直到遍历完。广搜一般只要求一个解,因而不需要考虑这个问题(广搜当然也可以求所有解,这时需要扩展到所有叶子节点,相当于在内存中存储整个状态转换图,非常占内存,因此广搜不适合解这类问题)。
@@ -1048,8 +1041,8 @@ \subsection{思考的步骤}
10481041

10491042
\item 关于判重
10501043
\begin{enumerate}
1051-
\item 如果状态转换图是一棵树,则不需要判重,因为在遍历过程中不可能重复。
1052-
\item 如果状态转换图是一个图,则需要判重,方法跟广搜相同,见第 \S \ref{sec:bfs-template} 节。这里跟第8步中的加缓存是相同的,如果有重叠子问题,则需要判重,此时加缓存自然也是有效果的
1044+
\item 是否需要判重?如果状态转换图是一棵树,则不需要判重,因为在遍历过程中不可能重复;如果状态转换图是一个DAG,则需要判重。这一点跟BFS不一样,BFS的状态转换图总是DAG,必须要判重
1045+
\item 怎样判重?跟广搜相同,见第 \S \ref{sec:bfs-template} 节。同时,DAG说明存在重叠子问题,此时可以用缓存加速,见第8步
10531046
\end{enumerate}
10541047

10551048
\item 终止条件是什么?终止条件是指到了不能扩展的末端节点。对于树,是叶子节点,对于图或隐式图,是出度为0的节点。
@@ -1063,16 +1056,16 @@ \subsection{思考的步骤}
10631056
\item 如何加速?
10641057
\begin{enumerate}
10651058
\item 剪枝。深搜一定要好好考虑怎么剪枝,成本小收益大,加几行代码,就能大大加速。这里没有通用方法,只能具体问题具体分析,要充分观察,充分利用各种信息来剪枝,在中间节点提前返回。
1066-
\item 缓存。如果子问题的解会被重复利用,可以考虑使用缓存。
1059+
\item 缓存。
10671060
\begin{enumerate}
1068-
\item 前提条件:子问题的解会被重复利用,即子问题之间的依赖关系是有向无环图(DAG)。如果依赖关系是树状的(例如树,单链表),没必要加缓存,因为子问题只会一层层往下,用一次就再也不会用到,加了缓存也没什么加速效果。
1061+
\item 前提条件:状态转换图是一个DAG。DAG=>存在重叠子问题=>子问题的解会被重复利用,用缓存自然会有加速效果。如果依赖关系是树状的(例如树,单链表等),没必要加缓存,因为子问题只会一层层往下,用一次就再也不会用到,加了缓存也没什么加速效果。
10691062
\item 具体实现:可以用数组或HashMap。维度简单的,用数组;维度复杂的,用HashMap,C++有\fn{map},C++ 11以后有\fn{unordered_map},比\fn{map}快。
10701063
\end{enumerate}
10711064

10721065
\end{enumerate}
10731066
\end{enumerate}
10741067

1075-
拿到一个题目,当感觉它适合用深搜解决时,在心里面把上面8个问题默默回答一遍,代码基本上就能写出来了。对于树,不需要回答第5和第8个问题。如果读者对上面的经验总结看不懂或感觉“不实用”,很正常,因为这些经验总结是笔者做了很多深搜题后总结出来的,从思维的发展过程看,“经验总结”要晚于感性认识,所以这时候建议读者先做做后面的题目,积累一定的感性认识后,在回过头来看这一节的总结,相信会和笔者有共鸣
1068+
拿到一个题目,当感觉它适合用深搜解决时,在心里面把上面8个问题默默回答一遍,代码基本上就能写出来了。对于树,不需要回答第5和第8个问题。如果读者对上面的经验总结看不懂或感觉“不实用”,很正常,因为这些经验总结是我做了很多题目后总结出来的,从思维的发展过程看,“经验总结”要晚于感性认识,所以这时候建议读者先做做前面的题目,积累一定的感性认识后,再回过头来看这一节的总结,一定会有共鸣
10761069

10771070

10781071
\subsection{代码模板}
@@ -1081,14 +1074,15 @@ \subsection{代码模板}
10811074
/**
10821075
* dfs模板.
10831076
* @param[in] input 输入数据指针
1084-
* @param[inout] cur or gap 标记当前位置或距离目标的距离
10851077
* @param[out] path 当前路径,也是中间结果
10861078
* @param[out] result 存放最终结果
1079+
* @param[inout] cur or gap 标记当前位置或距离目标的距离
10871080
* @return 路径长度,如果是求路径本身,则不需要返回长度
10881081
*/
1089-
void dfs(type *input, type *path, int cur or gap, type *result) {
1082+
void dfs(type &input, type &path, type &result, int cur or gap) {
10901083
if (数据非法) return 0; // 终止条件
1091-
if (cur == input.size( or gap == 0)) { // 收敛条件
1084+
if (cur == input.size()) { // 收敛条件
1085+
// if (gap == 0) {
10921086
将path放入result
10931087
}
10941088

@@ -1120,10 +1114,10 @@ \subsection{深搜与递归的区别}
11201114

11211115
深搜,是逻辑意义上的算法,递归,是一种物理意义上的实现,它和迭代(iteration)是对应的。深搜,可以用递归来实现,也可以用栈来实现;而递归,一般总是用来实现深搜。可以说,\textbf{递归一定是深搜,深搜不一定用递归}。
11221116

1123-
递归有两种加速策略,一种是\textbf{剪枝(prunning)},对中间结果进行判断,提前返回;一种是\textbf{加缓存}(就变成了memoization,备忘录法),缓存中间结果,防止重复计算,用空间换时间。
1117+
递归有两种加速策略,一种是\textbf{剪枝(prunning)},对中间结果进行判断,提前返回;一种是\textbf{缓存},缓存中间结果,防止重复计算,用空间换时间。
11241118

1125-
其实,递归+缓存,就是一种 memorization 。所谓\textbf{memorization}(翻译为备忘录法,见第 \S \ref{sec:dp-vs-memorization}节),就是"top-down with cache"(自顶向下+缓存),它是Donald Michie 在1968年创造的术语,表示一种优化技术,在top-down 形式的程序中,使用缓存来避免重复计算,从而达到加速的目的。
1119+
其实,递归+缓存,就是 memorization。所谓\textbf{memorization}(翻译为备忘录法,见第 \S \ref{sec:dp-vs-memorization}节),就是"top-down with cache"(自顶向下+缓存),它是Donald Michie 在1968年创造的术语,表示一种优化技术,在top-down 形式的程序中,使用缓存来避免重复计算,从而达到加速的目的。
11261120

11271121
\textbf{memorization 不一定用递归},就像深搜不一定用递归一样,可以在迭代(iterative)中使用 memorization 。\textbf{递归也不一定用 memorization},可以用memorization来加速,但不是必须的。只有当递归使用了缓存,它才是 memorization 。
11281122

1129-
既然递归一定是深搜,为什么很多书籍都同时使用这两个术语呢?在递归味道更浓的地方,一般用递归这个术语,在深搜更浓的场景下,用深搜这个术语,读者心里要弄清楚他俩大部分时候是一回事。在单链表、二叉树等递归数据结构上,递归的味道更浓,这时用递归这个术语;在图、隐士图等数据结构上,递归的比重不大,深搜的意图更浓,这时用深搜这个术语。
1123+
既然递归一定是深搜,为什么很多书籍都同时使用这两个术语呢?在递归味道更浓的地方,一般用递归这个术语,在深搜更浓的场景下,用深搜这个术语,读者心里要弄清楚他俩大部分时候是一回事。在单链表、二叉树等递归数据结构上,递归的味道更浓,这时用递归这个术语;在图、隐式图等数据结构上,深搜的味道更浓,这时用深搜这个术语。

C++/chapDivideAndConquer.tex

+1-2
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,12 @@ \subsubsection{代码}
6464
public:
6565
int sqrt(int x) {
6666
int left = 1, right = x / 2;
67-
int mid;
6867
int last_mid; // 记录最近一次mid
6968

7069
if (x < 2) return x;
7170

7271
while(left <= right) {
73-
mid = left + (right - left) / 2;
72+
const int mid = left + (right - left) / 2;
7473
if(x / mid > mid) { // 不要用 x > mid * mid,会溢出
7574
left = mid + 1;
7675
last_mid = mid;

0 commit comments

Comments
 (0)