这题的状态转移方程其实相对而言没有那么难,但判0环过程真的挺繁琐。
这个题可以从数据范围的k <= 50 入手,由于我们最多走50及以下的无用路,所以可以考虑去利用不多的k来进行一些操作,这便可以大大减少枚举状态时要使用的复杂度。
我们可以想到把k作为一维状态进行dp。状态设置也比较清晰,dp[j][i]一维表示当前在点i,另一维表示从出发点到该点经过的路程为最短路加上j。然后从一个点转到下一个点时的状态转移方程就可以列出来了。设我们要从u点到v点,他们之间连边的大小为w,当前状态为dp[x][u],规定dis[i]为从起点到i的最短路。那么到达v点时走的多余的路则为dis[u] + x + w - dis[v],所以我们就可以把状态像这样转移:
dp[x][u] = ∑dp[dis[u] + x + w - dis[v]][v] ;
也就是把所有可以到v点的u点的状态都转移到v点。
至于判0环的部分,就较为复杂了,考虑将所有0边单独建一次图,然后在这张图里跑拓扑排序,所有不能进入队列里的点便是0环上或0环可以通过0边到达的点。
如果0环可以通过0边到达某个点,该点若可以作为答案路径上的一点,该路径不一定会成为无限可走的路径,只要路径不通过0环即可。所以我们要排除0环可以通向的点,建反向0图,再次拓扑排序。而该点成为无限路径上一点的情况,则会被0环排除,所以我们不需要担心有情况缺失。
除了0环之外,还要看环上的点是否属于可行路径,我们求出起点到它(负环上一点)的最短路径,加上终点到它的最短路径,若在可行范围内,则直接输出-1。
丑陋的代码如下:
#include <iostream> #include <cstdio> #include <cmath> #include <cstring> #include <algorithm> #include <queue> #define maxn 200005 int head[maxn],nex[maxn],to[maxn],edge[maxn],vis[maxn],n,m,k,p,T,tot = 0,last[maxn],in[maxn],cnt,tag[maxn],vis1[maxn],vis2[maxn],tot1 = 0,to1[maxn],nex1[maxn],head1[maxn]; int from[maxn],go[maxn],ed[maxn],num[maxn]; long long dp[60][100005],dis[maxn],dis1[maxn]; std::queue <int > q; struct dot{ int whi; }d[maxn]; void add(int x,int y,int z) { to[++tot] = y; edge[tot] = z; nex[tot] = head[x]; head[x] = tot; } void add1(int x,int y) { to1[++tot1] = y; nex1[tot1] = head1[x]; head1[x] = tot1; } void turn() { tot = 0; memset(head,0,sizeof(head)); for (int i = 1;i <= m;i++) { std::swap(from[i],go[i]); add(from[i],go[i],ed[i]); } } bool cmp(dot a1,dot a2) { if (dis[a1.whi] == dis[a2.whi]) return num[a1.whi] < num[a2.whi]; else return dis[a1.whi] < dis[a2.whi]; } void spfa(int u) { for (int i = 1;i <= n;i++) dis[i] = 0x3f3f3f3f,vis[i] = 0,d[i].whi = i,num[i] = 0; dis[u] = 0; q.push(u); vis[u] = 1; while (!q.empty()) { int x = q.front(); q.pop(); vis[x] = 0; for (int i = head[x];i;i = nex[i]) { int y = to[i]; if (dis[y] > dis[x] + edge[i]) { dis[y] = dis[x] + edge[i]; last[y] = x; if (!vis[y]) q.push(y),vis[y] = 1; } } } } void top_sort1() { tot1 = 0; memset(in,0,sizeof(in)); memset(head1,0,sizeof(head1)); for (int i = 1;i <= n;i++) for (int j = head[i];j;j = nex[j]) if (!edge[j]) tag[i] = 1,add1(i,to[j]),in[to[j]]++; for (int i = 1;i <= n;i++) { if (tag[i] && !in[i]) q.push(i),num[i] = ++cnt; } while (!q.empty()) { int x = q.front(); q.pop(); vis1[x] = 1; for (int i = head1[x];i;i = nex1[i]) { int y = to1[i]; in[y]--; if (!in[y]) q.push(y),num[y] = ++cnt; } } } void top_sort2() { tot1 = 0; memset(head1,0,sizeof(head1)); memset(in,0,sizeof(in)); for (int i = 1;i <= n;i++) for (int j = head[i];j;j = nex[j]) { int y = to[j]; if (!edge[j]) { in[i]++; add1(y,i); } } for (int i = 1;i <= n;i++) if (tag[i] && !in[i]) q.push(i); while (!q.empty()) { int x = q.front(); vis2[x] = 1; q.pop(); for (int i = head1[x];i;i = nex1[i]) { int y = to1[i]; in[y]--; if (!in[y]) q.push(y); } } } int main() { scanf("%d",&T); while (T--) { long long ans = 0,yes = 1; memset(head,0,sizeof(head)); tot = 0; memset(dp,0,sizeof(dp)); memset(vis1,0,sizeof(vis1)); memset(vis2,0,sizeof(vis2)); scanf("%d%d%d%d",&n,&m,&k,&p); for (int i = 1;i <= m;i++) { int x,y,z; scanf("%d%d%d",&x,&y,&z); from[i] = x; go[i] = y; ed[i] = z; add(x,y,z); } spfa(1); for (int i = 1;i <= n;i++) dis1[i] = dis[i]; turn(); spfa(n);//跑两次spfa得知起点与终点的最短路情况 turn(); for (int i = 1;i <= n;i++) std::swap(dis1[i],dis[i]); top_sort1(); top_sort2();//两次拓扑,确定零环 for (int i = 1;i <= n;i++) if (tag[i] && !vis1[i] && !vis2[i] && dis[i] + dis1[i] <= dis[n] + k) {yes = 0;break;} if (!yes) {printf("-1 ");continue;} //若一点在零环上且通过它的起点向终点的最短路径在可行范围内,则直接输出-1 std::sort(d + 1,d + n + 1,cmp); dp[0][1] = 1; for (int i = 0;i <= k;i++) { for (int j = 1;j <= n;j++) { int u = d[j].whi; if (!dp[i][u]) continue; for (int l = head[u];l;l = nex[l]) { int v = to[l]; if (dis[u] + i + edge[l] - dis[v] > k || dis[v] == 0x3f3f3f3f) continue; dp[dis[u] + i + edge[l] - dis[v]][v] += dp[i][u]; dp[dis[u] + i + edge[l] - dis[v]][v] %= p; }//状态转移过程 } ans += dp[i][n]; ans %= p; } printf("%lld ",ans); } }