最短路算法简单应用部分题目详解

最短路算法简单应用部分题目详解


简介

总结了一下一些简单的最短路算法的应用,并在最短路入门的基础上发掘一些算法的性质


P1821 [USACO07FEB] Cow Party S

题目链接
多次最短路
所有牛都会选择最短路径,求 n n n头牛中来回路程最长的长度。
直接暴力竟也可以过。。。所有点都跑一遍单源最短路,然后求 max ⁡ 1 ≤ i ≤ n , i ≠ x { d i s ( i → x ) + d i s ( x → i ) } \max_{1\le i \le n,i \ne x}\{dis(i\rightarrow x) + dis(x\rightarrow i)\} max1in,i=x{dis(ix)+dis(xi)}
SPFA做法 O ( n k m ) O(nkm) O(nkm)看数据范围像是会爆,但是过了。。。
同样暴力思路用Dijkstra也能过。
当然,看到大家的题解多数是在正向图即原图跑一遍以 x x x为起点的单源最短路,然后将所有边取反建一个反向图,再跑一遍以 x x x为起点的单源最短路,同样取 max ⁡ \max max就完成了,这样当然更快,同时也更巧妙。我们发现,在反向图上以 x x x为起点跑一遍单源最短路等价于在其他所有节点上跑以 x x x为终点的单终点最短路。
暴力参考程序:

#include<iostream>
#include<cstdio>
#include<queue>
#define inf 9999999999

using namespace std;
int n,m,target;
int cnt,x,y,z;
long long ans;
int head[1005];
struct edge{
	int next;
	int to;
	int dis;
}e[100005];

long long spfa(int start)
{
	long long outpt=0;
	queue<int > q;
	bool vis[1005];
	long long dis[1005];
	for(int i=0;i<=1004;i++)
		vis[i]=0;
	for(int i=0;i<=1004;i++)
		dis[i]=inf;
	q.push(start);
	vis[start]=1;
	dis[start]=0;
	int temp=0;
	while(!q.empty())
	{
		temp=q.front();
		q.pop();
		vis[temp]=0;
		for(int i=head[temp];i!=0;i=e[i].next)
			if(dis[e[i].to]>dis[temp]+e[i].dis)
			{
				dis[e[i].to]=dis[temp]+e[i].dis;
				if(vis[e[i].to]==0)
				{
					q.push(e[i].to);
					vis[e[i].to]=1;
				}
			}
	}
	outpt=dis[target];
	for(int i=0;i<=1004;i++)
		vis[i]=0;
	for(int i=0;i<=1004;i++)
		dis[i]=inf;
	q.push(target);
	vis[target]=1;
	dis[target]=0;
	while(!q.empty())
	{
		temp=q.front();
		q.pop();
		vis[temp]=0;
		for(int i=head[temp];i!=0;i=e[i].next)
			if(dis[e[i].to]>dis[temp]+e[i].dis)
			{
				dis[e[i].to]=dis[temp]+e[i].dis;
				if(vis[e[i].to]==0)
				{
					q.push(e[i].to);
					vis[e[i].to]=1;
				}
			}
	}
	outpt+=dis[start];
	return outpt;
}

void addedge(int from,int to,int dis)
{
	cnt++;
	e[cnt].next=head[from];
	e[cnt].to=to;
	e[cnt].dis=dis;
	head[from]=cnt;
}

int main()
{
	scanf("%d%d%d",&n,&m,&target);
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d%d",&x,&y,&z);
		addedge(x,y,z);
	}
	for(int i=1;i<=n;i++)
		ans=max(ans,spfa(i));
	cout<<ans;
	return 0;
}

P6833 [Cnoi2020]雷雨

题目链接
多次最短路
第一眼想到的竟然是暴力跑每个格子到云层中一点和地面两点的最短路,结果看了看数据不敢下手了。再转换一下思维,那就只跑3个点的单源最短路吧!跑完之后枚举三支闪电的交汇点,取 min ⁡ \min min即得答案。
参考程序:

#include<iostream>
#include<cstdio>
#include<queue>

using namespace std;
const long long inf = 0xfffffffffffffff;
int n,m,a,b[2],dx[6] = {0,1,-1,0,0},dy[6] = {0,0,0,1,-1};
long long map[1005][1005];
bool vis[1005][1005];
long long dis[3][1005][1005],ans;

void Dij(int k ,int sx,int sy)
{
	int x,y,nx,ny;
	for(int i = 1;i <= n;i ++)
		for(int j = 1;j <= m;j ++)
		{
			vis[i][j] = 0;
			dis[k][i][j] = inf;
		}
	priority_queue< pair< long long , pair<int ,int > > > q;
	dis[k][sx][sy] = map[sx][sy];
	q.push(make_pair(-map[sx][sy],make_pair(sx,sy)));
	while(!q.empty())
	{
		x = q.top().second.first;
		y = q.top().second.second;
		q.pop();
		if(vis[x][y]) continue;
		vis[x][y] = 1;
		for(int i = 1;i <= 4;i ++)
		{
			nx = x + dx[i];ny = y + dy[i];
			if(nx > 0 && nx <= n && ny > 0 && ny <= m)
			{
				if(dis[k][nx][ny] > dis[k][x][y] + map[nx][ny])
				{
					dis[k][nx][ny] = dis[k][x][y] + map[nx][ny];
					q.push(make_pair(-dis[k][nx][ny],make_pair(nx,ny)));
				}
			}
		}
	}
}

