求最短路径时,一般来说会优先考虑使用BFS算法。BFS算法在广度优先搜索的过程中会有一个类似vis的数组去重,避免重复访问
但是在一些情况下,题目需要求最短路径的同时,有可能对某个节点进行重复访问。如果出现这种情况,则要在使用BFS算法的同时加上状态压缩来减少BFS的时间复杂度
说人话就是:如果一道题目出求图的最短路径,并且还有可能在广度优先搜索时走回头路的话,那么需要优先考虑使用BFS + 状态压缩
状态压缩即为用一个二进制变量来表示当前节点的状态。通常使用n位二进制位mask来表示当前节点访问的状态。访问过的节点对应二进制位数下变为1
例如:
一个图有三个节点,如果mask = 001,表示第一个节点被访问过,第二第三个节点未被访问过,mask = 000 表示三个节点都未被访问过,mask = 111表示三个节点都被访问过
s = mask & (1 << i )
mask = mask | (1 << i)
基本操作需要熟练记忆或者掌握,访问是&,更改是|
清楚了以上概念和操作,看例题
存在一个由 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
假设 k 为 钥匙/锁 的个数,且满足 1 <= k <= 6,字母表中的前 k 个字母在网格中都有自己对应的一个小写和一个大写字母。换言之,每个锁有唯一对应的钥匙,每个钥匙也有唯一对应的锁。另外,代表钥匙和锁的字母互为大小写并按字母顺序排列。
返回获取所有钥匙所需要的移动的最少次数。如果无法获取所有钥匙,返回 -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;
}
};