AcWing 175 电路维修

题目描述:

达达是来自异世界的魔女,她在漫无目的地四处漂流的时候,遇到了善良的少女翰翰,从而被收留在地球上。

翰翰的家里有一辆飞行车。

有一天飞行车的电路板突然出现了故障,导致无法启动。

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

电路.png

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

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

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

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

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

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

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

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

输入格式

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

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

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

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

输出格式

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

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

数据范围

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

输入样例:

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

输出样例:

1

样例解释

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

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

电路2.png

分析:

我们知道BFS常用于求解边权相同的最短路问题,但是部分边权不等的最短路问题也可以用BFS求,本题就是其中的一个例子。

当一个图的边权只可能是0或者1时,就可以使用双端队列BFS求解。本题求从左上角到右下角要想连通的最小旋转次数,一个方格的对角线之间,如果有连线,就可以被视为这两点之间的边权长度是0,如果需要旋转才有连线,则视边权的长度为1,所以本题就转化为了一个边权只有01两种情况的最短路问题。为什么BFS可以解决边权不等的最短路问题,可以说是此时BFS的队列相当于dijkstra算法中的优先级队列,是单调的,也是由BFS队列中元素的两段性和单调性决定的。

首先回忆下dijkstra算法,AcWing 849 Dijkstra求最短路 IAcWing 850 Dijkstra求最短路 II中分别介绍了dijkstra算法的朴素版解法和堆优化版解法。简单的说,d[i]表示第i个点到起点的距离,dijkstra算法的思路就是先将起点加入点集,然后在点集中取出一个d最小的点,去更新相邻点的d数组,如果某个点被松弛了,就加入优先级队列,也就等价于加入了点集,已经出队的点不再参与更新操作,每次取还未出队的离起点距离最小的点取进行松弛操作。

再说下BFS的两段性和单调性,两段性可以理解为BFS过程中用到的队列中的顶点最多涉及BFS生成树中两层顶点,因为BFS相当于对树做层次遍历,这个性质是显然的,两段性意味着任何时刻队列中的点离起点的距离差最多是1。单调性指的是从队头到队尾的顶点离起点的距离是单调增加的。两段性和单调性保证了BFS算法求出的一定是最短路径。本题要考虑的是如何在边权不等的情况下维持队列的这两个性质,边权有01之分,遍历到边权为0和1的边我们要如何如何处理才能够维持队列的单调性。取出队头元素后,如果当前边权是0,就加入队头;当前边权是1,就加入队尾,很显然这种操作维持了队列的两段性和单调性,只是需要双端队列实现而已。

既然知道了这样操作在BFS过程中队列始终是单调的,并且队头元素一定是距离最近的,下面要做的就是仿照dijkstra算法求最短路了,这里的双端队列就相当于dijkstra中的优先级队列。流程如下:

首先,将起点加入队列,接着当队列非空时执行如下循环:

取出队头元素,如果队头元素还没有出队过,就去更新周围点的最短距离,如果周围的点被松弛了,就将该点也加入队列,如果松弛的边权是0,就加入队头,否则加入队尾。

当然,本题的原理部分只有上面这么多,但是实现的细节值得注意的地方却很多。首先,由于只能斜着走,所以横纵坐标要么同时加1,要么同时减1,要么一个加1一个减1。不论是那种,从起点(0,0)出发,能够到达的点的横纵坐标之和一定是偶数,所以当R + C是奇数时,就无解。第二个要注意的是,题目给定的是格子内部的线的方向,一共有R * C个格子,但是一个有(R+1)*(C + 1)个点,我们读取线只需要从(0,0)读到(r-1,c-1),求的是(0,0)到(r,c)的最短距离,这点一定要注意。第三点,本题将方向向量玩出了新花样,从左边(x,y)出发,能够到达哪些点呢?可以到达的点的方向向量是dx[] = {-1,-1,1,1};dy[] = {-1,1,1,-1},毫无疑问。要想到达这四点需要经过哪些坐标的格子呢?不难推出需要经过格子的方向向量依次是ix[] = {-1,-1,0,0};iy[] = {-1,0,0,-1};然后这四个格子中的线是朝什么方向的才可以不用旋转,char op[] = "\\/\\/";,就是op中定义的这四个方向,注意\前面要加转义字符\。最后一点,也是相当重要的一点,标志数组st应该在何处剪枝,是否需要剪枝?三个地方可以放置剪枝的语句,第一个地方,取出队头元素时,如果队头元素已经出过队,就不用再去执行松弛操作了,这也是推荐加入剪枝语句的地方;第二个地方,在判断某个点距离是否可以被更新前,如果一个点已经出过队,说明最短距离已经确定,松弛操作必将是徒劳的,所以可以剪枝,但是效果不大,因为并没有节省多少操作;第三个地方,松弛操作后,将被松弛的点加入队列前,如果这个点出过队,就不再入队,这个地方剪枝毫无意义,因为已经出过队的元素最短路径长度已经确定了,不可能再被松弛。既然出过队的元素不会再背松弛,自然不会再次被加入队列,这是否意味着每个顶点只会入队一次呢?如果是,剪枝操作将毫无意义,我开始也是这样以为的,加不加剪枝语句提交时间都差不多,但这仅仅是数据水的缘故,为此,还特地问了下y总。之后才想起来,尽管每个顶点出队后不会再次被松弛,不会再入队,但是出队前却可能多次入队,比如C点被A点松弛先入队了,之后又被B点松弛,再次入队,然后C第一次出队,执行松弛操作,当C第二次出队时,因为标志数组是true,便会跳过C,此时的松弛操作毫无意义,而且耗费时间,这也是为何要在出队时剪枝的原因。这里的剪枝操作dijkstra也同样适用,或者说就是由dijkstra的剪枝操作推广来的。

#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 505;
typedef pair<int,int> PII;
char g[N][N];
int r,c,d[N][N];
deque<PII> q;
int dx[] = {-1,-1,1,1};
int dy[] = {-1,1,1,-1};
int ix[] = {-1,-1,0,0};
int iy[] = {-1,0,0,-1};
char op[] = "\\/\\/";
bool st[N][N];
void bfs(int x,int y){
    memset(d,0x3f,sizeof d);
    memset(st,false,sizeof st);
    q.push_front({x,y});
    d[x][y] = 0;
    while(q.size()){
        PII t = q.front();
        q.pop_front();
        if(st[t.first][t.second])   continue;
        st[t.first][t.second] = true;
        for(int i = 0;i < 4;i++){
            int nx = t.first + dx[i],ny = t.second + dy[i];
            if(nx < 0 || nx > r || ny < 0 || ny > c)  continue;
            int ex = t.first + ix[i],ey = t.second + iy[i];
            int dist = d[t.first][t.second] + (g[ex][ey] != op[i]);
            if(dist < d[nx][ny]){
                d[nx][ny] = dist;
                if(g[ex][ey] == op[i])  q.push_front({nx,ny});
                else    q.push_back({nx,ny});
            }
        }
    }
}
int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        scanf("%d%d",&r,&c);
        for(int i = 0;i < r;i++)    scanf("%s",g[i]);
        if((r + c) & 1) puts("NO SOLUTION");
        else{
            bfs(0,0);
            printf("%d\n",d[r][c]);
        }
    }
    return 0;
}

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值