今天做leetcode的一道新题847题,顺便复习了下DFS和BFS这两种图论遍历方法,用其进行解题,加深了理解。
这道题是非常经典的遍历题,直接用DFS会超时,并且无法通过所有的案例,用DFS+memo可以解决,BFS的效率更高,因为这道题本身就是求最短路径类的题目。
先来看题目描述:
二维数组给出了连通的无向图中每个节点的邻接节点列表,因此二维数组的行数是图中节点的个数。图中节点从0到n-1进行编号。求出遍历所有节点的最小路径,节点可以重复访问,边也可以重复走。
因为图的节点个数不超过12个,因此,我们完全可以用一个整数的比特位来表示该节点当前有没有被访问,这个整数就是访问的status,用二维数组,第一维就是status,第二维就是当前访问的节点编号。(status, i)表示当前访问节点i,访问状态为status的即时状态。以样例中第一个为例,输入[[1,2,3],[0],[0],[0]]。我们在这画出状态转移的树形图,方便我们解题。下面我们可以看到,我们既可以用这个树形图来写出dfs+memo的代码,也可以用它来写出bfs的代码。取决于我们对于题目的分析和对下面这张图的解读。
方法一:DFS+memo:一个bool数组visited用来剪枝,DFS路径上遇到之前已经访问过的状态的时候,就不继续进行DFS遍历了,终止(END),另一个数组mp为记忆数组,mp[(status,i)]表示(status,i)距离目标状态的最小距离,避免重复计算。在DFS的上层函数中,依次从图的各个节点开始进行DFS,求出从每个节点开始进行遍历而得到的最小遍历距离,再从这些距离中找到全局最小距离即可。
代码如下:
class Solution {
public:
//DFS+memo
int shortestPathLength(vector<vector<int>>& graph)
{
map<pair<int,int>, int> mp; //记录当前状态到目标的最小转化步数
vector<vector<bool>> visited(1<<graph.size(),vector<bool>(graph.size(),false)); //访问状态数组,访问过的状态就不再访问了
int n=graph.size();
int globalMin=INT_MAX;
for(int i=0;i<n;i++)
{
pair<int,int> temp;
temp.first=1<<i;
temp.second=i;
globalMin=min(globalMin,dfs(graph,mp,temp,visited)); //求出全局最小遍历距离
}
return globalMin;
}
int dfs(vector<vector<int>> &graph, map<pair<int,int>,int> &mp, pair<int,int> cur, vector<vector<bool> >&visited)
{
if(cur.first+1==(1<<(graph.size()))) //base case,终点状态
{
mp[cur]=0;
return 0;
}
if(mp.count(cur)) return mp[cur]; //如果已经求出cur距离目标的最小距离,直接将其返回就好
int mn=INT_MAX;
for(auto j:graph[cur.second]) //每层的分支就是节点cur.second的各个邻接节点
{
auto temp=cur;
cur.first=cur.first | (1<<j); //状态转移
cur.second=j; //状态转移
if(!visited[cur.first][cur.second]) //转移之后的状态没有被访问过
{
visited[cur.first][cur.second]=true; //访问转移之后的状态
int dist=dfs(graph,mp,cur,visited); //用DFS求从转移之后的状态到目标状态的最小距离。
if(dist!=-1) mn=min(dist+1,mn); //求得的距离不为-1,更新最小距离
visited[cur.first][cur.second]=false; //状态返回转移之前,继续从cur到下一个邻接节点的状态转移
}
cur=temp; //状态返回转移之前,继续从cur到下一个邻接节点的状态转移
}
if(mn==INT_MAX) return -1; //如果从cur状态开始向目标状态转移的过程中,cur的邻接状态全部被访问过了,则该从该路径走肯定不是最短距离,那么mn还是INT_MAX,此时返回-1.
mp[cur]=mn; //求出了从cur状态到目标状态的最小转移距离
return mp[cur]; //向上递归,返回从cur状态到目标状态的最小转移距离,兼具记忆数组的功能,避免重复计算,因为倘若在之前的其它分支先求出cur到目标节点的最小距离,在本分支再遇到该状态cur,就直接返回之前计算的结果,而不需要再递归调用和计算。
}
};
方法二:一次BFS就能得到最终结果。该方法还是参照上面那张图,先把第一层的初始状态全部放到队列里面去,接着进行BFS遍历,等找到目标状态时返回对应的层数就好了。当然这个过程中还是会用到一个辅助数组dis[status][i],这个数组表示的意义和在DFS中的不一样,这个数组表示,从(status,i)到根节点的距离,就是(status,i) 所处的层数,那么当status==目标状态,则返回dis[status][i]就是本题的答案, 这个代码看起来就很简洁明了。
代码如下:
class Solution
{
public:
int shortestPathLength(vector<vector<int>>& graph)
{
vector<vector<int>> dis(1<<graph.size(),vector<int> (graph.size(),INT_MAX));
queue<pair<int,int>> q;
for(int i=0;i<graph.size();i++)
{
dis[1<<i][i]=0;
q.push(make_pair(1<<i,i));
}
while(!q.empty())
{
auto p=q.front();
q.pop();
if(p.first+1==(1<<graph.size()))
return dis[p.first][p.second];
int x=p.first, y=p.second;
for(auto num:graph[y])
{
int status=x | (1<<num);
if(dis[x][y]+1<dis[status][num])
{
dis[status][num]=dis[x][y]+1;
q.push(make_pair(status,num));
}
}
}
return 0;
}
};
当然,上面的代码用了一个二维数组记录状态,其实开的空间稍微大了些,有些空间并未直接用上,那么我们只需要用一个set来保存已经访问的状态即可,因为是一层一层地遍历,访问过的状态就不再访问了,最后当我们找到目标状态的时候,返回目标状态所在的层数就可以了。下面是按照这个思路写的BFS代码:
class Solution
{
public:
int shortestPathLength(vector<vector<int>>& graph)
{
set<pair<int,int>> s;
int n=graph.size();
queue<pair<int,int>> q;
for(int i=0;i<n;i++)
{
pair<int,int> p;
p.first=1<<i;
p.second=i;
s.insert(p);
q.push(p);
}
int layer=0; //当前层数
while(!q.empty())
{
int m=q.size();
for(int i=0;i<m;i++)
{
auto p=q.front();
q.pop();
int x=p.first;
int y=p.second;
if(x+1==(1<<n)) return layer;
for(auto num:graph[y])
{
int temp=x|(1<<num);
if(!s.count({temp,num}))
{
s.insert({temp,num});
q.push({temp,num});
}
}
}
layer++; //层数加1
}
return layer;
}
};
通过这道题,充分复习了DFS+memo和BFS的解题思路以及具体细节,首先要对状态转移进行合理有效的建模,然后画出状态转移的树状图,再按照DFS和BFS的标准步骤进行代码编写。只有思路清晰才能在最短的时间内写出最正确的代码来。