通信线路([USACO08JAN] Telephone Lines S)(二分,01最短路)
通信线路([USACO08JAN] Telephone Lines S)(二分,01最短路)
今天练习最短路题目时遇到了一道比较有意思的题:通信线路([USACO08JAN] Telephone Lines S的简化版)。我们来看看吧:
题目描述
在郊区有 $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题意
给了一张边权无向图,要寻找从$1$到$N$的最短路,最短路上可以不计$K$条边的权重,只计剩下边的最大权重,找出这个最大权重的最小值。
思考
刚读完这道题后,我怀疑了一下,这道题真的是最短路问题吗?因为这种可以不计某些边的权重的最短路问题我只在分层图见过。对于这道题同样可以考虑一下分层图的做法。
分层图做法
思考:既然可以免去$K$条边,那就建$K+1$层图,再去跑最短路算法,最后每层$N$点的最小值就是答案了。不过我很快就打消了这个念头,因为这里的$K$最大能有$1000$,这么多层图,时间空间复杂度都很大,大概率超时。
但是,后来我看题解时发现确实能用分层图做,建完图之后跑一遍SFPA就可以了。这种做法时间复杂度$O(npk)$,数据严一点就会超时,所以并不是最优解。
别样的Dijkstra?
最终答案可以抽象为某条路上第$K+1$大的权重,我们能不能从这里入手呢?考虑对Dijkstra进行变形,每次计算的路程是截止当前节点第$K+1$大的权重,这样最后应该能找到答案。不过有个问题:每次该怎么计算第$K+1$大的权重是多少。我们似乎只能用一个数据结构把路给记下来,甚至每次还要查找第$K+1$大的权重。这样的话,好复杂,而且貌似还容易超时。
怎么办呢,没有办法了吗Orz。
二分
这道题有一个特点就是可以免费K条边,为了找最小值我们当然希望让最大的几条边免费。那么摒弃传统的最短路思想,我们只需要找一个阈值权重,这个阈值使得路上大于它的边的数量不大于K,并且这个阈值要尽可能的小。转化到这里,其实已经是一个很经典的二分问题了,二分待定阈值,每次检测该值是否符合要求,符合则继续缩小…是的,这简直可以套一个二分的模板!
接下来就是怎么检查了,以待检查值为阈值,大于它的为$1$,否则为$0$,转换成一个01最短路问题,我们就可以找出以$x$为阈值,大于$x$的边最少的一条路了。那个$x$就是我们要的答案!
代码
二分部分
这里就是很普通的二分写法了,用位运算除$2$。
while(l<=r){
int mid = (r+l)>>1;
if(check(mid)){
r = mid-1;
res = mid;
}
else l = mid+1;
}check函数
这里定义了整数对PII,即pair<int,int>。这样就不用再为优先队列重载运算符了,不过要注意的是,first是总权重,second是节点编号值,千万不要搞反了,这会影响优先队列的正常运作。
priority_queue<PII, vector<PII>, greater<PII>> hp;
bool check(int x){
// cout << "checking " << x << "\n";
int dis[1005];
fill(dis, dis+1+n, 1e9);
bool u[1005] = {false};
hp.push({0,1});
while(!hp.empty()){
PII cp = hp.top();
hp.pop();
if(u[cp.second]) continue;
u[cp.second] = true;
dis[cp.second] = cp.first;
if(!rs[cp.second].empty()){
for(r cr : rs[cp.second]){
if(u[cr.t]) continue;
if(cr.l>x) hp.push({cp.first+1, cr.t});
else hp.push({cp.first, cr.t});
}
}
}
if(dis[n]<=k) return true;
else return false;
}最后给出完整代码:
#include <iostream>
#include <queue>
#include <algorithm>
#include <cstring>
#include <vector>
#include <utility>
#define int long long
using namespace std;
typedef pair<int,int> PII;
struct r{
int t;
int l;
};
vector<r> rs[1005];
int n, p, k;
priority_queue<PII, vector<PII>, greater<PII>> hp;
bool check(int x){
// cout << "checking " << x << "\n";
int dis[1005];
fill(dis, dis+1+n, 1e9);
bool u[1005] = {false};
hp.push({0,1});
while(!hp.empty()){
PII cp = hp.top();
hp.pop();
if(u[cp.second]) continue;
u[cp.second] = true;
dis[cp.second] = cp.first;
if(!rs[cp.second].empty()){
for(r cr : rs[cp.second]){
if(u[cr.t]) continue;
if(cr.l>x) hp.push({cp.first+1, cr.t});
else hp.push({cp.first, cr.t});
}
}
}
if(dis[n]<=k) return true;
else return false;
}
signed main(){
cin >> n >> p >> k;
for(int i=1;i<=p;++i){
int a, b, l;
cin >> a >> b >> l;
rs[a].push_back({b,l});
rs[b].push_back({a,l});
}
int l=0;
int r = 1e6+1;
int res = 1e6;
while(l<=r){
int mid = (r+l)>>1;
if(check(mid)){
r = mid-1;
res = mid;
}
else l = mid+1;
}
if(res==1e6) cout << -1;
else cout << res;
return 0;
}