int main()
{
	ans = inf;
	scanf("%d%d",&n,&m);
	scanf("%d%d%d",&a,&b[0],&b[1]);
	for(int i = 1;i <= n;i ++)
		for(int j = 1;j <= m;j ++)
			scanf("%lld",&map[i][j]);
	long long tmp  = 0;
	Dij(0,1,a);
	Dij(1,n,b[0]);
	Dij(2,n,b[1]);
	for(int i = 1;i <= n;i ++)
		for(int j = 1;j <= m;j ++)
		{
			tmp = dis[0][i][j] + dis[1][i][j] + dis[2][i][j] - 2*map[i][j];
			ans = min(ans,tmp);
		}
	cout << ans; 
	return 0;
}

P3905 道路重建

题目链接
最短路变形
本题并没有要求 A A A B B B之间的距离最小,而是要求 A A A B B B之间修复的道路长度最小,那么在跑最短路的时候,维护要修复的道路长度就好了,不必记录最短距离。
参考程序:

#include<iostream>
#include<cstdio>
#include<queue>
#include<cstdlib>

using namespace std;
const int inf = 0xfffffff;
int n,m,l,u,v,w,map[105][105],from,to;
bool vis[105];
int repair[105], broken[105][105];

void Dijkstra(int x,int y)
{
	for(int i = 1;i <= n;i ++) repair[i] = inf;
	priority_queue<pair<int ,int > > q;
	repair[x] = 0;
	q.push(make_pair(0,x));
	while(!q.empty())
	{
		u = q.top().second;
		q.pop();
		if(vis[u]) continue;
		vis[u] = 1;
		for(int i = 1;i <= n;i ++)
			if(map[u][i] > -1)
			{
				if(repair[i] > repair[u]+map[u][i]*broken[u][i])//最短修复距离
				{
					repair[i] = repair[u]+map[u][i]*broken[u][i];
					q.push(make_pair(-repair[i],i));
				}
			}
	}
	printf("%d",repair[y]);
	return ;
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i = 1;i <= n;i ++)
		for(int j = 1;j <= n;j ++)
			map[i][j] = -1;
	for(int i = 1;i <= m;i ++)
	{
		scanf("%d%d%d",&u,&v,&w);
		map[u][v] = w;
		map[v][u] = w;
	}
	scanf("%d",&l);
	for(int i = 1;i <= l;i ++)
	{
		scanf("%d%d",&u,&v);
		broken[u][v] = 1;
		broken[v][u] = 1;
	}
	scanf("%d%d",&from,&to);
	Dijkstra(from,to);
	return 0;
}

P2850 [USACO06DEC]Wormholes G

题目链接

我们把花费的时间看作路程,那么穿越虫洞就是走负权边,将问题转化为求是否有一个环使得走 n n n遍后 ( 1 ≤ n ) (1\le n) (1n),回到原起点时所用的时间为负数。显然这里涉及到Bellman-Ford算法可跑负权边并且可以判断负环的特殊性质,而其他最短路算法都没有这个性质,即不可跑有负权边的图。
我这里直接判 d i s ( s t a r t ) < 0 dis(start)<0 dis(start)<0就跑了,成功水过去。。。
参考程序:

#include<iostream>
#include<queue>
#include<cstdio>
#include<string>
#include<cstring>
#include<cstdlib>

using namespace std;
const int inf = 0xfffffff;
int F,n,m,h,u,v,w;
int head[505],cnt,dis[505];
bool vis[505],flag;
struct edge{
	int nxt;
	int to;
	int val;
}e[10005];

bool SPFA_check(int s)
{
	memset(vis,0,sizeof(vis));
	for(int i = 1;i <= n;i ++) dis[i] = inf;
	dis[s] = 0;vis[s] = 1;
	queue<int > q;
	q.push(s);
	while(!q.empty())
	{
		u = q.front();
		q.pop();
		vis[u] = 0;
		if(dis[s] < 0) return 1;
		for(int i = head[u];i ;i = e[i].nxt)
		{
			v = e[i].to;w = e[i].val;
			if(dis[v] > dis[u] + w)
			{
				dis[v] = dis[u] + w;
				if(!vis[v])
				{
					vis[v] = 1;
					q.push(v);
				}
			}
		}
	}
	return 0;
}

