1. 程式人生 > 實用技巧 >分層圖求最短路

分層圖求最短路

1495:【例 2】孤島營救問題

分層最短路做。以獲取鑰匙的狀態建立分層圖,然後BFS就行了

https://blog.csdn.net/a_pathfinder/article/details/100537489 裡面寫了BFS+狀壓 和 最短路得解法

like 汽車加油行駛問題(另一個分層圖的問題)

要先算出最多邊數:1>>10*10*10=1024000;
把原來矩陣每個座標存成一個點,再對可到鄰邊賦權值1,在建圖的時候,我們只要連線一個雙向邊即可,假設j是從i左移一格,那麼i就是j右移一格。上下同理。
值得注意的是,我們每次在一層的時候,在沒有鑰匙i的時候,我們需要把i鑰匙鎖在的點u,連線到有了i鑰匙的那一層對應的位置上v,權值是0,這樣就建成了1>>種類 的圖,

並且圖直接有連線。最後跑一遍最短路即可。
ps:要記錄總層數的總點數,之後dis[]的初始化時所有點。

//下面是最短路得做法,我還沒看
/*
這裡涉及到的變化還蠻多的,我們要先算出最多邊數:1>>10*10*10=1024000;
把原來矩陣每個座標存成一個點,再對可到鄰邊賦權值1,在建圖的時候,我們只要連線一個雙向邊即可,假設j是從i左移一格,那麼i就是j右移一格。上下同理。
值得注意的是,我們每次在一層的時候,在沒有鑰匙i的時候,我們需要把i鑰匙鎖在的點u,連線到有了i鑰匙的那一層對應的位置上v,權值是0,這樣就建成了1>>種類 的圖,
u(鎖在的位置)--->v(鑰匙的位置),權值為0
並且圖直接有連線。最後跑一遍最短路即可。
ps:要記錄總層數的總點數,之後dis[]的初始化時所有點。
*/
 //分層求最短路
#include<bits/stdc++.h>
using namespace std;
const int N = 12;
const int M = 1024100;
const int INF = 0x3f3f3f3f;
typedef pair<int,int> P;
struct keyn{
    int x,y;
}key[N][20];//key[i][j] 種類為i的鑰匙第j把的座標
int n,m,p,s,k,cnt,layer,nn,nsum;//n寬,m長,p種類,s總鑰匙數,k總障礙數,cnt計數器,layer層數,nn每層的點,nsum總點數
    //layer = 1<<p; //層數為 2^(鑰匙種類數)
    //nn = n*m;  //每一層點數
    //nsum = n*m*layer;   //總共點數
