双端队列BFS


双端队列广搜

基本介绍

双端队列主要是解决边权只有 01 的这类的最短路问题,我们把边权为 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),如下图所示。

电路.png

每个格点都是电线的接点,每个格子都包含一个电子元件。

电子元件的主要部分是一个可旋转的、连接一条对角线上的两个接点的短电缆。

在旋转之后,它就可以连接另一条对角线的两个接点。

电路板左上角的接点接入直流电源,右下角的接点接入飞行车的发动装置。

达达发现因为某些元件的方向不小心发生了改变,电路板可能处于断路的状态。

她准备通过计算,旋转最少数量的元件,使电源与发动装置通过若干条短缆相连。

不过,电路的规模实在是太大了,达达并不擅长编程,希望你能够帮她解决这个问题。

注意:只能走斜向的线段,水平和竖直线段不能走。

输入格式

输入文件包含多组测试数据。

第一行包含一个整数 TT,表示测试数据的数目。

对于每组测试数据,第一行包含正整数 R 和 C,表示电路板的行数和列数。

之后 R 行,每行 C 个字符,字符是"/""\"中的一个,表示标准件的方向。

输出格式

对于每组测试数据,在单独的一行输出一个正整数,表示所需的最小旋转次数。

如果无论怎样都不能使得电源和发动机之间连通,输出 NO SOLUTION

数据范围

1≤R,C≤500,
1≤T≤5

输入样例:

1
3 5
\\/\\
\\///
/\\\\

输出样例:

1

样例解释

样例的输入对应于题目描述中的情况。

只需要按照下面的方式旋转标准件,就可以使得电源和发动机之间连通。

电路2.png

问题解决:

双端队列主要解决图中边的权值只有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寒假每日一题;洛谷题库。

部分内容转载:作者:小呆呆 参考地址

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值