算法竞赛进阶指南 搜索 0x26 广搜变形

news2024/12/25 9:22:50

双端队列BFS

在最基本的广度优先搜索中,每次沿着分支的扩展都记为“一步”,我们通过逐层搜索,解决了求从起始状态到每个状态的最少步数的问题。这其实等价于在一张边权均为1的图上执行广度优先遍历,求出每个点相对于起点的最短距离(层次)。在第0x21节中我们曾讨论过这个问题,并得到了“队列中的状态的层数满足两段性和单调性”的结论,从而我们可以知道,每个状态在第一次被访问并入队时,计算出的步数即为所求。

然而,如果图上的边权不全是1呢?换句话说,如果每次扩展都有各自不同的“代价“,我们想求出起始状态到每个状态的最小代价,应该怎么办呢?我们不妨先讨论图上的边权要么是1、要么是0的情况。

1、AcWing 175. 电路维修

题意 :

  • 电路板的整体结构是一个 R 行 C 列的网格(R,C≤500),如下图所示。

在这里插入图片描述

  • 每个格点都是电线的接点,每个格子都包含一个电子元件。
  • 电子元件的主要部分是一个可旋转的、连接一条对角线上的两个接点的短电缆。
  • 在旋转之后,它就可以连接另一条对角线的两个接点。
  • 电路板左上角的接点接入直流电源,右下角的接点接入飞行车的发动装置。
  • 达达发现因为某些元件的方向不小心发生了改变,电路板可能处于断路的状态。
  • 她准备通过计算,旋转最少数量的元件,使电源与发动装置通过若干条短缆相连。
  • 1≤T≤5

转义:
在这里插入图片描述

思路 :

  • 我们可以把电路板上的每个格点(横线与竖线的交叉点)看作无向图中的节点。若两个节点x和y是某个小方格的两个对焦,则在x与y之间连边。若该方格中的标准件(对角线)与x到y的线段重合,则边权为0;若垂直相交,则边权为1(说明需要旋转1次才能连上)。然后,我们在这个无向图中求出左上角到右下角的最短距离,就得到了答案。
  • 这是一张边权要么是0、要么是1的无向图。在这样的图上,我们可以通过双端队列广搜来计算。算法的整体框架与一般的广搜类似,只是在每个节点上沿分支扩展时稍作改变。如果这条分支是边权为0的边,就把沿该分支到达的新节点从队头入队;如果这条分支是边权为1的边,就像一般的广搜一样从队尾入队。这样一来,我们就仍然能保证,任意时刻广搜队列中的节点对应的距离值都具有“两段性”和“单调性”,每个节点虽然可能被更新(入队)多次,但是它第一次被扩展(出队)时,就能得到从左上角到该节点的最短距离之后再被取出可以直接忽略,时间复杂度为 O ( R ∗ C ) O(R*C) O(RC)
  • 每个点虽然可能入队多次,但第一次出来的时候就已经取到最小值了(dijkstra算法的性质),因此后面重复出队的点应该直接忽略。当忽略重复出队的点后,就能保证每个点只会更新其他点一次,从而总共更新的次数等于总边数,这样时间复杂度就能保证是 O(RC)了

在这里插入图片描述

#include <iostream>
#include <cstring>
#include <deque>
using namespace std;
typedef pair<int, int> PII;
const int N = 510;

int n, m;
char g[N][N];
bool st[N][N];
int dist[N][N];

int bfs() {
    deque<PII> que;
    que.push_back({0, 0});
    dist[0][0] = 0;
    
    int dx[] = {1, -1, -1, 1}, dy[] = {1, 1, -1, -1};
    int ix[] = {0, -1, -1, 0}, iy[] = {0, 0, -1, -1};
    char cs[] = "\\/\\/";
    
    while (que.size()) {
        PII t = que.front(); que.pop_front();
        
        int x = t.first, y = t.second;
        if (x == n && y == m) return dist[x][y];
        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];
            int j = x + ix[i], k = y + iy[i];
            if (a >= 0 && a <= n && b >= 0 && b <= m) {
                int w = 0;
                if (g[j][k] != cs[i]) w = 1;
                if (dist[x][y] + w < dist[a][b]) {
                    dist[a][b] = dist[x][y] + w;
                    if (w) que.push_back({a, b});
                    else que.push_front({a, b});
                }
            }
        }
    }
    return -1;
}

int main() {
    int _; scanf("%d", &_);
    while (_ -- ) {
        scanf("%d%d", &n, &m);
        for (int i = 0; i < n; ++ i) scanf("%s", g[i]);
        memset(st, 0, sizeof st);
        memset(dist, 0x3f, sizeof dist);
        int t = bfs();
        if (t == -1) puts("NO SOLUTION");
        else printf("%d\n", t);
    }
}

