图论·最短路径

最短路径

一、Dijkstra 单源最短路径

Dijkstra是在非负权图上求单源最短路径的方法,复杂度 \(O(m\) \(log_{2}\) \(n+nk\) \(log_{2}\) \(n)\)

当一个点\(u\)的最短路被松弛过后,与该点相连的点\(v\)也有可能需要松弛,所以遍历一遍所有与\(u\)相连的点\(v\)并松弛\(dis_{v}\),若\(v\)被松弛,那么放入队列重复上述过程。

贪心地想,对于被影响的点\(v\)一定是被\(dis_{u}\)较小的\(u\)松弛,所以用小根堆维护目前最小的\(dis_{u}\)即可。

\(Dijkstra\)优就优在若点\(u\)成为堆顶,那么此时\(dis_{u}\)一定是最优的,不会再被松弛,这样也就保证了复杂度。

但是,dij不能跑带负环的也不能跑最长路。

板子代码:

void dijkstra()
{
	memset(dis,0x3f,sizeof dis);
	dis[s]=0;
	q.push({0,s});
	while (!q.empty())
	{
		int u=q.top().second;
		q.pop();
		if (vis[u]) continue;
		vis[u]=true;
		int _size=e[u].size();
		for (int i=0;i<_size;i++)
		{
			int v=e[u][i].nxt,w=e[u][i].val;
			if (dis[v]>dis[u]+w)
			{
				dis[v]=dis[u]+w;
				q.push({dis[v],v});
			}
		}
	}
}

二、SPFA 单源最短路径——用队列优化的Bellman-Ford

\(dij\)不同的是,它每次不需要特别记录点 \(u\) 是否确定了最短路径,因为跑完若干轮后肯定可以确定最短路。它在一般情况下和\(dijkstra\) 的复杂度一样好,但有些时候会退化到 \(O(nm)\)

SPFA的优势是边权可以为负,也可以判负环

板子代码↓

bool spfa()
{
	memset(dis,0x3f,sizeof dis);
	memset(neq,0,sizeof neq);
	memset(inq,0,sizeof inq);
	
	queue <int> q; 
	q.push(1),neq[1]++,dis[1]=0,inq[1]=true;
	while (!q.empty())
	{
		int u=q.front(),q.pop();
		inq[u]=false;
		
		int _size=e[u].size();
		for (int i=0;i<_size;i++)
		{
			int v=e[u][i].nxt,w=e[u][i].val;
			if (dis[v]>dis[u]+w)
			{
				dis[v]=dis[u]+w;
				if (inq[v]) continue;
				q.push(v),inq[v]=true;
				neq[v]++;
				if (neq[v]>n) return true;//有负环
			}
		}
	}
	return false;
}

还有一个最优比率环,不会待补

三、Floyd 多源最短路

十分暴力的最短路方法,代码简单但效率不高,在某些场景下有自己的优势。

用滚动数组将dp方程优化到二维\(f_{i,j}\),表示点对 \(i,j\) 之间的最短路径长度。时间复杂度为 \(O(n^{3})\),只适用于 \(n<300\) 的小规模图。

判断负环:若存在 \(f_{i,i}<0\) ,那么一定有负环。(呃呃 绕一圈之后边权和是负数)

传递闭包:\(f_{i,j}\) 表示点对 \(i,j\) 是否连通,然后跑Floyd就行。可以 bitset 优化,不会待补。

板子代码:

for (int k=1;k<=n;k++)//一定要在外层循环k,因为它是逐步递推的
{
	for (int i=1;i<=n;i++) 
	{
		for (int j=1;j<=n;j++) 
			f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
	}
}

四、差分约束系统

这个东西是一种特殊的 \(n\) 元一次不等式组,包括 \(n\) 个变量, \(m\) 个约束条件。

将约束条件 \(x_{i}-x_{j}\leqslant c_{k}\) 转化为 \(x_{i}\leqslant x_{j}+c_{k}\) ,没错这仍很像最短路中的松弛操作。对于每个约束条件,从节点 \(j\) 向节点 \(i\) 连一条权值为 \(c_{k}\) 的边,再建一个虚点 \(0\) ,跑一遍SPFA就能跑出来了。若有负环,那么差分约束无解。


【YbtOj】例题

A.单源最短路径

\(Dijkstra\)板子题,秒了

