luoguP3953 逛公园 最短路计数 拓扑序

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lvzelong2014/article/details/83857096

luoguP3953 逛公园

题目传送门

分析1

作为一名标准的NOIP退役选手,果然过了一年之后仍然不会做这道题。
首先肯定先求最短路,一种思路是 f [ k ] [ u ] f[k][u] 表示松弛了 k k 的最短路从起点走到 u u 节点。
很容易写出方程:
f [ k ] [ u ] > f [ k + w + d i s [ u ] d i s [ v ] ] [ v ] f[k][u]->f[k+w+dis[u]-dis[v]][v]
其中 k + w + d i s [ u ] k+w+dis[u] 表示的是这一回走到 v v 的步数,减去原来的最短路 d i s [ v ] dis[v] 就是走到 v v 的松弛步数。
由于最短路的性质,所以 w + d i s [ u ] d i s [ v ] 0 w+dis[u]-dis[v]\ge 0 所以这个 D p Dp 的转移实际上形成了一张分层图。我们从小到大枚举 k k 这样就可以保证转移的拓扑有序。
但是要注意一种 w + d i s [ u ] d i s [ v ] = 0 w+dis[u]-dis[v]= 0 的情况。这意味着 u > v u->v 这条边在最短路径中。也就是说,对于所有的在最短路径上的边,都需要特殊考虑其拓扑序。
首先在没有 0 0 边的情况,最短路径边一定形成了一张 D A G DAG ,到1的距离 d i s 1 dis_1 就是他们的拓扑偏序。按照 d i s 1 dis_1 排序即可转移。
考虑 0 0 边的情况。如果有 0 0 环,判断环中的节点是不是合法的。合法定义为 d i s 1 [ i ] + d i s n [ i ] d i s 1 [ n ] + K dis_1[i]+dis_n[i]\ge dis_1[n]+K ,如果这样的话,方案数一定无穷,输出-1即可。否则的话,这个 0 0 环中的所有节点一定不会出现在我们方程的转移中,可以不用管他们。
这个时候剩下的图一定仍然是一张 D A G DAG 。只不过我们不能仅仅按照 d i s 1 dis_1 排序,考虑 d i s 1 dis_1 相同的节点,还需要考虑 0 0 边的顺序。所以对于所有 0 0 边做一遍拓扑排序,以 d i s 1 dis_1 为第一关键字, 0 0 边拓扑序为第二关键字重新排序即可。
复杂度 O ( K M ) O(KM)

代码1

#include<bits/stdc++.h>
const int N = 1e5 + 10, M = 2e5 + 10, Nt = 131072, inf = 0x3f3f3f3f;
int ri() {
	char c = getchar(); int x = 0, f = 1; for(;c < '0' || c > '9'; c = getchar()) if(c == '-') f = -1;
	for(;c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) - '0' + c; return x * f;
}
int n, q[N], d[N], rk[N], f[N], g[N], *D, id[N], T[Nt << 1], dp[51][N], K, P;
struct Edge {
	int nx[M], pr[N], to[M], w[M], tp;
	void add(int u, int v, int W) {to[++tp] = v; nx[tp] = pr[u]; pr[u] = tp; w[tp] = W;}
	void adds(int u, int v, int w) {add(u, v, w); add(v, u, w);}
	void Pre() {for(int i = 1;i <= n; ++i) pr[i] = 0; tp = 0;}
}G, R;
int min(int a, int b) {return D[a] < D[b] ? a : b;}
bool cmp(int a, int b) {return f[a] == f[b] ? rk[a] < rk[b] : f[a] < f[b];}
void Up(int i, int v) {for(T[i += Nt] = v;i >>= 1;) T[i] = min(T[i << 1], T[i << 1 | 1]);}
void Dij(const Edge &G, int st) {
	for(int i = 0;i <= n; ++i) D[i] = inf;
	D[st] = 0; Up(st, st);
	for(;D[T[1]] != inf;) {
		int u = T[1]; Up(u, 0);
		for(int i = G.pr[u], v, w; i; i = G.nx[i])
			if(D[v = G.to[i]] > (w = D[u] + G.w[i]))
				D[v] = w, Up(v, v);
	}
}
bool Topsort() {
	int L = 1, R = 0;
	for(int i = 1;i <= n; ++i) 
		if(!d[i]) 
			q[++R] = i;
	for(int u = q[L];L <= R; u = q[++L])
		for(int i = G.pr[u]; i; i = G.nx[i])
			if(!G.w[i] && !--d[G.to[i]]) 
				q[++R] = G.to[i];
	for(int i = 1;i <= R; ++i) 
		rk[q[i]] = i;
	for(int i = 1;i <= n; ++i)
		if(d[i] && f[i] + g[i] <= f[n] + K)
			return false;
	for(int i = 1;i <= n; ++i) 
		id[i] = i;
	std::sort(id + 1, id + n + 1, cmp);
	return true;
}
void Inc(int &a, int b) {a += b; if(a >= P) a -= P;}
void Dp() {
	memset(dp, 0, sizeof(dp));
	dp[0][id[1]] = 1;
	for(int k = 0;k <= K; ++k)
		for(int x = 1, u = id[1];x <= n; u = id[++x]) 
			for(int i = G.pr[u], w, v; i; i = G.nx[i]) 
				if((w = f[u] + k + G.w[i] - f[v = G.to[i]]) <= K)
					Inc(dp[w][v], dp[k][u]);
}
int main() {
	for(int C = ri();C--;) {
		n = ri(); int m = ri(); K = ri(), P = ri();
		G.Pre(); R.Pre();
		for(int i = 1;i <= n; ++i) 
			d[i] = rk[i] = 0;
		for(int u, v, w;m--;) 
			u = ri(), v = ri(), w = ri(), 
			G.add(u, v, w), R.add(v, u, w), d[v] += !w;
		D = f; Dij(G, 1); 
		D = g; Dij(R, n);
		if(!Topsort()) {
			puts("-1"); continue;
		}
		Dp();
		int r = 0;
		for(int i = 0;i <= K; ++i) Inc(r, dp[i][n]);
		printf("%d\n", r);
	}
	return 0;
}

