@@ -23,7 +23,7 @@ \subsubsection{描述}
23
23
\subsubsection {分析 }
24
24
在每一步都可以判断中间结果是否为合法结果,用回溯法。
25
25
26
- 一个长度为n的字符串,有n+1个地方可以砍断 ,每个地方可断可不断,前后两个隔板默认已经使用 ,因此复杂度为$ O(2 ^{n-1})$
26
+ 一个长度为n的字符串,有 $ n- 1 $ 个地方可以砍断 ,每个地方可断可不断,因此复杂度为$ O(2 ^{n-1})$
27
27
28
28
29
29
\subsubsection {深搜1 }
@@ -34,40 +34,40 @@ \subsubsection{深搜1}
34
34
public:
35
35
vector<vector<string>> partition(string s) {
36
36
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 );
39
39
return result;
40
40
}
41
41
42
42
// s[0, prev-1]之间已经处理,保证是回文串
43
43
// 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 ) {
46
46
if (start == s.size()) { // 最后一个隔板
47
47
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();
51
51
}
52
52
return;
53
53
}
54
54
// 不断开
55
- DFS (s, prev, start + 1, output, result );
55
+ dfs (s, path, result, prev, start + 1);
56
56
// 如果[prev, start-1] 是回文,则可以断开,也可以不断开(上一行已经做了)
57
57
if (isPalindrome(s, prev, start - 1)) {
58
- // 不断开,if 上一行已经做了
59
58
// 断开
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();
63
62
}
64
63
}
65
64
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;
69
69
}
70
- return true ;
70
+ return start >= end ;
71
71
}
72
72
};
73
73
\end {Code }
@@ -81,32 +81,31 @@ \subsubsection{深搜2}
81
81
public:
82
82
vector<vector<string>> partition(string s) {
83
83
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 );
86
86
return result;
87
87
}
88
88
// 搜索必须以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 ) {
91
91
if (start == s.size()) {
92
- result.push_back(output );
92
+ result.push_back(path );
93
93
return;
94
94
}
95
95
for (int i = start; i < s.size(); i++) {
96
96
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(); // 撤销上上行
100
100
}
101
101
}
102
102
}
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;
108
107
}
109
- return true ;
108
+ return start >= end ;
110
109
}
111
110
};
112
111
\end {Code }
@@ -452,22 +451,21 @@ \subsubsection{代码}
452
451
public:
453
452
vector<vector<string> > solveNQueens(int n) {
454
453
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);
457
456
458
457
vector<vector<string> > result;
459
458
vector<int> C(n, 0); // C[i]表示第i行皇后所在的列编号
460
- dfs(0, C, result);
459
+ dfs(C, result, 0 );
461
460
return result;
462
461
}
463
462
private:
464
463
// 这三个变量用于剪枝
465
464
vector<int> columns; // 表示已经放置的皇后占据了哪些列
466
- vector<int> principal_diagonals ; // 占据了哪些主对角线
467
- vector<int> counter_diagonals ; // 占据了哪些副对角线
465
+ vector<int> main_diag ; // 占据了哪些主对角线
466
+ vector<int> anti_diag ; // 占据了哪些副对角线
468
467
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) {
471
469
const int N = C.size();
472
470
if (row == N) { // 终止条件,也是收敛条件,意味着找到了一个可行解
473
471
vector<string> solution;
@@ -483,20 +481,16 @@ \subsubsection{代码}
483
481
}
484
482
485
483
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;
500
494
}
501
495
}
502
496
};
@@ -533,42 +527,41 @@ \subsubsection{代码}
533
527
int totalNQueens(int n) {
534
528
this->count = 0;
535
529
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);
538
532
539
533
vector<int> C(n, 0); // C[i]表示第i行皇后所在的列编号
540
- dfs(0, C );
534
+ dfs(C, 0 );
541
535
return this->count;
542
536
}
543
537
private:
544
538
int count; // 解的个数
545
539
// 这三个变量用于剪枝
546
540
vector<int> columns; // 表示已经放置的皇后占据了哪些列
547
- vector<int> principal_diagonals ; // 占据了哪些主对角线
548
- vector<int> counter_diagonals ; // 占据了哪些副对角线
541
+ vector<int> main_diag ; // 占据了哪些主对角线
542
+ vector<int> anti_diag ; // 占据了哪些副对角线
549
543
550
- void dfs(int row, vector<int> &C) {
544
+ void dfs(vector<int> &C, int row ) {
551
545
const int N = C.size();
552
546
if (row == N) { // 终止条件,也是收敛条件,意味着找到了一个可行解
553
- this->count++ ;
547
+ ++ this->count;
554
548
return;
555
549
}
556
550
557
551
for (int j = 0; j < N; ++j) { // 扩展状态,一列一列的试
558
552
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;
572
565
}
573
566
}
574
567
};
@@ -1036,8 +1029,8 @@ \subsection{思考的步骤}
1036
1029
\begin {enumerate }
1037
1030
\item 是求路径条数,还是路径本身(或动作序列)?深搜最常见的三个问题,求可行解的总数,求一个可行解,求所有可行解。
1038
1031
\begin {enumerate }
1032
+ \item 如果是路径条数,则不需要存储路径。
1039
1033
\item 如果是求路径本身,则要用一个数组\fn {path[]}存储路径。跟宽搜不同,宽搜虽然最终求的也是一条路径,但是需要存储扩展过程中的所有路径,在没找到答案之前所有路径都不能放弃;而深搜,在搜索过程中始终只有一条路径,因此用一个数组就足够了。
1040
- \item 如果是路径条数,则不需要存储路径。
1041
1034
\end {enumerate }
1042
1035
1043
1036
\item 只要求一个解,还是要求所有解?如果只要求一个解,那找到一个就可以返回;如果要求所有解,找到了一个后,还要继续扩展,直到遍历完。广搜一般只要求一个解,因而不需要考虑这个问题(广搜当然也可以求所有解,这时需要扩展到所有叶子节点,相当于在内存中存储整个状态转换图,非常占内存,因此广搜不适合解这类问题)。
@@ -1048,8 +1041,8 @@ \subsection{思考的步骤}
1048
1041
1049
1042
\item 关于判重
1050
1043
\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步 。
1053
1046
\end {enumerate }
1054
1047
1055
1048
\item 终止条件是什么?终止条件是指到了不能扩展的末端节点。对于树,是叶子节点,对于图或隐式图,是出度为0的节点。
@@ -1063,16 +1056,16 @@ \subsection{思考的步骤}
1063
1056
\item 如何加速?
1064
1057
\begin {enumerate }
1065
1058
\item 剪枝。深搜一定要好好考虑怎么剪枝,成本小收益大,加几行代码,就能大大加速。这里没有通用方法,只能具体问题具体分析,要充分观察,充分利用各种信息来剪枝,在中间节点提前返回。
1066
- \item 缓存。如果子问题的解会被重复利用,可以考虑使用缓存。
1059
+ \item 缓存。
1067
1060
\begin {enumerate }
1068
- \item 前提条件:子问题的解会被重复利用,即子问题之间的依赖关系是有向无环图(DAG) 。如果依赖关系是树状的(例如树,单链表 ),没必要加缓存,因为子问题只会一层层往下,用一次就再也不会用到,加了缓存也没什么加速效果。
1061
+ \item 前提条件:状态转换图是一个DAG。DAG=>存在重叠子问题=> 子问题的解会被重复利用,用缓存自然会有加速效果 。如果依赖关系是树状的(例如树,单链表等 ),没必要加缓存,因为子问题只会一层层往下,用一次就再也不会用到,加了缓存也没什么加速效果。
1069
1062
\item 具体实现:可以用数组或HashMap。维度简单的,用数组;维度复杂的,用HashMap,C++有\fn {map},C++ 11以后有\fn {unordered_map},比\fn {map}快。
1070
1063
\end {enumerate }
1071
1064
1072
1065
\end {enumerate }
1073
1066
\end {enumerate }
1074
1067
1075
- 拿到一个题目,当感觉它适合用深搜解决时,在心里面把上面8个问题默默回答一遍,代码基本上就能写出来了。对于树,不需要回答第5和第8个问题。如果读者对上面的经验总结看不懂或感觉“不实用”,很正常,因为这些经验总结是笔者做了很多深搜题后总结出来的 ,从思维的发展过程看,“经验总结”要晚于感性认识,所以这时候建议读者先做做后面的题目 ,积累一定的感性认识后,在回过头来看这一节的总结,相信会和笔者有共鸣 。
1068
+ 拿到一个题目,当感觉它适合用深搜解决时,在心里面把上面8个问题默默回答一遍,代码基本上就能写出来了。对于树,不需要回答第5和第8个问题。如果读者对上面的经验总结看不懂或感觉“不实用”,很正常,因为这些经验总结是我做了很多题目后总结出来的 ,从思维的发展过程看,“经验总结”要晚于感性认识,所以这时候建议读者先做做前面的题目 ,积累一定的感性认识后,再回过头来看这一节的总结,一定会有共鸣 。
1076
1069
1077
1070
1078
1071
\subsection {代码模板 }
@@ -1081,14 +1074,15 @@ \subsection{代码模板}
1081
1074
/**
1082
1075
* dfs模板.
1083
1076
* @param[in] input 输入数据指针
1084
- * @param[inout] cur or gap 标记当前位置或距离目标的距离
1085
1077
* @param[out] path 当前路径,也是中间结果
1086
1078
* @param[out] result 存放最终结果
1079
+ * @param[inout] cur or gap 标记当前位置或距离目标的距离
1087
1080
* @return 路径长度,如果是求路径本身,则不需要返回长度
1088
1081
*/
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) {
1090
1083
if (数据非法) return 0; // 终止条件
1091
- if (cur == input.size( or gap == 0)) { // 收敛条件
1084
+ if (cur == input.size()) { // 收敛条件
1085
+ // if (gap == 0) {
1092
1086
将path放入result
1093
1087
}
1094
1088
@@ -1120,10 +1114,10 @@ \subsection{深搜与递归的区别}
1120
1114
1121
1115
深搜,是逻辑意义上的算法,递归,是一种物理意义上的实现,它和迭代(iteration)是对应的。深搜,可以用递归来实现,也可以用栈来实现;而递归,一般总是用来实现深搜。可以说,\textbf {递归一定是深搜,深搜不一定用递归 }。
1122
1116
1123
- 递归有两种加速策略,一种是\textbf {剪枝(prunning) },对中间结果进行判断,提前返回;一种是\textbf {加缓存 }(就变成了memoization,备忘录法) ,缓存中间结果,防止重复计算,用空间换时间。
1117
+ 递归有两种加速策略,一种是\textbf {剪枝(prunning) },对中间结果进行判断,提前返回;一种是\textbf {缓存 } ,缓存中间结果,防止重复计算,用空间换时间。
1124
1118
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 形式的程序中,使用缓存来避免重复计算,从而达到加速的目的。
1126
1120
1127
1121
\textbf {memorization 不一定用递归 },就像深搜不一定用递归一样,可以在迭代(iterative)中使用 memorization 。\textbf {递归也不一定用 memorization },可以用memorization来加速,但不是必须的。只有当递归使用了缓存,它才是 memorization 。
1128
1122
1129
- 既然递归一定是深搜,为什么很多书籍都同时使用这两个术语呢?在递归味道更浓的地方,一般用递归这个术语,在深搜更浓的场景下,用深搜这个术语,读者心里要弄清楚他俩大部分时候是一回事。在单链表、二叉树等递归数据结构上,递归的味道更浓,这时用递归这个术语;在图、隐士图等数据结构上,递归的比重不大,深搜的意图更浓 ,这时用深搜这个术语。
1123
+ 既然递归一定是深搜,为什么很多书籍都同时使用这两个术语呢?在递归味道更浓的地方,一般用递归这个术语,在深搜更浓的场景下,用深搜这个术语,读者心里要弄清楚他俩大部分时候是一回事。在单链表、二叉树等递归数据结构上,递归的味道更浓,这时用递归这个术语;在图、隐式图等数据结构上,深搜的味道更浓 ,这时用深搜这个术语。
0 commit comments