int num[N][N],fg[200][200];
int head[M],nex[M],ver[M],edge[M];
int hadk[N],kn[N],vis[M],dis[M];
void add(int x,int y,int w){
    ver[++cnt] = y;
    nex[cnt] = head[x];
    edge[cnt] = w;
    head[x] = cnt;
}
void read(){
    cnt = 0;
    int x,y;
    scanf("%d%d%d%d",&n,&m,&p,&k);
    for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)
    num[i][j] = ++cnt; ///把每個左邊當成一個點
    for(int i=1,g,u,v;i<=k;i++){
        scanf("%d%d",&x,&y); u = num[x][y];  //把兩個座標連成一條邊
        scanf("%d%d",&x,&y); v = num[x][y];
        scanf("%d",&g);
        if(g==0) g=-1;
        fg[u][v] = fg[v][u] = g;
    }
    scanf("%d",&s);
    for(int i = 1,q;i <= s;i++){
        scanf("%d%d%d",&x,&y,&q);
        kn[q]++; //這種鑰匙的數量
        key[q][kn[q]].x = x;
        key[q][kn[q]].y = y;
    }
}
void build(){
    layer = 1<<p; //層數為 2^(鑰匙種類數)
    nn = n*m;  //每一層點數
    nsum = n*m*layer;   //總共點數
     
    for(int t=0;t<layer;t++){
        for(int i=1;i<=p;i++){
            if(t&(1<<(i-1))) hadk[i] = 1;
            else hadk[i] = 0;
        }
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++){
                int u = num[i][j],v = num[i][j+1];//向右連邊
                if(v && fg[u][v]!=-1)
                if(fg[u][v]==0 || hadk[fg[u][v]]){  //可以走或者是有鑰匙 就連邊
                    add(t*nn+u,t*nn+v,1);
                    add(t*nn+v,t*nn+u,1);
                }
                 
                v = num[i+1][j]; //向下連邊
                if(v && fg[u][v]!=-1)
                if(fg[u][v]==0 || hadk[fg[u][v]]){  //可以走或者是有鑰匙 就連邊
                    add(t*nn+u,t*nn+v,1);  //層數*每一層點數
                    add(t*nn+v,t*nn+u,1);
                }
            }
        for(int i=1;i<=p;i++){
            if(!hadk[i]) //沒有鑰匙才可以移動狀態
            for(int j=1;j<=kn[i];j++){       //如果這一層沒有這樣的鑰匙,那麼就把這種所對應的鑰匙的地方連線,邊權為0
                int u = num[key[i][j].x][key[i][j].y];
                add(t*nn+u,( t|(1<<(i-1)) ) *nn+u,0);
            }
        }  
    }
}
void dj(){
    priority_queue<P> q;
    for(int i = 0;i <= nsum; i++) dis[i] = INF;
    q.push(make_pair(0,1)),dis[1] = 0;
    //first是距離,second是位置
    while(q.size()){
        int u = q.top().second;
        q.pop();
        if(vis[u]) continue;
        vis[u] = 1;
        for(int i = head[u];i;i = nex[i]){
            int v = ver[i],w=edge[i];
            if(dis[v] > dis[u] + w){
                dis[v] = dis[u] + w;
                q.push(make_pair(-dis[v],v));
            }
        }
    }
}
void spfa(){
    queue<int> q;
    for(int i = 0;i <= nsum;i++) dis[i] = INF;
    q.push(1),dis[1] = 0,vis[1] = 1;
    while(q.size()){
        int u = q.front(); q.pop();vis[u] = 0;
        for(int i=head[u];i;i = nex[i]){
            int v = ver[i],w=edge[i];
            if(dis[v] > dis[u] + w){
                dis[v] = dis[u] + w;
                if(!vis[v]){
                    q.push(v),vis[v] = 1;
                }
            }
        }
         
    }
}
void solve(){
    int ans = INF;
    for(int i =0;i<layer;i++)
    ans = min(ans,dis[i*nn+num[n][m]]);
    if(ans==INF) printf("-1\n");
    else printf("%d\n",ans);
}
int main(){
    read();
    build();
    //spfa();
    dj();
    solve();
    return 0;
}

  

1496:【例 3】架設電話線

這個不是求最短路了,而是需要求出第k+1最短邊

分層求最短路
我們把節點擴充套件到二維,二元組(x,p)代表一個節點,從(x,p)到(y,p)上有一個長為w的邊,(x,p)到(y,p+1)上有長度為0的邊,最後對所有層求最短路,
分層的時候只能從低層到高層連大家都知道吧,因為你不能說我把一個邊變成0後再變回去。