分析2

事实上还有一种更优秀的记忆化搜索解法。直接从起始节点记忆化搜索即可。刚才的算法说明了这张分层图一定是 D A G DAG ,否则一定不合法。对于不合法的情况用一个 i n in 数组特殊判断一下,剩下的就可以放心搜索了,因为这和在 D A G DAG 上跑记忆化搜索是一样的。
本题的关键在于,所有 D p Dp 的转移都是依赖于最短路径的拓扑有序性才得以用 O ( K M ) O(KM) 的优秀复杂度通过。

代码2

#include<bits/stdc++.h>
const int Nt = 131071, N = 1e5 + 10, M = 2e5 + 10, inf = 0x3f3f3f3f;
int ri() {
    char c = getchar(); int x = 0, f = 1; for(;c < '0' || c > '9'; c = getchar()) if(c == '-') f = -1;
    for(;c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) - '0' + c; return x * f;
}
int D[N], f[N][51], n, K, p; bool vis[N][51];
struct Edge {
    int to[M], nx[M], w[M], pr[N], tp;
    void add(int u, int v, int W) {to[++tp] = v; nx[tp] = pr[u]; pr[u] = tp; w[tp] = W;}
    void Clr() {
        for(int i = 1;i <= n; ++i) pr[i] = 0;
        tp = 0;
    }
}G, R;
struct Node {int u, x;}T[Nt << 1];
Node min(Node a, Node b) {return a.x < b.x ? a : b;}
void Up(int i, int v) {for(T[i += Nt].x = v;i >>= 1;) T[i] = min(T[i << 1], T[i << 1 | 1]);}
void Add(int &a, int b) {a += b; if(a >= p) a -= p;}
void Dij() {
    Up(n, D[n] = 0);
    for(;T[1].x != inf;) {
        int u = T[1].u; Up(u, inf);
        for(int i = R.pr[u], d; i; i = R.nx[i])
            if(D[R.to[i]] > (d = D[u] + R.w[i]))
                Up(R.to[i], D[R.to[i]] = d);
    }
}
int Dfs(int u, int d) {
    if(d < 0 || d > K) return 0;
    if(vis[u][d]) return -1;
    if(~f[u][d]) return f[u][d];
    vis[u][d] = true;
    int ways = 0;
    for(int i = G.pr[u], t; i; i = G.nx[i])
        if(~(t = Dfs(G.to[i], D[u] + d - G.w[i] - D[G.to[i]])))
            Add(ways, t);
        else return -1;
    vis[u][d] = false;
    if(u == n && !d) Add(ways, 1);
    return f[u][d] = ways;
}
int Work() {
    memset(f, -1, sizeof(f));
    memset(vis, 0, sizeof(vis));
    n = ri(); int m = ri(); K = ri(); p = ri();
    for(int i = 1;i <= n; ++i) D[i] = inf; G.Clr(); R.Clr();
    for(int u, v, w;m--;) u = ri(), v = ri(), w = ri(), G.add(u, v, w), R.add(v, u, w);
    Dij(); int ways = 0;
    for(int i = 0;i <= K; ++i) {
        int t = Dfs(1, i);
        if(!~t) return -1;
        Add(ways, t);
    }
    return ways;
}
int main() {
    memset(T, 0x3f, sizeof(T));
    for(int i = 1;i <= 1e5; ++i) T[i + Nt].u = i;
    for(int Ca = ri();Ca--;) printf("%d\n", Work());
    return 0;
}

猜你喜欢

转载自blog.csdn.net/lvzelong2014/article/details/83857096
今日推荐