最短路笔记

好用的

算法

记号

为了方便叙述,这里先给出下文将会用到的一些记号的含义。

  • n 为图上点的数目,m 为图上边的数目;
  • s 为最短路的源点;
  • D(u)s 点到 u 点的 实际 最短路长度;
  • dis(u)s 点到 u 点的 估计 最短路长度。任何时候都有 dis(u)D(u)。特别地,当最短路算法终止时,应有 dis(u)=D(u)
  • w(u,v)(u,v) 这一条边的边权。

Dijkstra

注:在使用此算法时应默认存在条件:图的边权非负

算法思想

贪心或DP

算法流程

把当前这个图分为两个集合,一个为已确定最短路的S集合,另一个为没有确定最短路的T集合。算法最开始时所有节点都在T集合,dis(s)=0,其余节点均设为正无穷。每次从T集合取出dis值最小的节点u把它放入S集合中,确定这个节点的最短路D(u)=dis(u),并枚举它的相邻节点v更新dis(v)值,方式为dis(v)=min(dis(v),dis(u)+w(u,v)),直至T集合为空。

正确性证明

证明的命题为:每个节点只会更新一次,即每次从T集合取出的dis值最小的点u,一定有D(u)=dis(u)

从正面直接攻破貌似很困难,考虑反证法。假设存在路径使得点u满足D(u)<dis(u)。那么显然这条更短的路径与当前路径存在有至少一个点不一样。

case1: 路径s...v2...u上所有点都在S集合中

设最后路径s...v2...uu的上一个点为x1,路径s...v1...uu的上一个点为x2,因为S集合的每个点都更新过其他节点,也就是每个节点的D值都确定下来了,所以x1x2都更新过u的最短路径了,故这种情况不存在。

case2:路径s...v2...u上部分点都在T集合中,且第一个在T集合中的点为v2

因为v2T集合,所以dis(v2)>dis(u),然而最终因为v2u之前且边权非负,所以D(v2)<D(u),因为在S集合里的点都更新了dis(v2),剩下T集合的一些点k,满足dis(u)dis(k)<dis(v2)都有可能会将dis(v2)的值变小,但因为边权非负,所以最终更新完后dis(v2)的值一定不小于dis(u),同理其余剩下在集合T中的点x,满足dis(x)>dis(v2),经过更新也一定满足dis(x)一定不小于dis(u),从而无法在更新使得 dis(v2)小于dis(u)。所以最终得到D(v2)D(u)与上述D(v2)<D(u)矛盾,故这种情况也不存在。

故得证,每次从T集合取出dis值最小的一个点u一定满足D(u)=dis(u)

推理

dijkstra第k次从T集合取出的点为到起点第k小(非严格)的节点

证明:

考虑算法流程,每次从未求出最短路径的点集中取出距离起点最近的点,然后以这个点为跳板刷新其他点才符合贪心(或DP)的证明,所以是的。当然,非严格。

小优化

只要搜到终点后就停止搜索,正确性显然。

时间复杂度分析

朴素的Dijkstra共处理n个点,每次用O(n)的复杂度处理暴力枚举出在集合T的最小值,故总时间复杂度为O(n2)

朴素Dijkstra代码实现

void dijkstra(int s)
{
	memset(dis,0x3f,sizeof(dis));
	dis[s]=0;
	for(int i=1;i<=n;i++)
	{
		int u=0,res=0x3f3f3f3f;
		for(int j=1;j<=n;j++)
			if(!vis[j]&&dis[j]<res)res=dis[j],u=j;
		vis[u]=1;
		for(int j=head[u];j;j=nxt[j])
		{
			int v=to[j];
			if(dis[v]>dis[u]+res)dis[v]=dis[u]+res;
		}
	}
}

我们发现维护最大值可以用优先队列来维护,所以采用STLpriority来进行编码。此时共计O(m)次修改,O(n)次删除堆顶元素,因为不支持删除操作,所以每个点的最短路被更新多次,堆中元素个数为m个。所以时间复杂度为O((n+m)logm)但因题目常给的是n,m同级,所以时间复杂度为O(mlogm)

STL优先队列优化Dijkstra代码实现

struct node{
	int val,pos;
	bool operator>(const node &x)const{
		return val>x.val;
	}
};
priority_queue<node,vector<node>,greater<node> >q;
void dijkstra(int s)
{
	memset(dis,0x3f,sizeof(dis));
	memset(vis,0,sizeof(vis));
	dis[s]=0;
	q.push(node{dis[s],s});
	while(!q.empty())
	{
		node t=q.top();q.pop();
		if(vis[t.pos])continue;
		vis[t.pos]=1;
		for(int i=head[t.pos];i;i=nxt[i])
		{
			int v=to[i];
			if(dis[v]>dis[t.pos]+w[i])
			{
				dis[v]=dis[t.pos]+w[i];
				q.push(node{dis[v],v});
			}
		}
	}
	return;
}

BellmanFord/SPFA

BellmanFord 算法是一种基于松弛(relax)操作的最短路算法,可以求出有负权的图的最短路,并可以对最短路不存在的情况进行判断。

BellmanFord算法流程

对于边 (u,v),松弛操作对应下面的式子:dis(v)=min(dis(v),dis(u)+w(u,v))

这么做的含义是显然的:我们尝试用 Suv(其中 Su 的路径取最短路)这条路径去更新 v 点最短路的长度,如果这条路径更优,就进行更新。

Bellman–Ford 算法所做的,就是不断尝试对图上每一条边进行松弛。我们每进行一轮循环,就对图上所有的边都尝试进行一次松弛操作,当一次循环中没有成功的松弛操作时,算法停止。

每次循环是 O(m) 的,那么最多会循环多少次呢?

在最短路存在的情况下,由于一次松弛操作会使最短路的边数至少 +1,而最短路的边数最多为 n1,因此整个算法最多执行 n1 轮松弛操作。故总时间复杂度为 O(nm)

但还有一种情况,如果从 S 点出发,抵达一个负环时,松弛操作会无休止地进行下去。注意到前面的论证中已经说明了,对于最短路存在的图,松弛操作最多只会执行 n1 轮,因此如果第 n 轮循环时仍然存在能松弛的边,说明从 S 点出发,能够抵达一个负环。

代码实现

struct edge {
    int v, w;
};        
vector<edge> e[maxn];
int dis[maxn];
const int inf = 0x3f3f3f3f;      
bool bellmanford(int n, int s) {
    memset(dis, 63, sizeof(dis));
    dis[s] = 0;
    bool flag;
    for (int i = 1; i <= n; i++) {
    	flag = false;
        for (int u = 1; u <= n; u++) {
            if (dis[u] == inf) continue;
            for (auto ed : e[u]) {
                int v = ed.v, w = ed.w;
                if (dis[v] > dis[u] + w) {
                  	dis[v] = dis[u] + w;
                  	flag = true;
                }
            }
        }
        if (!flag) break;
    }
    return flag;
}

算法优化

考虑只有每次更新操作更中被新的节点才会进入下一轮松弛,所以建一个队列每次从队首取出一个元素进行更新,再将更新成功的节点放入队尾。这个优化称为SPFA算法,在随机的图上平均复杂度为O(km)k一般取23的常数,但在网格图中还是会跑满O(nm)的复杂度,所以平时题目中没说有负权边一般都要用Dijkstra来求。 可是SPFA也有自己的优势,比如可以判负环,还可以做Dijkstra干不到的事情,因为Dijkstra每个节点只会入队更新一次,所以关于每条路径的最大值和最小值不能正确地求出来。

