BFS + 状态压缩使用条件
求最短路径时,一般来说会优先考虑使用BFS算法。BFS算法在广度优先搜索的过程中会有一个类似vis的数组去重,避免重复访问
但是在一些情况下,题目需要求最短路径的同时,有可能对某个节点进行重复访问。如果出现这种情况,则要在使用BFS算法的同时加上状态压缩来减少BFS的时间复杂度
说人话就是:如果一道题目出求图的最短路径,并且还有可能在广度优先搜索时走回头路的话,那么需要优先考虑使用BFS + 状态压缩
状态压缩
状态压缩即为用一个二进制变量来表示当前节点的状态。通常使用n位二进制位mask来表示当前节点访问的状态。访问过的节点对应二进制位数下变为1
例如:
一个图有三个节点,如果mask = 001,表示第一个节点被访问过,第二第三个节点未被访问过,mask = 000 表示三个节点都未被访问过,mask = 111表示三个节点都被访问过
状态压缩的基本操作
- 访问第i个点的状态
s = mask & (1 << i )
- 更改第i个点的状态
mask = mask | (1 << i)
基本操作需要熟练记忆或者掌握,访问是&,更改是|
清楚了以上概念和操作,看例题
例题
力扣 847.访问所有节点的最短路径
存在一个由 n 个节点组成的无向连通图,图中的节点按从 0 到 n - 1 编号。
给你一个数组 graph 表示这个图。其中,graph[i] 是一个列表,由所有与节点 i 直接相连的节点组成。
返回能够访问所有节点的最短路径的长度。你可以在任一节点开始和停止,也可以多次重访节点,并且可以重用边。
示例 1:
输入:graph = [[1,2,3],[0],[0],[0]]
输出:4
解释:一种可能的路径为 [1,0,2,0,3]
输入:graph = [[1],[0,2,4],[1,3,4],[2],[1,2]]
输出:4
解释:一种可能的路径为 [0,1,4,2,3]
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/shortest-path-visiting-all-nodes
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
/*
状态压缩DP + BFS
最短路径,优先BFS,但是节点需要被多次访问,这样就会导致时间复杂度很高
同时此题的节点数很少,所以可以用状态压缩
用n为二进制数来表示该节点是否被访问过,一般来说从低位到高位
1表示已经访问过,0表示仍为访问过 全访问过时为全1,都没访问过时为0
(1)访问第 i 个点的状态:state=(1 << i) & mask
(2)更改第 i 个点状态为 11:mask = mask | (1 << i)
*/
class Solution {
public:
int shortestPathLength(vector<vector<int>>& graph) {
int n = graph.size();
// 1.初始化队列及标记数组,存入起点,状态压缩需要三个元素
queue< tuple<int, int, int> > q; // 三个属性分别为 idx, mask, dist
vector<vector<bool>> vis(n, vector<bool>(1 << n)); // 节点编号及当前状态
for(int i = 0; i < n; i++) {
q.push({i, 1 << i, 0}); // 存入起点,起始距离0,标记
vis[i][1 << i] = true;
}
// 开始搜索
while(!q.empty()) {
auto [cur, mask, dist] = q.front(); // 弹出队头元素
q.pop();
// 找到答案,返回结果,都访问过了,全1
if(mask == (1 << n) - 1) return dist;
// 扩展
for(int x : graph[cur]) {
//更改当前节点的状态
int nextmask = mask | (1 << x);
if(!vis[x][nextmask]) {
//未访问过的节点入队
q.push({x, nextmask, dist + 1});
vis[x][nextmask] = true;
}
}
}
return 0;
}
};
一般来说,在BFS和状态压缩配合使用时,所使用的队列queue<tuple<int,int,int>>,三个元素分别代表着,节点,当前节点状态mask和题目要求求出的距离dist,对比原本的BFS不难发现,这只是将原来的入队标记这些操作替换成了状态压缩的基本操作而已
力扣864. 获取所有钥匙的最短路径
- 获取所有钥匙的最短路径
给定一个二维网格 grid ,其中:
- ‘.’ 代表一个空房间
- ‘#’ 代表一堵墙
- ‘@’ 是起点
- 小写字母代表钥匙
- 大写字母代表锁
我们从起点开始出发,一次移动是指向四个基本方向之一行走一个单位空间。我们不能在网格外面行走,也无法穿过一堵墙。如果途经一个钥匙,我们就把它捡起来。除非我们手里有对应的钥匙,否则无法通过锁。
假设 k 为 钥匙/锁 的个数,且满足 1 <= k <= 6,字母表中的前 k 个字母在网格中都有自己对应的一个小写和一个大写字母。换言之,每个锁有唯一对应的钥匙,每个钥匙也有唯一对应的锁。另外,代表钥匙和锁的字母互为大小写并按字母顺序排列。
返回获取所有钥匙所需要的移动的最少次数。如果无法获取所有钥匙,返回 -1 。
示例 1:
输入:grid = ["@.a.#","###.#","b.A.B"]
输出:8
解释:目标是获得所有钥匙,而不是打开所有锁。
示例 2:
输入:grid = ["@..aA","..B#.","....b"]
输出:6
示例 3:
输入: grid = ["@Aa"]
输出: -1
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 30
grid[i][j] 只含有 ‘.’, ‘#’, ‘@’, ‘a’-‘f’ 以及 ‘A’-‘F’
钥匙的数目范围是 [1, 6]
每个钥匙都对应一个 不同 的字母
每个钥匙正好打开一个对应的锁
/*
BFS + 状态压缩
因为能走回头路,所以需要状态压缩,mask是一个表示钥匙是否被访问过的二进制数
全1表示都访问,全0表示都没有被访问
访问该状态 state = (1 << i) & mask
更改该状态 mask = mask | (1 << i)
*/
class Solution {
public:
int shortestPathAllKeys(vector<string>& grid) {
//方向数组
int dx[4] = {0,0,1,-1};
int dy[4] = {-1,1,0,0};
//开始和结束的坐标
int start = 0;
int end = 0;
//行列
int n = grid.size();
int m = grid[0].size();
//计算钥匙的个数的表
unordered_map<char,int> numKeyTable;
for (int i = 0;i < n;i++)
{
for (int j = 0;j < m;j++)
{
if (grid[i][j] == '@')
{
start = i;
end = j;
}
else if (grid[i][j] >= 'a' && grid[i][j] <= 'z')
{
if (numKeyTable.count(grid[i][j]) == false)
{
//钥匙有多把,所以需要6位的二进制数来表示钥匙是否被收集的状态
int number = numKeyTable.size();
numKeyTable[grid[i][j]] = number;
}
}
}
}
//distance数组,存储第i,j个位置mask状态时的距离
vector<vector<vector<int>>> distance(n, vector<vector<int>>(m,vector<int>(1 << numKeyTable.size(),-1)));
//队列元素分别是x,y,mask
queue<tuple<int,int,int>> qe;
//起点入队,并且标记状态
qe.push({start,end,0});
distance[start][end][0] = 0;
while (qe.empty() == false)
{
auto [x,y,mask] = qe.front();
qe.pop();
for (int i = 0;i < 4;i++)
{
int newX = x + dx[i];
int newY = y + dy[i];
//先判断这一步合不合法,有没有撞上墙
if (newX >= 0 && newX < n && newY >= 0 && newY < m && grid[newX][newY] != '#')
{
//如果这个点是起点或者是普通点
if (grid[newX][newY] == '@' || grid[newX][newY] == '.')
{
//如果这个点没被访问过
if (distance[newX][newY][mask] == -1)
{
//由于没有捡到钥匙,所以mask状态不变,此时该点这个状态的距离为BFS过来的的点 + 1
distance[newX][newY][mask] = distance[x][y][mask] + 1;
//点入队继续BFS
qe.push({newX,newY,mask});
}
}
//如果遇到了钥匙点
else if (grid[newX][newY] >= 'a' && grid[newX][newY] <= 'z')
{
//先看看这是第几把钥匙,方便下面改状态
int number = numKeyTable[grid[newX][newY]];
//更改状态
int newMask = mask | (1 << number);
//该点的这个状态未被访问过
if (distance[newX][newY][newMask] == -1)
{
//该点该状态的距离为BFS过来的点 + 1
distance[newX][newY][newMask] = distance[x][y][mask] + 1;
//如果这个状态是全1,直接返回结果(即找到了全部的钥匙)
if (newMask == ((1 << numKeyTable.size()) - 1))
return distance[newX][newY][newMask];
qe.push({newX,newY,newMask});
}
}
//如果遇到了锁
else
{
//看看锁对应哪一把钥匙
int number = numKeyTable[grid[newX][newY] + ('a' - 'A')];
//访问此时的状态
int state = (1 << number) & mask;
//如果此状态下已经拿到了对应的钥匙,并且锁没开
if (distance[newX][newY][mask] == -1 && state)
{
distance[newX][newY][mask] = distance[x][y][mask] + 1;
qe.push({newX,newY,mask});
}
}
}
}
}
return -1;
}
};