【题解】NOIP2017 逛公园

  题目链接

 

  这题的状态转移方程其实相对而言没有那么难,但判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\n");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\n",ans);
    }
}

 

posted @ 2020-11-11 15:05  I11usi0ns  阅读(135)  评论(1编辑  收藏  举报