#incIude <bits/stdc++.h>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=1e5+5;
int n,m,s;
struct node{
	int nxt,val;
};
vector <node> e[N];
priority_queue < pii,vector <pii>,greater<pii> > q;
int dis[N];

void dijkstra()
{
	memset(dis,0x3f,sizeof dis);
	dis[s]=0;
	q.push({0,s});
	while (!q.empty())
	{
		int u=q.top().second;
		q.pop();
		int _size=e[u].size();
		for (int i=0;i<_size;i++)
		{
			int v=e[u][i].nxt,w=e[u][i].val;
			if (dis[v]>dis[u]+w)
			{
				dis[v]=dis[u]+w;
				q.push({dis[v],v});
			}
		}
	}
}
signed main()
{
	scanf("%lld%lld%lld",&n,&m,&s);
	for (int i=1;i<=m;i++)
	{
		int u,v,w;
		scanf("%lld%lld%lld",&u,&v,&w);
		e[u].push_back({v,w}); 
	}
	dijkstra();
	for (int i=1;i<=n;i++) printf("%lld ",dis[i]);
	return 0;
}

B.负环判断

\(SPFA\)板子题,秒了

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e3+5;
int T;
int n,m;
struct node{
	int nxt,val;
};
vector <node> e[N];
int inq[N];
int dis[N];
int cnt[N];
queue <int> q;

bool spfa()
{
	memset(inq,0,sizeof inq);
	memset(dis,0x3f,sizeof dis);
	memset(cnt,0,sizeof cnt);
	while (!q.empty()) q.pop();
	q.push(1);
	inq[1]=1;
	dis[1]=0;
	cnt[1]++;
	while (!q.empty())
	{
		int u=q.front();
		q.pop();
		inq[u]=0;
		
		int _size=e[u].size();
		for (int i=0;i<_size;i++)
		{
			int v=e[u][i].nxt,w=e[u][i].val;
			if (dis[v]>dis[u]+w)
			{
				dis[v]=dis[u]+w;
				if (inq[v]==0)
				{
					inq[v]=1;
					q.push(v);
					cnt[v]++;
					if (cnt[v]>=n) return true;
				}
			}
		}
	}
	return false;
}
signed main()
{
	scanf("%lld",&T);
	while (T--)
	{
		scanf("%lld%lld",&n,&m);
		for (int i=1;i<=n;i++) e[i].clear();
		for (int i=1;i<=m;i++)
		{
			int u,v,w;
			scanf("%lld%lld%lld",&u,&v,&w);
			if (w<0) e[u].push_back({v,w});
			else 
			{
				e[u].push_back({v,w});
				e[v].push_back({u,w});
			}
		}
		if (spfa()) printf("YE5\n");
		else printf("N0\n");
	}
	return 0;
}

C.最优贸易

显然,我们需要找到一条路径,在前面某段找到点权最小值,在后面某段找到点权最大值,这两个值的差值就是答案。再看这句话,可以转化为求源点到某点\(u\)的最小值与\(u\)到终点的最大值,所以正反建图、分别跑两边\(Dijkstra\)求出答案即可。

以上方法有误,\(dijkstra\)不能跑“最长路”,待补

(错误代码)
#include <bits/stdc++.h>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=1e5+5;
int n,m;
int c[N];
vector <int> e1[N],e2[N];
priority_queue < pii > q1;
priority_queue < pii,vector <pii>,greater<pii> > q2;
bool vis1[N],vis2[N];
int dis1[N],dis2[N];
int ans; 

