3728 城市通电(prim算法--添加超级源点)

1. 问题描述:

平面上遍布着 n 座城市,编号 1∼n。第 i 座城市的位置坐标为 (xi,yi)。不同城市的位置有可能重合。现在要通过建立发电站和搭建电线的方式给每座城市都通电。一个城市如果建有发电站,或者通过电线直接或间接的与建有发电站的城市保持连通,则该城市通电。在城市 i 建立发电站的花费为 ci 元。在城市 i 与城市 j 之间搭建电线所需的花费为每单位长度 ki + kj 元。电线只能沿上下左右四个方向延伸,电线之间可以相互交叉,电线都是双向的。每根电线都是由某个城市沿最短路线搭建到另一个城市。也就是说,如果在城市 i 与城市 j 之间搭建电线,则电线的长度为 |xi−xj| + |yi−yj|。请问,如何合理设计通电方案,可以使得所有城市都成功通电,且花费最少?输出最少花费和具体方案。如果方案不唯一,则输出任意一种合理方案均可。

输入格式

第一行包含整数 n。接下来 n 行,其中第 i 行包含两个整数 xi,yi,用来描述城市 i 的横纵坐标。再一行包含 n 个整数 c1,c2,…,cn,用来描述每个城市建立发电站的花费。最后一行包含 n 个整数 k1,k2,…,kn。

输出格式

第一行输出所需要的最少花费。第二行输出一个整数 v,表示需要建立发电站的数量。第三行输出 v 个整数,表示建立发电站的城市编号,注意输出编号要在范围 [1,n] 内。且输出编号不应重复。输出编号顺序随意。第四行输出一个整数 e,表示需要搭建的电线数量。接下来 e 行,每行输出两个整数 a,b,表示要在城市 a 和 b 之间搭建电线。注意,任意两个城市之间最多只需要搭建一根电线,也就是说,对于每个 (a,b),不要有多余的 (a,b) 或 (b,a) 输出。a 和 b 不能相同,且要在范围 [1,n] 内。输出电线顺序随意。如果答案不唯一,输出任意合理方案即可。

数据范围

对于前三个测试点,1 ≤ n ≤ 3。
对于全部测试点,1 ≤ n ≤ 2000,1 ≤ xi,yi ≤ 10 ^ 6,1 ≤ ci,ki ≤ 10 ^ 9。

输入样例1:

3
2 3
1 1
3 2
3 2 3
3 2 3

输出样例1:

8
3
1 2 3 
0

输入样例2:

3
2 1
1 2
3 3
23 2 23
3 2 3

输出样例2:

27
1

2
1 2
2 3
来源:https://www.acwing.com/problem/content/description/3731/

2. 思路分析:

分析题目可以知道我们需要让所有的点联通起来,本质上考察的是最小生成树模型(题目一般会提示"直接"连通或者是间接连通),最小生成树的思想是从一个点出发,然后往周围进行扩展,将最靠近连通块的点扩展到当前的连通块中,类似于dijkstra算法,这道题目比较恶心的点是需要记录具体的方案,我们可以声明两个数组dis,disp,其中dis记录某个点的最小花费,一开始的时候每一个点的最小花费为建立发电站的最小花费;如果disp[i] = 0说明当前点需要建立发电站,如果disp[i] = j说明当前点i需要与靠近的点j搭建电线,这样我们就可以将所有点分为两大类,建立发电站还是与其它点搭建电线,在遍历节点的过程中更新dis和disp的值即可。我们可以使用prim算法来解决,时间复杂度为O(n ^ 2);除了上面的做法之外,可以发现这道题目类似于1146题,由于城市的编号为1~n所以我们可以将编号为0的点作为超级源点,由于输入的不是对称矩阵所以有些细节需要特殊处理一下,对称矩阵会相对比较好处理。

一般最小生成树有以下三种模型:

  • 建立超级源点
  • 将若干个连通块连起来
  • 不需要让所有点连通,让其数值小于等于某一个定值

3. 代码如下:

一开始的时候每一个点的最小花费为wc[i],也即每一个点建立发电站的花费:

from typing import List