优先队列BFS

对于更加具有普适性的情况,也就是每次扩展都有各自不同的代价时,求出起始状态到每个状态最小代价,就相当于在一张带权图上求出从起点到每个节点的最短路。此时,我们有两个解决方案:

方法一:

  • 仍然使用一般的广搜,采用一般的队列
  • 这是我们不再能保证每个状态第一次入队时就能得到最小代价,所以只能允许一个状态被多次更新多次进出队列。我们不断执行搜索,直到队列为空
  • 整个广搜算法对搜索树进行了重复遍历与更新,直至“收敛”到最优解,其实也就是“迭代”的思想。最坏情况下,该算法的时间复杂度会从一般广搜的 O ( N ) O(N) O(N)增长到 O ( N 2 ) O(N^2) O(N2)。对应在最短路问题中,就是我们将在0x61节介绍的 SPFA算法

方法二:

  • 改用优先队列进行广搜
  • 这里的“优先队列”就相当于一个二叉堆。我们可以每次从队列中取出当前代价最小的状态进行扩展(该状态一定已经是最优的,因为队列中其他状态的当前代价都不小于它,所以以后都不可能再更新它了),沿着各条分支到达的新状态加入优先队列。不断执行搜索,知道队列为空
  • 在优先队列BFS中,每个状态也会被多次更新、多次进出队列,一个状态也可能以不同的代价在队列中同时存在。不过,当每个状态第一次从队列中被取出时,就得到了起始状态到当前状态的最小代价。之后若再被取出,则可以直接忽略,不进行扩展。所以,优先队列BFS中每个状态只扩展一次,时间复杂度只多了维护二叉堆的代价。若一般广搜复杂度为 O ( N ) O(N) O(N),则优先队列BFS的复杂度为 O ( N l o g N ) O(NlogN) O(NlogN)。对应在最短路问题中,就是我们将在0x61节介绍的堆优化的 Dijkstra算法

至此,我们就可以对BFS的形式,按照对应在图上的边权情况进行分类总结:
在这里插入图片描述

1、AcWing 176. 装满的油箱

题意 :

  • 有 N 个城市(编号 0、1…N−1)和 M 条道路,构成一张无向图。
  • 在每个城市里边都有一个加油站,不同的加油站的单位油价不一样。
  • 现在你需要回答不超过 100 个问题,在每个问题中,请计算出一架油箱容量为 C 的车子,从起点城市 S 开到终点城市 E 至少要花多少油钱?
  • 1≤N≤1000,
  • 1≤M≤10000,

思路 :

  • 我们使用二元组(city, fuel)来表示每个状态,其中city为城市编号,fuel为油箱中剩余的汽油量,并使用记录数组d[city][fuel]存储最少花费
  • 对于每个问题,我们都单独进行一次优先队列BFS。起始状态为(S, 0)。每个状态(city, fuel)的分支有:
    1、若fuel < C,可以加1升油,扩展到新状态(city, fuel + 1),花费在城市city加1升油的钱
    2、对于每条从city出发的边(city, next),若边权大小w不超过fuel,可以开车前往城市next,扩展到新状态(next, fuel - w)
  • 我们不断取出优先队列中“当前花费最少”的状态(堆顶)进行扩展,更新扩展到的新状态在记录数组d中存储的值,直到终点T某个状态第一次被取出,即可停止BFS,输出答案。
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
const int N = 1e3 + 10, M = 2e4 + 10, C = 110;

struct Ver {
    int d, u, c;
    bool operator< (const Ver&w) const {
        return d > w.d;
    }
};

int n, m;
int prices[N];
int e[M], ne[M], w[M], h[N], idx;
bool st[N][C];
int dist[N][C];