同一層邊權是多少就是多少,不同層就是0

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1010;
const int maxm=2e5+10;
const int INF=0x3fffffff;
typedef long long LL;
/*
第二種:分層求最短路
我們把節點擴充套件到二維,二元組(x,p)代表一個節點,從(x,p)到(y,p)上有一個長為w的邊,(x,p)到(y,p+1)上有長度為0的邊,最後對所有層求最短路,
分層的時候只能從低層到高層連大家都知道吧,因為你不能說我把一個邊變成0後再變回去。
*/
typedef pair<int,int> pp;
int n,m,k,cnt;
int head[maxm],to[maxm],wei[maxm],next[maxm];
int vis[maxm],dis[maxm];
void add(int x,int y,int z){
    to[++cnt]=y;
    wei[cnt]=z;
    next[cnt]=head[x];
    head[x]=cnt;
}
void inti(){
    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);
        add(x,y,z);
        add(y,x,z);
        for(int j=1;j<=k;j++){
            //同一層滴
            add(j*n+x,j*n+y,z);add(j*n+y,j*n+x,z);
            //不是同一層的
            add((j-1)*n+x,j*n+y,0);add((j-1)*n+y,j*n+x,0);
        }
    }
}
void dij1(){
    priority_queue<pp,vector<pp>,greater<pp> > q;
    for(int i=0;i<=maxm;i++) dis[i]=INF;
    dis[1]=0;
    q.push(make_pair(0,1));
    while(!q.empty()){
        int op=q.top().second;  //下標
        q.pop();
        if(vis[op]) continue;
        vis[op]=1;
        for(int i=head[op];i;i=next[i]){
            int v=to[i];
            int w=wei[i];
            if(dis[v]>max(dis[op],w)){  //最大值
                dis[v]=max(dis[op],w);
                if(!vis[v]) q.push(make_pair(dis[v],v));
            }
        }
    }
}
 
 
int main(){
    inti();
    dij1();
    if(dis[k*n+n]==INF) printf("-1\n");
    else printf("%d\n",dis[k*n+n]);
    return 0;
} 

  

1502:汽車加油行駛問題