void dijkstra1()//最大的 
{
	memset(dis1,0xc1,sizeof dis1);
	dis1[n]=max(dis1[n],c[n]);
	q1.push({dis1[n],n});
	while (!q1.empty())
	{
		int u=q1.top().second;
		q1.pop();
		if (vis1[u]) continue;
		vis1[u]=true;
		
		int _size=e2[u].size();
		for (int i=0;i<_size;i++)
		{
			int v=e2[u][i];
			if (max(dis1[u],c[v])>dis1[v])
			{
				dis1[v]=max(dis1[u],c[v]);
				q1.push({dis1[v],v});
			}
		}
	}
}
void dijkstra2()//最大的 
{
	memset(dis2,0x3f,sizeof dis2);
	dis2[1]=min(dis2[1],c[1]);
	q2.push({dis2[1],1});
	while (!q2.empty())
	{
		int u=q2.top().second;
		q2.pop();
		if (vis2[u]) continue;
		vis2[u]=true;
		
		int _size=e1[u].size();
		for (int i=0;i<_size;i++)
		{
			int v=e1[u][i];
			if (min(dis2[u],c[v])<dis2[v])
			{
				dis2[v]=min(dis2[u],c[v]);
				q2.push({dis2[v],v});
			}
		}
	}
}
signed main()
{
	scanf("%lld%lld",&n,&m);
	for (int i=1;i<=n;i++) scanf("%lld",&c[i]);
	for (int i=1;i<=m;i++)
	{
		int x,y,z;
		scanf("%lld%lld%lld",&x,&y,&z);
		if (z==1) e1[x].push_back(y),e2[y].push_back(x);
		else
		{
			e1[x].push_back(y),e1[y].push_back(x);
			e2[x].push_back(y),e2[y].push_back(x);
		}
	}
	dijkstra1();
	dijkstra2();
	for (int i=1;i<=n;i++) ans=max(ans,dis1[i]-dis2[i]);
	printf("%lld",ans);
	return 0;
}

D.汽车加油

注意到\(n\)的范围并不大,于是我们可以设状态 \(f_{i,j,k}\) 表示到 \((i,j)\) 还可以走 \(k\) 步的所需最小代价。这是好转移的,先考虑两种加油的情况,再考虑移动的情况,按照跑\(Dijkstra\)的方式跑一遍即可。

#incIude <bits/stdc++.h>
#define int long long
#define pin pair<int,node>
using namespace std;
const int N=105;
const int K=12;
int n,k,a,b,c;
int mp[N][N];
struct node{
	int u,v,k,w;
	bool operator < (const node &x) const{
		return x.w<w;
	}
};
priority_queue <node> q;
int vis[N][N][K];
int f[N][N][K];//f[i][j][k]:走到i,j还剩k步的最小费用 
int dx[4]={0,1,0,-1},dy[4]={1,0,-1,0};
int ans=9e18;

void dijkstra()
{
	memset(f,0x3f,sizeof f);
	f[1][1][k]=0;
	q.push((node){1,1,k,0});
	while (!q.empty())
	{
		int x=q.top().u,y=q.top().v,cost=q.top().k;
		q.pop();
		if (vis[x][y][cost]) continue;
		vis[x][y][cost]=true;
		
		if (mp[x][y]&&cost!=k)
		{
			if (f[x][y][k]>f[x][y][cost]+a)
			{
				f[x][y][k]=f[x][y][cost]+a;
				q.push((node){x,y,k,f[x][y][k]});
			}
			continue;
		}else{
			if (f[x][y][k]>f[x][y][cost]+a+c){
				f[x][y][k]=f[x][y][cost]+a+c;
				q.push((node){x,y,k,f[x][y][k]});
			}
		}
		
		if (cost>0)
		{
			for (int i=0;i<4;i++)
			{
				int xx=x+dx[i],yy=y+dy[i];
				if (xx<1||xx>n||yy<1||yy>n) continue;
				int w;
				if (i<=1) w=0;
				else w=b;
				if (f[xx][yy][cost-1]>f[x][y][cost]+w)
				{
					f[xx][yy][cost-1]=f[x][y][cost]+w;
					q.push({xx,yy,cost-1,f[xx][yy][cost-1]});
				}
			}
		}
	}
}
signed main()
{
	scanf("%lld%lld%lld%lld%lld",&n,&k,&a,&b,&c);
	for (int i=1;i<=n;i++)
	{
		for (int j=1;j<=n;j++) scanf("%lld",&mp[i][j]);
	}
	dijkstra();
	for (int i=0;i<=k;i++) ans=min(ans,f[n][n][i]);
	printf("%lld",ans);
	return 0;
}

E.比较大小

这就是一个\(Floyd\)传递闭包

注意到\(n\)的范围并不大,所以可以用\(Floyd\)维护连通性。若出现\(f_{u,v}=f_{v,u}=1\)的情况,那么就出现了环,给出关系有误;否则判断\(f_{u,v}\)输出即可。

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5;
int m,n,q;
int f[N][N];

