340 通信线路(单源最短路径 + 二分)

1. 问题描述:

在郊区有 N 座通信基站,P 条双向电缆,第 i 条电缆连接基站 Ai 和 Bi。特别地,1 号基站是通信公司的总站,N 号基站位于一座农场中。现在,农场主希望对通信线路进行升级,其中升级第 i 条电缆需要花费 Li。电话公司正在举行优惠活动。农产主可以指定一条从 1 号基站到 N 号基站的路径,并指定路径上不超过 K 条电缆,由电话公司免费提供升级服务。农场主只需要支付在该路径上剩余的电缆中,升级价格最贵的那条电缆的花费即可。求至少用多少钱可以完成升级。

输入格式

第 1 行:三个整数 N,P,K。第 2..P+1 行:第 i+1 行包含三个整数 Ai,Bi,Li。

输出格式

包含一个整数表示最少花费。若 1 号基站与 N 号基站之间不存在路径,则输出 −1。

数据范围

0 ≤ K < N ≤ 1000,
1 ≤ P ≤ 10000,
1 ≤ Li ≤ 1000000

输入样例:

5 7 1
1 2 5
3 1 4
2 4 8
3 2 3
5 2 9
3 4 7
4 5 6

输出样例:

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

2. 思路分析:

分析题目可以知道这道题目的背景还是挺复杂的,而且题目中其实要求的是最小化最大花费,也即找到一条路径使得这条路径去掉最多k条权重最大的边之后剩余的边的权重的最大值最小,对于"最大最小问题"的题目,一般可以使用二分来解决,我们需要检验一下是否可以使用二分来解决,一般可以使用二分来解决的题目都有有一个特点,我们需要找到一种性质,这种性质可以使得答案确定在mid为分界线的左边还是右边,怎么样找这个性质呢?其实可以发现我们需要判断从1~N中最少经过权重大于x的边的数量是否小于等于k即可(最少经过的边的数目可以使用最短路径来解决),如果二分的区间为[0,10 ^ 6 + 1],答案在x这个位置,那么x位置的后面元素肯定满足数量小于等于k,对于x左边的区间则可以使用反证法来证明,如果x左边存在一个更小的元素使得满足数量小于等于k那么与答案x就矛盾了所以x左边的区间也是满足要求的,所以可以使用二分来枚举答案,在二分枚举mid判断是否满足当前图中大于mid的数量是否小于等于k,如果满足说明答案是在左边(因为可以去掉最多k条边,所以答案可以更小),否则答案是在右边;这样问题就转化为了如何在无向图中边的权重求解大于等于mid的数量,其实我们可以对图中的边进行分类,当大于mid的时候将权重设置为1,小于等于mid的边的权重设置为0,然后求解从1到N的最短路径即可(这一点很巧妙),因为边的权重只有0和1所以在求解最短路径的时候可以使用双端队列广搜的方法。为什么二分的区间为[0,10 ^ 6 + 1]呢?还是按照定义出发,因为答案可以是0的,也即可以去掉所有的边,因为边的权重最大为10 ^ 6,我们在二分枚举的时候最终无解的时候答案也是10 ^ 6,这个时候就无法区分有解的情况答案是10 ^ 6还是无解的情况最终到达的10 ^ 6,为了区分属于哪一种情况所以可以多枚举一个端点也即10 ^ 6 + 1,其实可以发现,可以发现题目的核心还是如何分析问题。

3. 代码如下:

from typing import List


class Solution:
    def check(self, mid: int, k: int, n: int, g: List[List[int]]):
        # 使用dijkstra算法求解最短路径中大于mid的边的数目是否小于等于k, 因为权重只有0或者1也可以使用双端队列进行广搜的方法求解最短路径
        INF = 10 ** 10
        dis = [INF] * (n + 1)
        dis[1] = 0
        vis = [0] * (n + 1)
        for i in range(n):
            t = -1
            for j in range(1, n + 1):
                if vis[j] == 0 and (k == -1 or dis[t] > dis[j]):
                    t = j
            vis[t] = 1
            for next in g[t]:
                # 将边分为两大类, 第一类是大于mid的边那么权重为1第二类是小于等于mis的边权重为0
                x = 1 if next[1] > mid else 0
                dis[next[0]] = min(dis[next[0]], dis[t] + x)
        # 最短路径中权重大于mid的边的数目是否是小于等于x
        return dis[n] <= k

    def process(self):
        n, m, k = map(int, input().split())
        g = [list() for i in range(n + 1)]
        for i in range(m):
            x, y, z = map(int, input().split())
            # 因为是无向边所以需要创建两次
            g[x].append((y, z))
            g[y].append((x, z))
        l, r = 0, 10 ** 6 + 1
        while l < r:
            mid = l + r >> 1
            # 如果小于等于k说明答案肯定是在左边, 否则是在右边
            if self.check(mid, k, n, g):
                r = mid
            else:
                l = mid + 1
        # 二分的时候增加右端点的原因是为了判断是有解的情况还是无解的情况如果是有解的情况那么r最多是10 ** 6
        if r == 10 ** 6 + 1: return -1
        return r


if __name__ == "__main__":
    print(Solution().process())
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值