【01BFS】概念讲解 && 解法 && 例题讲解:P4554小明的游戏

01BFS的概念

“01BFS” 是一种图遍历算法,常用于 边权为 0 或 1 的图 中找最短路径。你需要从起点出发,找到到所有点的最短路径。相比普通的 Dijkstra 算法,01BFS 更高效,在这种特殊图结构下能达到 线性时间复杂度 O(N + M),其中 N 是节点数,M 是边数。
这个图的边的权重要么是 0,要么是 1。

核心思想

使用 双端队列(deque) 实现 BFS:

  • 如果当前边权为 0,则将目标节点 加入队首;
  • 如果当前边权为 1,则将目标节点 加入队尾。

Q:为什么是这样的方式来处理 01BFS 呢?
理解方式一:
如果边权为 0,我们希望更快到达,应该优先处理它;
如果边权为 1,代价高一点,就可以稍后处理;

这样就能保证更短路径更优先被遍历,维护了最短路径的拓展顺序。
这其实就是用 deque 中的顺序,模拟了 Dijkstra算法 优先队列中「更小代价优先」的行为。

如果这样理解不通,还可以用多源BFS理解:
将边权为 0 的各个点全部视为 BFS 的源点,对于多源 BFS 的处理办法就是将多个源点全部添加到队首,然后再依次进行宽搜。于是我们就可以在访问到边权为 0 的这个状态时将这个点 push_front 到 deque 中;在访问到边权为 1 的点时将这个点看作是常规需要找出最短路径的点,push_back 到 deque 中。这样是不是更容易理解了呢?

再不理解耄耋就生气了。。。我看看谁还不理解
【01BFS】概念讲解 && 解法 && 例题讲解:P4554小明的游戏_第1张图片

松弛操作

在图的最短路算法中,“松弛”(Relaxation)是指:
如果通过当前点到达邻接点的路径比已知路径更短,那么更新最短路径。
在代码中一般写作:

if(dist[v] > dist[u] + w) 
{
    dist[v] = dist[u] + w;
    // 然后入队更新
}

松弛是 Dijkstra、Bellman-Ford、SPFA 等算法的核心。

小细节:01BFS 的设计能天然保证「更优路径先被处理」,所以一旦 dist 更新就不再更新,不需要额外松弛。

关注我,学习更多细节!

时间复杂度

每条边最多被访问一次:O(M)

每个点最多入队一次:O(N)

总复杂度:O(N + M) —— 比 Dijkstra 的 O((N + M)logN) 更快。

如果边权为随机值怎么办呢?

如果边权是任意正整数(比如 2、5、100 等),普通的 BFS 就不能用了,因为 BFS 只能处理所有边权相等(等价于权重为 1)的情况,这是因为 BFS 本质是按「步数」一层一层扩展,而「步数」只有在边权为 1 时才和「路径长度」对应。这点很重要,有很多同学在学完 BFS 之后却忘记了它的使用范围。

这时候我们需要使用 Dijkstra 算法,这是一个能处理任意非负边权图的最短路算法。
它使用 来维护「当前最短路径」的节点;每次从堆里弹出距离最短的节点,然后更新它的邻居。

例题讲解

点击跳转:洛谷P4554 小明的游戏
【01BFS】概念讲解 && 解法 && 例题讲解:P4554小明的游戏_第2张图片

分析

其实没什么好分析的。。。很简单。重点就是 移动到相同字符格子代价为 0,移动到不同字符格子代价为 1。这句话揭示了这题是个 01BFS。

但是其实这题根本不需要松弛操作,这是为什么呢?
因为边权是非负的,且只有 0 或 1
如果你第一次访问到某个点 (a, b),就已经是从某条最短路径到达它了;换句话说只要 (a, b) 这个点进入到队列中,他就已经是最优情况了。
因为只要你走到目标这个点上就至少要花费 1 的代价,如果后面再访问到它时,只可能是更长的路径或者还是 1(因为代价是非负,不能变小);
所以不用再“松弛”它,直接跳过即可,dist[a][b] 第一次赋值就是最小值。

剪枝的位置也需要考虑清楚,在注释中有写。

题解

#include 
#include 
#include 

using namespace std;

typedef pair<int,int> PII;

const int N = 510;

char p[N][N];
int dist[N][N];
int n,m;
int x1,y1,x2,y2;

int dx[] = {0,0,1,-1};
int dy[] = {1,-1,0,0};

void bfs()
{
	if(x1 == x2 && y1 == y2)
	{
		dist[x2][y2] = 0;
		return;
	}
	
	memset(dist,-1,sizeof dist);
	deque<PII> dq;
	dq.push_back({x1,y1});
	dist[x1][y1] = 0;
	
	while(dq.size())
	{
		auto t = dq.front(); dq.pop_front();
		int x = t.first, y = t.second;
		//只有队列中弹出的(x,y)这个元素等于(x2,y2)的时候才能剪枝,pop掉之后说明这个点真正到了 
		if(x == x2 && y == y2) return;
		
		for(int i=0;i<4;i++)
		{
			int a = x + dx[i], b = y + dy[i];
			//判断a、b合法 
			if(a >= 0 && a < n && b >= 0 && b < m)
			{
				//判断(a,b)与(x,y)的字符是否相同 
				char ch = p[x][y], next = p[a][b];
				//计算距离增量w 
				int w = (ch == next ? 0 : 1);
				
				//第一次BFS到某个点 
				if(dist[a][b] == -1) 
				{
					dist[a][b] = dist[x][y] + w;
					//第一次BFS到某个点是需要将这个点加入队列的,如果增量为0加入队首,增量为1加入队尾 
					if(w == 0) dq.push_front({a,b});
					else dq.push_back({a,b}); 
				}
				//BFS到之前访问过的点,判断当前路径是否更优。注意再次遍历到某个点时不需要再将这个点加入队列的 
				else if(dist[a][b] + w < dist[x][y])
				{
					//松弛操作 
					dist[a][b] = dist[x][y] + w;
				}
				//注意在这里是不能剪枝的,这里可能是(x2,y2)相邻的某个点在上下左右移动时访问到的(x2,y2),而不是真正到(x2,y2)了。有可能还没找出(x2,y2)的最优路径 
//				if(x == x2 && y == y2) return;
			}
		}
	}
}

int main() 
{
	while(cin >> n >> m && n && m)
	{		
		for(int i=0;i<n;i++)
		{
			for(int j=0;j<m;j++)
			{
				cin >> p[i][j];
			}
		}
		cin >> x1 >> y1 >> x2 >> y2;
		
		bfs();
		
		cout << dist[x2][y2] << endl; 
	}
	return 0;
}

你可能感兴趣的:(01BFS,C++,BFS,算法)