signed main()
{
	scanf("%lld%lld%lld",&m,&n,&q);
	int a,b;
	while (m--)
	{
		scanf("%lld%lld",&a,&b);
		f[a][b]=1;
	}
	for (int k=1;k<=n;k++)
	{
		for (int i=1;i<=n;i++)
		{
			for (int j=1;j<=n;j++) f[i][j]|=f[i][k]&f[k][j];
		}
	}
	for (int i=1;i<=n;i++)
	{
		for (int j=1;j<=i;j++)
		{
			if (f[i][j]&&f[j][i])
			{
				printf("10000words to copy");
				return 0;
			}
		}
	}
	while (q--)
	{
		scanf("%lld%lld",&a,&b);
		if (f[a][b]) printf("YES\n");
		else if(f[b][a]) printf("NO\n");
		else printf("DK\n");
	}
	return 0;
}

F.删边问题

“最大的权值最小”,一眼二分。每次二分答案删边的价值,边权小于\(mid\)的就不走,然后照常跑\(Dijkstra\)就行,复杂度\(O(m\ log_{2}\ m)\)可以过

#incIude <bits/stdc++.h>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=2e4+5;
int n,m,T;
struct node{
	int nxt,len,val;
};
vector <node> e[N];
int mx;
int ans;

int dis[N],vis[N];
int dijkstra(int x)
{
	memset(dis,0x3f,sizeof dis);
	memset(vis,0,sizeof vis);
	priority_queue < pii,vector <pii>,greater <pii> > q;
	dis[1]=0;
	q.push(make_pair(0,1));
	while (!q.empty())
	{
		int u=q.top().second;
		q.pop();
		if (vis[u]) continue;
		vis[u]=true;
		
		int _size=e[u].size();
		for (int i=0;i<_size;i++)
		{
			int v=e[u][i].nxt,p=e[u][i].val,w=e[u][i].len;
			if (p<=x) continue;
			if (dis[v]>dis[u]+w)
			{
				dis[v]=dis[u]+w;
				q.push(make_pair(dis[v],v));
			}
		}
	}
	return dis[n];
}
bool check(int x)
{
	if (dijkstra(x)>=T) return true;
	return false;
}
signed main()
{
	scanf("%lld%lld%lld",&n,&m,&T);
	for (int i=1,u,v,len,val;i<=m;i++)
	{
		scanf("%lld%lld%lld%lld",&u,&v,&len,&val);
		e[u].push_back((node){v,len,val});
		mx=max(mx,val);
	}
	if (check(0)) { printf("-1 %lld",dis[n]); return 0; }
	int l=1,r=mx;
	while (l<r)
	{
		int mid=(l+r)/2;
		if (check(mid)) r=mid;
		else l=mid+1;
	}
	printf("%lld",l);
	return 0;
}

G.修建道路