void add(int a, int b, int c) {
    e[idx] = b; w[idx] = c; ne[idx] = h[a]; h[a] = idx ++ ;
}
int dijkstra(int start, int end, int cap) {
    memset(st, 0, sizeof st);
    memset(dist, 0x3f, sizeof dist);
    priority_queue<Ver> pq;
    pq.push({0, start, 0});
    dist[start][0] = 0;
    
    while (pq.size()) {
        Ver t = pq.top(); pq.pop();
        
        if (t.u == end) return dist[t.u][t.c];
        if (st[t.u][t.c]) continue;
        st[t.u][t.c] = true;
        
        if (t.c < cap) {
            if (dist[t.u][t.c + 1] > prices[t.u] + dist[t.u][t.c]) {
                dist[t.u][t.c + 1] = prices[t.u] + dist[t.u][t.c];
                pq.push({dist[t.u][t.c + 1], t.u, t.c + 1});
            }
        }
        
        for (int i = h[t.u]; ~i; i = ne[i]) {
            int j = e[i];
//            cout << j << endl;
            if (t.c >= w[i]) {
                if (dist[j][t.c - w[i]] > dist[t.u][t.c]) {
                    dist[j][t.c - w[i]] = dist[t.u][t.c];
                    pq.push({dist[j][t.c - w[i]], j, t.c - w[i]});
                }
            }
        }
    }
    return -1;
}

int main() {
    memset(h, -1, sizeof h);
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; ++ i) scanf("%d", &prices[i]);
    for (int i = 0; i < m; ++ i) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c); add(b, a, c);
    }
    int q;
    scanf("%d", &q);
    while (q -- ) {
        int a, b, c;
        scanf("%d%d%d", &c, &a, &b);
        int t = dijkstra(a, b, c);
        if (t == -1) puts("impossible");
        else printf("%d\n", t);
    }
}

双向BFS

在0x24节我们介绍了双向搜索的思想,并讲解了一道双向DFS的例题,双向BFS的思想与之完全相同,我们就不再重复描述。因为BFS本身就是逐层搜索的算法,所以双向BFS的实现更加自然、简便。以普通的求最少步数的双向BFS为例,我们只需从起始状态、目标状态分别开始,两边轮流进行,每次各扩展一整层。当两边各自有一个状态在记录数组中发生重复时,就说明这两个搜索过程相遇了,可以合并得出起点到终点的最少步数

1、AcWing 177. 噩梦

题意 :

  • 给定一张 N×M 的地图,地图中有 1 个男孩,1 个女孩和 2 个鬼。
  • 字符 . 表示道路,字符 X 表示墙,字符 M 表示男孩的位置,字符 G 表示女孩的位置,字符 Z 表示鬼的位置。
  • 男孩每秒可以移动 3 个单位距离,女孩每秒可以移动 1 个单位距离,男孩和女孩只能朝上下左右四个方向移动。
  • 每个鬼占据的区域每秒可以向四周扩张 2 个单位距离,并且无视墙的阻挡,也就是在第 k 秒后所有与鬼的曼哈顿距离不超过 2k 的位置都会被鬼占领。
  • 注意: 每一秒鬼会先扩展,扩展完毕后男孩和女孩才可以移动。
  • 求在不进入鬼的占领区的前提下,男孩和女孩能否会合,若能会合,求出最短会合时间。
  • 1<n,m<800

思路 :

  • 使用双向BFS算法。建立两个队列,分别从男孩的初始位置、女孩的初始位置开始进行BFS,两边轮流进行
  • 在每一轮中,男孩这边BFS三层(可以移动三步),女孩这边BFS一层(可以移动一步),使用数组d记录每个位置对于男孩和女孩的可达性
  • 当然,在BFS的每次扩展时,注意实时计算新状态与鬼之间的曼哈顿距离,如果已经小于等于当前轮数(即秒数)的2倍,那么就判定这个新状态不合法不再记录或入队
  • 在BFS的过程中,第一次出现某个位置(x, y)即能被男孩到达,也能被女孩到达时,当前轮数就是两人的最短相会时间
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 810;

int n, m;
char g[N][N];
int st[N][N];
PII boy, girl;
PII ghost[2];