代码实现

void spfa(int s)
{
	memset(dis,0x3f,sizeof(dis));
	memset(vis,0,sizeof(vis));
	dis[s]=0;
	queue<int> q;
	q.push(s);
	while(!q.empty())
	{
		int u=q.front();q.pop();
		vis[u]=0;
		for(int i=head[u];i;i=nxt[i])
		{
			int v=to[i];
			if(dis[v]>dis[u]+w[i])
			{
				dis[v]=dis[u]+w[i];
				if(vis[v]==0)
				{
					vis[v]=1;
					q.push(v);
				}
			}
		}
	}
}

SPFA判负环

如果不存在负环,每个节点的最短路径(包括起点和终点)最多n个点。用利用队列的性质,每次一个节点出队后有进队,那么说明这个节点的最短路径点数一定增加1,如果一个节点进队超过n次,那么说明存在从起点到达的负环。

也可以用另一种方式理解:su的最短路上的边数一定不多于 n1 。否则至少有一个结点被重复经过,这说明存在环,且经过该环能更新该结点的 dis 值,即存在负环。

代码实现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=2005,MAXM=20005;
int n,m,cnt0;
int dis[MAXN],vis[MAXN],head[MAXN],cnt[MAXN];
struct Edge{
	int nxt,to,dis;
}edge[MAXM];
void add(int u,int v,int w)
{
	edge[++cnt0].nxt=head[u];
	edge[cnt0].to=v;
	edge[cnt0].dis=w;
	head[u]=cnt0;
}
bool spfa()
{
	queue<int> q;
	for(int i=1;i<=n;i++)
	{
		dis[i]=0x3f3f3f3f;
		vis[i]=0;
	}
	q.push(1);
	dis[1]=0;
	vis[1]=1;
	cnt[1]++;
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		vis[u]=0;
		for(int i=head[u];i;i=edge[i].nxt)
		{
			int v=edge[i].to;
			if(dis[v]>dis[u]+edge[i].dis)
			{
				dis[v]=dis[u]+edge[i].dis;
				if(vis[v]==0)
				{
					q.push(v);
					vis[v]=1;
					cnt[v]++;
					if(cnt[v]>n)return 1;
				}
			}
		}
	}
	return 0;
}
void init()
{
	memset(dis,0,sizeof(dis));
	memset(vis,0,sizeof(vis));
	memset(cnt,0,sizeof(cnt));
	memset(edge,0,sizeof(edge));
	memset(head,0,sizeof(head));
	cnt0=0;
}
int main()
{
	int T;
	scanf("%d",&T);
	init();
	for(int a=1;a<=T;a++)
	{
		init();
		scanf("%d %d",&n,&m);
		for(int i=1;i<=m;i++)
		{
			int u,v,w;
			scanf("%d %d %d",&u,&v,&w);
			add(u,v,w);
			if(w>=0)add(v,u,w);
		}
		if(spfa())printf("YES\n");
		else printf("NO\n");
	}
	return 0;
}

Floyd

算法思想

采用动态规划的思路。设dis[i,j,k]表示当只途径编号为1k的点时(起点和终点不算),ij的最短路长度。特别地,dis[i,j,0]表示初试时ij点的边权。

考虑如何进行状态更新。显然从ij只经过编号为1k的点的路径分为两类:

ij,且只经过编号为1k1的点的路径。

ik,再从kj,且只经过编号为1k1点的路径。

显然对于第一类路径,最短者的长度就是dis[i,j,k1]。对于第二类路径,最短者的长度就是dis[i,k,k1]+dis[k,j,k1]。两者当中取最小值,就得到转移方程:

dis[i,j,k]=min(dis[i,j,k1],dis[i,k,k1]+dis[k,j,k1])

然后只需要依次枚举k,i,j,并进行转移即可。最终结果即为第三维为n的数组。注意由于在第三维为k时候需要用到第三维为k1的数据,所以在枚举时应当先枚举第三维k,再枚举ij

代码非常简洁,如下所示:

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

时间复杂度O(n3),空间复杂度O(n3)。在时间上已经难以对该算法优化,但在空间上可以。

显然dis[i,k,k1]dis[i,k,k]dis[k,j,k1]dis[k,j,k]分别相等,因为对于起点或终点为k的最短路径途径编号为1k1的点就相当于途径编号为1k的点,因为最没有环的图中,起点或终点为k的点到其它点的最短路径途径的点一定不包含k,因为上文对dis[i,j,k]的定义为当只途径编号为1k的点时(起点和终点不算),ij的最短路长度,于是发现数组第三维k是多余的,可以直接用二维数组滚动来解决此题。转移方程变为:

dis[i][j]=min(dis[i,j],dis[i,k]+dis[k,j])

空间复杂度变为O(n2),时间复杂度不变。

代码实现:

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

BFS

你就说它能不能求最短路吧

说白了算法思想还是 Dijkstra ,只不过运用的场景被局限了。如果考场上没想到,也可以用万能的 Dijkstra,复杂度也就多了个 log

它是一种由于边权相等的情况下使用,比如题目中常给的边权为1的情况,当然如果图中只有边的权值只有两种(比如边的权值为01时),那也可以用BFS来求最短路径。

1).边权相等

本质上是类似与dijkstra算法,只不过由于边的权值一样,所以就可以只用队列queue来优化掉dijkstra算法优先队列priorityqueuelog复杂度。

算法流程

每次从队中取出队顶元素,再把更新后的节点放到队尾。这样就可以保证每次从队顶取出的节点一定是d值最小的。

注:在BFS求最短路的过程中,任意时刻都满足队列中的元素的d值是单调(也就是当前遍历到u,那么du一定是队列里面最小的,也就是对于任意的v在队列中,都满足dvdu)的并且这个元素是满足两段性的。 两段性即为遍历这个图时的每个点在BFS树的层次的极差1

代码实现:

void bfs(int s)
{
	memset(dis,0x3f,sizeof(dis));
	memset(vis,0,sizeof(vis));
	queue<int> q;
	dis[s]=0;
	q.push(s);
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(int i=head[u];i;i=nxt[i])
		{
			int v=to[i];
			if(dis[v]>dis[u]+w[i])
			{
				dis[v]=dis[u]+w[i];
				q.push(v);
			}
		}
	}
}

2).边权为01

算法流程

不说了,直接上模板,大体上就是用个双端队列维护队列中d值最小的节点是谁(也就是 Djikstra 的思想)。

代码实现

void bfs(int s)
{
	memset(dis,0x3f,sizeof(dis));
	memset(vis,0,sizeof(vis));
	deque<int>q;
	dis[s]=0;
	q.push(s);
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(int i=head[u];i;i=nxt[i])
		{
			int v=to[i];
			if(dis[v]>dis[u]+w[i])
			{
				dis[v]=dis[u]+w[i];
				if(w[i]==0)q.push_front(v);
				else q.push_back(v);
			}
		}
	}
}

时间复杂度

显然一个点只会进队一次,整张图会被遍历一次,故时间复杂度为 O(n+m) ,所以考场上看到边权相等或为 1 或为
01 的情况下可以用 BFS ,复杂度略比 Dijkstra 优。