void addedge(int from,int to,int v)
{
	cnt ++;
	e[cnt].nxt = head[from];
	e[cnt].to = to;
	e[cnt].val = v;
	head[from] = cnt;
	return ;
}

int main()
{
	scanf("%d",&F);
	for(int t = 1;t <= F;t ++)
	{
		cnt = 0;flag = 1;
		for(int i = 0;i < 5205;i ++) e[i].nxt = e[i].to = e[i].val = 0;
		for(int i = 0;i < 505;i ++) head[i] = 0;
		scanf("%d%d%d",&n,&m,&h);
		for(int i = 1;i <= m;i ++)
		{
			scanf("%d%d%d",&u,&v,&w);
			addedge(u,v,w);
			addedge(v,u,w);
		}
		for(int i = 1;i <= h;i ++)
		{
			scanf("%d%d%d",&u,&v,&w);
			addedge(u,v,-w);
		}
		for(int i = 1;i <= n;i ++)
		{
			if(SPFA_check(i))
			{
				printf("YES\n");
				flag = 0;
				break;
			}
		}
		
		if(flag)
			printf("NO\n");
	}
	return 0;
}

请填坑:证明SPFA算法中如果某个点弹出队列的次数超过 n − 1 n-1 n1次,则存在负环。

正解:
重要结论:SPFA算法中如果某个点弹出队列的次数超过 n − 1 n-1 n1次,则存在负环。
有如下描述性证明:

对于Bellman-Ford算法,其迭代松弛操作实际上就是按顶点距离s的层次,逐层生成这棵最短路树的过程。
在没有负环的图中,最短路最多只包含 ∣ V ∣ − 1 |V|-1 V1条边,所以只需要循环 ∣ V ∣ − 1 |V|-1 V1
相似地,SPFA设立了一个队列保存待优化的点


P1119 灾后重建

题目链接
Floyd
读完题可以发现:本题描述的是一种动态的方法。由于要回答任意 x , y x,y x,y之间在 t t t时能否通行——暗示全源最短路,又看到瞩目的 N ≤ 200 N\le 200 N200 ,我们考虑用Floyd解决本题。
本题中规定了重建完成的村庄和未完成重建的村庄,村庄将按时间顺序一个一个地重建,又看到输入的 t t t不下降,我们可以考虑动态在线即一边输入一边更新全图的方式。
现在考虑有一部分村庄已经完成重建,且在当前 t ′ t' t,村庄 k k k是最近完成重建的村庄,此时记录全图最短路的 m a p map map里,显然有 m a p ( i , k ) = + ∞ , i ≠ k map(i,k)=+\infin,i\ne k map(i,k)=+,i=k,所以我们必然要将所有 m a p ( i , k ) map(i,k) map(i,k)更新一遍,由于使用Floyd,同时我们想到以 k k k为“中转站”更新一边全图,由此得到:

for(int i = 1;i <= n;i ++)
	for(int j = 1;j <= n;j ++)
		map[i][j] = min(map[i][j],map[i][k]+map[k][j])

因为村庄将按时间顺序一个一个地重建,相当于随着时间的推移,我们得到新的节点,且此节点不仅是起点、终点,且毫无疑问地也可以是中转站,相当于在询问的循环中,我们不断地枚举中转站 k k k,由此我们省去了原Floyd模板中枚举 k k k的一层循环。
将此Floyd代码套入输入询问的循环中,便达到动态在线更新全图的目的。
参考代码:

#include<iostream>
#include<cstdio>

using namespace std;
const int INF=0x3f3f3f3f;
int n,m,t[205],q,x,y,z,now=0;
int map[205][205];

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=0;i<=n;i++)
		for(int j=0;j<=n;j++)
		{
			map[i][j]=0x3f3f3f3f;
			if(i==j) map[i][j]=0;
		}
	for(int i=0;i<n;i++)
		scanf("%d",&t[i]);
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d%d",&x,&y,&z);
		map[x][y]=z;
		map[y][x]=z;
	}
	scanf("%d",&q);
	for(int i=1;i<=q;i++)//一边输入一遍更新最短路
	{
		scanf("%d%d%d",&x,&y,&z);
		while(t[now]<=z&&now<n)//在要求的时间前就求出最近更新的最短路
		{
			for(int i=0;i<n;i++)//核心Floyd
				for(int j=0;j<n;j++)
					if(map[i][j]>map[i][now]+map[now][j])
						map[j][i]=map[i][j]=map[i][now]+map[now][j];
			now++;
		}
		if(t[x]>z||t[y]>z)//同时用更新的图回答询问
			printf("-1\n");
		else
		{
			if(map[x][y]==INF) printf("-1\n");
			else printf("%d\n",map[x][y]);
		}
			
	}
	return 0;
}

P1144 最短路计数

