【题解】洛谷 P4201 [NOI2008] 设计路线 [树形 DP]
P4201 [NOI2008] 设计路线 - 洛谷 (luogu.com.cn)
(感觉上位蓝)
题意:在一颗树中,选几条互相不能有重复点的路径(单个路径内部也不能有重复点),使得所有点到根节点的非路径边距离最小(经过的非路径边最小),求最小值和方案数。
如果给出的图不连通(不是树是森林),则直接输出两行 -1。
0.思考
首先,所有点都应该在路径上,哪怕是单个点。
那问题就变成了:
给树路径剖分,使根到所有点经过非路径边的最大值最小(以下简称为不便值),
求最优方案不便值和合法剖分方案数。
这很像树链剖分,不难想到经典结论:
任意节点到根节点的路径上,轻边(或者重链)的数量不超过
(K 叉树)。
所以说再不济我们还可以按照重链来剖分,即本题最优方案不便值最大不会超过 。
1.进一步分析
话说回来,一般这种树味很重的题目,就可以考虑树形 dp。
状态定义一般长这样:
dp[x][k]: 表示以 x 为根的子树中,满足某种条件 k 的最优解
这个 k 可以是子树的性质,也可以是 x 自身的条件。
本题中很明显应该是 x 自身的某种条件,还和路径有关。
那么定义 dp[x][k] 表示 x 向子树内部路径连接了 k 个点,
发现 k 只能是:
0(不连), 1(作为父亲到子节点路径的中间点 或者 x 到子节点的路径的起始点), 2(作为子节点内部路径的中间点)
同时合法剖分方案数是不超过 的,于是我们可以再增加一维来表示不便值计算方案数。
即:
dp[x][k][i]: x 向子树内部路径连接了 k 个点,且最大不便值为 i
根据 x 是否向子节点 y 连路径分类,就有以下转移:
for (int i = 0; i < 20; i ++) {LL tmp0 = 0, tmp1 = 0, tmp2 = 0; // 因为转移时要用到转移前的 dp[x],所以这里新开变量if (i > 0) { LL t = (dp[y][0][i - 1] + dp[y][1][i - 1] + dp[y][2][i - 1]) % P;// t = y 内部连 0 / 1 / 2 个点的方案数总和// 这代表 y 内部子树自己搞定,不与 x 连成路径// 最大的不便值 + 1,所以 i 要求大于 0 tmp0 = dp[x][0][i] * t % P;// x 内部连 0 个点的方案数 = 之前连 0 个点 * ttmp1 = dp[x][1][i] * t % P;// x 内部连 1 个点的方案数 = 之前连 1 个点 * ttmp2 = dp[x][2][i] * t % P;// x 内部连 2 个点的方案数 = 之前连 2 个点 * t}// 剩下的情况是路径连上 y,不便值不变,所以 i 不用变 tmp1 = (tmp1 + dp[x][0][i] * (dp[y][0][i] + dp[y][1][i])) % P;// x 内部连 1 个点的方案数 += 之前连 0 个点 * y 内部连 0 / 1 个点的方案数tmp2 = (tmp2 + dp[x][1][i] * (dp[y][0][i] + dp[y][1][i])) % P;// x 内部连 2 个点的方案数 += 之前连 1 个点 * y 内部连 0 / 1 个点的方案数dp[x][0][i] = tmp0;dp[x][1][i] = tmp1;dp[x][2][i] = tmp2;
}
而最小不便值就看看什么时候 dp[1][0 / 1 / 2][i] 不等于 0,且 i 最小,输出 i 就好。
2.代码
要注意的是模数不是质数,可能模完是 0,导致最后判别最小不便值时出错。
可以多开一个数组判断是否可达(这是最正规的方法,+-1 什么的太玄学)。
而判断是否为森林,不仅要看边数是否 >= n - 1,还要看是否全部点都能被遍历。
因为题目没说是不是有重边,这样保险。
AC 代码:
#include<bits/stdc++.h>
using namespace std;typedef long long LL;
const int N = 1e5 + 10;int n, m; LL P;
vector<int> G[N];
int sum; // 判断是否联通的 LL dp[N][3][20];
// dp[x][k][i] x 向子树内部路径连接了 k 个点,且最大不便值为 i
// 0(不连), 1(作为父亲到子节点路径的中间点 或者 x 到子节点的路径的起始点), 2(作为子节点内部路径的中间点)
bool v[N][3][20]; // 判断是否可行 void treeDP(int x, int fa) {sum ++;for (int i = 0; i < 20; i ++) { // 初始化(叶子节点) dp[x][0][i] = 1; // 不连的方案数为 1,因为 x 内部没有不便值,所以 i 等于任何数都可以 dp[x][1][i] = 0;dp[x][2][i] = 0;v[x][0][i] = 1; v[x][1][i] = 0;v[x][2][i] = 0;}for (int y : G[x]) if (y != fa) {treeDP(y, x);for (int i = 0; i < 20; i ++) {LL tmp0 = 0, tmp1 = 0, tmp2 = 0; // 因为转移时要用到转移前的 dp[x],所以这里新开变量if (i > 0) { LL t = (dp[y][0][i - 1] + dp[y][1][i - 1] + dp[y][2][i - 1]) % P;// t = y 内部连 0 / 1 / 2 个点的方案数总和// 这代表 y 内部子树自己搞定,不与 x 连成路径// 最大的不便值 + 1,所以 i 要求大于 0 tmp0 = dp[x][0][i] * t % P;// x 内部连 0 个点的方案数 = 之前连 0 个点 * ttmp1 = dp[x][1][i] * t % P;// x 内部连 1 个点的方案数 = 之前连 1 个点 * ttmp2 = dp[x][2][i] * t % P;// x 内部连 2 个点的方案数 = 之前连 2 个点 * t}// 剩下的情况是路径连上 y,不便值不变,所以 i 不用变 tmp1 = (tmp1 + dp[x][0][i] * (dp[y][0][i] + dp[y][1][i])) % P;// x 内部连 1 个点的方案数 += 之前连 0 个点 * y 内部连 0 / 1 个点的方案数tmp2 = (tmp2 + dp[x][1][i] * (dp[y][0][i] + dp[y][1][i])) % P;// x 内部连 2 个点的方案数 += 之前连 1 个点 * y 内部连 0 / 1 个点的方案数dp[x][0][i] = tmp0;dp[x][1][i] = tmp1;dp[x][2][i] = tmp2;bool v0 = 0, v1 = 0, v2 = 0; if (i > 0) { bool t = v[y][0][i - 1] | v[y][1][i - 1] | v[y][2][i - 1];v0 = v[x][0][i] & t;v1 = v[x][1][i] & t;v2 = v[x][2][i] & t;}v1 |= (v[x][0][i] & (v[y][0][i] | v[y][1][i])) % P;v2 |= (v[x][1][i] & (v[y][0][i] | v[y][1][i])) % P;v[x][0][i] = v0;v[x][1][i] = v1;v[x][2][i] = v2;}}
}int main () {ios::sync_with_stdio(false);cin.tie(0);cin >> n >> m >> P;for (int i = 1; i <= m; i ++) {int x, y;cin >> x >> y;G[x].push_back(y);G[y].push_back(x);}if (m < n - 1) { // 首先边数至少要够 cout << "-1" << "\n" << "-1" << "\n";return 0;}sum = 0;treeDP(1, 0);if (sum != n) { // 然后是连通性 cout << "-1" << "\n" << "-1" << "\n";return 0;}for (int i = 0; i < 20; i ++) {if (v[1][0][i] | v[1][1][i] | v[1][2][i]) {cout << i << "\n"; cout << (dp[1][0][i] + dp[1][1][i] + dp[1][2][i]) % P << "\n";break;}}return 0;
}
