双端队列广搜
基本介绍
双端队列主要是解决边权只有 0
和 1
的这类的最短路问题,我们把边权为 0 的边加入到对头,把边权为 1 的边加入到队尾,同堆优化版(优先队列)的 Dijkstra 一样,只有在出队的时候才能知道最小值。
**正确性证明:**Dijkstra是正确的那么它也一定是正确的!
由于我们最终目标是求路径权值和,而权值为0的边无论走多少条权值和依旧是+0,因此我们可以优先走权值为0的边,更新与这些边相连的点x的d[x](d[i] 为从s到i最小权值和),此时d[x]一定是最小的,因为它是由尽可能多的权值为0的边更新而来。所以在队列中取出的节点同时满足“连通性”和“权值最小”,因此每个节点仅需被更新一次。
双端队列BFS看似是广搜但它的本质是dijkstra算法,权重大于等于0嘛,而堆优化版的dijkstra的效率关键在于判重!
例题
1.电路维修
【题目链接】175. 电路维修 - AcWing题库
达达是来自异世界的魔女,她在漫无目的地四处漂流的时候,遇到了善良的少女翰翰,从而被收留在地球上。
翰翰的家里有一辆飞行车。
有一天飞行车的电路板突然出现了故障,导致无法启动。
电路板的整体结构是一个 R 行 C 列的网格(R,C≤500),如下图所示。
每个格点都是电线的接点,每个格子都包含一个电子元件。
电子元件的主要部分是一个可旋转的、连接一条对角线上的两个接点的短电缆。
在旋转之后,它就可以连接另一条对角线的两个接点。
电路板左上角的接点接入直流电源,右下角的接点接入飞行车的发动装置。
达达发现因为某些元件的方向不小心发生了改变,电路板可能处于断路的状态。
她准备通过计算,旋转最少数量的元件,使电源与发动装置通过若干条短缆相连。
不过,电路的规模实在是太大了,达达并不擅长编程,希望你能够帮她解决这个问题。
注意:只能走斜向的线段,水平和竖直线段不能走。
输入格式
输入文件包含多组测试数据。
第一行包含一个整数 TT,表示测试数据的数目。
对于每组测试数据,第一行包含正整数 R 和 C,表示电路板的行数和列数。
之后 R 行,每行 C 个字符,字符是
"/"
和"\"
中的一个,表示标准件的方向。输出格式
对于每组测试数据,在单独的一行输出一个正整数,表示所需的最小旋转次数。
如果无论怎样都不能使得电源和发动机之间连通,输出
NO SOLUTION
。数据范围
1≤R,C≤500,
1≤T≤5输入样例:
1 3 5 \\/\\ \\/// /\\\\
输出样例:
1
样例解释
样例的输入对应于题目描述中的情况。
只需要按照下面的方式旋转标准件,就可以使得电源和发动机之间连通。
问题解决:
双端队列主要解决图中边的权值只有0或者1的最短路问题
操作:
每次从队头取出元素,并进行拓展其他元素时
1、若拓展某一元素的边权是0,则将该元素插入到队头
2、若拓展某一元素的边权是1,则将该元素插入到队尾
按左上角,右上角,右下角,左下角遍历的顺序
1、dx[]
和dy[]
表示可以去其他点的方向
2、id[]
和iy[]
表示需要踩某个方向的各种才能去到相应的点
3、cs[]
表示当前点走到4
个方向的点理想状态下格子形状(边权是0的状态)
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
#include<deque>
using namespace std;
const int N = 510;
typedef pair<int, int> PII;
char g[N][N];
int dist[N][N];
bool st[N][N];
int n, m;
int bfs()
{
memset(dist, 0x3f, sizeof dist);
memset(st, 0, sizeof st);
deque<PII> q;
dist[0][0] = 0;
q.push_back({0, 0});// 首元素入队
char cs[] = "\\/\\/"; // 需要转义
int dx[4] = {-1, -1, 1, 1}, dy[4] = {-1, 1, 1, -1};
int ix[4] = {-1, -1, 0, 0}, iy[4] = {-1, 0, 0, -1};
while(q.size())
{
auto t = q.front();
q.pop_front();
int x = t.first, y = t.second;
if(st[x][y]) continue;
st[x][y] = true;
for(int i = 0; i < 4; i ++)
{
int a = x + dx[i], b = y + dy[i];
if(a < 0 || a > n || b < 0 || b > m) continue;
int ga = x + ix[i], gb = y + iy[i];
int w = g[ga][gb] == cs[i] ? 0 : 1;//观察是否需要转动,若处于理想状态则权值是0,否则需要旋转1次权值是1
int distance = dist[x][y] + w;
if(distance < dist[a][b])
{
dist[a][b] = distance;
if(w == 1) q.push_back({a, b});// 1的话队尾插入
else q.push_front({a, b}); // 0的话队头插入
}
}
}
return dist[n][m];
}
int main()
{
int T;
cin >> T;
while(T --)
{
cin >> n >> m;
for (int i = 0; i < n; i ++ ) cin >> g[i];
if((n + m) % 2 != 0) puts("NO SOLUTION");
else cout << bfs() << endl;
}
return 0;
}
2.拖拉机
【题目链接】2019. 拖拉机 - AcWing题库
解题思路:
- 读题:请帮助约翰确定他需要移除的干草捆的最小数量,以便他能够将拖拉机开到二维平面的原点。—— 矩阵
- 模型抽象、转化:矩阵转化为最短路模型
- 点:起点、终点
- 边:四个方向
- 权值:点权:障碍物权重为
1
,空地权重为0
—— 最终确定为双端队列广搜模型!
- 证明:矩阵中任何一个方案都可以和一条从起点到终点的路径一 一对应且权值相同。
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
#include <deque>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 1010;
bool g[N][N];// 障碍物
int dist[N][N];// 点是(x,y),之前一维是因为节点编号是1 2 3 ...
bool st[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int bfs(int sx, int sy)
{
memset(dist, 0x3f, sizeof dist);
dist[sx][sy] = 0;
deque<PII> q;
q.push_back({sx, sy});
while(q.size())
{
auto t = q.front();
q.pop_front();
if(st[t.x][t.y]) continue;// 判重(提高dijkstra效率的关键)
st[t.x][t.y] = true;
for(int i = 0; i < 4; i ++)
{
int a = t.x + dx[i], b = t.y + dy[i];
if(a < 0 || a > N || b < 0 || b > N) continue;
int w = 0;
if(g[a][b] == true) w = 1;// 如果是障碍物的化点权为 1
if(dist[a][b] > dist[t.x][t.y] + w)// 如果距离被更新了
{
dist[a][b] = dist[t.x][t.y] + w;
if(w == 0) q.push_front({a, b});
else q.push_back({a, b});
}
}
}
return dist[0][0];
}
int main()
{
int n, sx, sy;
cin >> n >> sx >> sy;
while (n -- )
{
int i, j;
cin >> i >> j;
g[i][j] = true;
}
cout << bfs(sx, sy);
return 0;
}
3.CF1063B Labyrinth
【题目链接】【CF1063B Labyrinth - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)】
解题思路:
**读题:**求从起点出发最多可扩展的点的个数,包括起点。二维平面。—— 矩阵
**模型抽象,转换:**由于上下走不限,左右的次数走有限,为了尽可能多的扩展点,我们尽可能多的上下走,尽可能少使用左右走的次数!
所以,我们需要取这些方法的最小值。
如何取得这些方法的最小值呢?——BFS最短路模型!
- 点:起点(给定),终点:扩展
- 方向:上下,左右(有限次)
- 边权:上下走:0(优先),左右:1(后走)
- 答案:统计可扩展的点的个数
由于扩展方向是有限制的,即有的点在某一个时刻是不能向左右走的,它的次数是有限的,因此我们在存储定义节点的时候,也顺便定义左右走的次数,用来记录向左右扩展还可以用多少次!
struct node { int x, y; int l, r;// 记录左右移动的次数 }
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
#include <deque>
using namespace std;
const int N = 2010;
int n, m, ans;
char g[N][N];
bool st[N][N];
int dx[4]={1,-1,0,-0};
int dy[4]={0,0,-1,1};
struct node
{
int x, y, l, r;
};
void bfs(int sx, int sy, int k1, int k2)
{
deque<node> q;
q.push_back({sx, sy, k1, k2});
while(q.size())
{
auto t = q.front();
q.pop_front();
int x = t.x, y = t.y;
if(st[x][y]) continue;
st[x][y] = true;
if(t.l < 0 || t.r < 0) continue;
ans ++;
for(int i = 0; i < 4; i ++)
{
int a = x + dx[i], b = y + dy[i];
if(a <= 0 || a > n || b <= 0 || b > m || st[a][b] || g[a][b] == '*') continue;
if(i == 0 || i == 1) q.push_front({a, b, t.l, t.r});// 优先走上下!
if(i == 2) q.push_back({a, b, t.l - 1, t.r});
if(i == 3) q.push_back({a, b, t.l, t.r - 1});
}
}
}
int main()
{
int sx, sy, k1, k2;
cin >> n >> m;
cin >> sx >> sy;
cin >> k1 >> k2;
for(int i = 1; i <= n; i ++)
for (int j = 1; j <= m; j ++ )
{
cin >> g[i][j];
}
bfs(sx , sy , k1, k2);
cout << ans;
return 0;
}
4.通信路线
【题目链接】340. 通信线路 - AcWing题库
读题:题目就是要我们求所有从 1−>n
的路径中第k+1
大的边的最小值。
思考:为什么可以这样概括呢?因为题意中的答案要最小,我们贪心肯定要使k次免费的资格用完,那么最划算的方案肯定是拿最长的k条路使之免费,然后付第k+1长路的长度的钱。
抽象模型:
1、最大的最小——二分答案
- 在长度区间
[0,1e5+1]
假设我们二分出一个值x,那意味着x应该满足:从1−>n
的路径中应该存在一条路,使得这条路上最多有k
条大于``x的边。(二分值需要满足的性质) - 满足什么性质呢?
check(x)
函数表示:从1
走到N
,最少经过的长度大于x
的边数的数量是否小于等于k
,若是则返回true
,否则返回false
- 如果边大于
x
,则边权看成1
- 如果边长小于等于
x
,则边权看成0
(免费)
- 如果边大于
**2、**上述性质判断的求解就可以转化为最短路模型(01双端队列BFS模型)
注:初始l = 0,r = 1000001的原因是:如果1号点到n号点是不连通的,最后二分出来的值一定是1000001,表示无解
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
#include <deque>
using namespace std;
const int N = 1010, M = 2e5 + 10;
int h[N], e[M], ne[M], w[M], idx;
int dist[N];
bool st[N];
int n, m, k;
void add(int a, int b, int c) // 添加一条边a->b,边权为c
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
bool check(int bound)
{
memset(dist, 0x3f, sizeof dist);
memset(st, 0, sizeof st);
dist[1] = 0;
deque<int> q;
q.push_back(1);
while(q.size())
{
auto t = q.front();
q.pop_front();
if(st[t]) continue;
st[t] = true;
for(int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
int val = w[i] > bound;
if(dist[j] > dist[t] + val)
{
dist[j] = dist[t] + val;
if(val) q.push_back(j);
else q.push_front(j);
}
}
}
return dist[n] <= k;
}
int main()
{
memset(h, -1, sizeof h);
cin >> n >> m >> k;
for (int i = 0; i < m; i ++ )
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
add(b, a, c);
}
//二分
int l = 0, r = 1e6 + 1;
while(l < r)
{
int mid = (l + r) / 2;
if(check(mid)) r = mid;
else l = mid + 1;
}
if(r == 1e6 + 1) r = -1;
cout << r;
return 0;
}
总结
双端队列bfs实现起来并不难,难点在于怎么将模型抽象转换出来,知道是要跟最短路挂钩。二分还是太不熟练了!QAQ…
学习内容参考:acwing算法基础课、提高课、2022寒假每日一题;洛谷题库。
部分内容转载:作者:小呆呆 参考地址