P3953 [NOIP2017 提高组] 逛公园(最短路(Dijksra)+拓扑排序+DP)

题目大意:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
(来源:洛谷)

解题思路:

首先我们先明确为什么会出现无穷多条路径

  • 这个答案显而易见,就是有0环

那么怎样的0环会使得答案出现无穷多条路径呢

  • 对于 k = 1 k=1 k=1的情况,作如下图:
    在这里插入图片描述
  • 因为最短路为3, k = 1 k=1 k=1,所以可行的路径大小在 [ 3 , 4 ] [3,4] [3,4]那么处于下方的0环就不在我们考虑的范围内,而上方的0环则会使我们的路径有无限多条
  • 所以当且仅当0边处在可走区间的范围内时,这些0边构成的0环才会对结果产生影响,同时我们把这些0边又叫做可行0边

那么如何把得到所有的可行边,再把这些可行边重新建图(注意是可行边,不单单指可行0边)

  • 此处借由大佬博客的一句话:
    判断是否可行的方法为如果某条边连接的两个端点分别到起点和终点的距离加上此条边的长度小于等于起点到终点的最短路+k,说明通过该条路径存在答案,并判断其是否在以1为源的最短路上,将其加入到新图(即最短路边集)
  • 这样子既能保证所有可行的0边加入到新图(因为所有边权非负,所以可行的0边连接的两端恰好满足以上两个条件),又能保证在该图上跑dp时不会使最短路有增量(此处后面会有解释)
  • 上图就会变成:
    在这里插入图片描述
  • 接下来只要利用拓扑排序判环即可(想想为什么如果有环一定是0环,再看看上面两个判断条件)

接下来就是dp在分层图上求解答案的过程

  • d p [ i ] [ j ] dp[i][j] dp[i][j]代表 1 1 1 i i i结点增量为 j j j的所有可行边
  • 设想有 k + 1 k+1 k+1层图,那么每一层得到的解即代表不同增量下的解,最后算总和即可
  • 而如何在某一层跑dp时不会影响到其他层呢(疯狂暗示,看看上面),其实只要在一开始建的最短路边集上跑就行啦
  • 那么层与层之间是否毫无关联呢,显然不是呀!!!
    如果毫无关联的话,那不就相当于k始终为0,k显然不会始终为0
  • 那么每一层之间的关系,就由那些没有加入最短路边集的边来联系因为跑它们,会有增量,所以就能用来联系每一层
    然后,每条边的增量又要重新计算(可看下面代码)
  • 最后累加dp[n][0~k]即可

AC代码

#include <bits/stdc++.h>
#define inf 1000000000
using namespace std;
const int maxn = 2e5 + 10;
int T, n, m, k, p;
int a[maxn], b[maxn], w[maxn], f[maxn], g[maxn], sumin[maxn], tuopu[maxn];
int dp[maxn][60];
bool vis[maxn];
struct edge {
    int u, w;
    friend bool operator < (edge A, edge B) {
        return A.w > B.w;
    }
};
vector <edge> G[maxn];

void init() {
    for (int i = 1; i <= n; i++)
        for (int j = 0; j <= k; j++)
            dp[i][j] = 0;
}
void clearedge() {
    for (int i = 1; i <= n; i++) G[i].clear();
    //for (int i = 1; i <= n; i++) sumin[i] = 0;
    memset(sumin, 0, sizeof(sumin));
    // memset(tuopu, 0, sizeof(tuopu)); //why?不是已经初始化了吗
}
priority_queue <edge> que1;
void dijstra(int s, int *d) {
    for (int i = 1; i <= n; i++) d[i] = inf, vis[i] = false;
    d[s] = 0;
    que1.push({s, 0});
    while (!que1.empty()) {
        edge tt = que1.top(); que1.pop();
        int u = tt.u, w = tt.w;
        if (vis[u]) continue;
        vis[u] = true;
        for (auto np : G[u]) {
            if (d[np.u] > d[u] + np.w) {
                d[np.u] = d[u] + np.w;
                que1.push({np.u, d[np.u]});
            }
        }
    }
}
queue <int> que2;
int cnt;
void Tuopu() {
    cnt = 0;
    que2.push(1);
    while (!que2.empty()) {
        int u = que2.front(); que2.pop();
        tuopu[++cnt] = u;
        for (auto np : G[u]) {
            sumin[np.u]--;
            if (!sumin[np.u]) que2.push(np.u);
        }
    }
}
int main() {
	//freopen("in.txt", "r", stdin);
    cin >> T;
    while (T--) {
        cin >> n >> m >> k >> p;
        init();
        clearedge();
        for (int i = 1; i <= m; i++) {
            cin >> a[i] >> b[i] >> w[i];
            G[a[i]].push_back({b[i], w[i]});
        }
        dijstra(1, f);
        clearedge();
        for (int i = 1; i <= m; i++)
            G[b[i]].push_back({a[i], w[i]});
        dijstra(n, g);
        clearedge();
        for (int i = 1; i <= m; i++) {
            if (f[a[i]] + w[i] + g[b[i]] <= f[n] + k && f[a[i]] + w[i] == f[b[i]]) 
                G[a[i]].push_back({b[i], 0}), sumin[b[i]]++;
        }
        Tuopu();
        //计算增量
        for (int i = 1; i <= m; i++) w[i] = f[a[i]] + w[i] - f[b[i]];
        bool flag = true;
        for (int i = 1; i <= n; i++) if (sumin[i]) flag = false;
        if (!flag) {
            cout << -1 << endl;
            continue;
        }
        dp[1][0] = 1;
        for (int j = 0; j <= k; j++) {
            for (int i = 1; i <= cnt; i++) { 
            //按照拓扑序,cnt不一定等于n,因为某些点可能不会被加入进去
                int now = tuopu[i];
                for (auto np : G[now])
                    dp[np.u][j] = (dp[np.u][j] + dp[now][j]) % p;
            }
            for (int i = 1; i <= m; i++)
                if (j + w[i] <= k && w[i])
                    dp[b[i]][j + w[i]] = (dp[b[i]][j + w[i]] + dp[a[i]][j]) % p;
        }
        int ans = 0;
        for (int i = 0; i <= k; i++)
            ans = (ans + dp[n][i]) % p;
        cout << ans << endl;
    }
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值