在此基于前面的刷题总结一点自己刷题时的思路:
- 算法:框架思路;
- D.S.:对应 数据结构的需要和构建;
- Note:有了大的框架、结构,注意一些细节点:①一方面,去除“变态/极端”的测试题目,②另一方面,针对题目不同点,去写/优化 对应的算法。
前言–综合巩固
一、934.最短的桥
解题思路: 我们可以首先使用搜索到(可用DFS)一个岛屿,然后再利用广度优先搜索,查找当前岛屿与另一个岛屿的最短距离。
学到的点:区分时间用时优化 以及 注意点 以及 小技巧。
1)基础解法:
class Solution
{
public:
/**
* @Description: 岛屿最短距离?首先找到一个岛屿 ; 然后从这个岛屿广搜到下个岛屿。出来最短距离。
* @Param:
* @Return:
* @Notes: 【注意:下方大佬的方法, 比我的快!100ms me:188ms】
* // 也可以不用,因为可以设置为 0;【小技巧】
*/
vector<int> direction = {-1, 0, 1, 0, -1};
int shortestBridge(vector<vector<int>> &A)
{
if (A.empty())
return -1;
int m = A.size(), n = A[0].size();
queue<pair<int, int>> points; // dfs 将第一个岛屿加入到queue中。
vector<vector<bool>> visited(m, vector<bool>(n, false)); // 也可以不用,因为可以设置为 0;【小技巧】
for (int i = 0; i < m; ++i)
{
for (int j = 0; j < n; ++j)
{
// 搜索到第一个1 即break;
if (A[i][j] == 0)
continue;
dfs(A, visited, points, i, j);
break;
}
if (!points.empty())
break;
}
int dis = -1;
// 有了queue,开始bfs
while (!points.empty())
{
int sz = points.size();
for (int j = 0; j < sz; ++j)
{
pair<int, int> p = points.front();
points.pop();
//结束条件 和 剪枝
if (A[p.first][p.second] == 1)
{
// 当前数值有1
return dis;
}
// 邻域入队----除却自身
for (int k = 0; k < 4; ++k)
{
//上下左右邻域入队 且 不能被访问过
int r_new = p.first + direction[k], c_new = p.second + direction[k + 1];
if (r_new >= 0 && r_new < A.size() && c_new >= 0 && c_new < A[0].size() && !visited[r_new][c_new]) // 【小心越界】
{
points.emplace(r_new, c_new);
visited[r_new][c_new] = true;
}
}
}
dis++;
}
return dis;
}
// 深度优先搜索,找到第一个岛屿;联通的岛屿
void dfs(vector<vector<int>> &A, vector<vector<bool>> &visited, queue<pair<int, int>> &points, int r, int c)
{
if (r >= 0 && r < A.size() && c >= 0 && c < A[0].size() && A[r][c]) // && A[r][c]
{
// do
points.emplace(r, c);
A[r][c] = 0;
visited[r][c] = true;
// printf("我当前位置为:[%d, %d]\n", r, c);
for (int k = 0; k < 4; ++k)
{
// 四个方向寻找
dfs(A, visited, points, r + direction[k], c + direction[k + 1]);
}
}
}
};
2)大佬解法:
class Solution
{
public:
vector<int> direction{-1, 0, 1, 0, -1};
// 主函数
int shortestBridge(vector<vector<int>> &grid)
{
int m = grid.size(), n = grid[0].size();
queue<pair<int, int>> points;
// dfs寻找第一个岛屿,并把1全部赋值为2
bool flipped = false;
for (int i = 0; i < m; ++i)
{
if (flipped)
break;
for (int j = 0; j < n; ++j)
{
if (grid[i][j] == 1)
{
dfs(points, grid, m, n, i, j);
flipped = true;
break;
}
}
}
// bfs寻找第二个岛屿,并把过程中经过的0赋值为2
int x, y;
int level = 0;
while (!points.empty())
{
++level;
int n_points = points.size();
while (n_points--)
{
auto [r, c] = points.front();
points.pop();
for (int k = 0; k < 4; ++k)
{
x = r + direction[k], y = c + direction[k + 1];
if (x >= 0 && y >= 0 && x < m && y < n)
{
if (grid[x][y] == 2)
{
continue;
}
if (grid[x][y] == 1)
{
return level;
}
points.push({x, y});
grid[x][y] = 2;
}
}
}
}
return 0;
}
// 辅函数
void dfs(queue<pair<int, int>> &points, vector<vector<int>> &grid, int m, int n, int i, int j)
{
if (i < 0 || j < 0 || i == m || j == n || grid[i][j] == 2)
{
return;
}
if (grid[i][j] == 0)
{
points.push({i, j});
return;
}
grid[i][j] = 2;
dfs(points, grid, m, n, i - 1, j);
dfs(points, grid, m, n, i + 1, j);
dfs(points, grid, m, n, i, j - 1);
dfs(points, grid, m, n, i, j + 1);
}
};
3)仿照解法:
/**
* @Description: 根据题解 修改第二版/最终版
* @Param:
* @Return:
* @Notes: 【通过 设置为2,进行剪枝访问。】
*/
class Solution2
{
public:
/**
* @Description: 岛屿最短距离?首先找到一个岛屿 ; 然后从这个岛屿广搜到下个岛屿。出来最短距离。
* @Param:
* @Return:
* @Notes: 【注意:下方大佬的方法, 比我的快!100ms me:188ms】
*/
vector<int> direction = {-1, 0, 1, 0, -1};
int shortestBridge(vector<vector<int>> &A)
{
if (A.empty())
return -1;
int m = A.size(), n = A[0].size();
queue<pair<int, int>> points; // dfs 将第一个岛屿加入到queue中。
vector<vector<bool>> visited(m, vector<bool>(n, false)); // 也可以不用,因为可以设置为 0;【小技巧】
bool findOneIs = false;
for (int i = 0; i < m; ++i)
{
if(findOneIs) break;
for (int j = 0; j < n; ++j)
{
// 搜索到第一个1 即break;
if (A[i][j]){
dfs(A, visited, points, i, j);
findOneIs = true;
break;
}
}
}
int dis = -1;
// int dis = 0; // 因为周边0的也加上了。
// 有了queue,开始bfs
while (!points.empty())
{
int sz = points.size();
for (int j = 0; j < sz; ++j)
{
pair<int, int> p = points.front();
points.pop();
//结束条件 和 剪枝
if (A[p.first][p.second] == 1)
{
// 当前数值有1
return dis;
}
// 邻域入队----除却自身
for (int k = 0; k < 4; ++k)
{
//上下左右邻域入队 且 不能被访问过
int r_new = p.first + direction[k], c_new = p.second + direction[k + 1];
if (r_new < 0 || r_new >= A.size() || c_new < 0 || c_new >= A[0].size()|| visited[r_new][c_new] || A[r_new][c_new] == 2){
continue;
}
points.emplace(r_new, c_new);
visited[r_new][c_new] = true;
}
}
dis++;
}
return dis;
}
// 深度优先搜索,找到第一个岛屿;联通的岛屿
void dfs(vector<vector<int>> &A, vector<vector<bool>> &visited, queue<pair<int, int>> &points, int r, int c)
{
if (r < 0 || r >= A.size() || c < 0 || c >= A[0].size() || A[r][c]==0 || A[r][c] ==2) // && A[r][c]
return ;
if(A[r][c] == 1){ A[r][c] = 2; }
points.emplace(r, c);
// A[r][c] = 0;
visited[r][c] = true;
// printf("我当前位置为:[%d, %d]\n", r, c);
for (int k = 0; k < 4; ++k)
{
// 四个方向寻找
dfs(A, visited, points, r + direction[k], c + direction[k + 1]);
// if (x < 0 || x >= a.length || y < 0 || y >= a[0].length || visited[x][y] || a[x][y] == 0|| a[x][y] == 2) {
// continue;
// }
}
}
};
4)细致修改提升解法:
①细致到 提前剪枝——能够缩短时间;return
②小技巧, 岛屿换为2 —— 能够提前回溯,去除visited[][]
使用。
③注意点:框架地方可以灵活应变, —— 下方 结束位置
,visited
等可以在当前状态 进行;而不用在下一层提取是使用 —— 又可以加速一点点。【分支深了 平均用时减少的 明显。】
二、WordLadder II(Hard)
注意:本体虽然为
Hard
但是却是常见的考题,没有特别难理解的技巧;完全可以有已有的知识迁移过来。
主要考察:思路编码和南信调试的能力。所以 建议理清思路后,自行编码和调试完成。
思路分析
如果我们从开始的单词,把与之能够转换的单词连起来,它就会长成下边的样子。
橙色表示结束单词,上图橙色的路线就是我们要找的最短路径。所以我们要做的其实就是遍历上边的树,然后判断当前节点是不是结束单词,找到结束单词后,还要判断当前是不是最短的路径。说到遍历当然就是两种思路了,DFS 或者 BFS。
又因为要找到最短路径,所以可以剪枝到一个搜索树到上图前四层
。—— 即使用 BFS 找到这个搜索树。
怎么知道结束单词在哪一层呢?只能一层层的找了,也就是 BFS。此外,因为上图需要搜索的树提前是没有的,我们需要边找边更新这个树。而在 DFS 中,我们也需要这个树,其实就是需要每个节点的所有相邻节点。
所以我们在 BFS 中,就把每个节点的所有相邻节点保存到 HashMap 中,就省去了 DFS 再去找相邻节点的时间。
最后用回溯思想的 DFS找到对应的所有可能即可。
解法一:简单BFS + 回溯【注意其中 & 引用的“问题”,传值 不加会超时!】
思路分析:
- 单词列表、起点、终点构成无向图,容易想到使用广度优先遍历找到最短路径:图中广度优先遍历要使用到;
- 队列;
- 标记是否访问过的「布尔数组」或者「哈希表」visited,这里 key 是字符串,故使用哈希表;
说明:本题可以在 wordList (添加到哈希表以后)上做减法,不过标准的、更可靠的做法是,另外使用一个 visited 哈希表,将访问以后的单词添加到 visited 哈希表中,下面给出的参考代码都采用这种不节约空间的做法。
- 重点:由于要记录所有的路径,广度优先遍历「当前层」到「下一层」的所有路径都得记录下来。因此找到下一层的结点 wordA 以后,不能马上添加到 visited 哈希表里,还需要检查当前队列中未出队的单词是否还能与 wordA 建立联系;
- 广度优先遍历位于同一层的单词,即使有联系,也是不可以被记录下来的,这是因为同一层的连接肯定不是起点到终点的最短路径的边;【如下图所示,但是我们 visited已经解决了这个问题。如我的解法】
- 使用 BFS 的同时记录遍历的路径,形式:哈希表。哈希表的 key 记录了「顶点字符串」,哈希表的值 value 记录了 key 对应的字符串在广度优先遍历的过程中得到的所有后继结点列表 successors;
- 最后根据 successors,使用回溯算法(全程使用一份路径变量搜索所有可能结果的深度优先遍历算法)得到所有的最短路径。
为此,设计算法流程如下:
第 1 步:使用广度优先遍历找到终点单词,并且记录下沿途经过的所有结点,以邻接表形式存储;
第 2 步:通过邻接表,使用回溯算法得到所有从起点单词到终点单词的路径。
我的C++解法
class Solution
{
public:
/** 每次变一个, 且邻域在 wordlist中。
* @Description: 单词接龙解题思路:①首先,BFS寻找最短路径的层数 和 构建搜索树的邻接表。②使用回溯思想,深搜并记录track 返回最短路径的 单词接龙多种解决方案。
* @Param: 结构需要:见下方
* @Return:
* @Notes: 注意:①subVisited ②不要 同一层相连,去除这样构建的搜索树——使不相连。
*/
vector<vector<string>> findLadders(string beginWord, string endWord, vector<string> &wordList)
{
vector<vector<string>> ans;
unordered_set<string> wordList1(wordList.begin(), wordList.end());
if(wordList1.count(endWord) == 0) { return ans; } // 上来就没有end,返回空列表。
queue<string> q; // 记录bfs的层
unordered_set<string> visited;
unordered_map<string, vector<string>> souTree;
// 开始BFS
q.push(beginWord);
visited.insert(beginWord);
bool isFound = false;
int level = 0;
while(!q.empty()){
unordered_set<string> subVisited; // 方便在最外层添加进入visited
int sz = q.size();
// vector<string> successor; // 错了位置
vector<string> preSuccessor;
while(sz--){
string cur = q.front();
q.pop();
// 结束条件,但是仍要在当前层结束后, 结束所有的bfs
// 但是再者说,我们在 下面直接 visit了。所以不需要等到当前层提取结束,我们在访问上次一层的时候 就添加到邻域中了。
if(cur == endWord && !isFound){
isFound = true;
break;
}
// 寻找邻域 并 添加到搜索树
vector<string> neiborWord;
vector<string> successor; // 错了位置
findCurNeibor(cur, neiborWord, wordList1);
for( string nei : neiborWord ){
if(!visited.count(nei)){
// [丢失 去除同一层的重复单词]
subVisited.insert(nei);
successor.push_back(nei);
q.push(nei);
}
}
souTree.emplace(cur, successor);
}
// 一层结束
if( isFound ) { break; } // 如果找到了 break;
visited.insert(subVisited.begin(), subVisited.end());
level++;
cout << "调试当前的层次为:" << level << endl;
}// BFS结束
// 深搜 搜索树,构建answer
vector<string> track;
track.push_back(beginWord);
dfs( souTree, track, beginWord, endWord, ans);
return ans;
}
// 辅助函数 —— 回溯搜索树 并 返回从begin 到 end的所有路径解
void dfs( unordered_map<string, vector<string>> &souTree, vector<string> &track, string curWord, string endWord, vector<vector<string>> &ans){
// 结束条件
if(curWord == endWord) {
ans.push_back(track);
return ;
}
if(!souTree.count(curWord)) return;
for(string s: souTree[curWord]){
// 剪枝
track.push_back(s);
// 深搜
dfs(souTree, track, s, endWord, ans);
// 状态重置
track.pop_back();
}
}
// 辅助函数--寻找当前string的所有邻域单词
/**
* @Description:
* @Param:
* @Return: 注意:①返回当前string的所有邻域单词 ②在里面 传入?去除同一层的单词 不要连接起来树。
* @Notes:
*/
void findCurNeibor(string cur, vector<string> &neiborWord, unordered_set<string> &wordList){
//26 字母只要有一个相等 且 在wordlist中就算是邻域单词
for( char ch = 'a'; ch <= 'z' ; ch++ ){
for(int i = 0; i<cur.size() ;++i){
string temp = cur;
if(ch != cur[i]){
cur[i] = ch;
if(wordList.count(cur)) { neiborWord.push_back(cur); } //存在
cur = temp;//还原
}
}
}
}
};
解法二:双向BFS + 回溯【且使用 大佬的方法】
由于我们这个问题知道目标值,因此可以让起点和终点「同时」做 BFS,直到它们又交集,两边一起扩散形成的路径也是最短路径;
双向 BFS 有点像山洞挖隧道,两边一起挖,直到打通为止。双向 BFS 搜索到的集合更小,相当于做了一些剪枝操作,因此会更快一些。
- 参考解法二 与参考解法一 仅仅把「广度优先遍历」方法的实现改成了「双向广度优先遍历」,回溯的代码是一样的;【但是 双向BFS中 提前
return
且found
的加入 更加速了算法时间用时。】 q1
和q2
交替使用,实现类似队列(符合顺序性,先进先出)的效果;【但用 hash_set实现 队的形态】- 注意:不一定非要严格按照「一左一右」的顺序交替向中间扩散,每次都从较小的集合扩散,统计意义上搜索的范围更小;【注意
reversed
的巧妙运用】
我的/大佬的C++解法
class Solution1 {
public:
/**
* @Description:
* @Param:
* @Return:
* @Notes:
*/
vector<vector<string>> findLadders(string beginWord, string endWord, vector<string> &wordList)
{
//开始 双向BFS + 回溯法
vector<vector<string>> ans;
unordered_set<string> dict(wordList.begin(), wordList.end());
if(!dict.count(endWord)){
return ans;
}
dict.erase(beginWord);
dict.erase(endWord);
unordered_set<string> q1{beginWord}, q2{endWord};
unordered_map<string, vector<string>> next;
bool reversed = false, found = false;
while(!q1.empty()){
unordered_set<string> q; //当前值 下一个邻域需要广搜的。
for(const auto &w:q1){
string s = w;
for(size_t i = 0; i<s.size() ; i++){
char ch = s[i];
for(int j = 0;j<26;j++){ // 当前位置寻找邻域
s[i] = j + 'a';
if(q2.count(s)){ // 相交了 —— 返回! 【这里是关键 —— 蕴含着 相交 + 如何加入到next中去】
reversed? next[s].push_back(w) : next[w].push_back(s);
found = true;
}
if(dict.count(s)){// 找到邻域
reversed ? next[s].push_back(w) : next[w].push_back(s);
q.insert(s);
}
}
//还原回去
s[i] = ch;
}
}
if(found){
break;
}
for(const auto &w : q){
dict.erase(w); //【新颖——防止重复访问。】
}
// 接来来下一层遍历, 取最小的一边双向BFS搜索
if(q.size() <= q2.size()){
q1 = q; // 照常
}else{
// 互换
reversed = !reversed;
q1 = q2;
q2 = q;
}
}
if(found) {
vector<string> track = {beginWord};
backtracking(beginWord, endWord, next, track, ans);
}
return ans;
}
// 辅助函数 —— 回溯搜索树 并 返回从begin 到 end的所有路径解
void backtracking( string curWord, string endWord, unordered_map<string, vector<string>> &souTree, vector<string> &track, vector<vector<string>> &ans){
// 结束条件
if(curWord == endWord) {
ans.push_back(track);
return ;
}
if(!souTree.count(curWord)) return;
for(string s: souTree[curWord]){
// 剪枝
track.push_back(s);
// 深搜
backtracking(s, endWord, souTree, track, ans);
// 状态重置
track.pop_back();
}
}
};
大纲
-
回溯算法团灭子集、排列、组合问题
-
回溯算法最佳实践:解数独
-
回溯算法最佳实践:括号生成
-
如何用 BFS 算法秒杀各种智力题