题目链接
最短路计数问题
经典的最短路套DP计数问题。
此题相对简单,由于每条边边权为1,所以可以按照每个点到原点的距离将图分层,若一个点到原点的距离为 d d d那么这个点就在图中的第 d d d层。
在这里插入图片描述
对于第 d e p i dep_i depi层,显然由题目的性质可知,只要满足 d e p j + 1 = d e p i dep_j+1=dep_i depj+1=depi,那么从 j j j i i i的边一定是原点到 i i i的最短路中的边。
因为给定的图是无向图,所以用单纯的拓扑排序划分阶段是行不通的,所以给图跑一遍最短路,按照最短路的更新顺序做DP,这样就确定了DP在图中的更新顺序。
当然可以一边跑最短路,一边DP,且有以下状态转移方程:
f ( i ) f(i) f(i)表示从原点到 i i i的最短路数目
f ( i ) = ∑ d ( j ) + 1 = d ( i ) f ( j ) f(i) = \sum_{d(j)+1=d(i)} f(j) f(i)=d(j)+1=d(i)f(j)
其中,函数 d ( i ) d(i) d(i)表示从原点到 i i i的最短距离
参考程序:

/*
由于每条边边权为1,所以将图分层,判断层数d[i+1] == d[i] + 1即可 
*/
#include<iostream>
#include<cstdio>
#include<queue>

using namespace std;
const int inf = 0xfffffff;
int n,m,x,y,u,v;
int head[1000005],cnt;
int f[1000005],d[1000005];
bool vis[10000005];
struct edge{
	int to;
	int next;
}e[4000005];

void addedge(int from,int to)
{
	cnt ++;
	e[cnt].next = head[from];
	e[cnt].to = to;
	head[from] = cnt;
	cnt ++;
	e[cnt].next = head[to];
	e[cnt].to = from;
	head[to] = cnt;
	return ;
}

void bfs()
{
	queue<int > q;
	for(int i = 1;i <= n;i ++) f[i] = 0,d[i] = inf;
	q.push(1);
	f[1] = 1;//显然的边界条件
	vis[1] = 1;
	d[1] = 0;
	while(!q.empty())
	{
		u = q.front();
		q.pop();
		for(int i = head[u];i != 0;i = e[i].next)
		{
			v = e[i].to;
			if(d[v] > d[u])//更新最短路
			{
				d[v] = d[u] + 1;
				if(!vis[v])
				{
					vis[v] = 1;
					q.push(v);
				}
			}
			else//发现d[v]+1 = d[u] 
				if(d[u] == d[v] + 1) f[u] = (f[u]%100003 + f[v]%100003)%100003;
		}
	}
	for(int i = 1;i <= n;i ++)
		printf("%d\n",f[i]%100003);
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i = 1;i <= m;i ++)
	{
		scanf("%d%d",&x,&y);
		addedge(x,y);
	}
	bfs();
	return 0;
}

P1608 路径统计

题目链接
最短路计数问题
此题同样是最短路计数的问题,不过边的权值不再固定,并且图已经由无向边变为有向边,尽管如此,我们还是能借助最短路更新的顺序做DP。
考虑跑最短路期间,来到一个点 v v v,我们统计到 v v v最短路数目时会出现两种情况:

  1. 有一个 u u u d i s ( u ) + w ( e u → v ) < d i s ( v ) dis(u)+w(e_{u\rightarrow v}) < dis(v) dis(u)+w(euv)<dis(v),此时到 v v v的最短路需要更新,所以 d i s ( v ) = d i s ( u ) + w ( e u → v ) , f ( v ) = f ( u ) dis(v) = dis(u) + w(e_{u\rightarrow v}),f(v) = f(u) dis(v)=dis(u)+w(euv),f(v)=f(u),既然最短路更新了,那么最短路的数目也更新了。
  2. 有一个 u u u d i s ( u ) + w ( e u → v ) = d i s ( v ) dis(u)+w(e_{u\rightarrow v}) = dis(v) dis(u)+w(euv)=dis(v),此时显然有 f ( v ) = f ( v ) + f ( u ) f(v)=f(v)+f(u) f(v)=f(v)+f(u)

参考程序:

#include<iostream>
#include<cstdio>
#include<queue>
#include<cstdlib>

using namespace std;
const int inf = 0xfffffff;
int n,m,u,v,w;
int head[2005],cnt,d[2005],f[2005];
bool vis[2005],map[2005][2005][12];
struct edge{
	int to;
	int nxt;
	int val;
}e[2005*1999];

void addedge(int from,int to,int val)
{
	cnt ++;
	e[cnt].nxt = head[from];
	e[cnt].to = to;
	e[cnt].val = val;
	head[from] = cnt;
	return ;
}

void Dijkstra()
{
	priority_queue<pair<int ,int > > q;
	for(int i = 1;i <= n;i ++) d[i] = inf;
	d[1] = 0;f[1] = 1;
	q.push(make_pair(0,1));
	while(!q.empty())
	{
		u = q.top().second;
		q.pop();
		if(vis[u]) continue;
		vis[u] = 1;
		for(int i = head[u];i != 0;i = e[i].nxt)
		{
			v = e[i].to;w = e[i].val;
			if(d[v] == d[u] + w) f[v]+=f[u];//判断的顺序不能调换,否则会重复计数
			if(d[v] > d[u] + w)
			{
				f[v] = f[u];
				d[v] = d[u] + w;
				q.push(make_pair(-d[v],v));
			}
		}
	}
}


