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