题型

常用技巧

  • 求所有点到一个点的最短路,可以用反图的方式解决,脑补一下,所有点到一个点,相当于这个点反向建边倒着这走回去。
  • 二进制分组

二进制分组处理多源多汇最短路例题:

题目传送门

做法

做法1

考虑直接将k个标记点当做起点进行Dijkstra会出现问题(因为把每个标记点的dis值设为0就不会在更新了,与题意说的起点和终点均为标记点且为两个不同的点不符。)

那我们看到我们要求k个标记点的最短路,那么就设起点为st,且stst均为标记点,st的最短路为k个标记点的最短路的最小值。

那么最直接的做法是随机讲这k个点分为两个部分,一个部分的点的dis值设为0,再进行Dijkstra,最后取另一部分的dis值的最小值与答案ans进行比较。这样做的正确性即为起点在dis值为0的部分且终点在另一部分,即每次操作有12×12=14的概率正确。所以多进行几次这样的操作就行了。

做法2

做法2为做法1的优化,且正确性有保障。

在这之前先引入一个概念二进制分组,因为要使st不相同,所以st拆分为二进制数后至少有一位不相同。所以我们直接按照每个二进制位是0还是1进行划分即可。

但值得注意的是本题的边为有向边,所以在划分时有可能把终点的dis值设为0,所以这样就显然会有问题,那就在反过来再跑一次Dijkstra就行了。

Code
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <vector>
#include <queue>

#define x first
#define y second
#define mp make_pair

using namespace std;
const int N = 1e5 + 10, M = 5e5 + 10;
typedef long long ll;
typedef pair<ll,int> PLI;
const ll INF = 1e15;

struct node{
	ll d;
	int x, st;
	bool operator > (const node& t) const
	{
		return d > t.d;
	}
};


PLI d[N][2];
int v[N][2], head[N], ver[M], Next[M], edge[M], tot, n, m, k, a[N];

void add(int x, int y, int z)
{
	ver[++tot] = y, edge[tot] = z, Next[tot] = head[x], head[x] = tot;
}

void dijkstra()
{
	priority_queue <node, vector<node>, greater<node> > q;
	for(int i = 1; i <= n; i++) v[i][0] = v[i][1] = 0, d[i][0] = d[i][1] = mp(INF, 0);
	sort(a + 1, a + k + 1);
	for(int i = 1; i <= k; i++)
	{
		if(a[i] == a[i - 1]) continue;
		int x = a[i];
		d[x][0] = mp(0ll, x);
		q.push(node({0, x, x}));
	}
	while(!q.empty())
	{
		node top = q.top(); q.pop();
		int x = top.x, st = top.st; ll dis = top.d;
		if((v[x][0] && v[x][1]) || (v[x][0] && st == d[x][0].y)) continue;
		if(!v[x][0])
		{
			v[x][0] = 1;
			d[x][0] = mp(dis, st);
		}
		else
		{
			v[x][1] = 1;
			d[x][1] = mp(dis, st);
		}
		for(int i = head[x]; i; i = Next[i])
		{
			int y = ver[i], z = edge[i];
			PLI t = mp(dis + z, st);
			if(t < d[y][0])
			{
				if(t.y != d[y][0].y) d[y][1] = d[y][0];
				d[y][0] = t;
				q.push(node({dis + z, y, st}));
			}
			else if(t < d[y][1] && st != d[y][0].y)
			{
				d[y][1] = t;
				q.push(node({dis + z, y, st}));
			}
		}
	}
}

int read()
{
	int x = 0, t = 1; char ch = getchar();
	while(ch < '0' || ch > '9')
	{
		if(ch == '-') t = -1;
		ch = getchar();
	}
	while(ch >= '0' && ch <= '9')
	{
		x = x * 10 + ch - '0';
		ch = getchar();
	}
	return x * t;
}

int main()
{
	int T = read();
	while(T--)
	{
		tot = 0;
		n = read(), m = read();k = read();
		for(int i = 1; i <= n; i++) head[i] = 0;
		for(int i = 1; i <= m; i++)
		{
			int x = read(), y = read(), z = read();
			add(x, y, z);
		} 
		for(int i = 1; i <= k; i++) a[i] = read();
		dijkstra();
		ll res = INF;
		for(int i = 1; i <= k; i++) res = min(res, d[a[i]][1].x);
		printf("%lld\n", res);
	}
	return 0;
}

判环

判负环

模板题

SPFA,前面也介绍过。

判无向图的最小环

模板题

考虑Floyd算法的过程。当外层循环到k刚开始时,d[i,j]保存着“经过编号不超过k1的节点”从ij的最短路长度。

于是,min1i<j<k(d[i,j]+a[j,k]+a[k,i])就是满足以下两个条件的最小环长度。

  • 有编号不超过k的节点构成。
  • 经过节点k

上式中相当于枚举了与k相邻的两个节点ij,所以正确性显然。

对于所有的整数k[1,n],都对它算出了最小环,答案取最小值即可。

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,ans;
const int N=150;
int d[N][N],g[N][N];
signed main()
{
	scanf("%lld %lld",&n,&m);
	for(int i=1;i<=140;i++)for(int j=1;j<=140;j++)d[i][j]=g[i][j]=2147483648;
	for(int i=1;i<=n;i++)g[i][i]=0,d[i][i]=0;
	for(int i=1,a,b,w;i<=m;i++)
	{
		scanf("%lld %lld %lld",&a,&b,&w);
		g[a][b]=g[b][a]=min(g[a][b],w);
		d[a][b]=g[a][b],d[b][a]=g[b][a];
	} 
	ans=2147483648;
	for(int k=1;k<=n;k++)
	{
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=n;j++)
			{
				if(i!=j&&g[i][k]>0&&g[k][j]>0)
					ans=min(ans,g[i][k]+g[k][j]+d[i][j]);
				d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
			}
		}
	}
	if(ans!=2147483648)printf("%lld\n",ans);
	else printf("No solution.\n");
	return 0;
}

判有向图的最小环

dijkstra做,对于一个起点s,现将dis[s]=0,先把它相邻的点松弛,再将dis[s]设为无穷大,等最后s再入对时,dis[s]的值即为有向图的最小环的长度。

最短路计数

前置知识

  • 最短路的一个很好的性质:从st的最短路上的一个节点k,都满足sk的路径是关于s单源最短路的最短路

证明:

反证法,假设sk的路径不为最短路,但skt为到t的最短路,那么skt的路径一定不会比sk的最短路再加上kt的路径优,所以st就不为最短路,与上文假设条件矛盾。

故得证。

  • st的最短路径上一定不存在环

证明:

考虑反证法,因为这个图的边权非负,所以环的权值为非负数,所以一定不会比不经过这个环更优。

故得证。

注: 上述结论是在图的边权非负时才成立的,如果图的边权为负数上述结论就不一定成立了


例题[HAOI2012] 道路

直接队每一条边进行枚举肯定会TLE,但如果只计算边的贡献就好像可以优化一点时间复杂度。

cnt1v表示从s到达v的最短路径条数,cnt2v表示在最短路图上以v作为起点的最短路径条数。

很显然,对于一条边uv,它的贡献就为以1n每个点作为起点cnt1u×cnt2v的和。

cnt1可以在dijkstra后进行判断哪些边在最短路图上再进行topo求得,显然cnt1s=1

