BFS——两天做了三道题。。。
好像是涉及图论了,太高深的不懂,记录下做题过程。
BFS简单理解,重点如下几步:
- 从起点状态搜到终点状态(队列实现)
- 初始化 把起点状态压入列表。
- 更新过程:当队列不为空时,每次从队首取一个元素作为这次搜索的起点,当前状态下的每次搜索得到的新合法状态压入队尾,用于下次更新。
- 题目里有死路的情况,死路就是非法状态,直接跳过。那如果遇到以前重复过的状态呢?那也是非法状态,因为已经重复过的路不会再搜到新的合法状态,所以也略过。(略过是用哈希表实现的,每次在哈希表里查一下是否曾经存在过,又快又好)
- 搜索到某每个状态都更新搜索到当前状态的步数(也是在哈希表里统计,优点是查的快,而且效果好)
- 所以哈希表是以每个合法的状态进行搜索,并记录来到这个状态所消耗的步数 unordered_map<state,step>
- 一直更新迭代,直到找到终点为止,返回走到终点消耗步数;或者所有合法状态都搜索完(队列空了都没找到,死局return -1)
有一个通俗的说法是 BFS是绘制一颗树的过程,每个节点都是一个状态,每个节点的子节点是从这个状态中搜索来的下一跳状态。
用队列来实现,每次新得到的状态放在队尾,只有当同一批兄弟节点全部出队后才轮得到后进队的孩子节点。挺形象的。
打开转盘锁:LC752
比较朴素的一种解法,直接用广度遍历搜索BFS。
//状态的搜索
//仿写的一个BFS,要多注意细节
class Solution {
public:
int openLock(vector<string>& deadends, string target) {
//八叉树BFS
string startstr = "0000";
if(target == startstr) return 0;
if(find(deadends.begin(),deadends.end(),"0000") != deadends.end()) return -1;
//queue是队列,先进先出,包含push()与emplace()方法
queue<string> q;
set<string> deadlist;
for(auto d:deadends){
deadlist.insert(d);
}
set<string> visited;
//push()先构造再压入,emplace()直接在尾部生成
q.emplace(startstr);
visited.insert(startstr);
int level = 0;
while(!q.empty()){
int size = q.size();
while(size-->0){
string st = q.front();
q.pop();
for(int i=0;i<4;i++){
char cht = st[i];
//注意把int转string 用to_string()
string stradd = st.substr(0,i) + to_string(cht == '9' ? 0 : cht - '0' + 1) + st.substr(i+1);
string strsub = st.substr(0,i) + to_string(cht == '0' ? 9 : cht - '0' - 1) + st.substr(i+1);
//如果找到了,直接返回层数,即搜索次数
if(st == target) return level;
//如果没找到,当前答案既不能属于deadend,也不能是曾经访问过的数
if(!visited.count(stradd) && !deadlist.count(stradd)){
q.emplace(stradd);
visited.insert(stradd);
}
if(!visited.count(strsub) && !deadlist.count(strsub)){
q.emplace(strsub);
visited.insert(strsub);
}
}
}
level++;
}
return -1;
}
};
另一种效率更高,也同样方便理解的方法是双向BFS
本题本质上是从一个起始状态 搜到结束状态。这个结束状态是已知的,而且在正向搜索的过程中没有什么跳步,没有什么复杂的约束规则。那么在单向BFS的基础上,同时从终点开始搜索,两个过程都是不断的试探,直到某个状态在正向搜索的过程中遇到了,逆向搜索的过程中也遇到了,就说明一定存在一个经过当前状态的路径,使得从起始状态演化到最终状态成为可能。
好比是从上往下画树,同时,也从下往上画一棵树,当两棵树存在共同的节点,那么两棵树的起点一定就也是相连的。
class Solution {
public:
string s,t;
unordered_set<string> dd;
int openLock(vector<string>& deadends, string target) {
s = "0000";
t = target;
if(s == t) return 0;
for(auto d:deadends){
dd.insert(d);
}
if(dd.count(s)) return -1;
int ans = dbfs();
return ans;
}
int dbfs(){
//双向BFS,那就是两边同步进行,因此有两个队列,两个记录经过状态的哈希表
queue<string> q1,q2;
unordered_map<string,int> m1,m2;
q1.push(s);
m1[s] = 0;
q2.push(t);
m2[t] = 0;
//当正向、逆向任意一方搜干净的时候都没有结果,那就是死路
while(!q1.empty() && !q2.empty()){
int t = -1;
if(q1.size() <= q2.size()) t = update(q1,m1,m2);
else t = update(q2,m2,m1);
if(t!=-1) return t;
}
return -1;
}
int update(queue<string>& q,unordered_map<string,int>& current,unordered_map<string,int>& other){
string temp = q.front();
int cstep = current[temp];
q.pop();
for(int i=0;i<4;i++){
//有每一位数有+1 -1两个转移路线,用数字j来操作。并略过j==0的情况
//我咋就学不会?
for(int j= -1;j<2;j++){
string bak = temp;
if(j==0) continue;
char ch = bak[i];
if(ch == '0' && j == -1) ch = '9';
else if(ch == '9' && j == 1) ch = '0';
else ch = (ch - '0' + j)+'0';
//人家写的这个状态转移,那叫一个漂亮!
// int origin = temp[i] - '0';
// int tn = (origin + j) % 10;
// if(tn == -1) tn = 9;
bak[i] = ch;
if(dd.count(bak) || current.count(bak)) continue;
if(other.count(bak)) return cstep + other[bak] + 1;
else {
q.push(bak);
current[bak] = cstep + 1;
}
}
}
return -1;
}
};
滑动谜题:LC773
同理的,也是个状态转移的过程,用BFS实现
注意 这个实现的时候,放入队列的就是一个Node类对象,因为在遍历过程中需要包含的信息比较多,可以理解为这个Node对象就是画的树上的一个节点吧,总之 在状态转移的时候要把所有需要包含的信息一起放入队列中。
class Solution {
public:
class Node{
public:
int x;
int y;
string str;
Node(int _x,int _y,string _str){
x = _x ; y = _y ; str = _str;
}
};
string s,t;
int n = 2,m = 3;
int x,y;
int slidingPuzzle(vector<vector<int>>& board) {
s = "";
t = "123450";
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
s += to_string(board[i][j]);
if(board[i][j] == 0){
x = i;
y = j;
}
}
}
int ans = bfs();
return ans;
}
//转移方向 上、下、左、右
int dir[4][2] = {{-1,0},{1,0},{0,-1},{0,1}};
int bfs(){
queue<Node> q;
unordered_map<string,int> mp;
Node root(x,y,s);
q.emplace(root);
mp[root.str] = 0;
while(!q.empty()){
Node nt = q.front();
q.pop();
int step = mp[nt.str];
if(nt.str == t) return step;
int dx = nt.x;
int dy = nt.y;
for(auto di:dir){
int nx = dx + di[0];
int ny = dy + di[1];
if(nx < 0 || ny <0 || nx >=n || ny >= m) continue;
//更新0的位置,看是怎么换的,上下左右哪个方向。
string nstr = update(nt.str,dx,dy,nx,ny);
if(mp.count(nstr)) continue;
Node next(nx,ny,nstr);
cout<<nstr<<endl;
q.push(next);
mp[nstr] = step+1;
}
}
return -1;
}
//注意 千万不要用引用的方式传参啊。。会破坏正在尝试探索的原状态
string update(string str,int dx,int dy,int nx,int ny){
string ts = str;
char temp = str[dx*m + dy];
str[dx*m + dy] = str[nx*m + ny];
str[nx*m + ny] = temp;
return str;
}
};
这个状态转移的过程一定要熟悉啊,每次看到新题,都有新的状态转移方式,像这种某个点在某个区域内上下左右滑动的过程是怎样模拟的,要熟悉啊。
同理,这个题也是起始状态与终止状态都已知,也可以用双向BFS。
class Solution {
public:
class Node{
public:
int x;
int y;
string str;
Node(int _x,int _y,string _str){
x = _x ; y = _y ; str = _str;
}
};
string s,t;
int n = 2,m = 3;
int x,y;
int slidingPuzzle(vector<vector<int>>& board) {
s = "";
t = "123450";
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
s += to_string(board[i][j]);
if(board[i][j] == 0){
x = i;
y = j;
}
}
}
//注意开局天胡的情况
if(s == t) return 0;
int ans = bfs();
return ans;
}
//转移方向 上、下、左、右
int dir[4][2] = {{-1,0},{1,0},{0,-1},{0,1}};
int bfs(){
queue<Node> q1,q2;
unordered_map<string,int> mp1,mp2;
Node start(x,y,s);
Node end(1,2,t);
q1.emplace(start);
q2.emplace(end);
mp1[start.str] = 0;
mp2[end.str] = 0;
int step;
//双向BFS就是要两边交替进展,因为每次节点更新都会得到新状态,所以每次取队列元素少的队先遍历。
while(!q1.empty() && !q2.empty()){
int t = -1;
if(q1.size() <= q2.size()){
t = update(q1,mp1,mp2);
}
else {
t = update(q2,mp2,mp1);
}
if(t != -1) return t;
}
return -1;
}
//注意 千万不要用引用的方式传参啊。。会破坏正在尝试探索的原状态
int update(queue<Node>& q,unordered_map<string,int>& cur,unordered_map<string,int>& other){
//队列要有进有出,很重要,否则死循环,别犯错了。
Node nt = q.front();
q.pop();
int dx = nt.x;
int dy = nt.y;
string origin = nt.str;
int step = cur[origin];
for(auto di:dir){
int nx = dx + di[0];
int ny = dy + di[1];
if(nx < 0 || ny <0 || nx >=n || ny >= m) continue;
//更新0的位置,看是怎么换的,上下左右哪个方向。
string bak = origin;
char temp = bak[dx*m + dy];
bak[dx*m + dy] = bak[nx*m + ny];
bak[nx*m + ny] = temp;
if(cur.count(bak)) continue;
if(other.count(bak)) return step + other[bak] + 1;
else{
Node next(nx,ny,bak);
q.push(next);
cur[bak] = step+1;
}
}
return -1;
}
};
蛇梯棋:LC909
这个题有点特殊,我们知道起始状态,也知道终止状态。但是因为有个状态跳变的情况,状态的更新并不是两个方向对等的,我们只能知道如何从起始状态搜索到达终止状态,而没有从终止状态反过来搜的方式(也许真的有?但是我还没想明白,认为是没有的)。所以用简单的BFS就可以了。
Q: 感觉还是BFS,每个节点有6个分支。可能有的节点能够跳跃。
A: 题目原意是once per move 每次移动只许跳一下 不许连跳。中文翻译我还以为是整个过程只允许经过一次虫洞。
Q: 但是真的存在无法到达的情况?
A: 存在的,比如在第29格的可选项是30 31 32 33 34 35,这些格子统统存在虫洞且全部指向29格以前的格子。
有个问题需要处理,就是格子的索引值怎么转换成下标?
一种办法是压缩成一维的,用一维数组来表示。
另一种接近模拟法的思路就是手动转换一下,,锻炼下逻辑能力
//输入版的规模N*N的一边,输入要转化的索引,输出该索引对应在版board中的下标表示。第r行第c列
pair<int,int> turned(int N,int idx){
int lor = 0;
int r = N - (idx-1)/N - 1;
int c;
if((idx-1)/N & 1) lor = 1;
if(lor){
c = N - (idx-1)%N -1;
}
else c = (idx-1)%N;
return {r,c};
}
class Solution {
//写个函数把索引转换成二维数组的行列。从索引值转化到矩阵下标。
//当然转成一维会更简单了,权当锻炼一下逻辑吧。。。
pair<int,int> turned(int N,int idx){
int lor = 0;
int r = N - (idx-1)/N - 1;
int c;
if((idx-1)/N & 1) lor = 1;
if(lor){
c = N - (idx-1)%N -1;
}
else c = (idx-1)%N;
return {r,c};
}
int bfs(int N,vector<vector<int>>& board){
int maxsize = N*N;
//矩阵方格的索引是从1开始的,索引1对应的搜索步数为0(还没开始)
queue<int> q;
q.push(1);
unordered_map<int,int> mp;
mp[1] = 0;
while(!q.empty()){
int idx = q.front();
int step = mp[idx];
q.pop();
pair<int,int> pos = turned(N,idx);
int r = pos.first;
int c = pos.second;
for(int i=1;i<=6;i++){
int idbak = idx+i;
//如果在基准索引idx的基础上选择了介于1-6之间的某个格子到头了,就返回找到idx的step步再加当前一步。
if(idbak == maxsize) return step+1;
if(idbak > maxsize) continue;
pair<int,int> npos = turned(N,idbak);
int nr = npos.first,nc = npos.second;
//如果有虫洞直通索引N*N 也是返回step+1
if(board[nr][nc] == maxsize) return step+1;
//如果下一步有虫洞但没有直通终点,那么先更新虫洞目的地,再将目的地加入哈希表
if(board[nr][nc] != -1) {
idbak = board[nr][nc];
}
//如果走到了已经出现过的索引说明这一步是无效的,跳过。
if(mp.count(idbak)) continue;
//如果这一步走过来没有虫洞,那么直接将当前索引加入队列下次遍历
//如果这一步经过了虫洞,那么记得加入队列的是冲动目的地的索引,别搞错了。。
q.push(idbak);
mp[idbak] = step+1;
}
}
return -1;
}
public:
int snakesAndLadders(vector<vector<int>>& board) {
int N = board.size();
//bfs 容器以idx即方格编号为索引查找
int ans = bfs(N,board);
return ans;
}
};
心得
BFS的话,模板是差不多的,我认为的难点是状态转移的这个模拟过程,很不熟练,,这个怎么说,感觉做一个题就看到一个新方法,多做多看多学把。。。
两天才看三道题,人要裂开了。。。😅