175 电路维修(双端队列广搜)

1. 问题描述:

达达是来自异世界的魔女,她在漫无目的地四处漂流的时候,遇到了善良的少女翰翰,从而被收留在地球上。翰翰的家里有一辆飞行车。有一天飞行车的电路板突然出现了故障,导致无法启动。电路板的整体结构是一个 R 行 C 列的网格(R,C≤500),如下图所示:

每个格点都是电线的接点,每个格子都包含一个电子元件。电子元件的主要部分是一个可旋转的、连接一条对角线上的两个接点的短电缆。在旋转之后,它就可以连接另一条对角线的两个接点。电路板左上角的接点接入直流电源,右下角的接点接入飞行车的发动装置。达达发现因为某些元件的方向不小心发生了改变,电路板可能处于断路的状态。她准备通过计算,旋转最少数量的元件,使电源与发动装置通过若干条短缆相连。不过,电路的规模实在是太大了,达达并不擅长编程,希望你能够帮她解决这个问题。注意:只能走斜向的线段,水平和竖直线段不能走。

输入格式

输入文件包含多组测试数据。第一行包含一个整数 T,表示测试数据的数目。对于每组测试数据,第一行包含正整数 R 和 C,表示电路板的行数和列数。之后 R 行,每行 C 个字符,字符是"/"和"\"中的一个,表示标准件的方向。

输出格式

对于每组测试数据,在单独的一行输出一个正整数,表示所需的最小旋转次数。如果无论怎样都不能使得电源和发动机之间连通,输出 NO SOLUTION。

数据范围

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

输入样例:
1
3 5
\\/\\
\\///
/\\\\

输出样例:
1
样例解释
样例的输入对应于题目描述中的情况。
只需要按照下面的方式旋转标准件,就可以使得电源和发动机之间连通。

来源:https://www.acwing.com/problem/content/description/177/

2. 思路分析:

分析题目可以知道我们只能够走斜线的方向,需要求解从左上角到右下角的最短距离,其中如果可以从当前格点的位置走到另外一个格点的位置那么边权为0,不能够直接走过去则需要旋转那么边权为1,所以相当于给我们一个无向图,图中边权为0和1,需要求解从起点到终点的最短距离,如果到达不了终点那么输出"NO SOLUTION",否则输出最短距离。对于边权只有0和1的从起点到终点的最短路径问题有一个比较经典的做法:双端队列广搜。双端队列广搜的做法与普通的宽搜只有一点不同,双端队列在弹出队首元素之后需要判断当前扩展的边的权重是0还是1,如果权重为0那么将节点加入到队首,权重为1则将元素加入到队尾,为什么这样做是正确的呢?其实对于bfs求解的最短路径问题,都可以将其转化为图论中dijkstra算法中的最短路径问题,一般需要证明两个方向:① 两段性;② 单调性;可以发现我们在使用队首元素进行扩展的时候,权重为0的元素会加入到队首,权重为1的元素会加入到队尾,这样整个队列相当于dijkstra算法中堆,满足两段性和单调性的要求,而dijkstra算法是正确的,所以双端队列广搜也是正确的,这样在搜索的时候队首元素离当前位置的距离一定是最短的,由于这里存在边权为0和边权为1所以类似于dijkstra的堆优化版的最短路径算法,需要使用一个标记数组st来记录当前元素是否在队列中(双向广搜不像普通的bfs,元素是可能多次入队的,所以需要使用一个标记数组来标记是否已经在队列中防止重复更新元素)。这里在使用双端队列广搜的时候需要注意格点的坐标与真实的坐标,我们是通过每个格点的坐标进行搜索的,从初始格点位置(0,0)开始搜索直到终点位置(n,m)结束,通过格点位置坐标确定到达当前格点对应的真实坐标对应的权重,然后计算到达当前格点的最短距离,举一个例子就比较好理解了:

3. 代码如下:

import collections
from typing import List


class Solution:
    # 本质上是dijkstra算法堆优化的简化版
    def bfs(self, n: int, m: int, g: List[str]):
        # deque为python的双端队列
        q = collections.deque([(0, 0)])
        # 类似于dijkstra算法的优化版, 需要声明一个标记数组用来标记是否在队列中, 防止重复更新
        st = [[0] * (m + 10) for i in range(n + 10)]
        # 格点坐标的偏移量
        d1 = [[-1, -1], [-1, 1], [1, 1], [1, -1]]
        # 通过格点的坐标计算一个格点走到另外一个格点对应的字符
        d2 = [[-1, -1], [-1, 0], [0, 0], [0, -1]]
        INF = 10 ** 10
        # dis记录最短距离
        dis = [[INF] * (m + 10) for i in range(n + 10)]
        dis[0][0] = 0
        # 格点对应的四个方向的正确走向, "\"属于转义字符所以需要两个斜杠, 分别是向左上, 右上, 右下, 左下的方向进行扩展, 如果发现两个格点走向与当前循环的cs走向一致说明可以直接走过去
        cs = ["\\", "/", "\\", "/"]
        while q:
            x, y = q.popleft()
            if st[x][y] == 1: continue
            # 标记已访问
            st[x][y] = 1
            for i in range(4):
                # 注意格点的坐标是大于n或者是大于m才是越界了, (a, b)是由(x, y)扩展的格点坐标
                a, b = x + d1[i][0], y + d1[i][1]
                if a < 0 or a > n or b < 0 or b > m: continue
                # ca, cb为扩展之后对应的格子的坐标, 计算ca, cb是为了计算到达当前格点坐标对应的权重, ca, cb为格点(x, y)转移到当前格点(a, b)对应的格子坐标
                ca, cb = x + d2[i][0], y + d2[i][1]
                w = g[ca][cb] != cs[i]
                d = dis[x][y] + (1 if w else 0)
                if d < dis[a][b]:
                    dis[a][b] = d
                    if not w:
                        # 权重为0加入到队首
                        q.appendleft((a, b))
                    else:
                        # 权重为1加入到队尾
                        q.append((a, b))
        return dis[n][m] if dis[n][m] != INF else "NO SOLUTION"

    def process(self):
        # T组测试数据
        T = int(input())
        while T > 0:
            n, m = map(int, input().split())
            g = list()
            for i in range(n):
                g.append(input())
            print(self.bfs(n, m, g))
            T -= 1


if __name__ == '__main__':
    Solution().process()

Guess you like

Origin blog.csdn.net/qq_39445165/article/details/121659863