注意到所求答案是所选边中剩下边权的最大值\(mx\),这就意味着另外\(k\)条路径的长度要大于\(mx\)才能最优。注意到对于一条路径,不同\(mx\)所对应的\(k'\)具有单调性,所以可以直接二分答案。

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e3+5;
const int inf=0x3f3f3f3f3f3f3f3f;
int n,p,k;
struct node{
	int v,w;
};
vector <node> e[N];

int dis[N],vis[N]; 
int dijkstra(int x)
{
	priority_queue < pair<int,int> > q;
	dis[1]=0;
	q.push({0,1});
	while (!q.empty())
	{
		int u=q.top().second;
		q.pop();
		if (vis[u]) continue;
		vis[u]=true;
		
		int _size=e[u].size();
		for (int i=0;i<_size;i++)
		{
			int v=e[u][i].v,w=e[u][i].w;
			if (dis[v]>dis[u]+(w>x))
			{
				dis[v]=dis[u]+(w>x);
				q.push({-dis[v],v});
			}
		}
	}
	return dis[n];
}
bool check(int x)
{
	memset(dis,0x3f,sizeof dis);
	memset(vis,0,sizeof vis);
	if (dijkstra(x)<=k) return true;
	return false;
}
signed main()
{
	scanf("%lld%lld%lld",&n,&p,&k);
	for (int i=1,u,v,l;i<=p;i++)
	{
		scanf("%lld%lld%lld",&u,&v,&l);
		e[u].push_back({v,l});
		e[v].push_back({u,l});
	}
	int l=0,r=1000001;
	while (l<r)
	{
		int mid=l+r>>1;
		if (check(mid)) r=mid;
		else l=mid+1;
	}
	if (r==1000001) printf("-1");
	else printf("%lld",r);
	return 0;
}

H.最小花费

注意到\(m,k\)并没有这么大,于是我们可以在\(Dijkstra\)中大胆地再设一维来维护还有几次0花费的机会。然后照常跑\(Dijkstra\)即可。

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5;
int n,m,k;
int s,t;
struct node{
	int nxt,val;
};
vector <node> e[N];
struct NODE{
	int dis,pos,k;
	bool operator < (const NODE &x) const{
		return x.dis<dis;
	}
};
int ans=0x3f3f3f3f3f3f3f3f;

priority_queue <NODE> q;
int dis[N][20],vis[N][20];//到i还能走j个0 
void dijkstra()
{
	memset(dis,0x3f,sizeof dis);
	memset(vis,0,sizeof vis);
	dis[s][k]=0;
	q.push({0,s,k});
	while (!q.empty())
	{
		int u=q.top().pos,kk=q.top().k;
		q.pop();
		if (vis[u][kk]) continue;
		vis[u][kk]=1;
		
		int _size=e[u].size();
		for (int i=0;i<_size;i++)
		{
			int v=e[u][i].nxt,w=e[u][i].val;
			if (dis[u][kk]+w<dis[v][kk])
			{
				dis[v][kk]=dis[u][kk]+w;
				q.push({dis[v][kk],v,kk});
			}
			if (kk&&dis[u][kk]<dis[v][kk-1])
			{
				dis[v][kk-1]=dis[u][kk];
				q.push({dis[v][kk-1],v,kk-1});
			}
		}
	}
}
signed main()
{
	scanf("%lld%lld%lld",&n,&m,&k);
	scanf("%lld%lld",&s,&t);
	for (int i=1,x,y,v;i<=m;i++)
	{
		scanf("%lld%lld%lld",&x,&y,&v);
		e[x].push_back({y,v});
		e[y].push_back({x,v});
	}
	dijkstra();
	for (int i=0;i<=k;i++) ans=min(ans,dis[t][i]);
	printf("%lld",ans);
	return 0;
}

I.收费站点

注意到答案具有单调性,所以可以二分所交费用的最大值,每次跑\(Dijkstra\)求出到\(v\)的最小花费、与总容量\(s\)比较并判断是否合法即可。

#incIude <bits/stdc++.h>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=1e4+5;
int inf=1e10;
int n,m,s,t,tol;
int f[N];
struct node{
	int nxt,val;
};
vector <node> e[N];
int mx;
int ans=inf;

int dis[N],vis[N];
int dijkstra(int x)
{
	priority_queue < pii,vector <pii>,greater <pii> > q;
	if (f[s]>x) return inf;
	dis[s]=0;
	q.push({0,s});
	while (!q.empty())
	{
		int u=q.top().second;
		q.pop();
		if (vis[u]) continue;
		vis[u]=1;
		
		int _size=e[u].size();
		for (int i=0;i<_size;i++)
		{
			int v=e[u][i].nxt,w=e[u][i].val;
			if (f[v]>x) continue;
			if (dis[v]>dis[u]+w)
			{
				dis[v]=dis[u]+w;
				q.push({dis[v],v});
			}
		}
	}
	return dis[t];
}
bool check(int x)
{	
	memset(dis,0x3f,sizeof dis);
	memset(vis,0,sizeof vis);
	if (dijkstra(x)<=tol) return true;
	else return false;
}
signed main()
{
	scanf("%lld%lld%lld%lld%lld",&n,&m,&s,&t,&tol);
	for (int i=1;i<=n;i++) scanf("%lld",&f[i]);
	for (int i=1,a,b,c;i<=m;i++)
	{
		scanf("%lld%lld%lld",&a,&b,&c);
		e[a].push_back({b,c});
		e[b].push_back({a,c});
	}
	int l=0,r=inf;
	while (l<r)
	{
		int mid=l+r>>1;
		if (check(mid)) r=mid,ans=min(ans,r);
		else l=mid+1;
	}
	if (ans==inf) printf("-1");
	else printf("%lld",ans);
	return 0;
}
posted @ 2024-11-18 19:57  还是沄沄沄  阅读(8)  评论(0编辑  收藏  举报