cnt2可以在拓扑序的逆序上求得,对于一个点ucnt2u=1+cnt2v,其中vu的相邻节点。

Code

#include<bits/stdc++.h>
using namespace std;
const int N=1550,M=5050;
const int mod=1e9+7;
int n,m,head[N],cnt,to[M],nxt[M],w[M],dis[N],fro[M],in[N],cnt1[N],cnt2[N],s[N],ans[M];
bool vis[N],mark[M];
void add(int u,int v,int f)
{
	to[++cnt]=v;
	nxt[cnt]=head[u];
	head[u]=cnt;
	w[cnt]=f;
	fro[cnt]=u;
}
struct node{
	int val,pos;
	bool operator >(const node &x)const{
		return val>x.val;
	}
};
void dij(int s)
{
	priority_queue<node,vector<node>,greater<node> >q;
	memset(dis,0x3f,sizeof(dis));
	memset(vis,0,sizeof(vis));
	memset(mark,0,sizeof(mark));
	dis[s]=0;
	q.push(node{dis[s],s});
	while(!q.empty())
	{
		node t=q.top();q.pop();
		if(vis[t.pos])continue;
		vis[t.pos]=1;
		for(int i=head[t.pos];i;i=nxt[i])
		{
			int v=to[i];
			if(dis[v]>dis[t.pos]+w[i])
			{
				dis[v]=dis[t.pos]+w[i];
				q.push(node{dis[v],v});
			}
		}
	}
	for(int i=1;i<=m;i++)
		if(dis[to[i]]==dis[fro[i]]+w[i])mark[i]=true;
	return;
}
void topo(int fs)
{
	memset(cnt1,0,sizeof(cnt1));
	memset(cnt2,0,sizeof(cnt2));
	memset(in,0,sizeof(in));
	queue<int>q;
	for(int i=1;i<=m;i++)
	{
		if(mark[i]==false)continue;
		in[to[i]]++;
	}
	q.push(fs);
	cnt1[fs]=1;
	int tag=0;
	while(!q.empty())
	{
		int x=q.front();q.pop();
		s[++tag]=x;
		for(int i=head[x];i;i=nxt[i])
		{
			if(mark[i]==false)continue;
			in[to[i]]--;
			if(in[to[i]]==0)q.push(to[i]);
			cnt1[to[i]]+=cnt1[x];
			cnt1[to[i]]%=mod;
		}
	}
	for(int i=tag;i;i--)
	{
		int x=s[i];
		cnt2[x]++;
		for(int j=head[x];j;j=nxt[j])
		{
			if(mark[j]==false)continue;
			cnt2[x]+=cnt2[to[j]];
			cnt2[x]%=mod;
		}
	}
}
int main()
{
	scanf("%d %d",&n,&m);
	for(int i=1,u,v,w;i<=m;i++)
	{
		scanf("%d %d %d",&u,&v,&w);
		add(u,v,w);
	}
	for(int i=1;i<=n;i++)
	{
		dij(i);topo(i);
		for(int i=1;i<=m;i++)
		{
//		cout<<cnt1[fro[i]]<<" "<<cnt2[to[i]]<<endl;
			if(mark[i])
			{
				ans[i]+=(cnt1[fro[i]]%mod*cnt2[to[i]]%mod)%mod;
				ans[i]%=mod;
			}
		}
	}
	for(int i=1;i<=m;i++)printf("%d\n",ans[i]);
	return 0;
}

时间复杂度O(nmlog m)

分层图最短路

概述

分层图最短路,如:有 k 次零代价通过一条路径,求总的最小花费。对于这种题目,我们可以采用 DP 相关的思想,设 disi,j表示当前从起点 i 号结点,使用了 j 次免费通行权限后的最短路径。显然,dis 数组可以这么转移:

disi,j=min(min(disv,j-1if(jk)),min(disv,j+w),disi,j)

其中v表是与i相邻的节点,w表示经过这条边的边权,特别地,如果j>k,那么disv,j=

概念理解:分层图最短路往往是与DP思想结合的体现,它适用于求最短路性质的问题但加了额外限制,在考场上如果要想作对做这类题,必须先发现这道题隐藏条件和题目意思与最短路有关(不要小看这一步,因为往往最开始就没想到这样做,导致后面的结果是错误的),再思考怎样对题目进行图论建模。

例题

1

Telephone Lines

做法

简单来说,本题是在无向图上求出一条从1N的路径,使路径上第K+1大的边权尽量小。

可以仿照前面所说的方法,用D[x,p]表示从1号节点到达基站x,途中已经指定了p条电缆免费时,经过的路径最贵的电缆的话费最小是多少(也就是选择一条从1x的路径,使路径上第p+1大的边权尽量少)。若有一条从xy长度为z的无向边,则应该用max(D[x,p],z)更新D[y,p]的最小值,用D[x,p]更新D[y,p+1]的最小值。前者表示不在电缆(x,y,z)上使用免费升级服务,后者表示使用。

显然,我们刚才设计的状态转移是有后效性的(因为本题是按照动态规划的思想解决的,动态规划对状态空间的遍历构成一张有向无环图。注意一定是有向无环图!遍历顺序就是该有向无环图的一个拓扑序。 有向无向图中的节点对应问题中的“状态”,图中的边对应状态之间的“转移”,转移的选取就是动态规划中的“决策”。 在本题中,比如有三个点 a,b,c,构成一个环,那么用dijkstra更新就会发生当前更新的值不是最终的值,那么转移时就会出现问题。)。在有后效性时,一种解决方案是利用迭代思想,借助SPFA算法进行动态规划, 直至所有状态收敛(不能再更新)。

从最短路径问题的角度去理解,图中的节点也不仅限于“整数编号”,可以扩展到二维,用二元组(x,p)代表一个节点,从(x,p)(y,p)有长度为z的边,从(x,p)(y,p+1)有长度为0的边。D[x,p]表示起点(1,0)到节点(x,p),路径上最长的边最短是多少。这是NK个点,PK条边的广义最短路径问题。对于非精心构造的数据,SPFA算法的时间复杂度为O(tNP),其中t为常数,实际测试可以AC。本题也让我们进一步领会了动态规划与最短路的共通性。

Code
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,p,k;
const int N=1000000+10,M=10000000+10;
int head[N],to[M],nxt[M],cnt,w[M],dis[N];
struct node{
	int val,pos;
	bool operator >(const node &x)const{
		return val>x.val;
	}
};
void add(int u,int v,int f){
	to[++cnt]=v;
	w[cnt]=f;
	nxt[cnt]=head[u];
	head[u]=cnt;
	return;
}
priority_queue<node,vector<node>,greater<node> >q;
bool v[N];
void dijkstra(){
	memset(dis,0x3f,sizeof(dis));
	dis[1]=0;
	q.push(node{0,1});
	while(!q.empty()){
		node t=q.top();q.pop();
		if(v[t.pos])continue;
		v[t.pos]=1;
		for(int i=head[t.pos];i;i=nxt[i]){
			int v=to[i],z=max(dis[t.pos],w[i]);
			if(dis[v]>z){
				dis[v]=z;
				q.push(node{dis[v],v});
			}
		}
	}
	return;
}
signed main(){
	scanf("%lld %lld %lld",&n,&p,&k);
	for(int i=1,x,y,z;i<=p;i++){
		scanf("%lld %lld %lld",&x,&y,&z);
		add(x,y,z);add(y,x,z);
		for(int j=1;j<=k;j++){
			add(x+(j-1)*n,y+j*n,0);
			add(y+(j-1)*n,x+j*n,0);
			add(x+j*n,y+j*n,z);
			add(y+j*n,x+j*n,z);
		}
	}
	dijkstra();
	int ans=1e18;
	for(int i=0;i<=k;i++){
		ans=min(ans,dis[n+i*n]);
	}
	if(ans==1e18)printf("-1\n");
	else printf("%lld\n",ans);
	return 0;
}

2

飞行路线

做法

由于购买机票不需要花钱,所以肯定不会多次重复乘坐同样的航线或者多次访问到同一个城市。如果k=0本题就是最基础的最短路问题。但题目中说明了对有限条边设置为免费),可以使用分层图的方式,将图多复制k次,原编号为i的节点复制为编号i+jn(1jk)的节点,然后对于原图存在的边,第j层和第j+1层的对应节点也需要连上,看起来就是相同的图上下堆叠起来。

Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+50,M=2200050;
int head[N*12],cnt,nxt[M],to[M],w[M],n,m,k,s,t,dis[N*12];
void add(int u,int v,int f)
{
	to[++cnt]=v;
	nxt[cnt]=head[u];
	w[cnt]=f;
	head[u]=cnt;
} 
struct node
{
	int val,pos;
	bool operator >(const node &x)const{
		return val>x.val;
	}
};
priority_queue<node,vector<node>,greater<node> >q;
bool b[N*12];
void dij()
{
	memset(dis,0x3f,sizeof(dis));
	dis[s]=0;
	q.push(node{dis[s],s});
	while(!q.empty())
	{
		node t=q.top();q.pop();
		if(b[t.pos])continue;
		b[t.pos]=1;
		for(int i=head[t.pos];i;i=nxt[i])
		{
			int v=to[i];
			if(dis[v]>dis[t.pos]+w[i])
			{
				dis[v]=dis[t.pos]+w[i];
				q.push(node{dis[v],v});
			}
		}
	}
	return;
}
int main()
{
	scanf("%d %d %d",&n,&m,&k);
	scanf("%d %d",&s,&t);
	for(int i=1,u,v,w;i<=m;i++)
	{
		scanf("%d %d %d",&u,&v,&w);
		add(u,v,w);add(v,u,w);
		for(int j=1;j<=k;j++)
		{
			add(u+j*n,v+j*n,w);
			add(v+j*n,u+j*n,w);
			add(u+j*n-n,v+j*n,0);
			add(v+j*n-n,u+j*n,0);
		}
	}
	dij();
	int ans=0x3f3f3f3f;
	for(int j=0;j<=k;j++)
		if(dis[t+j*n]<ans)
			ans=dis[t+j*n];
	printf("%d\n",ans);
	return 0;
}

3

考试真题。

做法

这道题就是一个很显然的二维最短路(特殊的分层图最短路),设 dis[i][j] 表示到达点 i 且当前的状态为 j 的最少代价。其中 j=0 时表示状态为 Lj=1 时表示状态为 R

很显然可以用 dijkstra 来求解,转移方程为 dis[v][v.type]=dis[u][u.type]+w(u.type==v.type)

也可以为 dis[v][u.type]=dis[u][u.type]+w(v.type==M)

还有种情况 dis[v][v.type]=dis[u][u.type]+w+x(v.typeu.type  and  v.type2)

答案为 min(dis[t][0],dis[t][1])

Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+50;
int T,cnt;
int n,m,s,t,x;
int head[N];
struct edge{
	int to,nxt,w;
}e[N*4];
void add(int u,int v,int f)
{
	e[++cnt].to=v;
	e[cnt].nxt=head[u];
	head[u]=cnt;
	e[cnt].w=f;
	return;
}
string ss;
bool vis[N][4];
int dis[N][4];
struct node{
	int val,pos,type;
	bool operator >(const node &x)const
	{
		return val>x.val;
	}
};
void dij(int S)
{
	for(int i=1;i<=n;i++){
		for(int j=0;j<=2;j++)
		{
			dis[i][j]=1e18;
			vis[i][j]=0;
		}
	}
	if(ss[S]=='L')dis[S][0]=0;
	else if(ss[S]=='R')dis[S][1]=0;
	else if(ss[S]=='M')dis[S][0]=0,dis[S][1]=0;
	priority_queue<node,vector<node>,greater<node> >q;
	q.push(node{dis[S][0],S,0});q.push(node{dis[S][1],S,1});
	while(!q.empty())
	{
		node t=q.top();q.pop();
		if(vis[t.pos][t.type])continue;
		vis[t.pos][t.type]=1;
		for(int i=head[t.pos];i;i=e[i].nxt)
		{
			int v=e[i].to;
			int op=-1;
			if(ss[v]=='L')op=0;
			else if(ss[v]=='R')op=1;
			else if(ss[v]=='M')op=2;
			if(op==t.type&&dis[v][op]>dis[t.pos][t.type]+e[i].w)
			{
				dis[v][op]=dis[t.pos][t.type]+e[i].w;
				q.push(node{dis[v][op],v,op});
			}
			else if(op==2&&dis[v][t.type]>dis[t.pos][t.type]+e[i].w)
			{
				dis[v][t.type]=dis[t.pos][t.type]+e[i].w;
				q.push(node{dis[v][t.type],v,t.type});
			}
			else if(dis[v][op]>dis[t.pos][t.type]+x+e[i].w)
			{
				dis[v][op]=dis[t.pos][t.type]+e[i].w+x;
				q.push(node{dis[v][op],v,op});
			}
		}
	}
	return;
}
signed main()
{
	scanf("%lld",&T);
	while(T--)
	{
		cnt=0;
		scanf("%lld %lld %lld %lld %lld",&n,&m,&s,&t,&x);
		for(int i=1;i<=n+50;i++)head[i]=0;
		cin>>ss;
		ss=" "+ss;
		for(int i=1,u,v,w;i<=m;i++)
		{
			scanf("%lld %lld %lld",&u,&v,&w);
			add(u,v,w);add(v,u,w);
		}
		dij(s);
		printf("%lld\n",min(dis[t][0],dis[t][1]));
	}
	return 0;
}

补图最短路

是一种很套路的考法,一般做法的主题思想都是不变的。

补图的定义

补图是相对于完全图定义的, 对于一个图G,存在有一个与图G,n个节点完全相同的完全图Kn,图G的补图即为将Kn的边集再减去图G的边集图G,则称G为图G的补图。

通俗的理解即为有n个节点的G,现将它的边都删除掉,再连接剩下的每个点与其他点的边(如果这条边在原图存在,那就不连接)。新的图即为原来的图。

所以补图的有一个性质为点还是原图的点,但边完全不一样。

补图最短路

前置条件:补图中的边权必须相等

补图最短路在补图求最短路,但如果原图的n很大,m很小,则相当于原图为一个稀疏图,那么它的补图的边数就很多,导致直接求最短路时间上不能接受。

一种常见的做法为创建两个set,S1,S2S1表示补图上能到达且没有更新的点,S2表示补图上不能到达且没有更新的点,也就是相邻的节点。

然后在原图跑最短路,遍历到一个点u,它的相邻节点为v,那么如果S1存在v,就把它从S1中删除,再放到S2中。剩下S1中的点即为不能直接与u相通的节点,那么就证明在补图中u和这些点一定有边连接,所以再在S1中更新这些点与u的最短路,注意这里可以直接删除,因为u当前是dis值最小的点,所以更新这些没有更新的点后,这些点的dis一定定下来了,所以可以直接删去。终止条件即为S2为空,即与u相邻且没有更新的节点没有了,那么就说明u为最后一个更新的节点,因为S1为空且S2也为空。

习题1Sparse Graph

Code
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+50,M=2e4+50;
bool vis[N];
int dis[N],T,n,m,s,head[N],cnt,nxt[M*2],to[M*2],del[N];
set<int> sx[2];
bool f=0;
void add(int u,int v)
{
	to[++cnt]=v;
	nxt[cnt]=head[u];
	head[u]=cnt;
}
void bfs(int x)
{
	memset(dis,0x3f,sizeof(dis));
	memset(vis,0,sizeof(vis));
	dis[x]=0;
	queue<int> q;
	q.push(x);
	while(!q.empty())
	{
		int u=q.front();q.pop();
		if(vis[u])continue;
		vis[u]=1;
		for(int i=head[u];i;i=nxt[i])
		{
			int v=to[i];
			if(sx[f].find(v)!=sx[f].end())
			{
				sx[f].erase(v);
				sx[!f].insert(v);
			}
		}
		int res=0;
		for(auto v:sx[f])
		{
//			if(dis[v]>dis[u]+1)
//			{
				dis[v]=dis[u]+1;
				q.push(v);
//			}
			del[++res]=v;
		}
		for(int i=1;i<=res;i++)sx[f].erase(del[i]);
		f=!f;
		if(sx[f].size()==0)break;//还没有更新且与u相邻的节点数量为0 
	}
	
}
int main()
{
	scanf("%d",&T);
	while(T--)
	{
		cnt=0;memset(head,0,sizeof(head));f=0;
		scanf("%d %d",&n,&m);
		for(int i=1,u,v;i<=m;i++)
		{
			scanf("%d %d",&u,&v);
			add(u,v);add(v,u);
		}
		for(int i=1;i<=n;i++)sx[f].insert(i);
		scanf("%d",&s);
		sx[f].erase(s);
		bfs(s);
		for(int i=1;i<=n;i++)
		{
			if(i!=s)
			{
				if(dis[i]==0x3f3f3f3f)printf("-1 ");
				else printf("%d ",dis[i]);
			}
		}
		printf("\n");
	}
	return 0;
} 

习题2神秘力量

算法:二维最短路+补图最短路

和上一题一样,这题做法为在正图上跑最短路,然后再在补图进行更新,很显然,如果上一条边为普通边,那就直接普普通通地进行更新即disu=min(disv+w,disu),如果上一条边为特殊边,那就将它相邻的点进行更新,即disu=min(disv+wk,disu),然后在S1里面删去u,将它放入S2里面,最后更新完后S1里的点即为不与v相邻的点,再将S1的点全部更新最后直接删去。

为什么可以直接删去?,因为边权为0而且当前点v的最短路已经定下来了,所以直接更新dis值再删去同样满足这个点不会再被更新。

具体实现过程为:
disu,0表示到u的上一条边为普通边,disu,1表示到u的上一条边为特殊边。所以对于当前从优先队列中取出并更新的点u

  • 如果是通过disu,0出队的,那么对于u的相邻节点v,如果连接的边为普通边disv,0=min(disv,0,disu,0+w),而如果为特殊边则有disv,1=min(disv,1,disu,0+w)

  • 如果是通过disu,1出队的,那么对于u的相邻节点v,如果连接的边为普通边disv,0=min(disv,0,disu,1+wk),而如果为特殊边则有disv,1=min(disv,1,disu,1+wk),然后在S1里面删去v,把它放入S2中。最后遍历S1中的元素,直接进行更新disv,0=disu,1,再删去。

Code
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+50,M=2e6+50;
int T,n,m,s,k;
int head[N],to[M],nxt[M],cnt,w[M],type[M],D[N];
long long dis[N][2];
bool vis[N][2];
void add(int u,int v,int f,int t)
{
	to[++cnt]=v;
	nxt[cnt]=head[u];
	head[u]=cnt;
	w[cnt]=f;
	type[cnt]=t;
}
struct node{
	int val,type,pos;
	bool operator >(const node &x)const{
		return val>x.val;
	}
};
void dij(int ss)
{
	priority_queue<node,vector<node>,greater<node> >q;
	dis[ss][0]=0;
	q.push(node{dis[ss][0],0,ss});
	bool f=0;
	set<int> S[2];
	//	int res=0x3f3f3f3f,pos=0;
	for(int i=1;i<=n;i++)S[f].insert(i);
	S[f].erase(ss);
	while(!q.empty())
	{
		node t=q.top();q.pop();
		if(vis[t.pos][t.type])continue;
		vis[t.pos][t.type]=1;
		//cout<<t.pos<<" "<<t.type<<endl;
		int type1=t.type,u=t.pos;
		for(int i=head[u];i;i=nxt[i])
		{
			int v=to[i];
			if(type1==1&&S[f].find(v)!=S[f].end())
			{
				S[f].erase(v);
				S[!f].insert(v);
			}
			if(type1==1)
			{
				if(type[i]==1)
				{
					if(dis[v][1]>dis[u][type1]+w[i]-k)
					{
						dis[v][1]=dis[u][type1]+w[i]-k;
						q.push(node{dis[v][1],1,v});
					}
				}
				else
				{
					if(dis[v][0]>dis[u][type1]+w[i]-k)
					{
						dis[v][0]=dis[u][type1]+w[i]-k;
						q.push(node{dis[v][0],0,v});
					}
				}
			}
			else
			{
				if(type[i]==1)
				{
					if(dis[v][1]>dis[u][type1]+w[i])
					{
						dis[v][1]=dis[u][type1]+w[i];
						q.push(node{dis[v][1],1,v});
					}
				}
				else
				{
					if(dis[v][0]>dis[u][type1]+w[i])
					{
						dis[v][0]=dis[u][type1]+w[i];
						q.push(node{dis[v][0],0,v});
					}
				}
			}
		}
		if(type1==1)
		{
			int res=0;
			for(auto x:S[f])
			{
				if(dis[x][0]>dis[u][type1])
				{
					dis[x][0]=dis[u][type1];
					q.push(node{dis[x][0],0,x});
				}
				D[++res]=x;
			}
			for(int i=1;i<=res;i++)S[f].erase(D[i]);
			f=!f;
			if(S[f].size()==0)break;
		}
		
	}
	return;	
}
int main()
{
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d %d %d %d",&n,&m,&s,&k);
		cnt=0;
		for(int i=0;i<=n+50;i++)
		{
			head[i]=0;
			dis[i][0]=dis[i][1]=1e18;
			vis[i][0]=vis[i][1]=0;
		}
		for(int i=1,u,v,w,t;i<=m;i++)
		{
			scanf("%d %d %d %d",&u,&v,&w,&t);
			add(u,v,w,t);
		}
		dij(s);
		for(int i=1;i<=n;i++)
		{
			if(dis[i][0]==1e18&&dis[i][1]==1e18)printf("-1 ");
			else printf("%lld ",min(dis[i][0],dis[i][1]));
		}
		printf("\n");
	}
	return 0;
}

杂题

1(最短路+拓扑排序)

题目传送门

解法

由题可知,一个城市u保护城市v,所以建一条边uv表示城市u保护城市v,因为题目说保证有解,所以建的图一定是一个有向无环图DAG 。再在此基础上求出最短路径。

具体过程为设disu表示实际到达(攻破)u的最短时间,arriveu表示到达u的时间(注意arriveudisu,而是arriveudisu)。然后考虑怎么转移,我们发现arriveu只会影响一个值就是disu,而disu能影响其他节点的arrivedis值。

所以先考虑arriveu的转移:

  • arriveu=min(disv,arriveu),其中v为与u相邻的节点。显然arrivev不能更新disu,因为arrivev表示只是到达了v点,但可能还没有进入v点,所以不能这样更新。

然后再来考虑disu的转移:

  • disu=max(arriveu,disv)v表示保护u的节点编号。如果arriveudisv表明到达u点可能还没有进入的最早时间比攻破v点的时间早,那么显然此时disu应该由disv决定。如果arriveudisv表明到达u点可能还没有进入的最早时间比攻破v点的时间晚,而因为此时保护u点的节点都被攻破了,所以由arriveu来决定,即disu=arriveu

转移方程想出来了,于是现在考虑用怎样的顺序能正确更新disuarriveu。我们发现上述过程与dijkstra算法的过程很相似,如果我们也每次取出dis值最小的点来进行更新就行。(为什么可以直接取出dis值最小的点进行更新?因为类似于djikstra的证明一定不存在另外的路径使得dis值再变小。)于是我们就可以用dijkstra+topo来实现啦。

Code

#include<bits/stdc++.h>
using namespace std;
const int N=3050,M=70050;
long long dis[N],arrive[N],into[N];
int n,m;
int head[N],to[M],w[M],nxt[M],cnt,in[N];
bool vis[N];
vector<int> g[N];
void add(int u,int v,int f)
{
	to[++cnt]=v;
	nxt[cnt]=head[u];
	w[cnt]=f;
	head[u]=cnt;
}
struct node{
	long long val;int pos;
	bool operator >(const node &x)const{
		return val>x.val;
	}
};
void dij(int s)
{
	for(int i=1;i<=n;i++)dis[i]=arrive[i]=1e18;
	dis[s]=into[s]=arrive[s]=0;
	priority_queue<node,vector<node>,greater<node> >q;
	q.push(node{0,s});
	while(!q.empty())
	{
		node t=q.top();q.pop();
		if(vis[t.pos])continue;
//		cout<<t.pos<<endl;
		vis[t.pos]=1;
		for(int i=head[t.pos];i;i=nxt[i])
		{
			int v=to[i];
			if(arrive[v]>dis[t.pos]+w[i])
			{
				arrive[v]=dis[t.pos]+w[i];
				if(!in[v])
				{
					dis[v]=max(into[v],arrive[v]);
					q.push(node{dis[v],v});
//					cout<<v<<endl;
				}
			}
		}
		for(auto v:g[t.pos])
		{
			in[v]--;
			into[v]=max(into[v],dis[t.pos]);
			if(!in[v])
			{
				dis[v]=max(arrive[v],into[v]);
				q.push(node{dis[v],v});
			}
		}
	}
	return;
}
int main()
{
	scanf("%d %d",&n,&m);
	for(int i=1,u,v,w;i<=m;i++)
	{
		scanf("%d %d %d",&u,&v,&w);
		add(u,v,w);
	}
	for(int i=1;i<=n;i++)
	{
		int x,v;
		scanf("%d",&x);
		while(x--)
		{
			scanf("%d",&v);
			g[v].push_back(i);
			in[i]++;
		}
	}
	dij(1);
	printf("%lld\n",dis[n]);
	return 0;
}

2dfs判环+简单dp+单源最短路)

题目传送门

解法

建出反图,再在正图上算出以n终点的最短路,设disu表示un的最短路径,f[u][k]表示dis(u,n)=disu+k的方案数。

考虑怎么转移f[u][k],考虑反图上的一条边vu,那么如果在正图上经过这条边到达n的最短路径为disu+kdisvw,所以f[u][k]就可以从f[v][disu+kdisvw]转移过来。

所以方程为f[u][k]=f[v][k+(disudisvw)]

考虑到这个方程转移边界会很复杂,可以用记忆化搜索来实现。

初始化f[1][0]=1

Code

#include<bits/stdc++.h>
#define re register int
#define fo(i,a,b) for (re i=a;i<=b;i++)
using namespace std;
const int maxn=200000+50;
const int mxn=100000+50;
int T,n,m,k,p,sum1,sum2;
bool flag;
int head[mxn],rev[mxn],dist[mxn],vis[mxn],wd[mxn][51],f[mxn][51],to1[maxn],to2[maxn],w1[maxn],w2[maxn];
int nxt1[maxn],nxt2[maxn];
void add(int x,int y,int z)
{
	to1[++sum1]=y;
	nxt1[sum1]=head[x];
	w1[sum1]=z;
	head[x]=sum1;
}
void addr(int x,int y,int z)
{
	to2[++sum2]=y;
	nxt2[sum2]=rev[x];
	w2[sum2]=z;
	rev[x]=sum2;
}
struct node{
	int val,pos;
	bool operator >(const node &x)const{
		return val>x.val;
	}
};
void dij()
{
	priority_queue<node,vector<node>,greater<node> > q;
	dist[1]=0;
	q.push(node{dist[1],1});
	while (!q.empty())
	{
		node t=q.top();q.pop();
		if(vis[t.pos])continue;
		vis[t.pos]=1;
		for (int i=head[t.pos];i;i=nxt1[i])
			if (dist[to1[i]]>dist[t.pos]+w1[i]){
				dist[to1[i]]=dist[t.pos]+w1[i];
				q.push(node{dist[to1[i]],to1[i]});	
			}
	}
}
int dfs(int u,int know)
{
	if(know>k||know<0)return 0;
	if(wd[u][know]){
		wd[u][know]=0;
		return -1;
	}
	if(f[u][know]!=-1)return f[u][know];
	wd[u][know]=1;
	long long sum=0;
	for (int i=rev[u];i;i=nxt2[i])
	{
		int tmp=dist[u]+know-w2[i]-dist[to2[i]];
		int val=dfs(to2[i],tmp);
		if(val==-1)
		{
			wd[u][know]=0;
			return -1;
		}
		sum=(sum+val)%p;
	}
	if(u==1&&know==0)sum++;
	wd[u][know]=0;
	return f[u][know]=sum;
}
int main()
{
	scanf("%d",&T);
	while (T--)
	{
		sum1=0;sum2=0;
		flag=false;
		scanf("%d%d%d%d",&n,&m,&k,&p);
		memset(f,-1,sizeof(f));
		for(int i=1;i<=n;i++)
		{
			head[i]=rev[i]=0;
			vis[i]=0;dist[i]=0x3f3f3f3f;
		}
		for(int i=1,x,y,z;i<=m;i++) 
		{
			scanf("%d %d %d",&x,&y,&z);
			add(x,y,z);
			addr(y,x,z);
		}
		dij();
		long long ans=0;
		for(int i=0;i<=k;i++)
		{
			int val=dfs(n,i);
			if(val==-1)
			{
				puts("-1");
				flag=true;
				break;
			}
			ans=(ans+val)%p;
		}
		if (!flag)printf("%lld\n",ans);
	}
	return 0;
}

3(排序+多次单源最短路)

题目传送门

解法

对所有边进行排序,然后只用取前k小的边因为不选的话会经过不是边权前k小的边,就一定不会比不经过不是前k小的边更优)。

然后对于这2×k个点进行最短路,用个堆维护dis值第k小的数就行了。

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+50;
int n,m,k,dis[N];
bool vis[N],mark[N];
struct edge{
	int fr,to,w;
}e[400050];
int head[N],cnt,nxt[4050],to[4050],w[4050];
void add(int u,int v,int f)
{
	to[++cnt]=v;
	nxt[cnt]=head[u];
	head[u]=cnt;
	w[cnt]=f;
}
bool cmp(edge a,edge b){return a.w<b.w;}
struct node{
	int val,pos;
	bool operator >(const node &x)const{
		return val>x.val;
	}
};
priority_queue<int> ans;
void dij(int s)
{
	for(int i=1;i<=n;i++)dis[i]=1e18,vis[i]=0;
	dis[s]=0;
	priority_queue<node,vector<node>,greater<node> >q;
	q.push(node{dis[s],s});
	while(!q.empty())
	{
		node t=q.top();q.pop();
		if(vis[t.pos])continue;
		if(mark[t.pos]&&t.pos>s&&dis[t.pos]!=1e18)
		{
			if(ans.size()<k)ans.push(dis[t.pos]);
			else{
				if(ans.top()>dis[t.pos])
				{
					ans.pop();
					ans.push(dis[t.pos]);				
				} 
			}
		}
		vis[t.pos]=1;
		for(int i=head[t.pos];i;i=nxt[i])
		{
			int v=to[i];
			if(dis[v]>dis[t.pos]+w[i])
			{
				dis[v]=dis[t.pos]+w[i];
				q.push(node{dis[v],v});
			}
		}
	}
}
signed main()
{
	scanf("%lld %lld %lld",&n,&m,&k);
	for(int i=1,u,v,w;i<=m;i++)
	{	
		scanf("%lld %lld %lld",&u,&v,&w);
		e[i].fr=u;e[i].to=v;e[i].w=w;
	}
	sort(e+1,e+m+1,cmp);
	for(int i=1;i<=k;i++)
	{
		mark[e[i].fr]=1,mark[e[i].to]=1;
		add(e[i].fr,e[i].to,e[i].w);
		add(e[i].to,e[i].fr,e[i].w);
	}
	for(int i=1;i<=n;i++)
	{
		if(mark[i])
			dij(i);
	}
	printf("%lld\n",ans.top());
	return 0;
}

4 红灯(图论建模)

题目传送门

解法

按照题意模拟即可,注意要把边看成点进行建模。具体地:看每个点连出去的四条边,设当前点的编号为 x ,则第一条边的编号为 4×x+0,第二条边编号为 4×x+1,第三条边编号为 4×x+2,第四条边为 4×x+3。然后进行再跑最短路即可,注意原来的起点 s 和终点 t 要按照新建的图的点的编号重新计算。

Code

#include<bits/stdc++.h>
using namespace std;
const int N=5e7+50;
int head[N],cnt,nxt[N],to[N],n,a[500050][5],s1,s2,t1,t2,w[N];
bool vis[N];
long long dis[N];
void add(int u,int v,int f)
{
	to[++cnt]=v;
	nxt[cnt]=head[u];
	head[u]=cnt;
	w[cnt]=f;
}
struct node{
	long long val,pos;
	bool operator >(const node &x)const{
		return val>x.val;
	}
};
priority_queue<node,vector<node>,greater<node> >q;
void dij(int s)
{
	for(int i=0;i<=(2000000)*4;i++)dis[i]=5611686018427387904;
	memset(vis,0,sizeof(vis));
	dis[s]=0;
	q.push(node{dis[s],s});
	while(!q.empty())
	{
		node t=q.top();q.pop();
		if(vis[t.pos])continue;
		//		cout<<endl<<t.pos<<endl; 
		vis[t.pos]=1;
		for(int i=head[t.pos];i;i=nxt[i])
		{
			int v=to[i];
			if(dis[v]>dis[t.pos]+w[i])
			{
				dis[v]=dis[t.pos]+w[i];
				q.push(node{dis[v],v});
			}
		}
	}
}
signed main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d %d %d %d",&a[i][0],&a[i][1],&a[i][2],&a[i][3]);
	for(int i=1;i<=n;i++)
	{
		for(int j=0,u,v;j<=3;j++)
		{
			if(a[i][j]==0)continue;
			for(int x=0;x<=3;x++)
				if(a[a[i][j]][x]==i)u=a[i][j]*4+x;
			//			i->(a[i][(j+1)%4])
			for(int x=0;x<=3;x++)
			{
				if(a[i][(j+1)%4]==a[i][x]&&a[i][(j+1)%4])v=i*4+x,add(u,v,0);
				else if(a[i][x])v=i*4+x,add(u,v,1);
			}
		}
	}
	scanf("%d %d %d %d",&s1,&s2,&t1,&t2);
	//	s=s1*4+s2;t=t1*4+t2;
	int s,t;
	for(int x=0;x<=3;x++)if(a[s1][x]==s2)s=s1*4+x;
	for(int x=0;x<=3;x++)if(a[t1][x]==t2)t=t1*4+x;
	dij(s);
	if(dis[t]!=5611686018427387904)printf("%lld\n",dis[t]);
	else printf("-1\n");
	return 0;
}

总结

只要是是图论,建模一定排在第一位,其次是算法特征与思想,最后才是套路,三者一结合起来那么你在图论这个版块就一定不会很差。

有时候图论题还需要考察你对模型的抽象化,有可能第一眼看上去和图论一点关系没有,结果最后是图论建模后就解决了,这类题型只能靠见多识广,再无其他门路。

一些图论套路题也要多见识,才能在考场上遇见这些题目不慌张。还有平时打图论模板也要一个字一个字的打,切忌不要直接复制,因为我们是 OI 赛制,考场上没有可以直接复制粘贴的模板。而且重新写一遍对你对该算法的认识又增加一个层次。

最后就是不要小看一些看上去复杂度很高很没有优势的算法,它们在特定的题型中还是会发挥它们自己的作用的。比如 BellmanFord 有时候判断负环时很有优势,在小数据下表现得也不比 Dijkstra 差,SPFA 虽然容易卡,但在一些转移需要重复更新的情况下,进行多轮迭代,直至状态全部收敛,这是与它的算法思想是紧密相关的。

Floyd 也有自己的优势比如判负环、负权回路与差分约束等。Dijkstra 可以处理有环的图的状态转移,这也与它的算法思想每次从优先队列里选取的数是最后且不会改变的紧密相关。处理不是 DAG 的时候很方便。

当然有时候一个题目需要运用到多种算法,这就很考验一个选手的综合能力与基本功了,所以我们需要及时复习以前学过的算法,以便临近考试时不慌张。

总之一句话,多见识题型,多训练思维,及时回顾算法思想与解题套路,这样就能在 OI 生涯中打下坚实的基础。

posted @   CQWYB  阅读(61)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
点击右上角即可分享
微信分享提示