這道題也可以用分層圖解決 +spfa(也就這個好些一點
也可以用普通的bfs+spfa解決(我更喜歡這個wwww
即總共建k+1層圖,只有層與層之間有邊,汽車每走一步就會向上移動一層。建邊規則滿足題目要求即可。

因為k是能走的長度,但是我還是不太能理解建邊的過程【這個建邊好麻煩】

#include<bits/stdc++.h>
#define N 200005
using namespace std;
int Map[105][105];
int num[105][105][15];
 
struct ss
{
    int v,next,w;
};
ss edg[N*4];
int head[N],now_edge=0;
 
void addedge(int u,int v,int w)
{
    edg[now_edge]=(ss){v,head[u],w};
    head[u]=now_edge++;
}
int dis[N];
int vis[N]={0};
 
void spfa()
{
    for(int i=0;i<N;i++)dis[i]=INT_MAX/2;
    dis[num[1][1][0]]=0;  //節點(離散後)
    queue<int>q;
    q.push(num[1][1][0]);
     
    vis[num[1][1][0]]=1;
     
    while(!q.empty())
    {
        int now=q.front();
        q.pop();
        vis[now]=0;
         
        for(int i=head[now];i!=-1;i=edg[i].next)
        {
            int v=edg[i].v;
            if(dis[v]>dis[now]+edg[i].w)
            {
                dis[v]=dis[now]+edg[i].w;
                 
                if(!vis[v])
                {
                    q.push(v);
                    vis[v]=1;
                }
            }
        }
    }
}
 
 
int main()
{
    int n,k,a,b,c;
    memset(head,-1,sizeof(head));
    scanf("%d %d %d %d %d",&n,&k,&a,&b,&c);
    for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)scanf("%d",&Map[i][j]);  //1為有油庫,0為沒有
     
    int cnt=1;
    for(int kk=0;kk<=k;kk++)
    {
        for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
        num[i][j][kk]=cnt++; //轉化為節點
    }
     
    //0--1層
    for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
    {
        if(i+1<=n)addedge(num[i][j][0],num[i+1][j][1],0);
        if(j+1<=n)addedge(num[i][j][0],num[i][j+1][1],0);
        if(i-1>=1)addedge(num[i][j][0],num[i-1][j][1],b);
        if(j-1>=1)addedge(num[i][j][0],num[i][j-1][1],b);
    }
    //(1~k-1)--(2~k)層
    for(int kk=1;kk<k;kk++)
    for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
    if(!Map[i][j])  //沒有油庫
     {
        if(i+1<=n)addedge(num[i][j][kk],num[i+1][j][kk+1],0);
        if(j+1<=n)addedge(num[i][j][kk],num[i][j+1][kk+1],0);
        if(i-1>=1)addedge(num[i][j][kk],num[i-1][j][kk+1],b);
        if(j-1>=1)addedge(num[i][j][kk],num[i][j-1][kk+1],b);
        addedge(num[i][j][kk],num[i][j][0],c+a);  //沒有油庫,還需要加上c
    }
    else   //有油庫的話,就只需要費用a並且是從0走到kk層
    {
        addedge(num[i][j][kk],num[i][j][0],a);
    }
     
    for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
    if(!Map[i][j])
    {
        addedge(num[i][j][k],num[i][j][0],c+a);   //沒有油庫,還需要加上c
    }
    else
    {
        addedge(num[i][j][k],num[i][j][0],a);//有油庫的話,就只需要費用a並且是從0走到kk層
    }
     
    spfa();
    int ans=INT_MAX; 
    for(int i=0;i<=k;i++)ans=min(ans,dis[num[n][n][i]]);  //求最小
    printf("%d\n",ans);
    return 0;
}

  

洛谷:

P3831 [SHOI2012]回家的路

https://www.luogu.com.cn/problem/P3831

先把題目讀懂,換乘表示可以轉彎(我一開始沒有意識到

洛谷的題解都寫得很好

經典的分層圖最短路裸題,在此簡要介紹:
我們可能遇到這樣的圖論模型:在一個正常的圖上可以進行 kk 次決策,對於每次決策,不影響圖的結構,隻影響目前的狀態或代價。
同時這個圖論模型和經典的最短路有關,這樣我們可以考慮運用分層圖最短路。
此題為樸素的裸題,在此僅介紹一種(時空非最優)的易於理解的實現方法:(我懶得再寫一遍了
此題的決策為轉向,由於只存在橫向和縱向兩個放學,我們對這兩個方向分別建立一層。即一層只連原圖橫向邊,一層只連縱向邊。
對於轉向這個決策,將決策前的狀態和決策後的狀態間連線一條權值為決策代價的邊,表示付出該代價轉換了狀態。
在本題中,即上下兩層對應點連線一條權值為1的邊,層內邊權均為2.
然後跑最短路即可。

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=200010;
const int maxm=800010;
const int INF=0x3f3f3f3f;
typedef long long LL;
typedef unsigned long long ull;
int red(){
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9') {
		x=x*10+ch-'0'; 
		ch=getchar();
	}
	return x*f;
}
int head[maxm],cnt=1;
struct node{
	int to,nex,wei;
}ed[maxm];
void adde(int x,int y,int z){
	ed[cnt].to=y;
	ed[cnt].nex=head[x];
	ed[cnt].wei=z;
	head[x]=cnt++;
	ed[cnt].to=x;
	ed[cnt].nex=head[y];
	ed[cnt].wei=z;
	head[y]=cnt++;
}
struct node1{
	int x,y,id;
}a[maxn];
//兩個比較函式
bool cmp1(node1 a,node1 b){  //根據x來排序 
	if(a.x==b.x) return a.y<b.y;
	return a.x<b.x;
} 
bool cmp2(node1 a,node1 b){ //根據y來排序 
	if(a.y==b.y) return a.x<b.x;
	return a.y<b.y; 
}
int n,m;
queue<int> q;
int d[maxn],S,T;
bool vis[maxn];
void spfa(){
	for(int i=1;i<=2*m+4;i++) d[i]=INF;
	d[S]=0;
	vis[S]=1;
	q.push(S);
	while(!q.empty()){
		int op=q.front();
		q.pop();
		vis[op]=0;
		for(int i=head[op];i;i=ed[i].nex){
			int t=ed[i].to;
			if(d[t]>d[op]+ed[i].wei){
				d[t]=d[op]+ed[i].wei;
				if(!vis[t]){
					vis[t]=1;
					q.push(t);
				}
			}
		}
	}
}
int main(){
	n=red();
	m=red();
	//s是m+1,t是m+2 
	S=m+1;T=m+2;
	for(int i=1;i<=m+2;i++){
		a[i].x=red();
		a[i].y=red();
		a[i].id=i;
	}
	sort(a+1,a+m+3,cmp1);  //x排的 
	for(int i=1;i<m+2;i++){
		if(a[i].x==a[i+1].x) adde(a[i].id,a[i+1].id,2*(a[i+1].y-a[i].y));
	}
	sort(a+1,a+m+3,cmp2); //y排的
	for(int i=1;i<m+2;i++){
		//兩層之間的節點應該區分,所以都要加上m+2 
		if(a[i].y==a[i+1].y) adde(a[i].id+2+m,a[i+1].id+m+2,2*(a[i+1].x-a[i].x));
	} 
	for(int i=1;i<=m;i++){ //所有的中轉站 兩層之間連線,權值為1 
		adde(i,i+2+m,1);
	}
	//兩層之間的起點與起點,終點與終點連線,權值為0
	adde(m+1,2*m+3,0);
	adde(m+2,2*m+4,0);
	spfa();
	if(d[T]==INF){
		printf("-1");
		return 0;
	}
	printf("%d",d[T]);
return 0;
}



// 

  

P4568 [JLOI2011]飛行路線

https://www.luogu.com.cn/problem/P4568

這道題和一本通上面的第k最短路很像,也是分層圖的模板題
//各層內部正常連邊,各層之間從上到下連權值為0的邊。每向下跑一層,就相當於免費搭一次飛機。跑一遍從s到t+n*k+t的最短路即可

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=110005;
const int INF=0x3fffffff;
typedef long long LL;
typedef unsigned long long ull;
//這道題和一本通上面的第k最短路很像,也是分層圖的模板題
//各層內部正常連邊,各層之間從上到下連權值為0的邊。每向下跑一層,就相當於免費搭一次飛機。跑一遍從s到t+n*k+t的最短路即可 
int n,m,k,s,t;
struct node{
	int to,nex,val;
}ed[2500001]; //這個範圍是怎麼求得
int head[maxn],cnt;
int vis[maxn],dis[maxn];
void adde(int x,int y,int z){
	ed[++cnt].nex=head[x];ed[cnt].to=y;ed[cnt].val=z;
	head[x]=cnt;
}
typedef pair<int,int> pp;
void dij(int st){
	memset(dis,0x3f,sizeof(dis));
	dis[st]=0;
	priority_queue<pp,vector<pp>,greater<pp> > q;
	q.push(make_pair(0,st));
	while(!q.empty()){
		int x=q.top().second;
		q.pop();
		if(vis[x]) continue;
		vis[x]=1;
		for(int i=head[x];i;i=ed[i].nex){
			int t=ed[i].to,wei=ed[i].val;
			if(dis[t]>dis[x]+wei){
				dis[t]=dis[x]+wei;
				q.push(make_pair(dis[t],t));
			}
		}
	}
}
int main(){
	scanf("%d %d %d %d %d",&n,&m,&k,&s,&t);
	int x,y,z;
	for(int i=1;i<=m;i++){
		scanf("%d %d %d",&x,&y,&z);
		adde(x,y,z);adde(y,x,z);
		for(int j=1;j<=k;j++){
			adde(x+(j-1)*n,y+j*n,0);  //不同層 
			adde(y+(j-1)*n,x+j*n,0);
			adde(x+j*n,y+j*n,z);
			adde(y+j*n,x+j*n,z);
		}
	}
	//這一步:防止hack資料 
	for(int i=1;i<=k;i++) adde(t+(i-1)*n,t+i*n,0);
	dij(s);
	printf("%d",dis[t+k*n]); 
return 0;
}