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++/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++/chapDFS.tex b/C++/chapDFS.tex index 2c9f0ab8..f6c4c107 100644 --- a/C++/chapDFS.tex +++ b/C++/chapDFS.tex @@ -1248,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++/chapDynamicProgramming.tex b/C++/chapDynamicProgramming.tex index 93a655d5..5327de51 100644 --- a/C++/chapDynamicProgramming.tex +++ b/C++/chapDynamicProgramming.tex @@ -514,7 +514,7 @@ \subsubsection{描述} \subsubsection{分析} -首先想到的是递归(即深搜),对两个string进行分割,然后比较四对字符串。代码虽然简单,但是复杂度比较高。有两种加速策略,一种是剪枝,提前返回;一种是加缓存,缓存中间结果,即memorization(翻译为记忆化搜索)。 +首先想到的是递归(即深搜),对两个string进行分割,然后比较四对字符串。代码虽然简单,但是复杂度比较高。有两种加速策略,一种是剪枝,提前返回;一种是加缓存,缓存中间结果,即memoization(翻译为记忆化搜索)。 剪枝可以五花八门,要充分观察,充分利用信息,找到能让节点提前返回的条件。例如,判断两个字符串是否互为scamble,至少要求每个字符在两个字符串中出现的次数要相等,如果不相等则返回false。 diff --git a/C++/chapImplement.tex b/C++/chapImplement.tex index 28c53f61..40b88a2b 100644 --- a/C++/chapImplement.tex +++ b/C++/chapImplement.tex @@ -556,7 +556,7 @@ \subsubsection{相关题目} \section{Pascal's Triangle} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -\label{sec:pascals-triangle} +\label{sec:pascal-s-triangle} \subsubsection{描述} @@ -638,7 +638,7 @@ \subsubsection{相关题目} \section{Pascal's Triangle II} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -\label{sec:pascals-triangle-ii} +\label{sec:pascal-s-triangle-ii} \subsubsection{描述} @@ -1098,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 76ac9624..bbc0ec1c 100644 --- a/C++/chapLinearList.tex +++ b/C++/chapLinearList.tex @@ -2666,7 +2666,7 @@ \subsubsection{相关题目} \subsection{Linked List Cycle} -\label{sec:Linked-List-Cycle} +\label{sec:linked-list-cycle} \subsubsection{描述} @@ -2709,7 +2709,7 @@ \subsubsection{相关题目} \subsection{Linked List Cycle II} -\label{sec:Linked-List-Cycle-II} +\label{sec:linked-list-cycle-ii} \subsubsection{描述} @@ -2769,7 +2769,7 @@ \subsubsection{相关题目} \subsection{Reorder List} -\label{sec:Reorder-List} +\label{sec:reorder-list} \subsubsection{描述} @@ -2841,7 +2841,7 @@ \subsubsection{相关题目} \subsection{LRU Cache} -\label{sec:LRU-Cachet} +\label{sec:lru-cache} \subsubsection{描述} diff --git a/C++/chapSorting.tex b/C++/chapSorting.tex index 39c31fd3..f92c115a 100644 --- a/C++/chapSorting.tex +++ b/C++/chapSorting.tex @@ -1,7 +1,7 @@ \chapter{排序} -\section{Merge Sorted Array} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -\label{sec:merge-sorted-array} +\section{Merge Two Sorted Arrays} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +\label{sec:merge-two-sorted-arrays} \subsubsection{描述} @@ -98,34 +98,38 @@ \subsubsection{代码} \begin{Code} //LeetCode, Merge k Sorted Lists // 时间复杂度O(n1+n2+...),空间复杂度O(1) -// TODO: 会超时 class Solution { public: - ListNode *mergeKLists(vector &lists) { - if (lists.size() == 0) return nullptr; - ListNode *p = lists[0]; - for (int i = 1; i < lists.size(); i++) { - p = mergeTwoLists(p, lists[i]); + ListNode * mergeTwo(ListNode * l1, ListNode * l2){ + if(!l1) return l2; + if(!l2) return l1; + ListNode dummy(-1); + ListNode * p = &dummy; + for(; l1 && l2; p = p->next){ + if(l1->val > l2->val){ + p->next = l2; l2 = l2->next; + } + else{ + p->next = l1; l1 = l1->next; + } } - return p; + p->next = l1 ? l1 : l2; + return dummy.next; } - // Merge Two Sorted Lists - ListNode *mergeTwoLists(ListNode *l1, ListNode *l2) { - ListNode head(-1); - for (ListNode* p = &head; l1 != nullptr || l2 != nullptr; p = p->next) { - int val1 = l1 == nullptr ? INT_MAX : l1->val; - int val2 = l2 == nullptr ? INT_MAX : l2->val; - if (val1 <= val2) { - p->next = l1; - l1 = l1->next; - } else { - p->next = l2; - l2 = l2->next; - } + ListNode* mergeKLists(vector& lists) { + if(lists.size() == 0) return nullptr; + + // multi pass + deque dq(lists.begin(), lists.end()); + while(dq.size() > 1){ + ListNode * first = dq.front(); dq.pop_front(); + ListNode * second = dq.front(); dq.pop_front(); + dq.push_back(mergeTwo(first,second)); } - return head.next; + + return dq.front(); } }; \end{Code} @@ -139,7 +143,7 @@ \subsubsection{相关题目} \section{Insertion Sort List} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -\label{sec:Insertion-Sort-List} +\label{sec:insertion-sort-list} \subsubsection{描述} @@ -188,7 +192,7 @@ \subsubsection{相关题目} \section{Sort List} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -\label{sec:Sort-List} +\label{sec:sort-list} \subsubsection{描述} @@ -276,7 +280,7 @@ \subsubsection{代码} public: int firstMissingPositive(vector& nums) { bucket_sort(nums); - + for (int i = 0; i < nums.size(); ++i) if (nums[i] != (i + 1)) return i + 1; @@ -385,7 +389,7 @@ \subsubsection{代码3} class Solution { public: void sortColors(vector& nums) { - partition(partition(nums.begin(), nums.end(), bind1st(equal_to(), 0)), + partition(partition(nums.begin(), nums.end(), bind1st(equal_to(), 0)), nums.end(), bind1st(equal_to(), 1)); } }; diff --git a/C++/chapStackAndQueue.tex b/C++/chapStackAndQueue.tex index fd130ee0..9dacb601 100644 --- a/C++/chapStackAndQueue.tex +++ b/C++/chapStackAndQueue.tex @@ -242,7 +242,7 @@ \subsubsection{相关题目} \subsection{Evaluate Reverse Polish Notation} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -\label{sec:Evaluate-Reverse-Polish-Notation} +\label{sec:evaluate-reverse-polish-notation} \subsubsection{描述} diff --git a/C++/chapTree.tex b/C++/chapTree.tex index 5453fb71..7e70c7b5 100644 --- a/C++/chapTree.tex +++ b/C++/chapTree.tex @@ -368,7 +368,7 @@ \subsubsection{相关题目} \subsection{Binary Tree Level Order Traversal} -\label{sec:binary-tree-tevel-order-traversal} +\label{sec:binary-tree-level-order-traversal} \subsubsection{描述} @@ -623,8 +623,8 @@ \subsubsection{递归版} \subsubsection{迭代版} \begin{Code} -//LeetCode, Binary Tree Zigzag Level Order Traversal -//广度优先遍历,用一个bool记录是从左到右还是从右到左,每一层结束就翻转一下。 +// LeetCode, Binary Tree Zigzag Level Order Traversal +// 广度优先遍历,用一个bool记录是从左到右还是从右到左,每一层结束就翻转一下。 // 迭代版,时间复杂度O(n),空间复杂度O(n) class Solution { public: @@ -1447,15 +1447,15 @@ \subsubsection{分析} \subsubsection{代码} \begin{Code} -// LeetCode, Validate Binary Search Tree +// Validate Binary Search Tree // 时间复杂度O(n),空间复杂度O(\logn) class Solution { public: bool isValidBST(TreeNode* root) { - return isValidBST(root, INT_MIN, INT_MAX); + return isValidBST(root, LONG_MIN, LONG_MAX); } - bool isValidBST(TreeNode* root, int lower, int upper) { + bool isValidBST(TreeNode* root, long long lower, long long upper) { if (root == nullptr) return true; return root->val > lower && root->val < upper diff --git a/C++/leetcode-cpp.pdf b/C++/leetcode-cpp.pdf index 69f93f66..72d3da55 100644 Binary files a/C++/leetcode-cpp.pdf and b/C++/leetcode-cpp.pdf differ diff --git a/Java/README.md b/Java/README.md index 8886a0b4..73032b56 100644 --- a/Java/README.md +++ b/Java/README.md @@ -1,3 +1,7 @@ #Java版 ----------------- -书的内容与C++版一摸一样,不过代码是用Java写的。本书的代码要求 Java 6 以上。 + +## 编译 + + docker pull soulmachine/texlive + docker run -it --rm -v $(pwd):/data -w /data soulmachine/texlive-full xelatex -synctex=1 -interaction=nonstopmode leetcode-java.tex diff --git a/README.md b/README.md index 21fce47a..9af9ce21 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,41 @@ -#LeetCode题解 ------------------ -##PDF下载 -LeetCode题解(C++版).pdf - -C++ 文件夹下是C++版,内容一模一样,代码是用C++写的。 - -Java 文件夹下是Java版,目前正在编写中,由于拖延症,不知道猴年马月能完成。 - -##LaTeX模板 -本书使用的是陈硕开源的[模板](https://github.com/chenshuo/typeset)。这个模板制作精良,很有taste,感谢陈硕 :) +# LeetCode题解 -##在Windows下编译 -1. 安装Tex Live 2013 。把bin目录例如`D:\texlive\2013\bin\win32`加入PATH环境变量。 -1. 安装字体。这个LaTex模板总共使用了9个字体,下载地址 ,有的字体Windows自带了,有的字体Ubuntu自带了,但都不全,还是一次性安装完所有字体比较方便。 -1. 安装TeXstudio -1. (可选)启动Tex Live Manager,更新所有已安装的软件包。 -1. 配置TeXstudio。 +## 在线阅读 - 启动Texstudio,选择 `Options-->Configure Texstudio-->Commands`,XeLaTex 设置为 `xelatex -synctex=1 -interaction=nonstopmode %.tex`; + - 选择 `Options-->Configure Texstudio-->Build` +## PDF下载 - Build & View 由默认的 PDF Chain 改为 Compile & View; - - Default Compiler 由默认的PdfLaTex 修改为 XeLaTex ; - - PDF Viewer 改为 “Internal PDF Viewer(windowed)”,这样预览时会弹出一个独立的窗口,这样比较方便。 - -1. 编译。用TeXstudio打开`leetcode-cpp.tex`,点击界面上的绿色箭头就可以开始编译了。 - - 在下方的窗口可以看到TeXstudio正在使用的编译命令是`xelatex -synctex=1 -interaction=nonstopmode "leetcode-cpp".tex` +LeetCode题解(C++版).pdf -##在Ubuntu下编译 -1. 安装Tex Live 2013 - - 1.1. 下载TexLive 2013 的ISO 光盘,地址 +C++ 文件夹下是C++版,内容一模一样,代码是用C++写的。 - 1.2 mount 光盘,`sudo ./install-tl` 开始安装 +Java 文件夹下是Java版,目前正在编写中,由于拖延症,不知道猴年马月能完成。 - 1.3 加入环境变量 +## 如何编译PDF - sudo vi /etc/profile - export PATH=$PATH:/usr/local/texlive/2013/bin/x86_64-linux - export MANPATH=$MANPATH:/usr/local/texlive/2013/texmf-dist/doc/man - export INFPATH=$INFPATH:/usr/local/texlive/2013/texmf-dist/doc/info +### 命令行编译 -1. 安装字体。这个LaTex模板总共使用了9个字体,下载地址 ,有的字体Windows自带了,有的字体Ubuntu自带了,但都不全,还是一次性安装完所有字体比较方便。 -1. 安装TeXstudio -1. 配置TeXstudio。 +```bash +docker run -it --rm -v $(pwd)/C++:/project -w /project soulmachine/texlive xelatex -interaction=nonstopmode leetcode-cpp.tex +``` - 启动Texstudio,选择 `Options-->Configure Texstudio-->Commands`,XeLaTex 设置为 `xelatex -synctex=1 -interaction=nonstopmode %.tex`; +### vscode下编译 - 选择 `Options-->Configure Texstudio-->Build` +本项目已经配置好了vscode devcontainer, 可以在 Windows, Linux 和 macOS 三大平台上编译。 - Build & View 由默认的 PDF Chain 改为 Compile & View; +用 vscode 打开本项目,选择右下角弹出的 `"Reopen in Container"`,就会在容器中打开本项目,该容器安装了 Tex Live 2022 以及所需要的10个字体。 - Default Compiler 由默认的PdfLaTex 修改为 XeLaTex ; +点击vscode左下角的齿轮图标,选择 `Command Palette`,输入`tasks`, 选择 `Run Task`, 选择 `leetcode-C++`,即可启动编译。 - PDF Viewer 改为 “Internal PDF Viewer(windowed)”,这样预览时会弹出一个独立的窗口,这样比较方便。 +## LaTeX模板 -1. 编译。用TeXstudio打开`leetcode-cpp.tex`,点击界面上的绿色箭头就可以开始编译了。 +本书使用的是陈硕开源的[模板](https://github.com/chenshuo/typeset)。这个模板制作精良,感谢陈硕 :) - 在下方的窗口可以看到TeXstudio正在使用的编译命令是`xelatex -synctex=1 -interaction=nonstopmode "leetcode-cpp".tex` -1. 懒人版镜像。如果不想进行上面繁琐的安装过程,我做好了一个Ubuntu VMware虚拟机镜像,已经装好了TexLive 2013, TexStudio和字体(详细的安装日志见压缩包注释),开箱即用,下载地址 。 +这个LaTex模板总共使用了10个字体,下载地址 。有的字体Windows自带了,有的字体Ubuntu自带了,但都不全,还是一次性安装完所有字体比较方便。 -##如何贡献代码 -编译通过后,就具备了完整的LaTeX编译环境了。 +也可以参考 [Dockerfile](https://github.com/soulmachine/docker-images/blob/master/texlive/Dockerfile) 去学习如何安装所有字体。 -本书模板已经写好了,基本上不需要很多LaTeX知识就可以动手了。 +## 贡献代码 欢迎给本书添加内容或纠正错误,在自己本地编译成PDF,预览没问题后,就可以发pull request过来了。 - -##北美求职QQ群 - -237669375 - -## 【友情推荐】九章算法 - -1. 算法辅导在线视频直播课程: - - -## AlgoHub - - 是我建立的一个刷题网站,即将上线,敬请期待 - -## 纸质书 -**本书即将由电子工业出版社出版,敬请期待** - diff --git "a/\345\217\202\350\200\203\350\265\204\346\226\231/silicon-job.jpeg" "b/\345\217\202\350\200\203\350\265\204\346\226\231/silicon-job.jpeg" new file mode 100644 index 00000000..dd349c0b Binary files /dev/null and "b/\345\217\202\350\200\203\350\265\204\346\226\231/silicon-job.jpeg" differ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy