1. 程式人生 > 實用技巧 >最短路合集

最短路合集

最短路合集

一.一些定義

​ 本篇純屬不想打程式碼之餘搞出來的東西,可能不會有什麼參考意義吧。這些定義也是我隨便取的名字,也不知道有沒有專業的叫法。並且所有的最短路求都是用的 \(Dijkstra\) 打的,不會用過世的演算法。

最短路樹

​ 從一個點 s 出發到其他所有點的最短路所構成的樹,此時每一個點只貢獻一條鏈。大概的構思來自於 D演算法中每個點只出隊入隊一次,相當於拿它去更新剩餘點的時候自身的最短路就已經確定了,這條最短路有且只有一個前驅點,此時記錄前驅(或者建樹)就行。然後可以運用優先佇列中 pair 的排序法則結合轉移的邊的編號做一些騷事情,後面有例題。

最短路圖

​ 從 s 到 t 的最短路圖由所有 s 到 t 的最短路所組成。性質是一個 DAG 。當然也可以做成一個無向圖。建圖的方法是分別從 s,t 都跑一邊最短路,列舉每條邊 \((u,v)\)

,看是否滿足 \(dis_{s,u}+w_{u,v}+dis_{v,t}=dis_{s,t}\),滿足就連邊。

二.一些變式

1.元

​ 求 s 到其它點的最短路。

​ D演算法即可。

2.邊數

​ 求解 s 到其他點的邊數 最大/最小 的最短路,即在單純的距離最短路上,對於距離相同的做一個邊數的約束。也就是魔改一下轉移方程的問題,記錄一個經過邊數就好。略微拓展可以做出最多經過 k 條邊的最短路。

3.計數

​ s 到其他點的最短路有多少條,也是魔改方程,很簡單就不多講,掛兩個例題。最短路計數路徑統計

4.嚴格次短路

​ s 到 t 的嚴格次短路。這個還是相當於魔改方程,只是對於每一個維護一個最短與次短,放上核心程式碼以及

例題

int u=q.top().second.second;
	q.pop();
	if(vis[u]==2)continue;
	vis[u]++;
	for(register int i=head[u];i;i=ne[i]){
		int k=fi[u]+dis[i],v=to[i],fl=0;
		if(k<fi[v])se[v]=fi[v],fi[v]=k,fl=1;
		else if(k>fi[v]&&k<se[v])se[v]=k,fl=1;
		if(se[v]>dis[i]+se[u])se[v]=dis[i]+se[u],fl=1;
		if(fl)q.push(make_pair(-fi[v],make_pair(-se[v],v)));
	}

由於每個點只出入堆一次只能算出最短路,但是最短路會影響其他的次短路,所以只出入堆一次不能將兩個都算出來,於是改為出入堆兩次就行。

5.最短路邊

​ 一個邊與 s 到 t 的最短路上的邊一共有三種關係,一定是,可能是,不是,有兩種做法。

第一個做法

​ 由於關聯到所有在最短路上的邊,於是建一個最短路圖,並把它建成無向的,去上邊找橋邊。不在圖上就不是,在圖上但不是橋邊就是可能,剩下的就是一定是。這個應該很裸。

第二個做法

​ 還是建最短路圖,但是在上面跑一個 DP,記錄一個點到 s 和 t 的方案數,不妨記為 \(f_{s,i},f_{i,t}\) ,那麼對於一個在最短路圖上面的邊 \((u,v)\) 若滿足 \(f_{s,u}*f_{v,t}=f_{s,t}\) 便是一定,剩下的類推。主要是用的乘法原理,但是很多時候容易乘崩,於是多選幾個模數吧。

例題與核心程式碼

連結,這道題只用知道關係後與最短路做差就行。

const int MAXN=200005;
int n,m,s,e; 
int head[3][MAXN],ne[3][MAXN],to[3][MAXN],w[3][MAXN],tot[3],fr[3][MAXN];
inline void add(int alf,int x,int y,int z){
	w[alf][++tot[alf]]=z;
	fr[alf][tot[alf]]=x;
	to[alf][tot[alf]]=y;
	ne[alf][tot[alf]]=head[alf][x];
	head[alf][x]=tot[alf];
}
long long dis[2][MAXN];
bool vis[2][MAXN];
priority_queue<pair<long long,int> > q[2];
inline void di(int alf,int x){
	dis[alf][x]=0;
	q[alf].push(make_pair(0,x));
	while(!q[alf].empty()){
		int u=q[alf].top().second;
		q[alf].pop();
		if(vis[alf][u])continue;
		vis[alf][u]=1;
		for(register int i=head[alf][u];i;i=ne[alf][i]){
			int v=to[alf][i];
			if(dis[alf][v]>dis[alf][u]+w[alf][i]){
				dis[alf][v]=dis[alf][u]+w[alf][i];
				q[alf].push(make_pair(-dis[alf][v],v));
			}
		}
	}
}
int dfn[MAXN],low[MAXN],date;
bool is[MAXN];
void tarjan(int x,int k){
	dfn[x]=low[x]=++date;
	for(register int i=head[2][x];i;i=ne[2][i]){
		int v=to[2][i];
		if(!dfn[v]){
			tarjan(v,i);
			low[x]=min(low[x],low[v]);
			if(low[v]>dfn[x])is[w[2][i]]=1;
		}
		else if(i!=(k^1))low[x]=min(low[x],dfn[v]);
	}
}
int main(){
	n=read();m=read();s=read();e=read();
	for(register int i=1,x,y,z;i<=m;++i){
		x=read();y=read();z=read();
		add(0,x,y,z),add(1,y,x,z);
	}
	memset(dis,0x3f,sizeof(dis));
	di(0,s);
	di(1,e);
	++tot[2];
	for(register int i=1;i<=n;++i)
		for(register int j=head[0][i];j;j=ne[0][j])
			if(dis[0][e]==dis[0][i]+w[0][j]+dis[1][to[0][j]])
				add(2,i,to[0][j],j),add(2,to[0][j],i,j);
	tarjan(s,0);
	for(register int i=1;i<=tot[0];++i){
		long long u=dis[0][e]-1-dis[0][fr[0][i]]-dis[1][to[0][i]];
		if(is[i])printf("YES\n");
		else if(u>0)printf("CAN %lld\n",w[0][i]-u);
		else printf("NO\n");
	}
	return 0;
}

6.最小環

​ 就是要求最小環,分為有向圖和無向圖

有向圖

​ 採用 \(Floyd\) 一開始將自己到自己的值設為正無窮,然後直接跑,最後自己到自己衝就行

無向圖

​ 同樣是採用 \(Floyd\) 但是並不是單純的直接跑,根據 DP 的順序來看,\(Floyd\) 的真意可以是以前 K 個點中轉的最短路,換而言之,第一維是在不斷地加中轉點,所以每次加入中轉點時,一邊計算最短路,一邊用 k 連線這個最短路的左右兩個端點,轉移為 \(a_{ki}+a_{kj}+dis_{ij}\) 這裡的 a 是指的原本就有的邊,而非求出來的最短路,用 \(dis_{ki}+dis_{kj}+dis_{ij}\) 的寫法是錯誤的,因為 \(dis_{ki}\) 所代表的路徑是可能包含 \(dis_{ij}\),如此便構不成環了。

​ 至於無向圖為什麼不能簡單的自己到自己,那是因為通常的無向圖建邊都是雙向的,也就是一條邊等效於一條環。

例題與程式碼

第一個例題是板子。

for(int k=1;k<=n;++k){
		for(int i=1;i<=n;++i){
			for(int j=1;j<=n;++j){
				if(i<j&&j<k&&a[i][k]<a[0][0]&&a[k][j]<a[0][0]&&i!=j&&j!=k)
					ans=min(ans,dp[i][j]+a[i][k]+a[j][k]);
				if(dp[i][k]<dp[0][0]&&dp[k][j]<dp[0][0])
					dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]);
			}
		}
	}

第二個例題則是對於 \(Floyd\) 的本質理解。一起放在這裡了,本質就是不斷地加中轉點,未被拿去中轉的就不會經過他。以下給出程式碼。

	for(i=0;i<n;++i)dp[i][i]=0,t[i]=read();
	for(i=1;i<=m;++i){
		x=read();y=read();d=read();
		dp[x][y]=dp[y][x]=min(dp[x][y],d);
	}
	Q=read();
	for(int I=1,T;I<=Q;++I){
		x=read();y=read();T=read();
		for(;t[la]<=T&&la<n;++la)
			for(i=0;i<n;++i)
				for(j=0;j<n;++j)dp[i][j]=min(dp[i][j],dp[i][la]+dp[la][j]);
		if(la<=x||la<=y||dp[x][y]==dp[n+1][n+1])printf("-1\n");
		else printf("%d\n",dp[x][y]);
	}

7.\(Floyd\) 的擴充套件

​ 說是 \(Floyd\) 其實也不是,只是寫到了就順便寫一些。

​ 問題是求解用 k 步從 s 到 t 的方案數。可以不怎麼思考的寫出方程 \(f_{k,i,j}=\sum\limits_{k\in V}f_{k-1,i,k}*f_{1,k,j}\),然後發現這個真的很像矩陣的乘法,即將上述抽象成 \(f_k=f_{k-1}*f_1\) 然後歸納一下得到 \(f_k=f_1^k\).且 \(f_1\) 恰為一個鄰接矩陣(僅表示是否相連)。

一些例題

第一個例題,發現長度在 0~9 之間,於是暴力拆點,一個變 10 個,只有一個主點,具體細節就不講述了,跟文章沒什麼大關係,愛看看。

const int mod=2009;
int n,t;
int jz[100][100],ans[100][100],res[100][100];
void mul(int a[100][100],int b[100][100],int c[100][100]){
	memset(res,0,sizeof(res));
	for(int i=1;i<=9*n;++i){
		for(int j=1;j<=9*n;++j){
			for(int k=1;k<=9*n;++k)res[i][j]=(res[i][j]+a[i][k]*b[k][j])%mod;
		}
	}
	for(int i=1;i<=9*n;++i){
		for(int j=1;j<=9*n;++j)c[i][j]=res[i][j];
	}
}
void jzksm(int a[100][100],int k){
	while(k){
		if(k&1)mul(ans,a,ans);
		k>>=1;
		mul(a,a,a);
	}
}
int main(){
	n=read();t=read();
	for(int i=1,st;i<=n;++i){
		st=(i-1)*9;
		for(int j=1;j<=8;++j)jz[st+j][st+j+1]=1;
		for(int j=1,x;j<=n;++j){
			scanf("%1d",&x);
			if(x)jz[st+x][9*(j-1)+1]=1;
		}
	}
	for(int i=1;i<=9*n;++i)ans[i][i]=1;
	jzksm(jz,t);
	printf("%d",ans[1][9*(n-1)+1]);
	return 0;
}

第二個例題,倒是不用管邊權了,但是不能馬上回手掏,於是採用點邊互換?大意是若有邊 \(a(x-y),b(y-z)\) 將 a,b 連邊,那麼只要一個無向圖拆出來的兩個邊不連就行。用一個虛點連起點的邊會方便計算答案,當然也可以記錄下來再挨個加起來。

const int mod=45989;
int n,m,t,s,e,tot,u[125],v[125],alf;
int jz[125][125],ans[125][125],res[125][125];
void mul(int a[125][125],int b[125][125],int c[125][125]){
	memset(res,0,sizeof(res));
	for(int i=1;i<=tot;++i){
		for(int j=1;j<=tot;++j){
			for(int k=1;k<=tot;++k)res[i][j]=(res[i][j]+a[i][k]*b[k][j])%mod;
		}
	}
	for(int i=1;i<=tot;++i){
		for(int j=1;j<=tot;++j)c[i][j]=res[i][j];
	}
}
void jzksm(int a[125][125],int k){
	while(k){
		if(k&1)mul(ans,a,ans);
		k>>=1;
		mul(a,a,a);
	}
}
int main(){
	n=read();m=read();t=read();s=read();e=read();
	s++,e++;
	u[++tot]=0,v[tot]=s;
	for(int i=1,x,y;i<=m;++i){
		x=read();y=read();
		x++;y++;
		u[++tot]=x,v[tot]=y;
		u[++tot]=y,v[tot]=x;
	}
	for(int i=1;i<=tot;++i){
		ans[i][i]=1;
		for(int j=1;j<=tot;++j){
			if(i!=j&&i!=(j^1)&&v[i]==u[j])jz[i][j]=1;
		}
	}
	jzksm(jz,t);
	for(int i=1;i<=tot;++i)
	if(v[i]==e)alf=(alf+ans[1][i])%mod;
	printf("%d",alf);
	return 0;
}

8.加邊最短路

​ 在沒有邊的兩點間加邊使得最短路不變的方案數。題目連線,反正蠻水的,兩遍最短路,\(n^2\)列舉就能過,程式碼不給。

​ 另一個是比較重要的,單位權無向圖,問刪除每條邊後點 1 到其餘所有點最短路是否最多加 1。需要用到 BFS樹,很顯然的是在 BFS樹上的深度和最短路直接掛鉤,然後考慮只跨一層和同層的非樹邊是否存在,沒找到例題。

9.刪邊最短路

​ 這個,有億點點複雜。求解斷掉詢問的一條邊後的最短路。首先若是完全不同的最短路徑(指 s 到 t ) 不止一條,隨便斷,都不會變的。如此考慮最短路徑只有一條的情況。在我的想象中,是兩棵最短路樹橫向插在了一起,共有一條鏈(最短路徑),當然程式碼不這麼寫XD,若是不是這條鏈上的邊斷了,沒有關係的,不會變。

​ 考慮若是鏈上的一條邊 \((u,v)\) 斷掉了,使得最短路經過了一個原本不在鏈上的 \((i,j)\) 那麼此時的最短路長度必然為 \(dis_{si}+w_{ij}+w_{jt}\), 這個距離所代表了一個鏈,而我們期望這個鏈與原最短鏈在開始和結尾各重疊了一部分,稱之為 \(L,R\),當然是可以不重疊的,但是在這個 \(L,R\) 中的邊,斷掉了之後是不能讓 \((i,j)\) 做這個最短路的,於是能讓 \((i,j)\) 成為最短路的只可能在最短鏈上除去\(L,R\)的部分中,可喜的是這一部分是連續的。於是列舉每一個不在最短路徑鏈上的邊,讓它對相應的部分更新,使用線段樹維護一個 min 值

​ 當然具體的細節也有很多,先跑一遍,對最短鏈上的邊編號過後,再跑兩邊,建出那兩棵樹的同時對於樹上的每一個點進行一個對應區間的左右端點傳遞。不詳細講,放程式碼。例題一例題二,都是大同小異的,這裡給出例題一的程式碼。

const int MAXN=100001;
int n,m;
int head[MAXN],fr[MAXN<<2],ne[MAXN<<2],to[MAXN<<2],w[MAXN<<2],tot;
inline void add(int x,int y,int z){
	w[++tot]=z,to[tot]=y,ne[tot]=head[x],head[x]=tot,fr[tot]=x;
}
int pre[MAXN],dis[3][MAXN],l[MAXN],r[MAXN];
priority_queue<pair<int,int> > q;
bool vis[MAXN],road[MAXN<<2];
inline void di(int alf,int s){
	memset(dis[alf],0x3f,sizeof(dis[alf]));
	memset(vis,0,sizeof(vis));
	dis[alf][s]=0;
	q.push(make_pair(0,-1));
	while(!q.empty()){
		int u,qwq=q.top().second;
		q.pop();
		if(qwq==-1)u=s;
		else u=to[qwq];
		if(vis[u])continue;
		vis[u]=1;
		if(qwq!=-1){
			if(alf==0)pre[u]=qwq;
			else if(alf==1&&!road[qwq])l[u]=l[fr[qwq]];
			else if(alf==2&&!road[qwq])r[u]=r[fr[qwq]];
		}
		for(int i=head[u];i;i=ne[i]){
			int v=to[i];
			if(dis[alf][v]>dis[alf][u]+w[i]){
				dis[alf][v]=dis[alf][u]+w[i];
				q.push(make_pair(-dis[alf][v],i));
			}
		}
	}
}
int minn[MAXN<<3];
void change(int k,int l,int r,int z,int y,int len){
	if(z==-1||y==-1||l>y||r<z||z>y)return ;
	if(l>=z&&r<=y){minn[k]=min(minn[k],len);return ;}
	int mid=(l+r)>>1;
	change(k<<1,l,mid,z,y,len);
	change((k<<1)|1,mid+1,r,z,y,len);
}
int hp=-1,num;
void Down(int k,int l,int r){
	if(l==r){
		if(minn[k]>hp)hp=minn[k],num=1;
		else if(minn[k]==hp)num++;
		return ;
	}
	int mid=(l+r)>>1;
	minn[k<<1]=min(minn[k<<1],minn[k]);
	Down(k<<1,l,mid);
	minn[(k<<1)+1]=min(minn[(k<<1)+1],minn[k]);
	Down((k<<1)+1,mid+1,r);
}
int main(){
	n=read();m=read();
	for(int i=1,s,t,c;i<=m;++i){
		s=read();t=read();c=read();
		add(s,t,c);add(t,s,c);
	}
	di(0,1);
	int now=n,k=-1;
	while(now!=1&&pre[now]){
		road[pre[now]]=road[dzx(pre[now])]=1;
		r[to[pre[now]]]=++k;
		l[to[pre[now]]]=k-1;
		now=fr[pre[now]];
	}
	r[1]=-1,l[1]=k;
	di(1,1);	
	di(2,n);
	memset(minn,0x3f,sizeof(minn));
	for(int i=1;i<=n;++i)
		for(int j=head[i],v;j;j=ne[j])
			if(dis[0][i]<=dis[0][(v=to[j])]&&!road[j])
				change(1,0,k,r[v],l[i],w[j]+dis[0][i]+dis[2][v]);
	Down(1,0,k);
	if(hp==dis[0][n])num=m;
	printf("%d %d",hp,num);
	return 0;
}

三.雜題淺講

題一

連結,求的是邊權異或最小,且所有的環的異或都是0,可以顯然得出任意兩點間的答案不管怎麼走都不會變。放程式碼。

int n,m,q,f[100005]; 
int get(int x){
	if(f[x]==x)return x;
	return (f[x]=get(f[x]));
}
int head[100005],ne[200005],to[200005],dis[200005],tot;
inline void add(int x,int y,int z){
	dis[++tot]=z,ne[tot]=head[x],head[x]=tot,to[tot]=y;
}
int xo[100005];
void dfs(int x,int pre){
	for(register int i=head[x];i;i=ne[i]){
		int v=to[i];
		if(v==pre)continue;
		xo[v]=dis[i]^xo[x];
		dfs(v,x);
	}
}
int main(){
	n=read();m=read();q=read();
	for(register int i=1;i<=n;++i)f[i]=i;
	for(register int i=1,x,y,z,xx,yy;i<=m;++i){
		x=read();y=read();z=read();
		xx=get(x);yy=get(y);
		if(xx!=yy){
			f[xx]=yy;
			add(x,y,z);
			add(y,x,z);
		}
	}
	dfs(1,0);
	for(register int i=1,x,y;i<=q;++i){
		x=read();y=read();
		printf("%d\n",xo[x]^xo[y]);
	}
	return 0;
}

題二

連結,求兩個最短路圖的並集上最長鏈,DAG拓撲瞎寫。

const int MAXN=400005;
int n,m,s1,e1,s2,e2; 
long long dis[10][MAXN];
bool vis[MAXN];
priority_queue<pair<int,int> > q;
inline void di(int k,int alf,int x){
	memset(vis,0,sizeof(vis));
	dis[k][x]=0;
	q.push(make_pair(0,x));
	while(!q.empty()){
		int u=q.top().second;
		q.pop();
		if(vis[u])continue;
		vis[u]=1;
		for(register int i=head[alf][u];i;i=ne[alf][i]){
			int v=to[alf][i];
			if(dis[k][v]>dis[k][u]+w[alf][i]){
				dis[k][v]=dis[k][u]+w[alf][i];
				q.push(make_pair(-dis[k][v],v));
			}
		}
	}
}
bool is[MAXN];
queue<int> que;
int ans[MAXN],kyl,degree[MAXN];
inline void topo(){
	for(int i=1;i<=n;++i)if(!degree[i])que.push(i);
	while(!que.empty()){
		int u=que.front();
		que.pop();
		kyl=max(kyl,ans[u]);
		for(int i=head[1][u];i;i=ne[1][i]){
			int v=to[1][i];
			--degree[v];
			ans[v]=max(ans[v],ans[u]+w[1][i]);
			if(!degree[v])que.push(v);
		}
	}
}
int main(){
	n=read();m=read();
	s1=read();e1=read();s2=read();e2=read();
	for(int i=1,u,v,d;i<=m;++i){
		u=read();v=read();d=read();
		add(0,u,v,d),add(0,v,u,d);
	}
	memset(dis,0x3f,sizeof(dis));
	di(0,0,s1);di(1,0,e1);di(2,0,s2);di(3,0,e2);
	for(int i=1;i<=n;++i){
		for(int j=head[0][i];j;j=ne[0][j]){
			if(w[0][j]+dis[0][i]+dis[1][to[0][j]]==dis[0][e1]){
				if(w[0][j]+dis[2][i]+dis[3][to[0][j]]==dis[2][e2]){
					add(1,i,to[0][j],w[0][j]);
					++degree[to[0][j]];
				}
			}
		}
	}
	topo();
	memset(head[1],0,sizeof(head[1]));
	tot[1]=0;
	for(int i=1;i<=n;++i){
		for(int j=head[0][i];j;j=ne[0][j]){
			if(w[0][j]+dis[0][i]+dis[1][to[0][j]]==dis[0][e1]){
				if(w[0][j]+dis[3][i]+dis[2][to[0][j]]==dis[2][e2]){
					add(1,i,to[0][j],w[0][j]);
					++degree[to[0][j]];
				}
			}
		}
	}
	topo();
	printf("%d",kyl);
	return 0;
}

題三

連結,儘量不選特殊邊,在最短路時堆中比較魔改就好,特殊邊後讀編號比較大。

priority_queue<pair<long long,pair<int,int> > > q;
bool vis[MAXN];
long long dis[2][MAXN];
inline void di(int alf,int s){
	memset(dis[alf],0x3f,sizeof(dis[alf]));
	memset(vis,0,sizeof(vis));
	dis[alf][s]=0;
	q.push(make_pair(0,make_pair(s,1)));
	while(!q.empty()){
		int u=q.top().second.first,qwq=-q.top().second.second;
		q.pop();
		if(vis[u])continue;
		vis[u]=1;
		if(qwq!=-1)road[qwq]=1;
		for(int i=head[u];i;i=ne[i]){
			int v=to[i];
			if(dis[alf][v]>=dis[alf][u]+w[i]){
				dis[alf][v]=dis[alf][u]+w[i];
				q.push(make_pair(-dis[alf][v],make_pair(v,-i)));
			}
		}
	}
}