bool check(int x, int y, int step) {
    if (x < 0 || x >= n || y < 0 || y >= m || g[x][y] == 'X' || g[x][y] == 'Z') return false;
    for (int i = 0; i < 2; ++ i) {
        if (abs(x - ghost[i].first) + abs(y - ghost[i].second) <= 2 * step)
            return false;
    }
    return true;
}
int bfs() {
    memset(st, 0, sizeof st);
    int cnt = 0;
    
    int dx[] = {1, 0, -1, 0}, dy[] = {0, 1, 0, -1};
    
    for (int i = 0; i < n; ++ i) {
        for (int j = 0; j < m; ++ j) {
            if (g[i][j] == 'M') boy = {i, j};
            else if (g[i][j] == 'G') girl = {i, j};
            else if (g[i][j] == 'Z') ghost[cnt ++ ] = {i, j};
        }
    }
    
    queue<PII> qb, qg;
    qb.push(boy);
    qg.push(girl);
    int step = 0;
    
    while (qb.size() || qg.size()) {
        ++ step;
        
        for (int i = 0; i < 3; ++ i) {
            for (int j = 0, len = qb.size(); j < len; ++ j) {
                PII t = qb.front(); qb.pop();
                int x = t.first, y = t.second;
                if (!check(x, y, step)) continue;
                for (int k = 0; k < 4; ++ k) {
                    int a = x + dx[k], b = y + dy[k];
                    if (!check(a, b, step)) continue;
                    if (st[a][b] == 2) return step;
                    if (!st[a][b]) {
                        st[a][b] = 1;
                        qb.push({a, b});
                    }
                }
            }
        }
        for (int i = 0; i < 1; ++ i) {
            for (int j = 0, len = qg.size(); j < len; ++ j) {
                PII t = qg.front(); qg.pop();
                int x = t.first, y = t.second;
                if (!check(x, y, step)) continue;
                for (int k = 0; k < 4; ++ k) {
                    int a = x + dx[k], b = y + dy[k];
                    if (!check(a, b, step)) continue;
                    if (st[a][b] == 1) return step;
                    if (!st[a][b]) {
                        st[a][b] = 2;
                        qg.push({a, b});
                    }
                }
            }
        }
    }
    return -1;
}