int main()
{
	scanf("%d%d",&n,&m);
	for(int i = 1;i <= m;i ++)
	{
		scanf("%d%d%d",&u,&v,&w);
		if(!map[u][v][w])
			addedge(u,v,w);
		map[u][v][w] = 1;
	}
	Dijkstra();
	if(d[n] == inf) printf("No answer");
	else printf("%d %d",d[n],f[n]);
	return 0;
}

P2865 [USACO06NOV]Roadblocks G

题目链接
严格次短路
刚开始肯定会想到各种各样的算法。
然而这道题其实是道证明题……

  • 定理:最短路的子路径一定是其中某两个节点的最短路径,即最短路满足最优子结构的性质

这个大家都知道,那么我们将其推广到严格次短路,能否证明严格次短路也满足最优子结构。显然,仔细思考发现,严格次短路一定由某些次短路和某些最短路组成。
证明:

  • 如果这条路径完全由次短路组成,那么存在一条路径比这条路径短,且比最短路长。
  • 如果这条路径完全由最短路组成,显然不能组成严格次短路。

所以根据动态规划的思想,我们的思路应当是:到某个顶点 v v v的次短路要么是到其他顶点 u u u的最短路再加上 u → v u\rightarrow v uv的边,要么是到 u u u的最短路再加上 u → v u\rightarrow v uv的边
即满足
{ d i s 2 ( v ) = min ⁡ { d i s ( u ) , d i s 2 ( u ) } + w ( e u → v ) d i s 2 ( v ) > d i s ( v ) \begin{cases} dis2(v)=\min\{dis(u),dis2(u)\}+w(e_{u\rightarrow v}) \\ dis2(v)>dis(v) \end{cases} {dis2(v)=min{dis(u),dis2(u)}+w(euv)dis2(v)>dis(v)
更新的时候就记录次短路和最短路,最后输出次短路就好了
参考程序:

#include<iostream>
#include<cstdio>
#include<queue>
#include<algorithm>

using namespace std;
const int inf = 0xfffffff;
int n,r,u,v,w;
int head[5005],cnt,dis[5005],dis2[5005],d;
bool vis[5005];
struct edge{
	int to;
	int nxt;
	int val;
}e[200005];

void Dijkstra()
{
	for(int i = 1;i <= n; i++) dis[i] = dis2[i] = inf;
	priority_queue<pair<int ,int > > q;
	dis[1] = 0; q.push(make_pair(0,1));
	while(!q.empty())
	{
		u = q.top().second;d = -q.top().first;
		q.pop();
		if(dis2[u] < d) continue;
		for(int i = head[u];i;i = e[i].nxt)
		{
			v = e[i].to; w = d + e[i].val;
			if(dis[v] > w)//更新最短路
			{
				dis2[v] = min(dis2[v],dis[v]);//同时更新次短路
				dis[v] = w;
				q.push(make_pair(-dis[v],v));//最后的答案可能由这条最短路组成
			}
			else
			{
				if(dis[v] < w && dis2[v] > w)//次短路更新
				{
					dis2[v] = w;
					q.push(make_pair(-dis2[v],v));//最终的严格次短路可能由这条次短路组成
				}
			}
		}
	}
}

void addedge(int from,int to,int v)
{
	cnt ++;
	e[cnt].nxt = head[from];
	e[cnt].to = to;	
	e[cnt].val = v;
	head[from] = cnt;
	cnt ++;
	e[cnt].nxt = head[to];
	e[cnt].to = from;
	e[cnt].val = v;
	head[to] = cnt;
	return ;
}

int main()
{
	scanf("%d%d",&n,&r);
	for(int i = 1;i <= r;i ++)
	{
		scanf("%d%d%d",&u,&v,&w);
		addedge(u,v,w);
	}
	
	Dijkstra();
	
	printf("%d",dis2[n]);
	return 0;
}

P1462 通往奥格瑞玛的道路

题目链接
二分答案+最短路
又是一道经典的综合建模题。
题目大意:在安全到达奥格瑞玛的情况下最高路费最小
显然本题中出现了“求最小的最大值”一类问题,考虑用二分答案解决。但是题目中又加了限制条件:安全回到奥格瑞玛,所以在每一次二分答案的过程中跑一遍最短路判断答案的合法性。
为了二分,我们必须使答案区间满足单调性,答案当然是城市费用,所以在二分答案前,把所有城市按照过路费从小到大排序,得到单调的答案区间。对于每一个二分得到的答案,意味着是全路程中最贵的费用,将所需费用比此费用高的城市全部封禁,在这种情况的图下跑一遍最短路,如果损失的血量不足以致死,那么答案合法,下调答案范围,寻求更优解,否则答案不合法,上调答案范围,寻求合法解。
参考程序:

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<string>
#include<cstring>
#include<queue>

using namespace std;
const long long inf = 0xffffffffff;
int n,m,x,y;
long long head[10005],cnt,ans,blood,z;
long long dis[19995];
bool vis[15555],ban[15555],check;

struct City{
	long long cost;
	int id;
}c[10005];

struct Edge{
	int next;
	int to;
	long long cost;
}e[200005];

bool cmp(City a,City b)
{
	return a.cost < b.cost;
}

void Build(int from,int to,long long c)
{
	cnt ++;e[cnt].next = head[from];e[cnt].cost = c;e[cnt].to = to;head[from] = cnt;
	cnt ++;e[cnt].next = head[to];e[cnt].cost = c;e[cnt].to = from;head[to] = cnt;
	return ;
}

bool SPFA()
{
	for(int i = 1;i <= n;i ++) dis[i] = inf,vis[i] = 0;
	dis[1] = 0;
	queue<int > q;
	q.push(1);
	vis[1] = 1;
	while(!q.empty())
	{
		int u = q.front();
		q.pop();
		vis[u] = 0;
		for(int i = head[u];i != 0; i = e[i].next)
		{
			int v = e[i].to,w = e[i].cost;
			if(!ban[v]&&dis[v] > dis[u] + w)
			{
				dis[v] = dis[u] + w;
				if(!vis[v]) vis[v] = 1,q.push(v);
			}
		}
	}
	if(dis[n] < blood) return 1;
	else return 0;
}

long long Binary_Answer()
{
	int lt = 0;
	int rt = n;
	long long mid = 0,res = inf,tmp = 0 ;
	while(lt <= rt)
	{
		memset(ban,0,sizeof(ban));
		mid = (lt + rt) / 2;
		for(int i = n;i >= 1;i --)
		{
			if(i > mid) ban[c[i].id] = 1;//封禁城市
			else { tmp = c[i].cost; break;}
		}
		bool flag = 0 ;
		if((!ban[1])&&(!ban[n])) flag = SPFA();//判断合法性
		if(flag) rt = mid - 1 ,res = min(tmp,res);
		else lt = mid + 1 ;
	}
	return res;
}

int main()
{
	scanf("%d%d%lld",&n,&m,&blood);
	for(int i = 1; i <= n ; i ++)
	{
		scanf("%lld",&c[i].cost);
		c[i].id = i;
	}
	for(int i = 1;i <= m;i ++)
	{
		scanf("%d%d%lld",&x,&y,&z);
		Build(x,y,z);
	}
	
	check = SPFA();
	if(!check)
	{
		printf("AFK");
		return 0;
	}
	sort(c+1,c+1+n,cmp);//先排序
		
	ans = Binary_Answer();//二分答案
	
	printf("%lld",ans);
	
	return 0;
}

P3008 [USACO11JAN]Roads and Planes G

题目链接

请填坑:SLF优化SPFA水过本题;正解:缩点+Dijkstra

#include<iostream>
#include<cstdio>
#include<queue>
#include<cstdlib>

using namespace std;
const int inf = 0xfffffff;
int t,r,p,s,u,v,w;
int head[25005],phead[25005],cnt,dis[25005],cnt1,tot;
bool vis[25005];
struct edge{
	int nxt;
	int to;
	int val;
}e[200005],pre[200005];

void addedge(int from,int to,int v)
{
	cnt ++;
	e[cnt].nxt = head[from];
	e[cnt].to = to;
	e[cnt].val = v;
	head[from] = cnt;
	return ;
}

void SPFA()
{
	bool flag =  0;
	for(int i = 1;i <= t;i ++) dis[i] = inf;
	deque<int > q;
	q.push_back(s);vis[s] = 1;dis[s] = 0;
	while(!q.empty())
	{
		u = q.front();
		q.pop_front();
		vis[u] = 0;
		for(int i = head[u];i;i = e[i].nxt)
		{
			v = e[i].to;w= e[i].val;
			if(dis[v] > dis[u] + w)
			{
				dis[v] = dis[u] + w;
				if(!vis[v])
				{
					vis[v] = 1;
					if(!q.empty()&&dis[v] < dis[q.front()]) q.push_front(v);
					else q.push_back(v);
				}
			}
		}
	}
}

int main()
{
	scanf("%d%d%d%d",&t,&r,&p,&s);
	for(int i = 1;i <= r;i ++)
	{
		scanf("%d%d%d",&u,&v,&w);
		addedge(u,v,w);
		addedge(v,u,w);
	}
	for(int i = 1;i <= p;i ++)
	{
		scanf("%d%d%d",&u,&v,&w);
		addedge(u,v,w);
	}
	
	SPFA();
		
	for(int i = 1;i <= t;i ++)
	{
		if(dis[i] == inf) printf("NO PATH\n");
		else printf("%d\n",dis[i]);
	}
	return 0;
}

P3953 逛公园

题目链接
祭2019.8.5~2020.11.4
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
然而并非自己做出来,而是看了一星期题解才弄懂了,我太菜了

  • 跑一遍原图1为起点最短路和反图n为起点的最短路
#include<iostream>
#include<cstdio>
#include<queue>
#include<algorithm>

using namespace std;
const int inf = 0xfffffff;
int T,n,m,k,p,x,y,z,ind[100005];
int head[2][100005],cnt,cnt2,dis[2][100005],ans,f[100005][55],pri[100005],head2[100005];
bool vis[2][100005],bo[100005];
struct node{
	int id;
	int key;
	int di;
}a[100005];
struct edge{
	int nxt;
	int to;
	int val;
}e[2][400005],e2[400005];

bool cmp(node x,node y)
{
	if(x.di != y.di) return x.di < y.di;//先按距离确定DP顺序
	return x.key < y.key;//0边上的点按照拓扑序确定DP顺序
}

void addedge2(int from,int to,int dist)
{
	cnt2 ++;
	e2[cnt2].nxt = head2[from];
	e2[cnt2].to = to;
	e2[cnt2].val = dist;
	head2[from] = cnt2;
	ind[to] ++;
	return ;
}

void addedge(int from,int to,int dist)
{
	cnt ++;
	e[0][cnt].nxt = head[0][from];
	e[0][cnt].to = to;
	e[0][cnt].val = dist;
	head[0][from] = cnt;
	e[1][cnt].nxt = head[1][to];
	e[1][cnt].to = from;
	e[1][cnt].val = dist;
	head[1][to] = cnt;
	return ;
}

bool Topol()//拓扑排序
{
	queue<int > q;
	int u = 0 ,v = 0;
	for(int i = 1;i <= n;i ++)//跑0边图
	{
		if(bo[i] == 1 && ind[i] == 0)
		{
			q.push(i);
			while(!q.empty())
			{
				u = q.front();bo[u] = 0;
				q.pop();
				for(int j = head2[u];j;j = e2[j].nxt)
				{
					v = e2[j].to;
					if(pri[v] < pri[u] + 1) pri[v] = pri[u] + 1;//确定0边的两个端点的DP顺序:拓扑序
					ind[v] --;
					if(ind[v] == 0 && bo[v] == 1) q.push(v);//
				}
			}
		}
	}
	for(int i = 1;i <= n;i ++)//出现0环且会影响答案
		if(ind[i] > 0 && dis[0][i] + dis[1][i] <= dis[0][n] + k) return 0;
	return 1;
}

void Dijkstra(int num)
{
	int u = 0;int v = 0;int w = 0;
	priority_queue<pair<int ,int > > q;
	if(num == 0 ) dis[0][1] = 0,q.push(make_pair(0,1));
	else dis[1][n] = 0,q.push(make_pair(0,n));
	while(!q.empty())
	{
		u = q.top().second;
		q.pop();
		if(vis[num][u]) continue;
		vis[num][u] = 1;
		for(int i = head[num][u];i;i = e[num][i].nxt)
		{
			v = e[num][i].to;w = e[num][i].val;
			if(dis[num][v] > dis[num][u] + w)
			{
				dis[num][v] = dis[num][u] + w;
				q.push(make_pair(-dis[num][v],v));
			}
		}
	}
	return ;
}

void DP()
{
	int u = 0,v = 0,tmp = 0,w = 0;f[1][0] = 1;
	for(int kk = 0;kk <= k;kk ++)//以多出量为阶段
	{
		for(int i = 1;i <= n;i ++)//按照排好的DP顺序
		{
			u = a[i].id;
			if(dis[0][u] >= inf) continue;
			for(int j = head[0][u];j;j = e[0][j].nxt)
			{
				v = e[0][j].to;w = e[0][j].val;
				tmp = dis[0][u] + w - dis[0][v] + kk;
				if(tmp <= k)//更新答案
				{
					f[v][tmp] += f[u][kk];
					f[v][tmp] %= p;
				}
			}
		}
	}
	for(int i = 0;i <= k;i ++) ans += f[n][i],ans %= p;
	return ;
}

int main()
{
	scanf("%d",&T);
	for(int t = 1;t <= T;t ++)
	{
		scanf("%d%d%d%d",&n,&m,&k,&p);
		cnt = 0;ans = 0;cnt2 = 0;
		for(int i = 1;i <= n;i ++)
		{
			vis[0][i] = vis[1][i] = 0;bo[i] = 0;ind[i] = 0;pri[i] = 0;
			dis[0][i] = dis[1][i] = inf;head[0][i] = head[1][i] = head2[i] = 0;
			for(int j = 0;j <= k;j ++) f[i][j] = 0;
		}
		for(int i = 1;i <= m;i ++)
			e2[i].nxt = e2[i].to = e2[i].val = e[1][i].nxt = e[1][i].to = e[1][i].val = e[0][i].nxt = e[0][i].to = e[0][i].val = 0;
		for(int i = 1;i <= m;i ++)
		{
			scanf("%d%d%d",&x,&y,&z);
			addedge(x,y,z);
			if(z == 0) addedge2(x,y,z),bo[x] = 1,bo[y] = 1;//建一个只含0边的图
		}
		Dijkstra(0);//原图最短路
		Dijkstra(1);//反向最短路
		if(Topol() == 0)//判断使得合法路径有无穷条的0环
		{
			printf("-1\n");
			continue;
		}
		for(int i = 1;i <= n;i ++)//登记结点信息
		{
			a[i].id = i;
			a[i].key = pri[i];
			a[i].di = dis[0][i];
		}
		sort(a+1,a+1+n,cmp);//确定DP顺序
		DP();//动态规划
		printf("%d\n",ans);
	}
	return 0;
}

P1772 [ZJOI2006]物流运输

P1772 [ZJOI2006]物流运输
最短路+DP
最开始想到的是状压 DP,因为看到 m ≤ 20 m\le20 m20,然而严重 TLE,只有 30 分,后来就算开了 O2 优化也仅有 70 分。主要原因是状态数看起来不会爆,但是在 DP 期间枚举状态数则会使时间复杂度变得特大。
考虑计算 i i i j j j 天走同一条路的最短路径,发现计算这些量的时间复杂度仅为 O ( ) O() O()。设 g ( i , j ) g(i,j) g(i,j) 表示 i i i j j j 天从 1 1 1 码头到 m m m 码头走同一条路的最短路径

#include<iostream>
#include<cstdio>
#include<queue>

using namespace std;
typedef long long ll;
const long long inf = 0xffffffff;
ll k,w,f[105];
ll cost[105][105],d[35];
int u,v,n,m,tot,cnt,da,a,b;
int head[35],bancnt[105];
int ban[105][35];
bool vis[35];

struct edge{
	int nxt;
	int to;
	ll len;
}e[666666];

void addedge(int from,int to,ll lent)
{
	cnt ++;
	e[cnt].nxt = head[from];
	e[cnt].to = to;
	e[cnt].len = lent;
	head[from] = cnt;
	cnt ++;
	e[cnt].nxt = head[to];
	e[cnt].to = from;
	e[cnt].len = lent;
	head[to] = cnt;
	return ;
}

ll Dijkstra()
{
	priority_queue<pair<ll,int > > q;
	for(int i = 1;i <= m;i ++) d[i] = inf;
	d[1] = 0;
	q.push(make_pair(0,1));
	while(!q.empty())
	{
		u = q.top().second;
		q.pop();
		if(vis[u]) continue;
		vis[u] = 1;
		for(int i = head[u];i;i = e[i].nxt)
		{
			v = e[i].to;w = e[i].len;
			if(d[u] + w < d[v])
			{
				d[v] = d[u] + w;
				q.push(make_pair(-d[v],v));
			}
		}
	}
	return d[m];
}

int main()
{
	scanf("%d%d%lld%d",&n,&m,&k,&tot);
	for(int i = 1;i <= tot;i ++)
	{
		scanf("%d%d%lld",&u,&v,&w);
		addedge(u,v,w);
	}
	scanf("%d",&da);
	for(int i = 1;i <= da;i ++)
	{
		scanf("%d%d%d",&u,&a,&b);
		for(int j = a;j <= b;j ++)
		{
			bancnt[j] ++;
			ban[j][bancnt[j]] = u;
		}
	}
	for(int i = 1;i <= n;i ++)
	{
		for(int j = i;j <= n;j ++)
		{
			for(int p = 1;p <= m;p ++) vis[p] = 0;
			for(int p = i;p <= j;p ++)
				for(int q = 1;q <= bancnt[p];q ++)
					vis[ban[p][q]] = 1;
			cost[i][j] = Dijkstra();
//			printf("%d->%d:%lld\n",i,j,cost[i][j]);
		}
	}
	for(int i = 1;i <= n;i ++) f[i] = inf;
	for(int i = 1;i <= n;i ++)
		for(int j = i;j >= 1;j --)
		{
			if(j == 1) f[i] = min(f[i],f[j-1]+cost[j][i]*(i-j+1));
			else f[i] = min(f[i],f[j-1]+cost[j][i]*(i-j+1)+k);
		}
//	for(int i = 1;i <= n;i ++) printf("%lld ",f[i]);
//	printf("\n");
	printf("%lld",f[n]);
	return 0;
}
/*
5 5 10 8
1 2 1
1 3 3
1 4 2
2 3 2
2 4 4
3 4 1
3 5 2
4 5 2
4
2 2 3
3 1 1
3 3 3
4 4 5
*/

未完待续…

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值