diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 00000000..ccd81dff
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,7 @@
+{
+ "name": "TeX Live",
+ "image": "soulmachine/texlive:latest",
+ "extensions": [
+ "James-Yu.latex-workshop"
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 00000000..72ac5ac3
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,31 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "lettcode-C++",
+ "type": "shell",
+ "command": "xelatex",
+ "args": [
+ "-synctex=1",
+ "-interaction=nonstopmode",
+ "leetcode-cpp.tex"
+ ],
+ "options": {
+ "cwd": "${workspaceFolder}/C++/"
+ }
+ },
+ {
+ "label": "lettcode-Java",
+ "type": "shell",
+ "command": "xelatex",
+ "args": [
+ "-synctex=1",
+ "-interaction=nonstopmode",
+ "leetcode-java.tex"
+ ],
+ "options": {
+ "cwd": "${workspaceFolder}/Java/"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/C++/.DS_Store b/C++/.DS_Store
new file mode 100644
index 00000000..cd3a2cf8
Binary files /dev/null and b/C++/.DS_Store differ
diff --git a/C++/README.md b/C++/README.md
index 3e67d41b..a856af82 100644
--- a/C++/README.md
+++ b/C++/README.md
@@ -1,5 +1,7 @@
-#C++版
------------------
-**下载**:LeetCode题解(C++版).pdf
+# C++版
-书的内容与Java版一摸一样,不过代码是用C++写的。本书的代码使用 C++ 11 标准。
+## 编译
+
+```bash
+docker run -it --rm -v $(pwd):/project -w /project soulmachine/texlive xelatex -interaction=nonstopmode leetcode-cpp.tex
+````
diff --git a/C++/chapBFS.tex b/C++/chapBFS.tex
index 9ad2297d..826d7c4a 100644
--- a/C++/chapBFS.tex
+++ b/C++/chapBFS.tex
@@ -36,9 +36,99 @@ \subsubsection{描述}
\subsubsection{分析}
+求最短路径,用广搜。
-\subsubsection{代码}
+\subsubsection{单队列}
+\begin{Code}
+//LeetCode, Word Ladder
+// 时间复杂度O(n),空间复杂度O(n)
+struct state_t {
+ string word;
+ int level;
+
+ state_t() { word = ""; level = 0; }
+ state_t(const string& word, int level) {
+ this->word = word;
+ this->level = level;
+ }
+
+ bool operator==(const state_t &other) const {
+ return this->word == other.word;
+ }
+};
+
+namespace std {
+ template<> struct hash {
+ public:
+ size_t operator()(const state_t& s) const {
+ return str_hash(s.word);
+ }
+ private:
+ std::hash str_hash;
+ };
+}
+
+
+class Solution {
+public:
+ int ladderLength(const string& start, const string &end,
+ const unordered_set &dict) {
+ queue q;
+ unordered_set visited; // 判重
+
+ auto state_is_valid = [&](const state_t& s) {
+ return dict.find(s.word) != dict.end() || s.word == end;
+ };
+ auto state_is_target = [&](const state_t &s) {return s.word == end; };
+ auto state_extend = [&](const state_t &s) {
+ unordered_set result;
+
+ for (size_t i = 0; i < s.word.size(); ++i) {
+ state_t new_state(s.word, s.level + 1);
+ for (char c = 'a'; c <= 'z'; c++) {
+ // 防止同字母替换
+ if (c == new_state.word[i]) continue;
+
+ swap(c, new_state.word[i]);
+
+ if (state_is_valid(new_state) &&
+ visited.find(new_state) == visited.end()) {
+ result.insert(new_state);
+ }
+ swap(c, new_state.word[i]); // 恢复该单词
+ }
+ }
+
+ return result;
+ };
+
+ state_t start_state(start, 0);
+ q.push(start_state);
+ visited.insert(start_state);
+ while (!q.empty()) {
+ // 千万不能用 const auto&,pop() 会删除元素,
+ // 引用就变成了悬空引用
+ const auto state = q.front();
+ q.pop();
+
+ if (state_is_target(state)) {
+ return state.level + 1;
+ }
+
+ const auto& new_states = state_extend(state);
+ for (const auto& new_state : new_states) {
+ q.push(new_state);
+ visited.insert(new_state);
+ }
+ }
+ return 0;
+ }
+};
+\end{Code}
+
+
+\subsubsection{双队列}
\begin{Code}
//LeetCode, Word Ladder
// 时间复杂度O(n),空间复杂度O(n)
@@ -49,24 +139,26 @@ \subsubsection{代码}
queue current, next; // 当前层,下一层
unordered_set visited; // 判重
- int level = 0; // 层次
- bool found = false;
+ int level = -1; // 层次
+ auto state_is_valid = [&](const string& s) {
+ return dict.find(s) != dict.end() || s == end;
+ };
auto state_is_target = [&](const string &s) {return s == end;};
auto state_extend = [&](const string &s) {
- vector result;
+ unordered_set result;
for (size_t i = 0; i < s.size(); ++i) {
string new_word(s);
for (char c = 'a'; c <= 'z'; c++) {
+ // 防止同字母替换
if (c == new_word[i]) continue;
swap(c, new_word[i]);
- if ((dict.count(new_word) > 0 || new_word == end) &&
- !visited.count(new_word)) {
- result.push_back(new_word);
- visited.insert(new_word);
+ if (state_is_valid(new_word) &&
+ visited.find(new_word) == visited.end()) {
+ result.insert(new_word);
}
swap(c, new_word[i]); // 恢复该单词
}
@@ -76,25 +168,28 @@ \subsubsection{代码}
};
current.push(start);
- while (!current.empty() && !found) {
+ visited.insert(start);
+ while (!current.empty()) {
++level;
- while (!current.empty() && !found) {
- const string str = current.front();
+ while (!current.empty()) {
+ // 千万不能用 const auto&,pop() 会删除元素,
+ // 引用就变成了悬空引用
+ const auto state = current.front();
current.pop();
- const auto& new_states = state_extend(str);
- for (const auto& state : new_states) {
- next.push(state);
- if (state_is_target(state)) {
- found = true; //找到了
- break;
- }
+ if (state_is_target(state)) {
+ return level + 1;
+ }
+
+ const auto& new_states = state_extend(state);
+ for (const auto& new_state : new_states) {
+ next.push(new_state);
+ visited.insert(new_state);
}
}
swap(next, current);
}
- if (found) return level + 1;
- else return 0;
+ return 0;
}
};
\end{Code}
@@ -142,24 +237,178 @@ \subsubsection{描述}
\subsubsection{分析}
跟 Word Ladder比,这题是求路径本身,不是路径长度,也是BFS,略微麻烦点。
-这题跟普通的广搜有很大的不同,就是要输出所有路径,因此在记录前驱和判重地方与普通广搜略有不同。
+求一条路径和求所有路径有很大的不同,求一条路径,每个状态节点只需要记录一个前驱即可;求所有路径时,有的状态节点可能有多个父节点,即要记录多个前驱。
+如果当前路径长度已经超过当前最短路径长度,可以中止对该路径的处理,因为我们要找的是最短路径。
-\subsubsection{代码}
+
+\subsubsection{单队列}
\begin{Code}
//LeetCode, Word Ladder II
// 时间复杂度O(n),空间复杂度O(n)
+struct state_t {
+ string word;
+ int level;
+
+ state_t() { word = ""; level = 0; }
+ state_t(const string& word, int level) {
+ this->word = word;
+ this->level = level;
+ }
+
+ bool operator==(const state_t &other) const {
+ return this->word == other.word;
+ }
+};
+
+namespace std {
+ template<> struct hash {
+ public:
+ size_t operator()(const state_t& s) const {
+ return str_hash(s.word);
+ }
+ private:
+ std::hash str_hash;
+ };
+}
+
+
class Solution {
public:
- vector > findLadders(string start, string end,
- const unordered_set &dict) {
- unordered_set current, next; // 当前层,下一层,用集合是为了去重
+ vector > findLadders(const string& start,
+ const string& end, const unordered_set &dict) {
+ queue q;
+ unordered_set visited; // 判重
+ unordered_map > father; // DAG
+
+ auto state_is_valid = [&](const state_t& s) {
+ return dict.find(s.word) != dict.end() || s.word == end;
+ };
+ auto state_is_target = [&](const state_t &s) {return s.word == end; };
+ auto state_extend = [&](const state_t &s) {
+ unordered_set result;
+
+ for (size_t i = 0; i < s.word.size(); ++i) {
+ state_t new_state(s.word, s.level + 1);
+ for (char c = 'a'; c <= 'z'; c++) {
+ // 防止同字母替换
+ if (c == new_state.word[i]) continue;
+
+ swap(c, new_state.word[i]);
+
+ if (state_is_valid(new_state)) {
+ auto visited_iter = visited.find(new_state);
+
+ if (visited_iter != visited.end()) {
+ if (visited_iter->level < new_state.level) {
+ // do nothing
+ } else if (visited_iter->level == new_state.level) {
+ result.insert(new_state);
+ } else { // not possible
+ throw std::logic_error("not possible to get here");
+ }
+ } else {
+ result.insert(new_state);
+ }
+ }
+ swap(c, new_state.word[i]); // 恢复该单词
+ }
+ }
+
+ return result;
+ };
+
+ vector> result;
+ state_t start_state(start, 0);
+ q.push(start_state);
+ visited.insert(start_state);
+ while (!q.empty()) {
+ // 千万不能用 const auto&,pop() 会删除元素,
+ // 引用就变成了悬空引用
+ const auto state = q.front();
+ q.pop();
+
+ // 如果当前路径长度已经超过当前最短路径长度,
+ // 可以中止对该路径的处理,因为我们要找的是最短路径
+ if (!result.empty() && state.level + 1 > result[0].size()) break;
+
+ if (state_is_target(state)) {
+ vector path;
+ gen_path(father, start_state, state, path, result);
+ continue;
+ }
+ // 必须挪到下面,比如同一层A和B两个节点均指向了目标节点,
+ // 那么目标节点就会在q中出现两次,输出路径就会翻倍
+ // visited.insert(state);
+
+ // 扩展节点
+ const auto& new_states = state_extend(state);
+ for (const auto& new_state : new_states) {
+ if (visited.find(new_state) == visited.end()) {
+ q.push(new_state);
+ }
+ visited.insert(new_state);
+ father[new_state].push_back(state);
+ }
+ }
+
+ return result;
+ }
+private:
+ void gen_path(unordered_map > &father,
+ const state_t &start, const state_t &state, vector &path,
+ vector > &result) {
+ path.push_back(state.word);
+ if (state == start) {
+ if (!result.empty()) {
+ if (path.size() < result[0].size()) {
+ result.clear();
+ result.push_back(path);
+ reverse(result.back().begin(), result.back().end());
+ } else if (path.size() == result[0].size()) {
+ result.push_back(path);
+ reverse(result.back().begin(), result.back().end());
+ } else { // not possible
+ throw std::logic_error("not possible to get here ");
+ }
+ } else {
+ result.push_back(path);
+ reverse(result.back().begin(), result.back().end());
+ }
+
+ } else {
+ for (const auto& f : father[state]) {
+ gen_path(father, start, f, path, result);
+ }
+ }
+ path.pop_back();
+ }
+};
+\end{Code}
+
+
+\subsubsection{双队列}
+
+\begin{Code}
+//LeetCode, Word Ladder II
+// 时间复杂度O(n),空间复杂度O(n)
+class Solution {
+public:
+ vector > findLadders(const string& start,
+ const string& end, const unordered_set &dict) {
+ // 当前层,下一层,用unordered_set是为了去重,例如两个父节点指向
+ // 同一个子节点,如果用vector, 子节点就会在next里出现两次,其实此
+ // 时 father 已经记录了两个父节点,next里重复出现两次是没必要的
+ unordered_set current, next;
unordered_set visited; // 判重
- unordered_map > father; // 树
+ unordered_map > father; // DAG
- bool found = false;
+ int level = -1; // 层次
+ auto state_is_valid = [&](const string& s) {
+ return dict.find(s) != dict.end() || s == end;
+ };
auto state_is_target = [&](const string &s) {return s == end;};
auto state_extend = [&](const string &s) {
unordered_set result;
@@ -167,12 +416,13 @@ \subsubsection{代码}
for (size_t i = 0; i < s.size(); ++i) {
string new_word(s);
for (char c = 'a'; c <= 'z'; c++) {
+ // 防止同字母替换
if (c == new_word[i]) continue;
swap(c, new_word[i]);
- if ((dict.count(new_word) > 0 || new_word == end) &&
- !visited.count(new_word)) {
+ if (state_is_valid(new_word) &&
+ visited.find(new_word) == visited.end()) {
result.insert(new_word);
}
swap(c, new_word[i]); // 恢复该单词
@@ -182,29 +432,37 @@ \subsubsection{代码}
return result;
};
+ vector > result;
current.insert(start);
- while (!current.empty() && !found) {
- // 先将本层全部置为已访问,防止同层之间互相指向
- for (const auto& word : current)
- visited.insert(word);
- for (const auto& word : current) {
- const auto new_states = state_extend(word);
- for (const auto &state : new_states) {
- if (state_is_target(state)) found = true;
- next.insert(state);
- father[state].push_back(word);
- // visited.insert(state); // 移动到最上面了
+ while (!current.empty()) {
+ ++ level;
+ // 如果当前路径长度已经超过当前最短路径长度,可以中止对该路径的
+ // 处理,因为我们要找的是最短路径
+ if (!result.empty() && level+1 > result[0].size()) break;
+
+ // 1. 延迟加入visited, 这样才能允许两个父节点指向同一个子节点
+ // 2. 一股脑current 全部加入visited, 是防止本层前一个节点扩展
+ // 节点时,指向了本层后面尚未处理的节点,这条路径必然不是最短的
+ for (const auto& state : current)
+ visited.insert(state);
+ for (const auto& state : current) {
+ if (state_is_target(state)) {
+ vector path;
+ gen_path(father, path, start, state, result);
+ continue;
+ }
+
+ const auto new_states = state_extend(state);
+ for (const auto& new_state : new_states) {
+ next.insert(new_state);
+ father[new_state].push_back(state);
}
}
current.clear();
swap(current, next);
}
- vector > result;
- if (found) {
- vector path;
- gen_path(father, path, start, end, result);
- }
+
return result;
}
private:
@@ -213,7 +471,19 @@ \subsubsection{代码}
vector > &result) {
path.push_back(word);
if (word == start) {
- result.push_back(path);
+ if (!result.empty()) {
+ if (path.size() < result[0].size()) {
+ result.clear();
+ result.push_back(path);
+ } else if(path.size() == result[0].size()) {
+ result.push_back(path);
+ } else {
+ // not possible
+ throw std::logic_error("not possible to get here");
+ }
+ } else {
+ result.push_back(path);
+ }
reverse(result.back().begin(), result.back().end());
} else {
for (const auto& f : father[word]) {
@@ -226,6 +496,139 @@ \subsubsection{代码}
\end{Code}
+\subsubsection{图的广搜}
+
+本题还可以看做是图上的广搜。给定了字典 \fn{dict},可以基于它画出一个无向图,表示单词之间可以互相转换。本题的本质就是已知起点和终点,在图上找出所有最短路径。
+
+\begin{Code}
+//LeetCode, Word Ladder II
+// 时间复杂度O(n),空间复杂度O(n)
+class Solution {
+public:
+ vector > findLadders(const string& start,
+ const string &end, const unordered_set &dict) {
+ const auto& g = build_graph(dict);
+ vector pool;
+ queue q; // 未处理的节点
+ // value 是所在层次
+ unordered_map visited;
+
+ auto state_is_target = [&](const state_t *s) {return s->word == end; };
+
+ vector> result;
+ q.push(create_state(nullptr, start, 0, pool));
+ while (!q.empty()) {
+ state_t* state = q.front();
+ q.pop();
+
+ // 如果当前路径长度已经超过当前最短路径长度,
+ // 可以中止对该路径的处理,因为我们要找的是最短路径
+ if (!result.empty() && state->level+1 > result[0].size()) break;
+
+ if (state_is_target(state)) {
+ const auto& path = gen_path(state);
+ if (result.empty()) {
+ result.push_back(path);
+ } else {
+ if (path.size() < result[0].size()) {
+ result.clear();
+ result.push_back(path);
+ } else if (path.size() == result[0].size()) {
+ result.push_back(path);
+ } else {
+ // not possible
+ throw std::logic_error("not possible to get here");
+ }
+ }
+ continue;
+ }
+ visited[state->word] = state->level;
+
+ // 扩展节点
+ auto iter = g.find(state->word);
+ if (iter == g.end()) continue;
+
+ for (const auto& neighbor : iter->second) {
+ auto visited_iter = visited.find(neighbor);
+
+ if (visited_iter != visited.end() &&
+ visited_iter->second < state->level + 1) {
+ continue;
+ }
+
+ q.push(create_state(state, neighbor, state->level + 1, pool));
+ }
+ }
+
+ // release all states
+ for (auto state : pool) {
+ delete state;
+ }
+ return result;
+ }
+
+private:
+ struct state_t {
+ state_t* father;
+ string word;
+ int level; // 所在层次,从0开始编号
+
+ state_t(state_t* father_, const string& word_, int level_) :
+ father(father_), word(word_), level(level_) {}
+ };
+
+ state_t* create_state(state_t* parent, const string& value,
+ int length, vector& pool) {
+ state_t* node = new state_t(parent, value, length);
+ pool.push_back(node);
+
+ return node;
+ }
+ vector gen_path(const state_t* node) {
+ vector path;
+
+ while(node != nullptr) {
+ path.push_back(node->word);
+ node = node->father;
+ }
+
+ reverse(path.begin(), path.end());
+ return path;
+ }
+
+ unordered_map > build_graph(
+ const unordered_set& dict) {
+ unordered_map > adjacency_list;
+
+ for (const auto& word : dict) {
+ for (size_t i = 0; i < word.size(); ++i) {
+ string new_word(word);
+ for (char c = 'a'; c <= 'z'; c++) {
+ // 防止同字母替换
+ if (c == new_word[i]) continue;
+
+ swap(c, new_word[i]);
+
+ if ((dict.find(new_word) != dict.end())) {
+ auto iter = adjacency_list.find(word);
+ if (iter != adjacency_list.end()) {
+ iter->second.insert(new_word);
+ } else {
+ adjacency_list.insert(pair>(word, unordered_set()));
+ adjacency_list[word].insert(new_word);
+ }
+ }
+ swap(c, new_word[i]); // 恢复该单词
+ }
+ }
+ }
+ return adjacency_list;
+ }
+};
+\end{Code}
+
+
\subsubsection{相关题目}
\begindot
@@ -296,7 +699,7 @@ \subsubsection{代码}
const int m = board.size();
const int n = board[0].size();
- auto is_valid = [&](const state_t &s) {
+ auto state_is_valid = [&](const state_t &s) {
const int x = s.first;
const int y = s.second;
if (x < 0 || x >= m || y < 0 || y >= n || board[x][y] != 'O')
@@ -312,7 +715,7 @@ \subsubsection{代码}
const state_t new_states[4] = {{x-1,y}, {x+1,y},
{x,y-1}, {x,y+1}};
for (int k = 0; k < 4; ++k) {
- if (is_valid(new_states[k])) {
+ if (state_is_valid(new_states[k])) {
// 既有标记功能又有去重功能
board[new_states[k].first][new_states[k].second] = '+';
result.push_back(new_states[k]);
@@ -323,7 +726,7 @@ \subsubsection{代码}
};
state_t start = { i, j };
- if (is_valid(start)) {
+ if (state_is_valid(start)) {
board[i][j] = '+';
q.push(start);
}
@@ -353,7 +756,7 @@ \subsection{适用场景}
\textbf{输入数据}:没什么特征,不像深搜,需要有“递归”的性质。如果是树或者图,概率更大。
-\textbf{状态转换图}:树或者图。
+\textbf{状态转换图}:树或者DAG图。
\textbf{求解目标}:求最短。
@@ -374,10 +777,16 @@ \subsection{思考的步骤}
\item 如何扩展状态?这一步跟第2步相关。状态里记录的数据不同,扩展方法就不同。对于固定不变的数据结构(一般题目直接给出,作为输入数据),如二叉树,图等,扩展方法很简单,直接往下一层走,对于隐式图,要先在第1步里想清楚状态所带的数据,想清楚了这点,那如何扩展就很简单了。
-\item 关于判重,状态是否存在完美哈希方案?即将状态一一映射到整数,互相之间不会冲突。
+\item 如何判断重复?如果状态转换图是一颗树,则永远不会出现回路,不需要判重;如果状态转换图是一个图(这时候是一个图上的BFS),则需要判重。
\begin{enumerate}
- \item 如果不存在,则需要使用通用的哈希表(自己实现或用标准库,例如\fn{unordered_set})来判重;自己实现哈希表的话,如果能够预估状态个数的上限,则可以开两个数组,head和next,表示哈希表,参考第 \S \ref{subsec:eightDigits}节方案2。
- \item 如果存在,则可以开一个大布尔数组,作为哈希表来判重,且此时可以精确计算出状态总数,而不仅仅是预估上限。
+ \item 如果是求最短路径长度或一条路径,则只需要让“点”(即状态)不重复出现,即可保证不出现回路
+ \item 如果是求所有路径,注意此时,状态转换图是DAG,即允许两个父节点指向同一个子节点。具体实现时,每个节点要\textbf{“延迟”}加入到已访问集合\fn{visited},要等一层全部访问完后,再加入到\fn{visited}集合。
+ \item 具体实现
+ \begin{enumerate}
+ \item 状态是否存在完美哈希方案?即将状态一一映射到整数,互相之间不会冲突。
+ \item 如果不存在,则需要使用通用的哈希表(自己实现或用标准库,例如\fn{unordered_set})来判重;自己实现哈希表的话,如果能够预估状态个数的上限,则可以开两个数组,head和next,表示哈希表,参考第 \S \ref{subsec:eightDigits}节方案2。
+ \item 如果存在,则可以开一个大布尔数组,来判重,且此时可以精确计算出状态总数,而不仅仅是预估上限。
+ \end{enumerate}
\end{enumerate}
\item 目标状态是否已知?如果题目已经给出了目标状态,可以带来很大便利,这时候可以从起始状态出发,正向广搜;也可以从目标状态出发,逆向广搜;也可以同时出发,双向广搜。
@@ -389,8 +798,8 @@ \subsection{代码模板}
对于队列,可以用\fn{queue},也可以把\fn{vector}当做队列使用。当求长度时,有两种做法:
\begin{enumerate}
-\item 只用一个队列,但在状态结构体\fn{state_t}里增加一个整数字段\fn{step},表示走到当前状态用了多少步,当碰到目标状态,直接输出\fn{step}即可。这个方案,可以很方便的变成A*算法,把队列换成优先队列即可。
-\item 用两个队列,\fn{current, next},分别表示当前层次和下一层,另设一个全局整数\fn{level},表示层数(也即路径长度),当碰到目标状态,输出\fn{level}即可。这个方案,状态可以少一个字段,节省内存。
+\item 只用一个队列,但在状态结构体\fn{state_t}里增加一个整数字段\fn{level},表示当前所在的层次,当碰到目标状态,直接输出\fn{level}即可。这个方案,可以很容易的变成A*算法,把\fn{queue}替换为\fn{priority_queue}即可。
+\item 用两个队列,\fn{current, next},分别表示当前层次和下一层,另设一个全局整数\fn{level},表示层数(也即路径长度),当碰到目标状态,输出\fn{level}即可。这个方案,状态里可以不存路径长度,只需全局设置一个整数\fn{level},比较节省内存;
\end{enumerate}
对于hashset,如果有完美哈希方案,用布尔数组(\fn{bool visited[STATE_MAX]}或\fn{vector visited(STATE_MAX, false)})来表示;如果没有,可以用STL里的\fn{set}或\fn{unordered_set}。
@@ -398,15 +807,16 @@ \subsection{代码模板}
对于树,如果用STL,可以用\fn{unordered_map father}表示一颗树,代码非常简洁。如果能够预估状态总数的上限(设为STATE_MAX),可以用数组(\fn{state_t nodes[STATE_MAX]}),即树的双亲表示法来表示树,效率更高,当然,需要写更多代码。
-\subsubsection{双队列的写法}
-\begin{Codex}[label=bfs_template1.cpp]
+\subsubsection{如何表示状态}
+
+\begin{Codex}[label=bfs_common.h]
/** 状态 */
struct state_t {
int data1; /** 状态的数据,可以有多个字段. */
int data2; /** 状态的数据,可以有多个字段. */
// dataN; /** 其他字段 */
int action; /** 由父状态移动到本状态的动作,求动作序列时需要. */
- int count; /** 所花费的步骤数(也即路径长度-1),求路径长度时需要;
+ int level; /** 所在的层次(从0开始),也即路径长度-1,求路径长度时需要;
不过,采用双队列时不需要本字段,只需全局设一个整数 */
bool operator==(const state_t &other) const {
return true; // 根据具体问题实现
@@ -435,14 +845,12 @@ \subsubsection{双队列的写法}
int m; // 存放外面传入的数据
};
-
/**
- * @brief 反向生成路径.
+ * @brief 反向生成路径,求一条路径.
* @param[in] father 树
* @param[in] target 目标节点
* @return 从起点到target的路径
*/
-template
vector gen_path(const unordered_map &father,
const state_t &target) {
vector path;
@@ -458,132 +866,334 @@ \subsubsection{双队列的写法}
}
/**
- * @brief 广搜.
- * @param[in] state_t 状态,如整数,字符串,一维数组等
+ * 反向生成路径,求所有路径.
+ * @param[in] father 存放了所有路径的树
+ * @param[in] start 起点
+ * @param[in] state 终点
+ * @return 从起点到终点的所有路径
+ */
+void gen_path(unordered_map > &father,
+ const string &start, const state_t& state, vector &path,
+ vector > &result) {
+ path.push_back(state);
+ if (state == start) {
+ if (!result.empty()) {
+ if (path.size() < result[0].size()) {
+ result.clear();
+ result.push_back(path);
+ } else if(path.size() == result[0].size()) {
+ result.push_back(path);
+ } else {
+ // not possible
+ throw std::logic_error("not possible to get here");
+ }
+ } else {
+ result.push_back(path);
+ }
+ reverse(result.back().begin(), result.back().end());
+ } else {
+ for (const auto& f : father[state]) {
+ gen_path(father, start, f, path, result);
+ }
+ }
+ path.pop_back();
+}
+\end{Codex}
+
+
+\subsubsection{求最短路径长度或一条路径}
+
+\textbf{单队列的写法}
+
+\begin{Codex}[label=bfs_template.cpp]
+#include "bfs_common.h"
+
+/**
+ * @brief 广搜,只用一个队列.
* @param[in] start 起点
- * @param[in] grid 输入数据
+ * @param[in] data 输入数据
* @return 从起点到目标状态的一条最短路径
*/
-template
-vector bfs(const state_t &start, const vector> &grid) {
+vector bfs(state_t &start, const vector> &grid) {
+ queue q; // 队列
+ unordered_set visited; // 判重
+ unordered_map father; // 树,求路径本身时才需要
+
+ // 判断状态是否合法
+ auto state_is_valid = [&](const state_t &s) { /*...*/ };
+
+ // 判断当前状态是否为所求目标
+ auto state_is_target = [&](const state_t &s) { /*...*/ };
+
+ // 扩展当前状态
+ auto state_extend = [&](const state_t &s) {
+ unordered_set result;
+ for (/*...*/) {
+ const state_t new_state = /*...*/;
+ if (state_is_valid(new_state) &&
+ visited.find(new_state) != visited.end()) {
+ result.insert(new_state);
+ }
+ }
+ return result;
+ };
+
+ assert (start.level == 0);
+ q.push(start);
+ while (!q.empty()) {
+ // 千万不能用 const auto&,pop() 会删除元素,
+ // 引用就变成了悬空引用
+ const state_t state = q.front();
+ q.pop();
+ visited.insert(state);
+
+ // 访问节点
+ if (state_is_target(state)) {
+ return return gen_path(father, target); // 求一条路径
+ // return state.level + 1; // 求路径长度
+ }
+
+ // 扩展节点
+ vector new_states = state_extend(state);
+ for (const auto& new_state : new_states) {
+ q.push(new_state);
+ father[new_state] = state; // 求一条路径
+ // visited.insert(state); // 优化:可以提前加入 visited 集合,
+ // 从而缩小状态扩展。这时 q 的含义略有变化,里面存放的是处理了一半
+ // 的节点:已经加入了visited,但还没有扩展。别忘记 while循环开始
+ // 前,要加一行代码, visited.insert(start)
+ }
+ }
+
+ return vector();
+ //return 0;
+}
+\end{Codex}
+
+
+\textbf{双队列的写法}
+\begin{Codex}[label=bfs_template1.cpp]
+#include "bfs_common.h"
+
+/**
+ * @brief 广搜,使用两个队列.
+ * @param[in] start 起点
+ * @param[in] data 输入数据
+ * @return 从起点到目标状态的一条最短路径
+ */
+vector bfs(const state_t &start, const type& data) {
queue next, current; // 当前层,下一层
unordered_set visited; // 判重
- unordered_map father; // 树
+ unordered_map father; // 树,求路径本身时才需要
+
+ int level = -1; // 层次
- int level = 0; // 层次
- bool found = false; // 是否找到目标
- state_t target; // 符合条件的目标状态
+ // 判断状态是否合法
+ auto state_is_valid = [&](const state_t &s) { /*...*/ };
// 判断当前状态是否为所求目标
- auto state_is_target = [&](const state_t &s) {return true; };
+ auto state_is_target = [&](const state_t &s) { /*...*/ };
+
// 扩展当前状态
auto state_extend = [&](const state_t &s) {
- vector result;
- // ...
+ unordered_set result;
+ for (/*...*/) {
+ const state_t new_state = /*...*/;
+ if (state_is_valid(new_state) &&
+ visited.find(new_state) != visited.end()) {
+ result.insert(new_state);
+ }
+ }
return result;
};
current.push(start);
- visited.insert(start);
- while (!current.empty() && !found) {
+ while (!current.empty()) {
++level;
- while (!current.empty() && !found) {
- const state_t state = current.front();
+ while (!current.empty()) {
+ // 千万不能用 const auto&,pop() 会删除元素,
+ // 引用就变成了悬空引用
+ const auto state = current.front();
current.pop();
- vector new_states = state_extend(state);
- for (auto iter = new_states.cbegin();
- iter != new_states.cend() && ! found; ++iter) {
- const state_t new_state(*iter);
-
- if (state_is_target(new_state)) {
- found = true; //找到了
- target = new_state;
- father[new_state] = state;
- break;
- }
+ visited.insert(state);
+ if (state_is_target(state)) {
+ return return gen_path(father, state); // 求一条路径
+ // return state.level + 1; // 求路径长度
+ }
+
+ const auto& new_states = state_extend(state);
+ for (const auto& new_state : new_states) {
next.push(new_state);
- // visited.insert(new_state); 必须放到 state_extend()里
father[new_state] = state;
+ // visited.insert(state); // 优化:可以提前加入 visited 集合,
+ // 从而缩小状态扩展。这时 current 的含义略有变化,里面存放的是处
+ // 理了一半的节点:已经加入了visited,但还没有扩展。别忘记 while
+ // 循环开始前,要加一行代码, visited.insert(start)
}
}
swap(next, current); //!!! 交换两个队列
}
- if (found) {
- return gen_path(father, target);
- //return level + 1;
- } else {
- return vector();
- //return 0;
+ return vector();
+ // return 0;
+}
+\end{Codex}
+
+
+\subsubsection{求所有路径}
+
+\textbf{单队列}
+
+\begin{Codex}[label=bfs_template.cpp]
+/**
+ * @brief 广搜,使用一个队列.
+ * @param[in] start 起点
+ * @param[in] data 输入数据
+ * @return 从起点到目标状态的所有最短路径
+ */
+vector > bfs(const state_t &start, const type& data) {
+ queue q;
+ unordered_set visited; // 判重
+ unordered_map > father; // DAG
+
+ auto state_is_valid = [&](const state_t& s) { /*...*/ };
+ auto state_is_target = [&](const state_t &s) { /*...*/ };
+ auto state_extend = [&](const state_t &s) {
+ unordered_set result;
+ for (/*...*/) {
+ const state_t new_state = /*...*/;
+ if (state_is_valid(new_state)) {
+ auto visited_iter = visited.find(new_state);
+
+ if (visited_iter != visited.end()) {
+ if (visited_iter->level < new_state.level) {
+ // do nothing
+ } else if (visited_iter->level == new_state.level) {
+ result.insert(new_state);
+ } else { // not possible
+ throw std::logic_error("not possible to get here");
+ }
+ } else {
+ result.insert(new_state);
+ }
+ }
+ }
+
+ return result;
+ };
+
+ vector> result;
+ state_t start_state(start, 0);
+ q.push(start_state);
+ visited.insert(start_state);
+ while (!q.empty()) {
+ // 千万不能用 const auto&,pop() 会删除元素,
+ // 引用就变成了悬空引用
+ const auto state = q.front();
+ q.pop();
+
+ // 如果当前路径长度已经超过当前最短路径长度,
+ // 可以中止对该路径的处理,因为我们要找的是最短路径
+ if (!result.empty() && state.level + 1 > result[0].size()) break;
+
+ if (state_is_target(state)) {
+ vector path;
+ gen_path(father, start_state, state, path, result);
+ continue;
+ }
+ // 必须挪到下面,比如同一层A和B两个节点均指向了目标节点,
+ // 那么目标节点就会在q中出现两次,输出路径就会翻倍
+ // visited.insert(state);
+
+ // 扩展节点
+ const auto& new_states = state_extend(state);
+ for (const auto& new_state : new_states) {
+ if (visited.find(new_state) == visited.end()) {
+ q.push(new_state);
+ }
+ visited.insert(new_state);
+ father[new_state].push_back(state);
+ }
}
+
+ return result;
}
\end{Codex}
-\subsubsection{只用一个队列的写法}
-双队列的写法,当求路径长度时,不需要在状态里设置一个\fn{count}字段记录路径长度,只需全局设置一个整数\fn{level},比较节省内存;只用一个队列的写法,当求路径长度时,需要在状态里设置一个\fn{count}字段,不过,这种写法有一个好处 —— 可以很容易的变为A*算法,把\fn{queue}替换为\fn{priority_queue}即可。
+\textbf{双队列的写法}
-\begin{Codex}[label=bfs_template2.cpp]
-// 与模板1相同的部分,不再重复
-// ...
+\begin{Codex}[label=bfs_template.cpp]
+#include "bfs_common.h"
/**
- * @brief 广搜.
- * @param[in] state_t 状态,如整数,字符串,一维数组等
+ * @brief 广搜,使用两个队列.
* @param[in] start 起点
- * @param[in] grid 输入数据
- * @return 从起点到目标状态的一条最短路径
+ * @param[in] data 输入数据
+ * @return 从起点到目标状态的所有最短路径
*/
-template
-vector bfs(state_t &start, const vector> &grid) {
- queue q; // 队列
+vector > bfs(const state_t &start, const type& data) {
+ // 当前层,下一层,用unordered_set是为了去重,例如两个父节点指向
+ // 同一个子节点,如果用vector, 子节点就会在next里出现两次,其实此
+ // 时 father 已经记录了两个父节点,next里重复出现两次是没必要的
+ unordered_set current, next;
unordered_set visited; // 判重
- unordered_map father; // 树
+ unordered_map > father; // DAG
+
+ int level = -1; // 层次
- int level = 0; // 层次
- bool found = false; // 是否找到目标
- state_t target; // 符合条件的目标状态
+ // 判断状态是否合法
+ auto state_is_valid = [&](const state_t &s) { /*...*/ };
// 判断当前状态是否为所求目标
- auto state_is_target = [&](const state_t &s) {return true; };
+ auto state_is_target = [&](const state_t &s) { /*...*/ };
+
// 扩展当前状态
auto state_extend = [&](const state_t &s) {
- vector result;
- // ...
+ unordered_set result;
+ for (/*...*/) {
+ const state_t new_state = /*...*/;
+ if (state_is_valid(new_state) &&
+ visited.find(new_state) != visited.end()) {
+ result.insert(new_state);
+ }
+ }
return result;
};
- start.count = 0;
- q.push(start);
- visited.insert(start);
- while (!q.empty() && !found) {
- const state_t state = q.front();
- q.pop();
- vector new_states = state_extend(state);
- for (auto iter = new_states.cbegin();
- iter != new_states.cend() && ! found; ++iter) {
- const state_t new_state(*iter);
-
- if (state_is_target(new_state)) {
- found = true; //找到了
- target = new_state;
- father[new_state] = state;
- break;
+ vector > result;
+ current.insert(start);
+ while (!current.empty()) {
+ ++ level;
+ // 如果当前路径长度已经超过当前最短路径长度,可以中止对该路径的
+ // 处理,因为我们要找的是最短路径
+ if (!result.empty() && level+1 > result[0].size()) break;
+
+ // 1. 延迟加入visited, 这样才能允许两个父节点指向同一个子节点
+ // 2. 一股脑current 全部加入visited, 是防止本层前一个节点扩展
+ // 节点时,指向了本层后面尚未处理的节点,这条路径必然不是最短的
+ for (const auto& state : current)
+ visited.insert(state);
+ for (const auto& state : current) {
+ if (state_is_target(state)) {
+ vector path;
+ gen_path(father, path, start, state, result);
+ continue;
}
- q.push(new_state);
- // visited.insert(new_state); 必须放到 state_extend()里
- father[new_state] = state;
+ const auto new_states = state_extend(state);
+ for (const auto& new_state : new_states) {
+ next.insert(new_state);
+ father[new_state].push_back(state);
+ }
}
- }
- if (found) {
- return gen_path(father, target);
- //return level + 1;
- } else {
- return vector();
- //return 0;
+ current.clear();
+ swap(current, next);
}
+
+ return result;
}
\end{Codex}
+
diff --git a/C++/chapBruteforce.tex b/C++/chapBruteforce.tex
index 08014864..4c9e4037 100644
--- a/C++/chapBruteforce.tex
+++ b/C++/chapBruteforce.tex
@@ -439,16 +439,16 @@ \subsubsection{代码}
// 时间复杂度O(n!),空间复杂度O(1)
class Solution {
public:
- vector> permute(vector& num) {
+ vector > permute(vector &num) {
+ vector > result;
sort(num.begin(), num.end());
- vector> permutations;
-
do {
- permutations.push_back(num);
- } while (next_permutation(num.begin(), num.end())); // 见第2.1节
-
- return permutations;
+ result.push_back(num);
+ // 调用的是 2.1.12 节的 next_permutation()
+ // 而不是 std::next_permutation()
+ } while(next_permutation(num.begin(), num.end()));
+ return result;
}
};
\end{Code}
@@ -733,6 +733,7 @@ \subsection{递归}
vector letterCombinations (const string &digits) {
vector result;
+ if (digits.empty()) return result;
dfs(digits, 0, "", result);
return result;
}
@@ -761,6 +762,7 @@ \subsection{迭代}
"ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" };
vector letterCombinations (const string &digits) {
+ if (digits.empty()) return vector();
vector result(1, "");
for (auto d : digits) {
const size_t n = result.size();
diff --git a/C++/chapDFS.tex b/C++/chapDFS.tex
index 87c4c7bf..f6c4c107 100644
--- a/C++/chapDFS.tex
+++ b/C++/chapDFS.tex
@@ -39,8 +39,7 @@ \subsubsection{深搜1}
return result;
}
- // s[0, prev-1]之间已经处理,保证是回文串
- // prev 表示s[prev-1]与s[prev]之间的空隙位置,start同理
+ // prev 表示前一个隔板, start 表示当前隔板
void dfs(string &s, vector& path,
vector> &result, size_t prev, size_t start) {
if (start == s.size()) { // 最后一个隔板
@@ -204,24 +203,24 @@ \subsubsection{代码}
class Solution {
public:
int uniquePaths(int m, int n) {
- // 0行和0列未使用
- this->f = vector >(m + 1, vector(n + 1, 0));
- return dfs(m, n);
+ // f[x][y] 表示 从(0,0)到(x,y)的路径条数
+ f = vector >(m, vector(n, 0));
+ f[0][0] = 1;
+ return dfs(m - 1, n - 1);
}
private:
vector > f; // 缓存
int dfs(int x, int y) {
- if (x < 1 || y < 1) return 0; // 数据非法,终止条件
+ if (x < 0 || y < 0) return 0; // 数据非法,终止条件
- if (x == 1 && y == 1) return 1; // 回到起点,收敛条件
+ if (x == 0 && y == 0) return f[0][0]; // 回到起点,收敛条件
- return getOrUpdate(x - 1, y) + getOrUpdate(x, y - 1);
- }
-
- int getOrUpdate(int x, int y) {
- if (f[x][y] > 0) return f[x][y];
- else return f[x][y] = dfs(x, y);
+ if (f[x][y] > 0) {
+ return f[x][y];
+ } else {
+ return f[x][y] = dfs(x - 1, y) + dfs(x, y - 1);
+ }
}
};
\end{Code}
@@ -248,9 +247,9 @@ \subsubsection{代码}
f[0] = 1;
for (int i = 0; i < m; i++) {
for (int j = 1; j < n; j++) {
- // 左边的f[j],表示更新后的f[j],与公式中的f[i[[j]对应
+ // 左边的f[j],表示更新后的f[j],与公式中的f[i][j]对应
// 右边的f[j],表示老的f[j],与公式中的f[i-1][j]对应
- f[j] = f[j - 1] + f[j];
+ f[j] = f[j] + f[j - 1];
}
}
return f[n - 1];
@@ -338,40 +337,41 @@ \subsubsection{代码}
// 深搜 + 缓存,即备忘录法
class Solution {
public:
- int uniquePathsWithObstacles(vector > &obstacleGrid) {
+ int uniquePathsWithObstacles(const vector >& obstacleGrid) {
const int m = obstacleGrid.size();
const int n = obstacleGrid[0].size();
- // 0行和0列未使用
- this->f = vector >(m + 1, vector(n + 1, 0));
- return dfs(obstacleGrid, m, n);
+ if (obstacleGrid[0][0] || obstacleGrid[m - 1][n - 1]) return 0;
+
+ f = vector >(m, vector(n, 0));
+ f[0][0] = obstacleGrid[0][0] ? 0 : 1;
+ return dfs(obstacleGrid, m - 1, n - 1);
}
private:
vector > f; // 缓存
- int dfs(const vector > &obstacleGrid,
+ // @return 从 (0, 0) 到 (x, y) 的路径总数
+ int dfs(const vector >& obstacleGrid,
int x, int y) {
- if (x < 1 || y < 1) return 0; // 数据非法,终止条件
+ if (x < 0 || y < 0) return 0; // 数据非法,终止条件
// (x,y)是障碍
- if (obstacleGrid[x-1][y-1]) return 0;
-
- if (x == 1 and y == 1) return 1; // 回到起点,收敛条件
+ if (obstacleGrid[x][y]) return 0;
- return getOrUpdate(obstacleGrid, x - 1, y) +
- getOrUpdate(obstacleGrid, x, y - 1);
- }
+ if (x == 0 and y == 0) return f[0][0]; // 回到起点,收敛条件
- int getOrUpdate(const vector > &obstacleGrid,
- int x, int y) {
- if (f[x][y] > 0) return f[x][y];
- else return f[x][y] = dfs(obstacleGrid, x, y);
+ if (f[x][y] > 0) {
+ return f[x][y];
+ } else {
+ return f[x][y] = dfs(obstacleGrid, x - 1, y) +
+ dfs(obstacleGrid, x, y - 1);
+ }
}
};
\end{Code}
\subsection{动规}
-与上一题类似,但要特别注意第一列的障碍。在上一题中,第一列全部是1,但是在这一题中不同,第一列如果某一行有障碍物,那么后面的行应该为0。
+与上一题类似,但要特别注意第一列的障碍。在上一题中,第一列全部是1,但是在这一题中不同,第一列如果某一行有障碍物,那么后面的行全为0。
\subsubsection{代码}
@@ -389,9 +389,11 @@ \subsubsection{代码}
vector f(n, 0);
f[0] = obstacleGrid[0][0] ? 0 : 1;
- for (int i = 0; i < m; i++)
- for (int j = 0; j < n; j++)
- f[j] = obstacleGrid[i][j] ? 0 : (j == 0 ? 0 : f[j - 1]) + f[j];
+ for (int i = 0; i < m; i++) {
+ f[0] = f[0] == 0 ? 0 : (obstacleGrid[i][0] ? 0 : 1);
+ for (int j = 1; j < n; j++)
+ f[j] = obstacleGrid[i][j] ? 0 : (f[j] + f[j - 1]);
+ }
return f[n - 1];
}
@@ -440,9 +442,74 @@ \subsubsection{描述}
\subsubsection{分析}
+
经典的深搜题。
-\subsubsection{代码}
+设置一个数组 \fn{vector C(n, 0)}, \fn{C[i]} 表示第i行皇后所在的列编号,即在位置 (i, C[i]) 上放了一个皇后,这样用一个一维数组,就能记录整个棋盘。
+
+
+\subsubsection{代码1}
+\begin{Code}
+// LeetCode, N-Queens
+// 深搜+剪枝
+// 时间复杂度O(n!*n),空间复杂度O(n)
+class Solution {
+public:
+ vector > solveNQueens(int n) {
+ vector > result;
+ vector C(n, -1); // C[i]表示第i行皇后所在的列编号
+ dfs(C, result, 0);
+ return result;
+ }
+private:
+ void dfs(vector &C, vector > &result, int row) {
+ const int N = C.size();
+ if (row == N) { // 终止条件,也是收敛条件,意味着找到了一个可行解
+ vector solution;
+ for (int i = 0; i < N; ++i) {
+ string s(N, '.');
+ for (int j = 0; j < N; ++j) {
+ if (j == C[i]) s[j] = 'Q';
+ }
+ solution.push_back(s);
+ }
+ result.push_back(solution);
+ return;
+ }
+
+ for (int j = 0; j < N; ++j) { // 扩展状态,一列一列的试
+ const bool ok = isValid(C, row, j);
+ if (!ok) continue; // 剪枝,如果非法,继续尝试下一列
+ // 执行扩展动作
+ C[row] = j;
+ dfs(C, result, row + 1);
+ // 撤销动作
+ // C[row] = -1;
+ }
+ }
+
+ /**
+ * 能否在 (row, col) 位置放一个皇后.
+ *
+ * @param C 棋局
+ * @param row 当前正在处理的行,前面的行都已经放了皇后了
+ * @param col 当前列
+ * @return 能否放一个皇后
+ */
+ bool isValid(const vector &C, int row, int col) {
+ for (int i = 0; i < row; ++i) {
+ // 在同一列
+ if (C[i] == col) return false;
+ // 在同一对角线上
+ if (abs(i - row) == abs(C[i] - col)) return false;
+ }
+ return true;
+ }
+};
+\end{Code}
+
+
+\subsubsection{代码2}
\begin{Code}
// LeetCode, N-Queens
// 深搜+剪枝
@@ -450,20 +517,20 @@ \subsubsection{代码}
class Solution {
public:
vector > solveNQueens(int n) {
- this->columns = vector(n, 0);
- this->main_diag = vector(2 * n, 0);
- this->anti_diag = vector(2 * n, 0);
+ this->columns = vector(n, false);
+ this->main_diag = vector(2 * n - 1, false);
+ this->anti_diag = vector(2 * n - 1, false);
vector > result;
- vector C(n, 0); // C[i]表示第i行皇后所在的列编号
+ vector C(n, -1); // C[i]表示第i行皇后所在的列编号
dfs(C, result, 0);
return result;
}
private:
// 这三个变量用于剪枝
- vector columns; // 表示已经放置的皇后占据了哪些列
- vector main_diag; // 占据了哪些主对角线
- vector anti_diag; // 占据了哪些副对角线
+ vector columns; // 表示已经放置的皇后占据了哪些列
+ vector main_diag; // 占据了哪些主对角线
+ vector anti_diag; // 占据了哪些副对角线
void dfs(vector &C, vector > &result, int row) {
const int N = C.size();
@@ -481,16 +548,16 @@ \subsubsection{代码}
}
for (int j = 0; j < N; ++j) { // 扩展状态,一列一列的试
- const bool ok = columns[j] == 0 && main_diag[row + j] == 0 &&
- anti_diag[row - j + N] == 0;
- if (!ok) continue; // 剪枝:如果合法,继续递归
+ const bool ok = !columns[j] && !main_diag[row - j + N - 1] &&
+ !anti_diag[row + j];
+ if (!ok) continue; // 剪枝,如果非法,继续尝试下一列
// 执行扩展动作
C[row] = j;
- columns[j] = main_diag[row + j] = anti_diag[row - j + N] = 1;
+ columns[j] = main_diag[row - j + N - 1] = anti_diag[row + j] = true;
dfs(C, result, row + 1);
// 撤销动作
- // C[row] = 0;
- columns[j] = main_diag[row + j] = anti_diag[row - j + N] = 0;
+ // C[row] = -1;
+ columns[j] = main_diag[row - j + N - 1] = anti_diag[row + j] = false;
}
}
};
@@ -517,7 +584,62 @@ \subsubsection{分析}
只需要输出解的个数,不需要输出所有解,代码要比上一题简化很多。设一个全局计数器,每找到一个解就增1。
-\subsubsection{代码}
+\subsubsection{代码1}
+\begin{Code}
+// LeetCode, N-Queens II
+// 深搜+剪枝
+// 时间复杂度O(n!*n),空间复杂度O(n)
+class Solution {
+public:
+ int totalNQueens(int n) {
+ this->count = 0;
+
+ vector C(n, 0); // C[i]表示第i行皇后所在的列编号
+ dfs(C, 0);
+ return this->count;
+ }
+private:
+ int count; // 解的个数
+
+ void dfs(vector &C, int row) {
+ const int N = C.size();
+ if (row == N) { // 终止条件,也是收敛条件,意味着找到了一个可行解
+ ++this->count;
+ return;
+ }
+
+ for (int j = 0; j < N; ++j) { // 扩展状态,一列一列的试
+ const bool ok = isValid(C, row, j);
+ if (!ok) continue; // 剪枝:如果合法,继续递归
+ // 执行扩展动作
+ C[row] = j;
+ dfs(C, row + 1);
+ // 撤销动作
+ // C[row] = -1;
+ }
+ }
+ /**
+ * 能否在 (row, col) 位置放一个皇后.
+ *
+ * @param C 棋局
+ * @param row 当前正在处理的行,前面的行都已经放了皇后了
+ * @param col 当前列
+ * @return 能否放一个皇后
+ */
+ bool isValid(const vector &C, int row, int col) {
+ for (int i = 0; i < row; ++i) {
+ // 在同一列
+ if (C[i] == col) return false;
+ // 在同一对角线上
+ if (abs(i - row) == abs(C[i] - col)) return false;
+ }
+ return true;
+ }
+};
+\end{Code}
+
+
+\subsubsection{代码2}
\begin{Code}
// LeetCode, N-Queens II
// 深搜+剪枝
@@ -526,9 +648,9 @@ \subsubsection{代码}
public:
int totalNQueens(int n) {
this->count = 0;
- this->columns = vector(n, 0);
- this->main_diag = vector(2 * n, 0);
- this->anti_diag = vector(2 * n, 0);
+ this->columns = vector(n, false);
+ this->main_diag = vector(2 * n - 1, false);
+ this->anti_diag = vector(2 * n - 1, false);
vector C(n, 0); // C[i]表示第i行皇后所在的列编号
dfs(C, 0);
@@ -537,9 +659,9 @@ \subsubsection{代码}
private:
int count; // 解的个数
// 这三个变量用于剪枝
- vector columns; // 表示已经放置的皇后占据了哪些列
- vector main_diag; // 占据了哪些主对角线
- vector anti_diag; // 占据了哪些副对角线
+ vector columns; // 表示已经放置的皇后占据了哪些列
+ vector main_diag; // 占据了哪些主对角线
+ vector anti_diag; // 占据了哪些副对角线
void dfs(vector &C, int row) {
const int N = C.size();
@@ -549,19 +671,19 @@ \subsubsection{代码}
}
for (int j = 0; j < N; ++j) { // 扩展状态,一列一列的试
- const bool ok = columns[j] == 0 &&
- main_diag[row + j] == 0 &&
- anti_diag[row - j + N] == 0;
+ const bool ok = !columns[j] &&
+ !main_diag[row - j + N] &&
+ !anti_diag[row + j];
if (!ok) continue; // 剪枝:如果合法,继续递归
// 执行扩展动作
C[row] = j;
- columns[j] = main_diag[row + j] =
- anti_diag[row - j + N] = 1;
+ columns[j] = main_diag[row - j + N] =
+ anti_diag[row + j] = true;
dfs(C, row + 1);
// 撤销动作
- // C[row] = 0;
- columns[j] = main_diag[row + j] =
- anti_diag[row - j + N] = 0;
+ // C[row] = -1;
+ columns[j] = main_diag[row - j + N] =
+ anti_diag[row + j] = false;
}
}
};
@@ -597,43 +719,43 @@ \subsubsection{代码}
// 时间复杂度O(n^4),空间复杂度O(n)
class Solution {
public:
- vector restoreIpAddresses(string s) {
+ vector restoreIpAddresses(const string& s) {
vector result;
- string ip; // 存放中间结果
- dfs(s, 0, 0, ip, result);
+ vector ip; // 存放中间结果
+ dfs(s, ip, result, 0);
return result;
}
/**
* @brief 解析字符串
* @param[in] s 字符串,输入数据
- * @param[in] startIndex 从s的哪里开始
- * @param[in] step 当前步骤编号,从0开始编号,取值为0,1,2,3,4表示结束了
- * @param[out] intermediate 当前解析出来的中间结果
+ * @param[out] ip 存放中间结果
* @param[out] result 存放所有可能的IP地址
+ * @param[in] start 当前正在处理的 index
* @return 无
*/
- void dfs(string s, size_t start, size_t step, string ip,
- vector &result) {
- if (start == s.size() && step == 4) { // 找到一个合法解
- ip.resize(ip.size() - 1);
- result.push_back(ip);
+ void dfs(string s, vector& ip, vector &result,
+ size_t start) {
+ if (ip.size() == 4 && start == s.size()) { // 找到一个合法解
+ result.push_back(ip[0] + '.' + ip[1] + '.' + ip[2] + '.' + ip[3]);
return;
}
- if (s.size() - start > (4 - step) * 3)
+ if (s.size() - start > (4 - ip.size()) * 3)
return; // 剪枝
- if (s.size() - start < (4 - step))
+ if (s.size() - start < (4 - ip.size()))
return; // 剪枝
int num = 0;
for (size_t i = start; i < start + 3; i++) {
num = num * 10 + (s[i] - '0');
- if (num <= 255) { // 当前结点合法,则继续往下递归
- ip += s[i];
- dfs(s, i + 1, step + 1, ip + '.', result);
- }
+ if (num < 0 || num > 255) continue; // 剪枝
+
+ ip.push_back(s.substr(start, i - start + 1));
+ dfs(s, ip, result, i + 1);
+ ip.pop_back();
+
if (num == 0) break; // 不允许前缀0,但允许单个0
}
}
@@ -684,24 +806,24 @@ \subsubsection{代码}
vector > combinationSum(vector &nums, int target) {
sort(nums.begin(), nums.end());
vector > result; // 最终结果
- vector intermediate; // 中间结果
- dfs(nums, target, 0, intermediate, result);
+ vector path; // 中间结果
+ dfs(nums, path, result, target, 0);
return result;
}
private:
- void dfs(vector& nums, int gap, int start, vector& intermediate,
- vector > &result) {
+ void dfs(vector& nums, vector& path, vector > &result,
+ int gap, int start) {
if (gap == 0) { // 找到一个合法解
- result.push_back(intermediate);
+ result.push_back(path);
return;
}
for (size_t i = start; i < nums.size(); i++) { // 扩展状态
if (gap < nums[i]) return; // 剪枝
- intermediate.push_back(nums[i]); // 执行扩展动作
- dfs(nums, gap - nums[i], i, intermediate, result);
- intermediate.pop_back(); // 撤销动作
+ path.push_back(nums[i]); // 执行扩展动作
+ dfs(nums, path, result, gap - nums[i], i);
+ path.pop_back(); // 撤销动作
}
}
};
@@ -719,9 +841,9 @@ \section{Combination Sum II} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\subsubsection{描述}
-Given a set of candidate numbers ($C$) and a target number ($T$), find all unique combinations in $C$ where the candidate numbers sums to $T$.
+Given a collection of candidate numbers ($C$) and a target number ($T$), find all unique combinations in $C$ where the candidate numbers sums to $T$.
-The same repeated number may be chosen from $C$ \emph{once} number of times.
+Each number in $C$ may only be used \emph{once} in the combination.
Note:
\begindot
@@ -752,24 +874,24 @@ \subsubsection{代码}
public:
vector > combinationSum2(vector &nums, int target) {
sort(nums.begin(), nums.end()); // 跟第 50 行配合,
- // 确保每个元素最多只用一次
+ // 确保每个元素最多只用一次
vector > result;
- vector intermediate;
- dfs(nums, target, 0, intermediate, result);
+ vector path;
+ dfs(nums, path, result, target, 0);
return result;
}
private:
// 使用nums[start, nums.size())之间的元素,能找到的所有可行解
- static void dfs(vector &nums, int gap, int start,
- vector &intermediate, vector > &result) {
+ static void dfs(const vector &nums, vector &path,
+ vector > &result, int gap, int start) {
if (gap == 0) { // 找到一个合法解
- result.push_back(intermediate);
+ result.push_back(path);
return;
}
int previous = -1;
for (size_t i = start; i < nums.size(); i++) {
- // 如果上一轮循环没有选nums[i],则本次循环就不能再选nums[i],
+ // 如果上一轮循环已经使用了nums[i],则本次循环就不能再选nums[i],
// 确保nums[i]最多只用一次
if (previous == nums[i]) continue;
@@ -777,9 +899,9 @@ \subsubsection{代码}
previous = nums[i];
- intermediate.push_back(nums[i]);
- dfs(nums, gap - nums[i], i + 1, intermediate, result);
- intermediate.pop_back(); // 恢复环境
+ path.push_back(nums[i]);
+ dfs(nums, path, result, gap - nums[i], i + 1);
+ path.pop_back(); // 恢复环境
}
}
};
@@ -818,17 +940,27 @@ \subsubsection{代码1}
public:
vector generateParenthesis(int n) {
vector result;
- if (n > 0) generate(n, "", 0, 0, result);
+ string path;
+ if (n > 0) generate(n, path, result, 0, 0);
return result;
}
// l 表示 ( 出现的次数, r 表示 ) 出现的次数
- void generate(int n, string s, int l, int r, vector &result) {
+ void generate(int n, string& path, vector &result, int l, int r) {
if (l == n) {
+ string s(path);
result.push_back(s.append(n - r, ')'));
return;
}
- generate(n, s + '(', l + 1, r, result);
- if (l > r) generate(n, s + ")", l, r + 1, result);
+
+ path.push_back('(');
+ generate(n, path, result, l + 1, r);
+ path.pop_back();
+
+ if (l > r) {
+ path.push_back(')');
+ generate(n, path, result, l, r + 1);
+ path.pop_back();
+ }
}
};
\end{Code}
@@ -971,7 +1103,7 @@ \subsubsection{代码}
// 时间复杂度O(n^2*m^2),空间复杂度O(n^2)
class Solution {
public:
- bool exist(vector > &board, string word) {
+ bool exist(const vector > &board, const string& word) {
const int m = board.size();
const int n = board[0].size();
vector > visited(m, vector(n, false));
@@ -1039,12 +1171,6 @@ \subsection{思考的步骤}
\item 如何扩展状态?这一步跟上一步相关。状态里记录的数据不同,扩展方法就不同。对于固定不变的数据结构(一般题目直接给出,作为输入数据),如二叉树,图等,扩展方法很简单,直接往下一层走,对于隐式图,要先在第1步里想清楚状态所带的数据,想清楚了这点,那如何扩展就很简单了。
-\item 关于判重
- \begin{enumerate}
- \item 是否需要判重?如果状态转换图是一棵树,则不需要判重,因为在遍历过程中不可能重复;如果状态转换图是一个DAG,则需要判重。这一点跟BFS不一样,BFS的状态转换图总是DAG,必须要判重。
- \item 怎样判重?跟广搜相同,见第 \S \ref{sec:bfs-template} 节。同时,DAG说明存在重叠子问题,此时可以用缓存加速,见第8步。
- \end{enumerate}
-
\item 终止条件是什么?终止条件是指到了不能扩展的末端节点。对于树,是叶子节点,对于图或隐式图,是出度为0的节点。
\item {收敛条件是什么?收敛条件是指找到了一个合法解的时刻。如果是正向深搜(父状态处理完了才进行递归,即父状态不依赖子状态,递归语句一定是在最后,尾递归),则是指是否达到目标状态;如果是逆向深搜(处理父状态时需要先知道子状态的结果,此时递归语句不在最后),则是指是否到达初始状态。
@@ -1053,6 +1179,12 @@ \subsection{思考的步骤}
为了判断是否到了收敛条件,要在函数接口里用一个参数记录当前的位置(或距离目标还有多远)。如果是求一个解,直接返回这个解;如果是求所有解,要在这里收集解,即把第一步中表示路径的数组\fn{path[]}复制到解集合里。}
+\item 关于判重
+ \begin{enumerate}
+ \item 是否需要判重?如果状态转换图是一棵树,则不需要判重,因为在遍历过程中不可能重复;如果状态转换图是一个DAG,则需要判重。这一点跟BFS不一样,BFS的状态转换图总是DAG,必须要判重。
+ \item 怎样判重?跟广搜相同,见第 \S \ref{sec:bfs-template} 节。同时,DAG说明存在重叠子问题,此时可以用缓存加速,见第8步。
+ \end{enumerate}
+
\item 如何加速?
\begin{enumerate}
\item 剪枝。深搜一定要好好考虑怎么剪枝,成本小收益大,加几行代码,就能大大加速。这里没有通用方法,只能具体问题具体分析,要充分观察,充分利用各种信息来剪枝,在中间节点提前返回。
@@ -1116,8 +1248,8 @@ \subsection{深搜与递归的区别}
递归有两种加速策略,一种是\textbf{剪枝(prunning)},对中间结果进行判断,提前返回;一种是\textbf{缓存},缓存中间结果,防止重复计算,用空间换时间。
-其实,递归+缓存,就是 memorization。所谓\textbf{memorization}(翻译为备忘录法,见第 \S \ref{sec:dp-vs-memorization}节),就是"top-down with cache"(自顶向下+缓存),它是Donald Michie 在1968年创造的术语,表示一种优化技术,在top-down 形式的程序中,使用缓存来避免重复计算,从而达到加速的目的。
+其实,递归+缓存,就是 memoization。所谓\textbf{memoization}(翻译为备忘录法,见第 \S \ref{sec:dp-vs-memoization}节),就是"top-down with cache"(自顶向下+缓存),它是Donald Michie 在1968年创造的术语,表示一种优化技术,在top-down 形式的程序中,使用缓存来避免重复计算,从而达到加速的目的。
-\textbf{memorization 不一定用递归},就像深搜不一定用递归一样,可以在迭代(iterative)中使用 memorization 。\textbf{递归也不一定用 memorization},可以用memorization来加速,但不是必须的。只有当递归使用了缓存,它才是 memorization 。
+\textbf{memoization 不一定用递归},就像深搜不一定用递归一样,可以在迭代(iterative)中使用 memoization 。\textbf{递归也不一定用 memoization},可以用memoization来加速,但不是必须的。只有当递归使用了缓存,它才是 memoization 。
既然递归一定是深搜,为什么很多书籍都同时使用这两个术语呢?在递归味道更浓的地方,一般用递归这个术语,在深搜更浓的场景下,用深搜这个术语,读者心里要弄清楚他俩大部分时候是一回事。在单链表、二叉树等递归数据结构上,递归的味道更浓,这时用递归这个术语;在图、隐式图等数据结构上,深搜的味道更浓,这时用深搜这个术语。
diff --git a/C++/chapDivideAndConquer.tex b/C++/chapDivideAndConquer.tex
index 0876f43f..9cf03559 100644
--- a/C++/chapDivideAndConquer.tex
+++ b/C++/chapDivideAndConquer.tex
@@ -20,7 +20,7 @@ \subsubsection{代码}
// 时间复杂度O(logn),空间复杂度O(1)
class Solution {
public:
- double pow(double x, int n) {
+ double myPow(double x, int n) {
if (n < 0) return 1.0 / power(x, -n);
else return power(x, n);
}
@@ -62,7 +62,7 @@ \subsubsection{代码}
// 时间复杂度O(logn),空间复杂度O(1)
class Solution {
public:
- int sqrt(int x) {
+ int mySqrt(int x) {
int left = 1, right = x / 2;
int last_mid; // 记录最近一次mid
diff --git a/C++/chapDynamicProgramming.tex b/C++/chapDynamicProgramming.tex
index a6ec1d08..5327de51 100644
--- a/C++/chapDynamicProgramming.tex
+++ b/C++/chapDynamicProgramming.tex
@@ -25,7 +25,7 @@ \subsubsection{描述}
\subsubsection{分析}
设状态为$f(i, j)$,表示从从位置$(i,j)$出发,路径的最小和,则状态转移方程为
$$
-f(i,j)=\min\left\{f(i,j+1),f(i+1,j+1)\right\}+(i,j)
+f(i,j)=\min\left\{f(i+1,j),f(i+1,j+1)\right\}+(i,j)
$$
@@ -101,10 +101,10 @@ \subsubsection{动规}
// 时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
- int maxSubArray(int A[], int n) {
+ int maxSubArray(vector& nums) {
int result = INT_MIN, f = 0;
- for (int i = 0; i < n; ++i) {
- f = max(f + A[i], A[i]);
+ for (int i = 0; i < nums.size(); ++i) {
+ f = max(f + nums[i], nums[i]);
result = max(result, f);
}
return result;
@@ -119,22 +119,24 @@ \subsubsection{思路5}
// 时间复杂度O(n),空间复杂度O(n)
class Solution {
public:
- int maxSubArray(int A[], int n) {
- return mcss(A, n);
+ int maxSubArray(vector& A) {
+ return mcss(A.begin(), A.end());
}
private:
// 思路5,求最大连续子序列和
- static int mcss(int A[], int n) {
- int i, result, cur_min;
+ template
+ static int mcss(Iter begin, Iter end) {
+ int result, cur_min;
+ const int n = distance(begin, end);
int *sum = new int[n + 1]; // 前n项和
sum[0] = 0;
result = INT_MIN;
cur_min = sum[0];
- for (i = 1; i <= n; i++) {
- sum[i] = sum[i - 1] + A[i - 1];
+ for (int i = 1; i <= n; i++) {
+ sum[i] = sum[i - 1] + *(begin + i - 1);
}
- for (i = 1; i <= n; i++) {
+ for (int i = 1; i <= n; i++) {
result = max(result, sum[i] - cur_min);
cur_min = min(cur_min, sum[i]);
}
@@ -191,7 +193,7 @@ \subsubsection{代码}
// 时间复杂度O(n^2),空间复杂度O(n^2)
class Solution {
public:
- int minCut(string s) {
+ int minCut(const string& s) {
const int n = s.size();
int f[n+1];
bool p[n][n];
@@ -369,7 +371,7 @@ \subsubsection{递归}
// 递归,会超时,仅用来帮助理解
class Solution {
public:
- bool isInterleave(string s1, string s2, string s3) {
+ bool isInterleave(const string& s1, const string& s2, const string& s3) {
if (s3.length() != s1.length() + s2.length())
return false;
@@ -400,7 +402,7 @@ \subsubsection{动规}
// 二维动规,时间复杂度O(n^2),空间复杂度O(n^2)
class Solution {
public:
- bool isInterleave(string s1, string s2, string s3) {
+ bool isInterleave(const string& s1, const string& s2, const string& s3) {
if (s3.length() != s1.length() + s2.length())
return false;
@@ -430,7 +432,7 @@ \subsubsection{动规+滚动数组}
// 二维动规+滚动数组,时间复杂度O(n^2),空间复杂度O(n)
class Solution {
public:
- bool isInterleave(string s1, string s2, string s3) {
+ bool isInterleave(const string& s1, const string& s2, const string& s3) {
if (s1.length() + s2.length() != s3.length())
return false;
@@ -512,7 +514,7 @@ \subsubsection{描述}
\subsubsection{分析}
-首先想到的是递归(即深搜),对两个string进行分割,然后比较四对字符串。代码虽然简单,但是复杂度比较高。有两种加速策略,一种是剪枝,提前返回;一种是加缓存,缓存中间结果,即memorization(翻译为记忆化搜索)。
+首先想到的是递归(即深搜),对两个string进行分割,然后比较四对字符串。代码虽然简单,但是复杂度比较高。有两种加速策略,一种是剪枝,提前返回;一种是加缓存,缓存中间结果,即memoization(翻译为记忆化搜索)。
剪枝可以五花八门,要充分观察,充分利用信息,找到能让节点提前返回的条件。例如,判断两个字符串是否互为scamble,至少要求每个字符在两个字符串中出现的次数要相等,如果不相等则返回false。
@@ -528,12 +530,12 @@ \subsubsection{分析}
\subsubsection{递归}
\begin{Code}
-// LeetCode, Interleaving String
+// LeetCode, Scramble String
// 递归,会超时,仅用来帮助理解
// 时间复杂度O(n^6),空间复杂度O(1)
class Solution {
public:
- bool isScramble(string s1, string s2) {
+ bool isScramble(const string& s1, const string& s2) {
return isScramble(s1.begin(), s1.end(), s2.begin());
}
private:
@@ -559,11 +561,11 @@ \subsubsection{递归}
\subsubsection{动规}
\begin{Code}
-// LeetCode, Interleaving String
+// LeetCode, Scramble String
// 动规,时间复杂度O(n^3),空间复杂度O(n^3)
class Solution {
public:
- bool isScramble(string s1, string s2) {
+ bool isScramble(const string& s1, const string& s2) {
const int N = s1.size();
if (N != s2.size()) return false;
@@ -597,16 +599,16 @@ \subsubsection{动规}
\subsubsection{递归+剪枝}
\begin{Code}
-// LeetCode, Interleaving String
+// LeetCode, Scramble String
// 递归+剪枝
// 时间复杂度O(n^6),空间复杂度O(1)
class Solution {
public:
- bool isScramble(string s1, string s2) {
+ bool isScramble(const string& s1, const string& s2) {
return isScramble(s1.begin(), s1.end(), s2.begin());
}
private:
- typedef string::iterator Iterator;
+ typedef string::const_iterator Iterator;
bool isScramble(Iterator first1, Iterator last1, Iterator first2) {
auto length = distance(first1, last1);
auto last2 = next(first2, length);
@@ -634,12 +636,12 @@ \subsubsection{递归+剪枝}
\subsubsection{备忘录法}
\begin{Code}
-// LeetCode, Interleaving String
+// LeetCode, Scramble String
// 递归+map做cache
-// 时间复杂度O(n^3),空间复杂度O(n^3)
+// 时间复杂度O(n^3),空间复杂度O(n^3), TLE
class Solution {
public:
- bool isScramble(string s1, string s2) {
+ bool isScramble(const string& s1, const string& s2) {
cache.clear();
return isScramble(s1.begin(), s1.end(), s2.begin());
}
@@ -694,14 +696,14 @@ \subsubsection{备忘录法}
};
}
-// LeetCode, Interleaving String
+// LeetCode, Scramble String
// 递归+unordered_map做cache,比map快
// 时间复杂度O(n^3),空间复杂度O(n^3)
class Solution {
public:
unordered_map cache;
- bool isScramble(string s1, string s2) {
+ bool isScramble(const string& s1, const string& s2) {
cache.clear();
return isScramble(s1.begin(), s1.end(), s2.begin());
}
diff --git a/C++/chapGreedy.tex b/C++/chapGreedy.tex
index 86b67e3a..2e098bea 100644
--- a/C++/chapGreedy.tex
+++ b/C++/chapGreedy.tex
@@ -38,11 +38,11 @@ \subsubsection{代码1}
// 思路1,时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
- bool canJump(int A[], int n) {
+ bool canJump(const vector& nums) {
int reach = 1; // 最右能跳到哪里
- for (int i = 0; i < reach && reach < n; ++i)
- reach = max(reach, i + 1 + A[i]);
- return reach >= n;
+ for (int i = 0; i < reach && reach < nums.size(); ++i)
+ reach = max(reach, i + 1 + nums[i]);
+ return reach >= nums.size();
}
};
\end{Code}
@@ -54,13 +54,13 @@ \subsubsection{代码2}
// 思路2,时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
- bool canJump (int A[], int n) {
- if (n == 0) return true;
+ bool canJump (const vector& nums) {
+ if (nums.empty()) return true;
// 逆向下楼梯,最左能下降到第几层
- int left_most = n - 1;
+ int left_most = nums.size() - 1;
- for (int i = n - 2; i >= 0; --i)
- if (i + A[i] >= left_most)
+ for (int i = nums.size() - 2; i >= 0; --i)
+ if (i + nums[i] >= left_most)
left_most = i;
return left_most == 0;
@@ -75,14 +75,14 @@ \subsubsection{代码3}
// 思路三,动规,时间复杂度O(n),空间复杂度O(n)
class Solution {
public:
- bool canJump(int A[], int n) {
- vector f(n, 0);
+ bool canJump(const vector& nums) {
+ vector f(nums.size(), 0);
f[0] = 0;
- for (int i = 1; i < n; i++) {
- f[i] = max(f[i - 1], A[i - 1]) - 1;
+ for (int i = 1; i < nums.size(); i++) {
+ f[i] = max(f[i - 1], nums[i - 1]) - 1;
if (f[i] < 0) return false;;
}
- return f[n - 1] >= 0;
+ return f[nums.size() - 1] >= 0;
}
};
\end{Code}
@@ -121,18 +121,18 @@ \subsubsection{代码1}
// 时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
- int jump(int A[], int n) {
+ int jump(const vector& nums) {
int step = 0; // 最小步数
int left = 0;
int right = 0; // [left, right]是当前能覆盖的区间
- if (n == 1) return 0;
+ if (nums.size() == 1) return 0;
while (left <= right) { // 尝试从每一层跳最远
++step;
const int old_right = right;
for (int i = left; i <= old_right; ++i) {
- int new_right = i + A[i];
- if (new_right >= n - 1) return step;
+ int new_right = i + nums[i];
+ if (new_right >= nums.size() - 1) return step;
if (new_right > right) right = new_right;
}
@@ -150,18 +150,18 @@ \subsubsection{代码2}
// 时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
- int jump(int A[], int n) {
+ int jump(const vector& nums) {
int result = 0;
// the maximum distance that has been reached
int last = 0;
// the maximum distance that can be reached by using "ret+1" steps
int cur = 0;
- for (int i = 0; i < n; ++i) {
+ for (int i = 0; i < nums.size(); ++i) {
if (i > last) {
last = cur;
++result;
}
- cur = max(cur, i + A[i]);
+ cur = max(cur, i + nums[i]);
}
return result;
@@ -282,21 +282,22 @@ \subsubsection{代码}
\begin{Code}
// LeetCode, Longest Substring Without Repeating Characters
// 时间复杂度O(n),空间复杂度O(1)
+// 考虑非字母的情况
class Solution {
public:
int lengthOfLongestSubstring(string s) {
- const int ASCII_MAX = 26;
+ const int ASCII_MAX = 255;
int last[ASCII_MAX]; // 记录字符上次出现过的位置
int start = 0; // 记录当前子串的起始位置
fill(last, last + ASCII_MAX, -1); // 0也是有效位置,因此初始化为-1
int max_len = 0;
for (int i = 0; i < s.size(); i++) {
- if (last[s[i] - 'a'] >= start) {
+ if (last[s[i]] >= start) {
max_len = max(i - start, max_len);
- start = last[s[i] - 'a'] + 1;
+ start = last[s[i]] + 1;
}
- last[s[i] - 'a'] = i;
+ last[s[i]] = i;
}
return max((int)s.size() - start, max_len); // 别忘了最后一次,例如"abcd"
}
diff --git a/C++/chapImplement.tex b/C++/chapImplement.tex
index 6c0aeab8..40b88a2b 100644
--- a/C++/chapImplement.tex
+++ b/C++/chapImplement.tex
@@ -33,15 +33,26 @@ \subsubsection{代码}
\begin{Code}
//LeetCode, Reverse Integer
// 时间复杂度O(logn),空间复杂度O(1)
+// 考虑 1.负数的情况 2. 溢出的情况(正溢出&&负溢出,比如 x = -2147483648(即-2^31) )
class Solution {
public:
int reverse (int x) {
- int r = 0;
-
- for (; x; x /= 10)
- r = r * 10 + x % 10;
-
- return r;
+ long long r = 0;
+ long long t = x;
+ t = t > 0 ? t : -t;
+ for (; t; t /= 10)
+ r = r * 10 + t % 10;
+
+ bool sign = x > 0 ? false: true;
+ if (r > 2147483647 || (sign && r > 2147483648)) {
+ return 0;
+ } else {
+ if (sign) {
+ return -r;
+ } else {
+ return r;
+ }
+ }
}
};
\end{Code}
@@ -545,7 +556,7 @@ \subsubsection{相关题目}
\section{Pascal's Triangle} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-\label{sec:pascals-triangle}
+\label{sec:pascal-s-triangle}
\subsubsection{描述}
@@ -627,7 +638,7 @@ \subsubsection{相关题目}
\section{Pascal's Triangle II} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-\label{sec:pascals-triangle-ii}
+\label{sec:pascal-s-triangle-ii}
\subsubsection{描述}
@@ -1087,7 +1098,7 @@ \subsubsection{相关题目}
\section{Max Points on a Line} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-\label{sec:Max-Points-on-a-Line}
+\label{sec:max-points-on-a-line}
\subsubsection{描述}
diff --git a/C++/chapLinearList.tex b/C++/chapLinearList.tex
index c23c4538..bbc0ec1c 100644
--- a/C++/chapLinearList.tex
+++ b/C++/chapLinearList.tex
@@ -29,13 +29,13 @@ \subsubsection{代码1}
// 时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
- int removeDuplicates(int A[], int n) {
- if (n == 0) return 0;
+ int removeDuplicates(vector& nums) {
+ if (nums.empty()) return 0;
int index = 0;
- for (int i = 1; i < n; i++) {
- if (A[index] != A[i])
- A[++index] = A[i];
+ for (int i = 1; i < nums.size(); i++) {
+ if (nums[index] != nums[i])
+ nums[++index] = nums[i];
}
return index + 1;
}
@@ -49,8 +49,8 @@ \subsubsection{代码2}
// 使用STL,时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
- int removeDuplicates(int A[], int n) {
- return distance(A, unique(A, A + n));
+ int removeDuplicates(vector& nums) {
+ return distance(nums.begin(), unique(nums.begin(), nums.end()));
}
};
\end{Code}
@@ -62,8 +62,8 @@ \subsubsection{代码3}
// 使用STL,时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
- int removeDuplicates(int A[], int n) {
- return removeDuplicates(A, A + n, A) - A;
+ int removeDuplicates(vector& nums) {
+ return distance(nums.begin(), removeDuplicates(nums.begin(), nums.end(), nums.begin()));
}
template
@@ -111,13 +111,13 @@ \subsubsection{代码1}
// @author hex108 (https://github.com/hex108)
class Solution {
public:
- int removeDuplicates(int A[], int n) {
- if (n <= 2) return n;
+ int removeDuplicates(vector& nums) {
+ if (nums.size() <= 2) return nums.size();
int index = 2;
- for (int i = 2; i < n; i++){
- if (A[i] != A[index - 2])
- A[index++] = A[i];
+ for (int i = 2; i < nums.size(); i++){
+ if (nums[i] != nums[index - 2])
+ nums[index++] = nums[i];
}
return index;
@@ -134,13 +134,14 @@ \subsubsection{代码2}
// 时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
- int removeDuplicates(int A[], int n) {
+ int removeDuplicates(vector& nums) {
+ const int n = nums.size();
int index = 0;
for (int i = 0; i < n; ++i) {
- if (i > 0 && i < n - 1 && A[i] == A[i - 1] && A[i] == A[i + 1])
+ if (i > 0 && i < n - 1 && nums[i] == nums[i - 1] && nums[i] == nums[i + 1])
continue;
- A[index++] = A[i];
+ nums[index++] = nums[i];
}
return index;
}
@@ -179,19 +180,19 @@ \subsubsection{代码}
// 时间复杂度O(log n),空间复杂度O(1)
class Solution {
public:
- int search(int A[], int n, int target) {
- int first = 0, last = n;
+ int search(const vector& nums, int target) {
+ int first = 0, last = nums.size();
while (first != last) {
const int mid = first + (last - first) / 2;
- if (A[mid] == target)
+ if (nums[mid] == target)
return mid;
- if (A[first] <= A[mid]) {
- if (A[first] <= target && target < A[mid])
+ if (nums[first] <= nums[mid]) {
+ if (nums[first] <= target && target < nums[mid])
last = mid;
else
first = mid + 1;
} else {
- if (A[mid] < target && target <= A[last-1])
+ if (nums[mid] < target && target <= nums[last-1])
first = mid + 1;
else
last = mid;
@@ -237,19 +238,19 @@ \subsubsection{代码}
// 时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
- bool search(int A[], int n, int target) {
- int first = 0, last = n;
+ bool search(const vector& nums, int target) {
+ int first = 0, last = nums.size();
while (first != last) {
const int mid = first + (last - first) / 2;
- if (A[mid] == target)
+ if (nums[mid] == target)
return true;
- if (A[first] < A[mid]) {
- if (A[first] <= target && target < A[mid])
+ if (nums[first] < nums[mid]) {
+ if (nums[first] <= target && target < nums[mid])
last = mid;
else
first = mid + 1;
- } else if (A[first] > A[mid]) {
- if (A[mid] < target && target <= A[last-1])
+ } else if (nums[first] > nums[mid]) {
+ if (nums[mid] < target && target <= nums[last-1])
first = mid + 1;
else
last = mid;
@@ -283,7 +284,7 @@ \subsubsection{分析}
$O(m+n)$的解法比较直观,直接merge两个数组,然后求第$k$大的元素。
-不过我们仅仅需要第$k$大的元素,是不需要“排序”这么复杂的操作的。可以用一个计数器,记录当前已经找到第$m$大的元素了。同时我们使用两个指针\fn{pA}和\fn{pB},分别指向A和B数组的第一个元素,使用类似于merge sort的原理,如果数组A当前元素小,那么\fn{pA++},同时\fn{m++};如果数组B当前元素小,那么\fn{pB++},同时\fn{m++}。最终当$m$等于$k$的时候,就得到了我们的答案,$O(k)$时间,$O(1)$空间。但是,当$k$很接近$m+n$的时候,这个方法还是$O(m+n)$的。
+不过我们仅仅需要第$k$大的元素,是不需要“排序”这么昂贵的操作的。可以用一个计数器,记录当前已经找到第$m$大的元素了。同时我们使用两个指针\fn{pA}和\fn{pB},分别指向A和B数组的第一个元素,使用类似于merge sort的原理,如果数组A当前元素小,那么\fn{pA++},同时\fn{m++};如果数组B当前元素小,那么\fn{pB++},同时\fn{m++}。最终当$m$等于$k$的时候,就得到了我们的答案,$O(k)$时间,$O(1)$空间。但是,当$k$很接近$m+n$的时候,这个方法还是$O(m+n)$的。
有没有更好的方案呢?我们可以考虑从$k$入手。如果我们每次都能够删除一个一定在第$k$大元素之前的元素,那么我们需要进行$k$次。但是如果每次我们都删除一半呢?由于A和B都是有序的,我们应该充分利用这里面的信息,类似于二分查找,也是充分利用了“有序”。
@@ -294,7 +295,7 @@ \subsubsection{分析}
\item \fn{A[k/2-1] < B[k/2-1]}
\myenddot
-如果\fn{A[k/2-1] < B[k/2-1]},意味着\fn{A[0]}到\fn{A[k/2-1}的肯定在$A \cup B$的top k元素的范围内,换句话说,\fn{A[k/2-1}不可能大于$A \cup B$的第$k$大元素。留给读者证明。
+如果\fn{A[k/2-1] < B[k/2-1]},意味着\fn{A[0]}到\fn{A[k/2-1]}的肯定在$A \cup B$的top k元素的范围内,换句话说,\fn{A[k/2-1]}不可能大于$A \cup B$的第$k$大元素。留给读者证明。
因此,我们可以放心的删除A数组的这$k/2$个元素。同理,当\fn{A[k/2-1] > B[k/2-1]}时,可以删除B数组的$k/2$个元素。
@@ -314,26 +315,29 @@ \subsubsection{代码}
// 时间复杂度O(log(m+n)),空间复杂度O(log(m+n))
class Solution {
public:
- double findMedianSortedArrays(int A[], int m, int B[], int n) {
+ double findMedianSortedArrays(const vector& A, const vector& B) {
+ const int m = A.size();
+ const int n = B.size();
int total = m + n;
if (total & 0x1)
- return find_kth(A, m, B, n, total / 2 + 1);
+ return find_kth(A.begin(), m, B.begin(), n, total / 2 + 1);
else
- return (find_kth(A, m, B, n, total / 2)
- + find_kth(A, m, B, n, total / 2 + 1)) / 2.0;
+ return (find_kth(A.begin(), m, B.begin(), n, total / 2)
+ + find_kth(A.begin(), m, B.begin(), n, total / 2 + 1)) / 2.0;
}
private:
- static int find_kth(int A[], int m, int B[], int n, int k) {
+ static int find_kth(std::vector::const_iterator A, int m,
+ std::vector::const_iterator B, int n, int k) {
//always assume that m is equal or smaller than n
if (m > n) return find_kth(B, n, A, m, k);
- if (m == 0) return B[k - 1];
- if (k == 1) return min(A[0], B[0]);
+ if (m == 0) return *(B + k - 1);
+ if (k == 1) return min(*A, *B);
//divide k into two parts
int ia = min(k / 2, m), ib = k - ia;
- if (A[ia - 1] < B[ib - 1])
+ if (*(A + ia - 1) < *(B + ib - 1))
return find_kth(A + ia, m - ia, B, n, k - ia);
- else if (A[ia - 1] > B[ib - 1])
+ else if (*(A + ia - 1) > *(B + ib - 1))
return find_kth(A, m, B + ib, n - ib, k - ib);
else
return A[ia - 1];
@@ -377,14 +381,14 @@ \subsubsection{代码}
// 时间复杂度O(n),空间复杂度O(n)
class Solution {
public:
- int longestConsecutive(const vector &num) {
+ int longestConsecutive(const vector &nums) {
unordered_map used;
- for (auto i : num) used[i] = false;
+ for (auto i : nums) used[i] = false;
int longest = 0;
- for (auto i : num) {
+ for (auto i : nums) {
if (used[i]) continue;
int length = 1;
@@ -422,18 +426,18 @@ \subsubsection{代码}
// Author: @advancedxy
class Solution {
public:
- int longestConsecutive(vector &num) {
+ int longestConsecutive(vector &nums) {
unordered_map map;
- int size = num.size();
+ int size = nums.size();
int l = 1;
for (int i = 0; i < size; i++) {
- if (map.find(num[i]) != map.end()) continue;
- map[num[i]] = 1;
- if (map.find(num[i] - 1) != map.end()) {
- l = max(l, mergeCluster(map, num[i] - 1, num[i]));
+ if (map.find(nums[i]) != map.end()) continue;
+ map[nums[i]] = 1;
+ if (map.find(nums[i] - 1) != map.end()) {
+ l = max(l, mergeCluster(map, nums[i] - 1, nums[i]));
}
- if (map.find(num[i] + 1) != map.end()) {
- l = max(l, mergeCluster(map, num[i], num[i] + 1));
+ if (map.find(nums[i] + 1) != map.end()) {
+ l = max(l, mergeCluster(map, nums[i], nums[i] + 1));
}
}
return size == 0 ? 0 : l;
@@ -488,14 +492,14 @@ \subsubsection{代码}
// 时间复杂度O(n),空间复杂度O(n)
class Solution {
public:
- vector twoSum(vector &num, int target) {
+ vector twoSum(vector &nums, int target) {
unordered_map mapping;
vector result;
- for (int i = 0; i < num.size(); i++) {
- mapping[num[i]] = i;
+ for (int i = 0; i < nums.size(); i++) {
+ mapping[nums[i]] = i;
}
- for (int i = 0; i < num.size(); i++) {
- const int gap = target - num[i];
+ for (int i = 0; i < nums.size(); i++) {
+ const int gap = target - nums[i];
if (mapping.find(gap) != mapping.end() && mapping[gap] > i) {
result.push_back(i + 1);
result.push_back(mapping[gap] + 1);
@@ -550,16 +554,16 @@ \subsubsection{代码}
// 先排序,然后左右夹逼,注意跳过重复的数,时间复杂度O(n^2),空间复杂度O(1)
class Solution {
public:
- vector> threeSum(vector& num) {
+ vector> threeSum(vector& nums) {
vector> result;
- if (num.size() < 3) return result;
- sort(num.begin(), num.end());
+ if (nums.size() < 3) return result;
+ sort(nums.begin(), nums.end());
const int target = 0;
- auto last = num.end();
- for (auto i = num.begin(); i < last-2; ++i) {
+ auto last = nums.end();
+ for (auto i = nums.begin(); i < last-2; ++i) {
auto j = i+1;
- if (i > num.begin() && *i == *(i-1)) continue;
+ if (i > nums.begin() && *i == *(i-1)) continue;
auto k = last-1;
while (j < k) {
if (*i + *j + *k < target) {
@@ -611,15 +615,15 @@ \subsubsection{代码}
// 先排序,然后左右夹逼,时间复杂度O(n^2),空间复杂度O(1)
class Solution {
public:
- int threeSumClosest(vector& num, int target) {
+ int threeSumClosest(vector