class Solution:
    # 求解两点之间架设电线的最小花费
    def getDis(self, a: int, b: int, p: List[tuple], wk: List[int]):
        return (abs(p[a][0] - p[b][0]) + abs(p[a][1] - p[b][1])) * (wk[a] + wk[b])

    def prim(self, n: int, dis: List[int], disp: List[int], p: List[tuple], wc: List[int], wk: List[int],
             res1: List[int], res2: List[tuple]):
        # res为使得所有的点连通的最小花费
        res = 0
        st = [0] * (n + 10)
        for i in range(n):
            k = -1
            for j in range(1, n + 1):
                if st[j] == 0 and (k == -1 or dis[k] > dis[j]):
                    k = j
            st[k] = 1
            res += dis[k]
            # disp[k] = 0表示当前这个点需要建立一个发电站, 因为这个点如果与其余点搭建电线那么花费更大所以自己建立一份发电站
            if disp[k] == 0:
                res1.append(k)
            # disp[k] != 0说明需要在最靠近的发电站架设电线
            else:
                res2.append((k, disp[k]))
            for j in range(1, n + 1):
                if st[j] == 0 and dis[j] > self.getDis(k, j, p, wk):
                    disp[j] = k
                    dis[j] = self.getDis(k, j, p, wk)
        return res

    def process(self):
        n = int(input())
        p = [(0, 0)]
        for i in range(n):
            x, y = map(int, input().split())
            p.append((x, y))
        wc, wk = [0] + list(map(int, input().split())), [0] + list(map(int, input().split()))
        dis = [0] * (n + 10)
        # 一开始的时候每一个城市的最小花费为这个城市建立一个发电站的花费
        for i in range(1, n + 1):
            dis[i] = wc[i]
        disp = [0] * (n + 10)
        res1, res2 = list(), list()
        res = self.prim(n, dis, disp, p, wc, wk, res1, res2)
        print(res)
        print(len(res1))
        for x in res1:
            print(x, end=" ")
        print()
        print(len(res2))
        for x in res2:
            print(x[0], x[1])


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

将编号为0的点看成是超级源点:

from typing import List


class Solution:
    def getDis(self, a: int, b: int, p: List[tuple], wk: List[int]):
        # 这里需要判断a是否是超级源点, 当a是编号为0的点的时候为超级源点, 此时最小花费为建立发电站的花费, 注意需要返回-1不能够返回0, 这样才可以判断是超级源点还是多个发电站重合的情况
        if a == 0: return -1
        return (abs(p[a][0] - p[b][0]) + abs(p[a][1] - p[b][1])) * (wk[a] + wk[b])

    def prim(self, n: int, p: List[tuple], wc: List[int], wk: List[int], res1: List[int], res2: List[int]):
        INF = 10 ** 18
        dis = [INF] * (n + 10)
        dis[0] = 0
        # disp是否等于-1可以用来判断需要建立发电站还是需要与其他的节点架设电线
        disp = [0] * (n + 10)
        st = [0] * (n + 10)
        res = 0
        # 注意是n + 1个点
        for i in range(n + 1):
            k = -1
            for j in range(n + 1):
                if st[j] == 0 and (k == -1 or dis[k] > dis[j]):
                    k = j
            st[k] = 1
            res += dis[k]
            if disp[k] == 0:
                # 注意不能够是超级源点这个点所以需要判断一下是否大于0
                if k != 0:
                    res1.append(k)
            else:
                res2.append((k, disp[k]))
            # 更新最短距离
            for j in range(1, n + 1):
                if st[j] == 0:
                    d = self.getDis(k, j, p, wk)
                    if d == -1:
                        dis[j] = wc[j]
                    elif d < dis[j]:
                        disp[j] = k
                        dis[j] = d
        return res

    def process(self):
        n = int(input())
        p = [(0, 0)]
        for i in range(n):
            x, y = map(int, input().split())
            p.append((x, y))
        wc = [0] + list(map(int, input().split()))
        wk = [0] + list(map(int, input().split()))
        res1, res2 = list(), list()
        res = self.prim(n, p, wc, wk, res1, res2)
        print(res)
        print(len(res1))
        for x in res1:
            print(x, end=" ")
        print()
        print(len(res2))
        for x in res2:
            print(x[0], x[1])


if __name__ == "__main__":
    Solution().process()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值