int main() {
    int _; scanf("%d", &_);
    while (_ -- ) {
        scanf("%d%d", &n, &m);
        for (int i = 0; i < n; ++ i) scanf("%s", g[i]);
        printf("%d\n", bfs());
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2302.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

程序员的数学好难学?一切从基础开始!

数学知识对编程很有用&#xff0c;但是很多写给程序员的数学书都比较难。我们为什么不从基础的数学知识开始学习呢&#xff1f; 程序员的数学基础 Python实战 1.本书的重点不在于如何解题&#xff0c;而在于帮助读者在计算机世界里如何利用数学解决算法问题&#xff0c;让程序…

WinForm应用实战开发指南 - 教你如何实现表头的全选操作?

WinForms分页控件&#xff0c;在很多场合都需要用到&#xff0c;由于整合较多的功能操作&#xff0c;使用起来效果更好&#xff0c;界面统一性也比较一致。其中的勾选操作&#xff0c;在有些场合下&#xff0c;也是比较有用的&#xff0c;因此提供该功能的整合。 PS&#xff1…

基于改进神经网络的风电功率预测(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑…

【K8S系列】Kubernetes 之kubectl 常用命令汇总

目录 一、kubetcl简单介绍 二、命令介绍 详细介绍&#xff1a; 三、部分命令详细介绍 3.1 create 3.2 get 3.3 describe 3.4 rolling-update 3.5 exec 3.6 log kubectl 是 Kubernetes 自带的客户端&#xff0c;可以用它来直接操作 Kubernetes 集群。 日常在使用 Kuber…

Swift 周报 第十六期

前言 本期是 Swift 编辑组自主整理周报的第七期&#xff0c;每个模块已初步成型。各位读者如果有好的提议&#xff0c;欢迎在文末留言。 欢迎投稿或推荐内容。目前计划每两周周一发布&#xff0c;欢迎志同道合的朋友一起加入周报整理。 当你来到双水村以外的大世界&#xff…

操作系统实验二死锁避免之银行家算法的模拟

文章目录 死锁 &#xff08;1&#xff09;定义 &#xff08;2&#xff09;死锁产生的原因 &#xff08;3&#xff09;死锁产生的必要条件 &#xff08;4&#xff09;死锁的处理策略 银行家算法 &#xff08;1&#xff09;核心思想 &#xff08;2&#xff09;数据结构 &#x…

2.2 Pycharm 的使用

文章目录1. PyCharm 安装2. Python 项目3. 外貌设置4. 配色方案5. 字体大小6. 自动换行7. 汉化8. 翻译插件9. 添加多个解释器10. Pycharm 常用快捷键11. 自定义文件模板内容12. 前端代码运行浏览器13. 关闭 with open 提示14. 双击shift查找15. 导出配置导入配置1. PyCharm 安装…

Gwas实战分析3_群体结构增加

1.sh plink 格式转化 plink1.map/plink1.ped ------plink2.bim/fam/bed plink --file 1001genomes_snps_only_ACGTN1 --make-bed --out plink2 2.sh 群体结构分析 时间过久&#xff1a; for K in 2 3 4 5 6 7 8 9 10; do admixture --cv plink2.bed K∣teeadmixtrueK | tee…

HTML小游戏4 —— 简易版英雄联盟(附完整源码)

&#x1f482; 网站推荐:【神级源码资源网】【摸鱼小游戏】&#x1f91f; 风趣幽默的前端学习课程&#xff1a;&#x1f449;28个案例趣学前端&#x1f485; 想寻找共同学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】&#x1f4ac; 免费且实用的计算机相关知…

基于C#实现的在线聊天室的桌面系统软件

资源下载地址&#xff1a;https://download.csdn.net/download/sheziqiong/86863237 资源下载地址&#xff1a;https://download.csdn.net/download/sheziqiong/86863237 目录 个人聊天室软件 1 需求分析与概要设计 1 项目说明 1 1.1. 项目目标&#xff1a; 1 1.2. 软硬件环境…

庖丁解牛 指针的高端操作

本章重点 写在前面 1.字符指针 2.指针数组 3.数组指针 3.1数组指针的定义 3.2 &数组名VS数组名 3.3 数组指针的使用 二维数组与数组指针 4.数组参数和指指针参数 4.1一维数组传参 4.2 二维数组传参 4.3一级指针传参 4.4二级指针传参 5.函数指针 5.1函数指针的…

SSM基于小程序的医院预约挂号系统 毕业设计-附源码260839

SSM医院预约挂号小程序的设计与实现 摘 要 信息化社会内需要与之针对性的信息获取途径&#xff0c;但是途径的扩展基本上为人们所努力的方向&#xff0c;由于站在的角度存在偏差&#xff0c;人们经常能够获得不同类型信息&#xff0c;这也是技术最为难以攻克的课题。针对医院排…

Prometheus系列(2)之EC2安装Node端

目标 为Prometheus安装Node程序。 步骤 node exporter程序 wget https://github.com/prometheus/node_exporter/releases/download/v1.4.0/node_exporter-1.4.0.linux-amd64.tar.gz tar xvzf node_exporter-1.4.0.linux-amd64.tar.gz cd node_exporter-1.4.0.linux-amd64/s…

技术分享 | 专项测试技术初识Hook

本文节选自霍格沃兹测试学院内部教材Hook 技术需要预先分析目标应用的源代码和逻辑&#xff0c;根据目标测试场景设置目标、逻辑和数据&#xff0c;然后运行时动态的对目标函数参数值、逻辑或者返回值做修改&#xff0c;达到修改现有函数逻辑、实现目标测试场景的目的。 Hook的…

汇编语言指令

文章目录算术运算指令ADDADDISUB伪指令LUILIAUIPCLA逻辑运算指令内存读写指令条件分支指令无条件跳转指令算术运算指令 ADD 语法ADD RD&#xff0c;RS1,RS2例子add x5,x6,x7x5x6x7编码格式&#xff1a;R-type opcode(7):0110011(OP) 从RS里面取出数据&#xff0c;把里面的数据…

【MyBatis框架】关联映射

关系映射1. 关联映射概述2. 环境搭建3.处理字段名和属性名不一致的情况4. 处理一对一映射5. 处理多对一映射5.1 级联方式处理5.2 使用association处理映射关系5.3 分步查询6. 处理一对多查询7. 小结1. 关联映射概述 在关系型数据库中&#xff0c;多表之间存在着三种关联关系&a…

Linux文件打包及压缩、解包及解压

目录 前言 什么是压缩&#xff1f; tar的介绍与使用 简介 打包压缩文件 打包文件&#xff08;不压缩&#xff09; gzip压缩类型压缩文件 bzip压缩类型压缩文件 xzip压缩类型压缩文件 解包解压文件 简介 解压缩&#xff08;解压到当前目录&#xff09; 解压缩&#x…

线代 | 【提神醒脑】自用笔记串联

一、初等变换 1、初等行变换与方程组的同解变换 2、初等行变换关系网 ※ 3、关联结论 —— 同解方程 4、行、列变换适用场景

Java 线程池之ThreadPoolExecutor学习总结

前提 java version "1.8.0_25" 池简述 软件开发活动中&#xff0c;我们经常会听到数据库连接池、内存池、线程池等各种“池”概念&#xff0c;这些“池”到底是什么东西呢&#xff1f;程序的世界里&#xff0c;我们可以将池简单的理解为一种容器类数据结构&#x…

哪本计算机书籍,让你有了醍醐灌顶突然开悟的感觉?

计算机书籍每年都会出版很多&#xff0c;但是能影响几代程序员的有这几本书&#xff0c;推荐一下&#xff0c;肯定让你有醍醐灌顶的开悟的感觉。 1、重构 改善既有代码的设计&#xff08;第2版 平装版&#xff09; 豆瓣评分&#xff1a;9.2 本书是一本为专业程序员编写的重构指…