最近钻研些奇怪的最短路问题,被深深的恶心到了
在此想和大家分享一下
一.分层图最短路
大多数都是改变边权形式出题,比如使给予k次机会使某条边免费,但是,有时二分最短路也会出现删边,注意这里删去一条边是与使某条边免费不同的,这时就要看好它想要求什么:如果求改变k条边边权之后的最短路径,那么没跑了,就是你了!分层图!
那么分层图问题如何解决呢?
显然,对于不同的删除次数,我们可以将它想成神奇的平行空间,每个节点都对应着平行空间的节点,而对于删除一条边,我们可以想象成打开了一扇折跃门(集结部队法宝),从某扇折跃门到达目标地点的距离为0,这样,我们就可以在删除路径时直接通过折跃门到达多次删除的平行世界的对应点,最后只要找到到达终点的平行空间中步数最小的即可
像这样↓
那么如何实现呢?
这里有两种方式
1.使用二维dis数组,第一维下标表示从出发点到某一点的最短路,第二维表示改变的边数。存图以及最短路部分均为正常
但是
我们既然要实现改变边权,就一定要改动些内容(废话)
首先,我们如何将多层图存到优先队列中,可以考虑用当前坐标加上层数与n的乘积,可以避免坐标重叠并解决问题,而当我们需要调用时,易得当前层数等于second除以n,点绝对坐标等于second%n
并且,显然,当我们每次枚举到一条边的出度时,我们需要考虑这条边是否能删,只需在计算当前层数后进行一次改权,并与上一层图的原边权进行比较
最终便得出了下面的代码,由于本人的奇怪爱好,用的dijkstra还是vector存图,同学们慎用↓
#include <bits/stdc++.h>
using namespace std;
#define N 100005
#define pa pair<int, int>
int n, m, k, dis[N][15], used[N][15], s, t, res = 0x3f3f3f3f,minn[10005][10005];
struct edge {
int to, cost;
};
vector<edge> v[N];
void insert(int x, int y, int z) { v[x].push_back((edge){ y, z }); }
void dijkstra() {
memset(dis, 0x3f3f3f3f, sizeof(dis));
memset(used, 0, sizeof(used));
priority_queue<pa, vector<pa>, greater<pa> > q;
dis[s][0] = 0;
q.push(make_pair(0, s));
while (!q.empty()) {
int x = q.top().second;
q.pop();
int c = x / n;
x %= n;
if (used[x][c])
continue;
used[x][c] = 1;
for (int i = 0; i < v[x].size(); i++) {
int y = v[x][i].to;
int z = v[x][i].cost;
if (dis[y][c] > dis[x][c] + z) {
dis[y][c] = dis[x][c] + z;
q.push(make_pair(dis[y][c], y + c * n));
}
if (c == k)
continue;
if (dis[y][c + 1] > dis[x][c]) {
dis[y][c + 1] = dis[x][c];
q.push(make_pair(dis[y][c + 1], y + (c + 1) * n));
}
}
}
}
int main() {
scanf("%d%d%d%d%d", &n, &m, &k, &s, &t);
memset(minn,0x3f,sizeof(minn));
for (int i = 1; i <= m; i++) {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
/*if(minn[x][y]>z||minn[y][x]>z){
minn[x][y]=z;
minn[y][x]=z;*/
insert(x, y, z);
insert(y, x, z);
/*} else
continue;*/
}
dijkstra();
for (int i = 0; i <= k; i++) res = min(res, dis[t][i]);
printf("%d", res);
return 0;
}
例题:luoguP4568飞行路线(难度:3)
拓展与提高:P4009汽车加油行驶问题(难度:5)
2.可以只开一维数组,但是用数组的第x+c*n表示当改变c次边权时到达第x个点时的最短路,代码没写QAQ (代码不是我写给你们你们就会了的,你们自己不悟,我给你你们也不明白)
二.二分+最短路
简而言之 (水一波),I have a 二分,I have a 最短路,hey,二分最短路
一般是伴随着边的价值和选择道路
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=10005;
const int INF=0x3f3f3f3f;
typedef pair<int,int> pa;
struct edge{
int to,cost;
};
int dis[N],link[N];
int n,m,k,ans,cnt;
bool used[N];
vector<edge>v[N];
//vector存储 pr优化
void insert(int x,int y,int z){
edge e;
e.to=y;
e.cost=z;
v[x].push_back(e);
}
bool dijkstra(int o){
//priority_queue<pa,vector<pa>,greater<pa> >q;
queue<int>q;
memset(dis,0x3f,sizeof(dis));
dis[1]=0;
//q.push(make_pair(0,s));
q.push(1);
used[1]=1;
while(!q.empty()){
int x=q.front();
q.pop();
used[x]=0;
for(int i=0;i<v[x].size();i++){
int y=v[x][i].to;
int z=v[x][i].cost;
if(z<=o) z=0;
else z=1;
if(dis[y]>dis[x]+z){
link[y]=x;
dis[y]=dis[x]+z;
if(!used[y]) used[y]=1,q.push(y);
}
}
}
return dis[n]<=k;
}
int main(){
//加快cin,cout
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int r=0,l=0;
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=m;i++){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
insert(x,y,z);//边
insert(y,x,z);
r+=z;
}
ans=r;
while(l<=r){
int mid=(l+r)>>1;
if(dijkstra(mid)) ans=mid,r=mid-1;
else l=mid+1;
}
printf("%d",ans==r?-1:ans);
return 0;
}
//Dijkstra算法
/*int main(){
scanf("%d%d",&n,&m);
memset(cost,0x3f,sizeof(cost));
for(int i=1;i<=n;i++){
cost[i][i]=0;//对角线
}
for(int i=1;i<=m;i++){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
cost[x][y]=z;//边
}
for(int i=1;i<=n;i++){
dis[i]=cost[1][i];
}
memset(used,false,sizeof(used));
used[1]=true;
for(int i=1;i<=n-1;i++){
int mn=INF,pos;
for(int j=1;j<=n;j++){
if(dis[j]<mn&&!used[j]){
mn=dis[j];
pos=j;
}
}
used[pos]=true;
for(int j=1;j<=n;j++){
if(cost[pos][j]<INF){
dis[j]=min(dis[j],dis[pos]+cost[pos][j]);
}
}
}
for(int i=1;i<=n;i++){
printf("%d ",dis[i]);
}
return 0;
}*/
//Floyd算法 两两之间
/*int main(){
scanf("%d%d",&n,&m);
memset(cost,0x3f,sizeof(cost));
for(int i=1;i<=n;i++){
cost[i][i]=0;//对角线
}
for(int i=1;i<=m;i++){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
cost[x][y]=z;//边
}
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
cost[i][j]=min(cost[i][j],cost[i][k]+cost[k][j]);
//判断不同中间点的更新
}
}
}
//输出
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
printf("%d ",cost[i][j]);
}
printf("\n");
}
return 0;
}*/
例题:P1948 [USACO08JAN]Telephone Lines S(难度:2.5)
三.差分约束
定义:如果一个系统由n个变量和m个约束条件组成,形成m个形如ai-aj≤k的不等式(i,j∈[1,n],k为常数),则称其为差分约束系统(system of difference constraints)。亦即,差分约束系统是求解关于一组变量的特殊不等式组的方法。(来自百度百科)
简单来说,就是给你一堆不等式,求这一组不等式的最大解或最小解或是否有解
比如:
那么如何求解呢,显然对于每一个形如a-b<=或>=c,我们都可以视为一个图中的一条边,即当<=c时,a->b边权为c,当>=c时,b->a边权为c。那么我们回头看下上面的不等式,如果要求出x3-x0的最大值的话,叠加不等式可以推导出x3-x0<=7,最大值即为7,我们可以通过建立一个图,包含6个顶点,对每个xj-xi<=bk,建立一条i到j的有向边,权值为bk。通过求出这个图的x0到x3的最短路可以知道也为7,这是巧合吗?并不是。
之所以差分约束系统可以通过图论的最短路来解,是因为xj-xi<=bk,会发现它类似最短路中的三角不等式d[v] <=d[u]+w[u,v],即d[v]-d[u]<=w[u,v]。而求取最大值的过程类似于最短路算法中的松弛过程。
即求C-A的最大值,可以知道max(C-A)= min(b,a+c),而这正对应了下图中C到A的最短路。
而求最长路是一样的,只是将不等号方向改变即可
那么如何判断是否有解呢,其实就是判环与自环,详见环的判断
还有一个重点:
一定要用spfa或者Bellman-Ford,一定不要用Dijkstra,因为有可能会有负边权
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 1000005
typedef pair<int,int> pa;
struct edge{
int to,cost;
};
long long n,k,dis[N],used[N],flag,ans;
vector<edge>v[N];
void insert(int x,int y,int cost){
v[x].push_back((edge){y,cost});
}
void judge(int x){
if(flag) return;
used[x]=1;
for(int i=0;i<v[x].size();i++){
int y=v[x][i].to;
int z=v[x][i].cost;
if(dis[y]<dis[x]+z){
if(!used[y]){
dis[y]=dis[x]+z;
judge(y);
}
else{
flag=1;
return;
}
}
}
used[x]=0;
}
void spfa(){
memset(dis,0,sizeof(dis));
memset(used,0,sizeof(used));
queue<int>q;
used[0]=1;
q.push(0);
while(!q.empty()){
int x=q.front();
q.pop();
used[x]=0;
for(int i=0;i<v[x].size();i++){
int y=v[x][i].to;
int z=v[x][i].cost;
if(dis[y]<dis[x]+z){
dis[y]=dis[x]+z;
if(!used[y]){
used[y]=1;
q.push(y);
}
}
}
}
}
int main(){
scanf("%lld%lld",&n,&k);
for(int i=1;i<=n;i++)
insert(0,i,1);
for(int i=1;i<=k;i++){
int obt,x,y;
scanf("%lld%lld%lld",&obt,&x,&y);
if(obt==1){
insert(x,y,0);
insert(y,x,0);
}
else if(obt==2){
if(x==y){
printf("-1");
return 0;
}
insert(x,y,1);
}
else if(obt==3)
insert(y,x,0);
else if(obt==4){
if(x==y){
printf("-1");
return 0;
}
insert(y,x,1);
}
else
insert(x,y,0);
}
for(int i=1;i<=n;i++){
judge(i);
if(flag){
printf("-1");
return 0;
}
}
spfa();
for(int i=1;i<=n;i++)
ans+=dis[i];
printf("%lld",ans);
return 0;
}
例题:P3275糖果(难度:4)
拓展与提高:P7515 [省选联考 2021 A 卷] 矩阵游戏(不可做)
四.负环的判断
负环的判断呢,其实很简单,相信很多人都想到了,就是判断走过的边数是否大于或等于总边数
没了
但是只能用SPFA,因为有负权
代码:
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
struct Edge{
int nxt,to,w;
}e[30001];
int head[30001];
int ecnt = -1;
int n,m;
void addEdge(int x,int y,int w){
++ecnt;
e[ecnt].nxt = head[x];
e[ecnt].to = y;
e[ecnt].w = w;
head[x] = ecnt;
}
int cnt[30001] = { };
int dis[10001] = { };
int vis[10001] = { };
int flag = 0;
void spfa()
{
queue<int>q;
memset(dis, 0x3f,sizeof(dis));
memset(cnt, 0,sizeof(cnt));
memset(vis, 0,sizeof(vis));
dis[1] = 0;
q.push(1);
vis[1]=1;
while(!q.empty())
{
int now = q.front(); q.pop();
vis[now] = 0;
for(int i = head[now]; ~i; i = e[i].nxt)
{
int v = e[i].to;
if(dis[v] > dis[now] + e[i].w)
{
dis[v] = dis[now] + e[i].w;
cnt[v]=cnt[now]+1;
if(cnt[v] >= n){
printf("YES\n");
return;
}
if(!vis[v]) {
q.push(v);
vis[v] = 1;
}
}
}
}
printf("NO\n");
}
int k;
int main(){
scanf("%d" ,&k);
for(int j = 1;j <= k; j++){
memset(head,-1,sizeof(head));
scanf("%d %d" ,&n,&m);
ecnt=-1;
for(int i = 1;i <= m; i++){
int a,b,c;
scanf("%d %d %d" ,&a,&b,&c);
addEdge(a,b,c);
if(c>=0) addEdge(b,a,c);
}
spfa();
}
}
例题:P3385 负环模版(难度:1)
拓展与提高:UVA11090 Going in Cycle!!(难度:1.5)
五.floyd的部分用法
真的没啥可说的,水了水了,本质就是DP,请读者根据题意自行理解
例题:比较大小(难度:1.5)
拓展与提高:P1119 灾后重建(难度:2)