文章目录
- 关于回溯
- 37. 解数独(37.sudoku-solver)
- 17. 电话号码的数字组合(17.letter-combinations-of-a-phone-number)
- 51. N皇后(51.n-queens)
- 212. 单词搜索 II(212.word-search-ii)
- 简单的回溯+剪枝(TLE)
- 新方法:字典树Trie+回溯
- 980. 不同路径III
- [写法1]用vector存三维数组(内存超限)
- [写法2]map存三元组(通过)
- 五题小结
关于回溯
回溯跟枚举差不多。要注意“回溯”,别忘记“回”之前把之前的改动都复原。
37. 解数独(37.sudoku-solver)
leetcode37是解数独问题。本题保证有且仅有唯一解。
思路:先把空格子的位置存下来,然后对每一个空位置挨个枚举1-9。枚举之前,先建立一个一维数组,把要排除的数先排除,效率会高些。
class Solution {// 空格的信息int x[100], y[100], cnt = 0;bool dfs(int i, vector<vector<char>>& board) {if (i == cnt) return true;bool s[60] = {false};// 检查行、列for (int j = 0; j < 9; j++) s[board[x[i]][j]] = s[board[j][y[i]]] = true;// 检查九宫格for (int j = x[i] / 3 * 3; j < x[i] / 3 * 3 + 3; j++)for (int k = y[i] / 3 * 3; k < y[i] / 3 * 3 + 3; k++)s[board[j][k]] = true;// 枚举尝试1-9for (char c = '1'; c <= '9'; c++) {if (s[c] == false) {board[x[i]][y[i]] = c;if (dfs(i + 1, board))return true;}}board[x[i]][y[i]] = '.';return false;}public:void solveSudoku(vector<vector<char>>& board) {// 检索空格子for (int i = 0; i < 9; i++) {for (int j = 0; j < 9; j++) {if (board[i][j] == '.') {x[cnt] = i;y[cnt++] = j;}}}dfs(0, board);return;}
};
17. 电话号码的数字组合(17.letter-combinations-of-a-phone-number)
leetcode17是纯纯的枚举问题。
逐位处理那串数字,把记录好的当作参数string alreadyHave。由于这个形参是每递归一下就新开辟一个栈帧,所以这样写不涉及到“改动复原”的事。如果占用空间太大了,就需要把这个参数改为引用,那么就需要“复原”了。
class Solution {vector<string> ans;string d;void dfs(int index, string alreadyHave) // index是待处理下标{if (index == d.length()) {if (alreadyHave != "")ans.push_back(alreadyHave);return;}int num = d[index] - '0', start, end;if (num >= 2 && num <= 7) {start = (num - 2) * 3 + 'a';end = start + 2;}if (num == 7)end++;if (num == 8) {start = 't';end = 'v';}if (num == 9) {start = 'w';end = 'z';}for (int i = start; i <= end; i++) {dfs(index + 1, alreadyHave + (char)(i));}return;}public:vector<string> letterCombinations(string digits) {d = digits;dfs(0, "");return ans;}
};
51. N皇后(51.n-queens)
信息记录的不是棋盘格,而是皇后们的列索引
class Solution {int q[10];int n;vector<vector<string>> ans;void r(int k){if (k == n){// 0~n-1个皇后都摆好了,存答案vector<string> thisans;for (int i=0; i<n; i++){//q[i]个点,1个Q,n-1-q[i]个点string curr = "";int a = q[i];int b = n-1-q[i];while (a--)curr += '.';curr+='Q';while (b--)curr += '.';thisans.push_back(curr);}ans.push_back(thisans);return;}//这个皇后将会在k行、i列for (int i=0; i<n; i++){//依次和之前的每个皇后检查冲突int j;for (j=0; j<k; j++){if (i == q[j] || abs(j-k) == abs(i-q[j])) break;}if (j != k) continue;q[k] = i;r(k+1);}return;}
public:vector<vector<string>> solveNQueens(int n) {this->n = n;r(0);return ans;}
};
212. 单词搜索 II(212.word-search-ii)
简单的回溯+剪枝(TLE)
class Solution {vector<string> ans;int dx[4] = {0,0,1,-1};int dy[4] = {1,-1,0,0};int row, col;bool dfs(vector<vector<char>>& board, vector<string>& words, int wordIndex, int charIndex, int x, int y){if (charIndex == words[wordIndex].length()){ans.push_back(words[wordIndex]);words.erase(words.begin()+ wordIndex);return true;}if (x < 0 || x >= row || y < 0 || y >= col) return false;if (board[x][y] != words[wordIndex][charIndex]) return false;char tmp = board[x][y]; //必须备份board[x][y] = '#';for (int i=0; i<4; i++){int nx = x+dx[i], ny = y+dy[i]; //上下左右if (dfs(board, words, wordIndex, charIndex+1, nx, ny)){board[x][y] = tmp;return true;}}board[x][y] = tmp;return false;}
public:vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {row = board.size();col = board[0].size();for (int i=0; i<row; i++)for (int j=0; j<col; j++)for (int k=0; k<words.size(); )if (!dfs(board, words, k, 0, i, j))k++;return ans;}
};
通过了42/65个。
新方法:字典树Trie+回溯
struct TrieNode {string word = ""; //只有叶节点的word是非空的map<char, TrieNode *> child; //孩子节点的指针
};void insertTrie(TrieNode * root, string word) // 在字典树中插入新单词
{TrieNode * p = root;for (auto c : word) {if (p->child[c] == 0) //当前节点的孩子中没有word的某个字符(即变量c)p->child[c] = new TrieNode(); //新建一片叶子p = p->child[c]; //沿着树的节点从上往下捋}p->word = word; //因为单词表之内没有重复,所以最后一定停在新的叶子上
}class Solution {set<string> tmp_ans; //为了去重vector<string> ans;int dx[4] = {0, 0, 1, -1};int dy[4] = {1, -1, 0, 0};int row, col;bool dfs(vector<vector<char>>& board, int x, int y, TrieNode * root) {if (root->word != "") //走到了叶子{tmp_ans.insert(root->word); //找到,插入到返回值中//return true;}if (x < 0 || x >= row || y < 0 || y >= col)return false;if (root->child[board[x][y]] == 0)return false;// 上下左右继续找char tmp = board[x][y];board[x][y] = '#';for (int i = 0; i < 4; i++) {int nx = x + dx[i], ny = y + dy[i];if (dfs(board, nx, ny, root->child[tmp])) {board[x][y] = tmp;return true;}}board[x][y] = tmp;return false;}public:vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {TrieNode * root = new TrieNode(); //空字典树for (auto & word : words)insertTrie(root, word);row = board.size();col = board[0].size();for (int i = 0; i < row; i++)for (int j = 0; j < col; j++)dfs(board, i, j, root);for (auto & word : tmp_ans)ans.push_back(word);return ans;}
};
虽然还是超时,但可以把测试用例通过了,也就是超时的没那么离谱。可以继续优化。
最终的官方题解和我这个代码是高度相似的,所以针对卡常再修补修补即可通过。
980. 不同路径III
在二维网格 grid 上,有 4 种类型的方格:
1 表示起始方格。且只有一个起始方格。
2 表示结束方格,且只有一个结束方格。
0 表示我们可以走过的空方格。
-1 表示我们无法跨越的障碍。
返回在四个方向(上、下、左、右)上行走时,从起始方格到结束方格的不同路径的数目。
每一个无障碍方格都要通过一次,但是一条路径中不能重复通过同一个方格。
状态压缩、记忆化dfs
用vector存三维数组,内存超限的代码。那么就尝试用map进行优化
[写法1]用vector存三维数组(内存超限)
class Solution {int row, col, dx[4] = {0, 0, 1, -1}, dy[4] = {1, -1, 0, 0};int dfs(int s, int x, int y, vector<vector<int>>& grid, vector<vector<vector<int>>>& f) {if (x < 0 || x >= row || y < 0 || y >= col) return 0;if (f[s][x][y] != -1) return f[s][x][y];int ans = 0;if (grid[x][y] == 2) {// 检查是否每一个无障碍方格都到过int i;for (i = 0; i < col * row; i++)if ((s >> i & 1) == 0 &&grid[i / col][i - i / col * col] == 0) {break;}if (i == col * row)ans = 1;elseans = 0;} else if ((s >> (col * x + y) & 1) == 1 || grid[x][y] == -1) {ans = 0;} else {grid[x][y] = -1;for (int i = 0; i < 4; i++) {int nx = x + dx[i], ny = y + dy[i];ans += dfs(s | 1 << (col * x + y), nx, ny, grid, f);}grid[x][y] = 0;}f[s][x][y] = ans;return ans;}public:int uniquePathsIII(vector<vector<int>>& grid) {row = grid.size();col = grid[0].size();vector<vector<vector<int>>> f(1 << (row * col), vector<vector<int>>(row, vector<int>(col, -1)));for (int i = 0; i < row; i++)for (int j = 0; j < col; j++)if (grid[i][j] == 1) {grid[i][j] = 0; // 为了方便return dfs(0, i, j, grid, f); // i,j是起始点}return -1;}
};
(*  ̄︿ ̄)
[写法2]map存三元组(通过)
class Solution {// 为了节约内存,把f[1<<20][20][20]变为map,缺点是速度变慢map<pair<int, pair<int, int>>, int> f;int row, col, dx[4] = {0, 0, 1, -1}, dy[4] = {1, -1, 0, 0};int dfs(int s, int x, int y, vector<vector<int>>& grid) {if (x < 0 || x >= row || y < 0 || y >= col) return 0;if (f.count(make_pair(s, make_pair(x,y)))) return f[make_pair(s, make_pair(x,y))];int ans = 0;if (grid[x][y] == 2) {// 检查是否每一个无障碍方格都到过int i;for (i = 0; i < col * row; i++)if ((s >> i & 1) == 0 && grid[i / col][i - i / col * col] == 0)break;if (i == col * row) ans = 1;} else if ((s >> (col * x + y) & 1) == 1 || grid[x][y] == -1) {ans = 0;} else {grid[x][y] = -1;for (int i = 0; i < 4; i++) {int nx = x + dx[i], ny = y + dy[i];ans += dfs(s | 1 << (col * x + y), nx, ny, grid);}grid[x][y] = 0;}f[make_pair(s, make_pair(x,y))] = ans;return ans;}public:int uniquePathsIII(vector<vector<int>>& grid) {row = grid.size();col = grid[0].size();for (int i = 0; i < row; i++)for (int j = 0; j < col; j++)if (grid[i][j] == 1) {grid[i][j] = 0; // 为了方便return dfs(0, i, j, grid); // i,j是起始点}return -1;}
};
继续优化:用map实现记忆化时,状态要压缩的彻底。比如本题要记忆的内容实际上是三维的(状态、x、y) 这三个不用写成三元组,一个32位的整型int就能存下来。