最短路變形
題意:你有K個點數,有N個點,M條邊,邊為有向邊,包含4個信息,兩個端點+邊長+走這條邊需要付出的點數。你的任務是,從1號點出發走到n號點,在點數夠用的情況下,走出一條最短路,單case
顯然是一個最短路的變形,而且是一種常見的模型。最短路本身是一個求解最優解的問題,在這里加多了一個限制條件,就是點數,所以變為“在一定的限制條件下求解一個最優化問題”的模型,這樣的模型,可以由一個大致的套路,就是,在滿足限制條件后,再進行更新
下面將講3個方法,前兩個其實都是BFS,第3個事DFS,是一個記憶化搜索。我們先說BFS
1.優先隊列+dij(最快)
判斷一個元素能否入隊,不再是看它的最短路估計值是否被更新,而是從當前點能到達的點,都可以放入隊列,在優先隊列中,每次取隊中最短路估計值最小的元素出來去更新
如果標號為n的點出隊了,那么其實算法結束了,因為之前的狀態都沒有更新出更小的值,在從現在開始,哪怕再怎么更新,都不會比現在更小了,所以直接跳出,輸出即可
#include <cstdio> #include <cstring> #include <vector> #include <queue> #include <algorithm> using namespace std; #define N 110 #define M 10010 #define INF 0x3f3f3f3f int n,m,cost,tot; struct State { int n,d,c; bool operator < (const struct State a)const { if(a.d == d) return a.c < c; return a.d < d; } }; struct edge { int u,v,w,c,next; }; typedef struct State State; typedef struct edge edge; int head[N]; int d[N]; edge e[M]; void add(int u , int v , int w , int c) { e[tot].u = u; e[tot].v = v; e[tot].w = w; e[tot].c = c; e[tot].next = head[u]; head[u] = tot++; } void Dij() { priority_queue<State>q; State sta; int res = INF ; memset(d,0x3f,sizeof(d)); while(!q.empty()) q.pop(); sta.d = 0; sta.n = 1; sta.c = 0; q.push(sta); while(!q.empty()) { State x,y; int u,v,w,d,c; x = q.top(); q.pop(); u = x.n; d = x.d; if(u == n) { res = x.d; break; } for(int k=head[u]; k!=-1; k=e[k].next) { v = e[k].v; w = e[k].w; c = e[k].c; if(x.c + c <= cost) //在花費允許的范圍內可以去到這個點 { y.n = v; y.d = d + w; y.c = x.c + c; q.push(y); } } } if(res == INF) printf("-1\n"); else printf("%d\n",res); } int main() { scanf("%d%d%d",&cost,&n,&m); memset(head,-1,sizeof(head)); tot = 0; while(m--) { int u,v,w,c; scanf("%d%d%d%d",&u,&v,&w,&c); add(u,v,w,c); } Dij(); return 0; }
2.普通隊列+spfa(或者說是直接的一個bfs,時間次之)
定義一個狀態d[i][j]表示從1號頂點走到i號頂點花費了j個點數能走出的最短路。那么狀態之間的轉移是不能想的,即便是加了點數這個限制條件也不難(不就是判斷的時候多判斷一下)。然后很快寫出了一個代碼,提交,TLE。然后怎么改都是TLE,最后就去思考是怎么TLE的
先放上這個TLE的代碼
#include <cstdio> #include <cstring> #include <vector> #include <queue> using namespace std; #define N 110 #define M 10010 #define INF 0x3f3f3f3f struct State { int n,c,d; }; struct edge { int u,v,w,c,next; }; typedef struct State State; typedef struct edge edge; int n,m,cost,tot; int d[N][M]; int head[N]; edge e[M]; bool inq[N]; void add(int u ,int v ,int w ,int c) { e[tot].u = u; e[tot].v = v; e[tot].w = w; e[tot].c = c; e[tot].next = head[u]; head[u] = tot++; } void spfa() { int res; queue<State>q; State tmp; while(!q.empty()) q.pop(); memset(d,0x3f,sizeof(d)); memset(inq,false,sizeof(inq)); d[1][0] = 0; inq[1] = true; tmp.n = 1; tmp.c = 0; tmp.d = 0; q.push(tmp); res = INF; while(!q.empty()) { State x ,y; x = q.front(); q.pop(); inq[x.n] = false; if(x.n == n && x.d < res) res = x.d; for(int k = head[x.n]; k!=-1; k=e[k].next) { int v = e[k].v; int w = e[k].w; int c = e[k].c; if(x.c + c <= cost) { int cc = x.c + c; if( x.d + w < d[v][cc]) { d[v][cc] = x.d + w; if(!inq[v]) { y.n = v; y.c = cc; y.d = d[v][cc]; q.push(y); inq[v] = true; } } } } } if(res == INF) printf("-1\n"); else printf("%d\n",res); } int main() { scanf("%d%d%d",&cost,&n,&m); memset(head,-1,sizeof(head)); tot = 0; while(m--) { int u,v,w,c; scanf("%d%d%d%d",&u,&v,&w,&c); add(u,v,w,c); } spfa(); return 0; }
在一個BFS搜索中,TLE的原因,一般就是因為沒有剪枝,即重復的狀態搜了太多次,那么就想了,怎么剪枝呢?怎么判斷重復狀態的搜索的?代碼中已經有inq[i][j]這樣的標記數組了啊,但是可以想到這樣的記錄其實意義不大,一是狀態數太多(n*m),再者是有重邊的關系,更新可以很頻繁,狀態出隊入隊的次數可以很多。
所以我們可以改變一下更新的策略和隊列中的元素的定義,每得到一個點,徹底更新所有點數對應的狀態,我們只看一個點是否被修改了最短路徑值,是的話才能入隊,這樣的限制,大大減少了元素入隊出隊的次數
這個是AC的代碼
#include <cstdio> #include <cstring> #include <queue> using namespace std; #define N 110 #define M 10010 #define INF 0x3f3f3f3f int head[N]; struct edge { int u,v,w,c,next; }e[M]; int cost,n,m,tot; int d[N][M]; bool inq[N]; void add(int u , int v , int w , int c) { e[tot].u = u; e[tot].v = v; e[tot].w = w; e[tot].c = c; e[tot].next = head[u]; head[u] = tot++; } void bfs() { int res; queue<int>q; memset(inq,false,sizeof(inq)); memset(d,0x3f,sizeof(d)); for(int i=0; i<=cost; i++) d[1][i] = 0; while(!q.empty()) q.pop(); inq[1] = true; q.push(1); while(!q.empty()) { int u = q.front(); q.pop(); inq[u] = false; for(int k=head[u]; k!=-1; k=e[k].next) { int v = e[k].v; int w = e[k].w; int c = e[k].c; for(int j=c; j<=cost; j++) //徹底更新所有的狀態 { if(d[u][j-c] + w < d[v][j]) { d[v][j] = d[u][j-c] + w; if(!inq[v]) { q.push(v); inq[v] = true; } } } } } res = INF; for(int i=0; i<=cost; i++) if(d[n][i] < res) res = d[n][i]; if(res == INF) printf("-1\n"); else printf("%d\n",res); } int main() { scanf("%d%d%d",&cost,&n,&m); memset(head,-1,sizeof(head)); tot = 0; while(m--) { int u,v,w,c; scanf("%d%d%d%d",&u,&v,&w,&c); add(u,v,w,c); } bfs(); return 0; }
3.DP,記憶化搜索,需要逆向建圖,容易寫(最慢)
狀態的定義和上面的spfa是一樣,d[i][j]表示從1到i點花費j點數走出的最短路,那么這個東西就很容易引導我們想到DP,而確實是這樣的,但是問題是,要DP,要寫遞歸式,需要的是反邊,即要知道點v的信息,是由點u得到的(有向邊u--->v),所以建圖的時候逆向建圖,剩下的記憶化搜索,是很容易寫出來的
#include <cstdio> #include <cstring> #define N 110 #define M 10010 #define INF 0x3f3f3f3f int head[N]; struct edge { int u,v,w,c,next; }e[M]; int cost,n,m,tot; int d[N][M]; void add(int u , int v , int w , int c) { e[tot].u = u; e[tot].v = v; e[tot].w = w; e[tot].c = c; e[tot].next = head[u]; head[u] = tot++; } void dfs(int u ,int c) { if(d[u][c] != -1) return ; d[u][c] = INF; for(int k=head[u]; k!=-1; k=e[k].next) { int v = e[k].v; int w = e[k].w; int cc = e[k].c; if(c - cc >= 0) { dfs(v,c-cc); if(d[v][c-cc]+w < d[u][c]) d[u][c] = d[v][c-cc]+w; } } } int main() { scanf("%d%d%d",&cost,&n,&m); memset(head,-1,sizeof(head)); tot = 0; while(m--) { int u,v,w,c; scanf("%d%d%d%d",&u,&v,&w,&c); add(v,u,w,c); //逆向建圖,為了逆向DP } memset(d,-1,sizeof(d)); for(int i=0; i<=cost; i++) d[1][i] = 0; int res = INF; for(int i=0; i<=cost; i++) { dfs(n,i); if(d[n][i] < res) res = d[n][i]; } if(res == INF) printf("-1\n"); else printf("%d\n",res); }