最短路变形
题意:你有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); }