树形DP
树上背包
树上背包1:http://oj.daimayuan.top/course/8/problem/269
给定一个n(<=2000)个点的树,每个点有一个权值,权值可能是负数,选择一个大小恰好为m的并且包含根节点的连通
块,使得权值和最大
时间复杂度:O(n^2)
两个背包的大小分别是sa和sb,那么合并这两个背包的代价大概是sa*sb级别的,也就是两个背包之间的每一对元素都有1
的贡献,并且可以发现的是这一对元素只有在他的最近公共祖先合并儿子背包的时候被计算一次,所以总的时间复杂度是
O(n^2)
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long LL;
const int N = 2010, inf = 1<<29;
vector<int> son[N];
int a[N], Size[N], f[N][N];
void dfs(int u){
static int tmp[N];
// 需要记录下子树的大小,这样在合并子树的时候可以减少遍历次数
Size[u] = 0;
for(auto s : son[u]){
dfs(s);
// 初始化成-inf,因为有的节点的权值是负数,这里踩坑了
// 采用tmp的写法更简单易懂,而且空间复杂度的上限在f那里,tmp不是主要因素
for(int i = 0; i <= Size[u] + Size[s]; i ++) tmp[i] = -inf;
// 遍历更新的时候只循环到子树的大小即可
for(int i = 0; i <= Size[u]; i ++){
for(int j = 0; j <= Size[s]; j ++ ){
tmp[i+j] = max(tmp[i+j], f[u][i] + f[s][j]);
}
}
for(int i = 0; i <= Size[u] + Size[s]; i ++ ) f[u][i] = tmp[i];
// 这里记得写
Size[u] += Size[s];
}
Size[u] ++;
// 倒序移动,这样不会冲突
for(int i = Size[u]; i >= 1; i -- ) f[u][i] = f[u][i - 1] + a[u];
}
int main(){
int n, q;
scanf("%d %d", &n, &q);
for(int i = 2; i <= n; i ++){
int x; scanf("%d", &x);
// 因为是有根树,父子关系是确定的,所以直接存儿子就行了。
son[x].push_back(i);
}
for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
dfs(1);
while(q--){
int u, m;
scanf("%d %d", &u, &m);
printf("%d\n", f[u][m]);
}
return 0;
}
树上背包2:http://oj.daimayuan.top/course/8/problem/270
给你一个n(1<=n<=50000)个节点的有根树,每个点都有一个权值,要求选择一个大小恰好为m(1<=m<=100)的连通块的
最大权值
时间复杂度:O(nm)
因为每个节点他的权重只是1,所以他的组合情况就比较少,当我们使用以下代码实现的时候复杂度就不会很高,例如四个
节点,他的状态只能构成0,1,2,3,4几个,但是当权重比较分散的时候,例如是1,2,4,8,虽然我们只有四个节点,但是他们能组成的状态确实0-15,这样的话复杂度就会退化成O(nm^2)
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long LL;
const int N = 50010, M = 110, inf = 1 << 29;
int f[N][M], Size[N];
vector<int> son[N];
int a[N];
void dfs(int u){
Size[u] = 0;
static int tmp[M];
for(auto v : son[u]){
dfs(v);
// 与上一题相比较,只增加了循环的限制,也就是和的最大值是M,其他基本上一样,这样就可以将复杂度优化到O(NM)
for(int i = 0; i <= Size[u] + Size[v] && i < M; i ++) tmp[i] = -inf;
for(int i = 0; i <= Size[u] && i < M; i ++){
for(int j = 0; j <= Size[v] && i + j < M; j ++){
tmp[i + j] = max(tmp[i + j], f[u][i] + f[v][j]);
}
}
for(int i = 0; i <= Size[u] + Size[v] && i < M; i ++) f[u][i] = tmp[i];
Size[u] += Size[v];
}
Size[u] ++;
for(int i = min(Size[u], M-1); i ; i --) f[u][i] = f[u][i-1] + a[u];
}
int main(){
int n, q;
cin >> n >> q;
for(int i = 2; i <= n; i ++){
int x; scanf("%d", &x);
son[x].push_back(i);
}
for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
dfs(1);
while(q -- ){
int u, m;
scanf("%d %d", &u, &m);
printf("%d\n", f[u][m]);
}
return 0;
}
题目链接:http://oj.daimayuan.top/course/8/problem/271
给你一个n(1<=n<=1000)个点的有根树,每个点有一个重量w和权值v,要求你选一个重量恰好为m的包含根的连通块
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long LL;
const int N = 1010, M = 1e4+10, inf = 1<<29;;
int f[N][M];
vector<int> son[N];
int a[N], w[N];
int tot, l[N], r[N], seg[N];
// 这里的dfs序的求解方法
// seg存放dfs序,角标从1开始一直到tot
// l[u]:表示节点u的子树在dfs中的左边界(闭区间的), r[u]表示节点u的子树在dfs中的右边界
// 需要注意的是搞清楚真实结点的编号和dfs序中的编号
// 下面的tot是dfs序中编号,u是节点编号,在后面写代码的时候不要使用混了
dfs(int u){
l[u] = ++tot;
seg[tot] = u;
for(auto v : son[u]){
dfs(v);
}
r[u] = tot;
}
int main(){
int n, m;
cin >> n >> m;
for(int i = 2; i <= n; i++){
int x; scanf("%d", &x);
son[x].push_back(i);
}
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
for(int i = 1; i <= n; i++) scanf("%d", &w[i]);
dfs(1);
// 由于是体积恰好为m,所以我们需要将非法的状态边界初始化成负无穷,这样这些非法状态就不会去更新其他状态了 // f[i][j]:表示i-n中,体积恰好为j的解,划分状态的话,根据的是当前节点seg[i](注意这里并不是节点i是否要选,而是dfs序里面第i个位置表示的节点是否要选)是否要选,当前节点要选的话,那么他的子节点也就可选可不选了,所以这种情况是f[i+1][j],如果该节点不选的话,说明他的子树不能选,所以需要跳过该节点的子树,这里是利dfs序直接跳到r[u]+1
for(int i = 1; i <= m; i ++ ) f[n+1][i] = -inf;
f[n+1][0] = 0;
for(int i = n; i >= 1; i -- ){
// u是当前遍历到的dfs序的节点真实编号,因为下面的r[]求该子树的右边界是角标是节点的真实角标
// 另外1号节点是根,所以他的dfs序的角标也1,所以答案是f[1][j]
int u = seg[i];
for(int j = 0; j <= m; j ++){
f[i][j] = f[r[u] + 1][j];
if(j >= w[i]) f[i][j] = max(f[i][j], f[i+1][j-w[i]] + a[i]);
}
}
for(int i = 0; i <= m; i ++ ){
if(f[1][i] > 0 ) printf("%d\n", f[1][i]);
else puts("0");
}
return 0;
}
总结:
1. 如果合并两个子树的代价等于树的大小的乘积的话,那么复杂度就是O(n^2)的
2. 如果合并两个子树的代价等于子树大小和m取min的乘积的话,复杂度就是O(nm)的
3. 如果不是这样,就没有相应的结论,比如合并的代价是O((su + sv)^2)
4. 如果是距离相关的话,因为距离不会超过子树的大小,所以有类似的结论
练习1:在树上找一个连通块,满足任意两点之间的距离都不超过d,使得权值之和最大,权值可能是负数
f[i][j]:表示i这个子树里面,选择一个包括i的连通块,并且这个联通块中距离i这个点最远距离是j
那么合并子树的联通块的时候,对于f[v1][j1],f[v2][j2]两个子树,他们需要满足j1+j2+2<=d并且将结果更新到
f[u][max(j1, j2)+1]里面去
练习2:在树上找一个点集,满足任意两点之间的距不小于d,使得权值之和最大,权值可能是负数
f[i][j]:表示在i个子树里面选一个点集,并且这个点集里面的点距离i的最小距离是j,当然如果j==0就表示选了i这个
点, 那么合并子树的时候,f[v1][j1],f[v2][j2], 需要满足j1+j2+2>=d并且将结果更新到f[u][min(j1, j2)+1]当中
树上路径
树上路径1
题目链接:http://oj.daimayuan.top/course/8/problem/272
题目大意:给定n(1<=n<=2000)个点的树,给定m(<=2000)条简单路径,每条路径有一个权值,要求选择一些路径,使得每个点至多在一条路径上面,使得这些权值的和最大
方法一:
时间复杂度O(nm)
f[i][j]:表示i这个子树,穿过i这个点的是j这条路径,如果j是0的话,就说明,选择的路径里面没有路径穿过i这个点
g[i]:表示i这个子树没有路径伸出来的最大值
转移:
f[i][j]值计算:
j==0的时候,i的每个子树都是独立的,所以需要求和每个封闭儿子子树的最大值即可,
j!=0的时候,看看哪个儿子也在这条路径上面,那么就是f[这些][j]的求和,然后其他的都没有伸出来的,也就是g[其他儿子]
g[i]值计算:
i的所有儿子子树都没有伸出来
i是通过他的直线的最高点
方法二:
时间复杂度O(nm)
f[i]表示考虑i这个子树能获得的最大权值(不考虑从i这个子树里面伸出来的子树)
转移:
i这个点不在任何一个路径里面,所以他的所有儿子全是封闭的,直接求和就行了
i如果在某一条路径里面的话,他必然是这条路径的最高点,所以对于每一条路径,就是在他的最高点考虑是否选择这条路径,由于选择的路径是不交的,所以假设现在i通过的路径是j,那么j这条路径上的点都不能在其他路径上面
了,所以j这条路径上面的节点向下伸出来的儿子都是封闭的,也就是f[这些伸出来的儿子]的求和了
优化:如果能够支持删除路径上的所有点后面快速求出剩下点的dp值的话,问题就会被优化到O(n+mlogn),这个可以使用DFS序和树状数组来解决
实现小技巧:在dfs的过程中,发现没往下递归线段的一段,那么带来的变化就是该节点的所有儿子的dp值之和减去该节点的dp值
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2010;
// f[i]:表示i这个子树在内部选直线的最大值
// sf[i]:表示不选经过i的直线,在子树里面选直线的最大值
LL f[N], sf[N];
int fa[N], depth[N];
vector<int> son[N];
vector< array<int, 3> > path[N];
void dfs(int u){
for(auto v : son[u]){
dfs(v);
sf[u] += f[v];
}
// f[u]初始化成u不选直线,但是他的儿子内部自己选的情况
f[u] = sf[u];
for(auto p : path[u]){
// u是一定在p直线上的,现在假设所有儿子是封闭的,那么求和就是sf了
// 但是有些儿子其实并不是封闭的,他是在p这条直线上的,那么我们就需要在tmp的基础上进行弥补了
// 假设他的某个儿子是在p这条直线上的,那么我们就需要减去他本来提供的贡献f[这个儿子],然后再假设
// 这个儿子的所有儿子都是封闭的,把他们的所有sf加起来,最终到了直线的端点也就结束了
// 但是下面的实现中,其实是倒着来的,他是从端点向上进行的
LL tmp = sf[u];
int p0 = p[0];
while(p0 != u){
tmp += sf[p0] - f[p0];
p0 = fa[p0];
}
int p1 = p[1];
while(p1 != u){
tmp += sf[p1] - f[p1];
p1 = fa[p1];
}
tmp += p[2];
f[u] = max(f[u], tmp);
}
}
int main(){
int n, m;
scanf("%d%d", &n, &m);
for(int i=2; i<=n; i++){
scanf("%d", &fa[i]);
son[fa[i]].push_back(i);
// 深度主要用来暴力求lca
depth[i] = depth[fa[i]] + 1;
}
for(int i=1; i<=m; i++){
int u, v, a;
scanf("%d %d %d", &u, &v, &a);
// 数据范围比较小,所以可以暴力地往上跳,第一个次跳到相同的位置就是lca了
// 这种dp的话,只在lca的位置对直线进行计算
// 然后我们把该条直线存在lca的一个数组里面也方便我们后面进行dp计算
int x = u, y = v;
while(x != y){
// 每次将深度大的往上跳
if(depth[x] > depth[y]) x = fa[x];
else y = fa[y];
}
path[x].push_back({u, v, a});
}
dfs(1);
printf("%lld\n", f[1]);
return 0;
}
树上路径2
题目链接:http://oj.daimayuan.top/course/8/problem/273
题目大意:给定n个点的有根树,有m条路径,保证每条路径都是从一个点到他的某个祖先的,要求选择一些路径,使得每个点至少在一条路径上面,最终路径的权值最小
因为一个点可能在多条路径上面,所以不能使用上面的方法,另外在最高点考虑选不选也不容易转移
f[i][j]:表示i这个子树里面选择的路径能够最多向上延申到j这个深度
转移:考虑子树,当合并两个子树的时候f[v1][j1],f[v2][j2],那么最多能够延申的位置就是min(j1, j2)(越靠上深度越浅),另外还需要考虑的是以i这个点为最低点的线段的情况
目前复杂度:O(n^3),因为一个子树可能很小,但是他的j可能比较比较大,所以有效的状态j就很大了,枚举的时候都需要进行枚举,所以复杂度较高
优化:记录一个后缀最小值
最终时间复杂度:O(n^2)
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2010;
const LL inf = 1ll<<60;
vector<int> son[N];
vector<array<int, 2>> path[N];
LL f[N][N];
int depth[N];
int n, m;
void merge(LL a[], LL b[], int len){
static LL sufa[N], sufb[N];
// 当我们合并两个背包的时候,发现是f[u][i]和f[v][j]合并到一个背包里面,当我们暴力枚举i,j的话
// 他的复杂度比较高,是接近O(n^3)的
// 我们可以进行前缀或者后缀的处理,这种技巧感觉挺常用的
// 我们处理一个后缀的i,j的最小值,那么就是f[u][min(i, j)] + min(f[v][j>=min(i, j)])以及
// min(f[u][>=min(i, j)]) + f[v][j>=min(i, j)]
sufa[len + 1] = inf;
sufb[len + 1] = inf;
for(int i = len; i >= 1; i --){
sufa[i] = min(sufa[i + 1], a[i]);
sufb[i] = min(sufb[i + 1], b[i]);
}
for(int i = 1; i <= len; i ++){
a[i] = min(a[i] + sufb[i], b[i] + sufa[i]);
}
}
void dfs(int u){
// static LL tmp[N];
for(int i = 1; i <= depth[u]; i ++) f[u][i] = inf;
// 为什么这样进行赋值,因为这里是按照树上背包做的,我们每次都在合并一个背包,所以最开的时候是没有儿
// 子的情况,之后我们再慢慢地进行合并
for(auto p : path[u]) f[u][depth[p[0]]] = min(f[u][depth[p[0]]], (LL)(p[1]));
for(auto v : son[u]){
dfs(v);
// 暴力写法
// 这里要根据实际的含义进行转移
// 在最开始的没有进入这个循环的时候,f[u][depth[v]]确实是零
// 但是当开始了一些分支之后f[u][depth[v]]就不一定是零了,可能下面的儿子向上延申刚好到depth[v]
// 所以状态转移需要保留着这些
// for(int i = 1; i <= depth[v]; i ++) tmp[i] = inf;
// for(int i = 1; i <= depth[v]; i ++){
// for(int j = 1; j <= depth[v]; j ++){
// tmp[min(i, j)] = min(f[u][i] + f[v][j], tmp[min(i, j)]);
// }
// }
// for(int i = 1; i <= depth[v]; i ++) f[u][i] = tmp[i];
merge(f[u], f[v], depth[v]);
}
}
int main(){
scanf("%d%d", &n, &m);
depth[1] = 1;
for(int i = 2; i <= n; i ++){
int x; scanf("%d", &x);
son[x].push_back(i);
depth[i] = depth[x] + 1;
}
for(int i = 1; i <= m; i ++){
int u, v, a; scanf("%d %d %d", &u, &v, &a);
path[v].push_back({u, a});
}
dfs(1);
// 为什么这里不能直接==(1ll<<60),因为他可能在更新的时候加上了一些东西
if(f[1][1] >= (1ll<<60)/2) puts("-1");
else printf("%lld\n", f[1][1]);
return 0;
}
树上连通块&换根dp
题目链接:http://oj.daimayuan.top/course/8/problem/274
题目大意:给定一个n个点的树,对每个点,求包含这个点的连通块的个数
如果这个树是一个有根树,那么就是一个简单树形dp,f[i]表示以i为根的子树里面连通块的个数,他的大小等于所有儿子的(f[v]+1)的乘积
但是当如果不是一个有根树的话,仿佛是一个换根dp,但是在做换根dp的时候发现,我们需要需要计算某个点去掉某个儿子之后的(dp+1)的乘积,当我们使用暴力的求解的话,时间复杂度会退化成O(n^2),而且不管mod是否为质数,我们都没办法使用求逆元的方法的求出来,因为当前点的dp值可能为0,他根本就没有逆元。
现在抽象出来的一个问题是,给定一个序列a1,a2,....an,需要求出除了i这个点之后a的乘积
我们需要求一个前缀积和后缀积pre[i] = a1 a2 ... ai suf[i] = an an-1 ... ai
那么ansi = prei-1 * sufi+1
// dls的换根dp根之前学的有点不太一样,主要区别在dfs2
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5+10;
int n, mod;
vector<int> son[N];
// ans表示包含i节点的连通块的个数,也就是最后的答案
// f1[i]表示以开始确定的一个根,做第一次dfs求得的以i为根的子树里面包含i的连通块个个数
// f2[i]是个什么的,我们在换根的时候除了我们之前的儿子,我们还需要知道我们父亲的贡献,f2[i]其实就是i的父亲对换根能带来的贡献,如果f2[i]中i是最开始根节点的儿子的话,也就是u==1,那么f2[i]其实就是u==1的所有儿子+1除了当前计算节点的值,如果不是的话,那么就是他的父亲u的儿子除了当前节点f1值+1的计算结果,然后再乘上u的父亲变成儿子带来的贡献
LL f1[N], f2[N], ans[N];
void dfs1(int u){
f1[u] = 1;
for(auto v : son[u]){
dfs1(v);
f1[u] = (f1[u] * (f1[v] + 1)) % mod;
}
}
void dfs2(int u){
// 这里要开LL不然会寄
static LL pre[N], suf[N];
int m = son[u].size();
if(m == 0) return ;
// 求前后缀的时候搞清楚是(x+1)的前后缀乘积
pre[0] = 1;
for(int i = 0; i < m; i ++){
pre[i + 1] = pre[i] * (f1[son[u][i]] + 1) % mod;
}
suf[m] = 1;
for(int i = m-1; i >= 0; i --){
suf[i] = suf[i + 1] * (f1[son[u][i]] + 1) % mod;
}
// 这里在求f2,也就是他的父亲能带来的贡献
for(int i = 0; i < m; i ++){
int v = son[u][i];
f2[v] = pre[i] * suf[i + 1] % mod;
if(u != 1) f2[v] = f2[v] * (f2[u] + 1) % mod;
}
// 自己的所有儿子合并上父亲能带来的贡献就是当前节点换根之后的结果了
for(int i = 0; i < m; i ++){
int v = son[u][i];
ans[v] = (f2[v] + 1) * f1[v] % mod;
dfs2(v);
}
}
int main(){
scanf("%d %d", &n, &mod);
for(int i = 2; i <= n; i ++){
int x; scanf("%d", &x);
son[x].push_back(i);
}
dfs1(1);
dfs2(1);
ans[1] = f1[1];
for(int i = 1; i <= n; i ++){
printf("%lld\n", ans[i]);
}
return 0;
}
总结
树上路径问题:
1.一般是在LCA处考虑路径,也可能是最低点
2.两种方法,记录每个点他在哪条路径,直接在LCA处考虑这条路径选或者不选(树上路径1的两种方法)
3. 一般第一种方法更加常用,因为记录了一些额外的信息,所以更能处理更加复杂的问题,但是由于是两维的所以
它的复杂度基本最低就是n^2的,而第二种是一维的所以更容易考虑优化
换根dp:
换根dp往往需要处理去掉一个子树这样的问题,我们可以使用前后缀和的方法来优化复杂度
例题讲解
ICPC Nanjing 2021 H, Crystalfly
题目链接:http://oj.daimayuan.top/course/8/problem/295
题目大意:给一个n个点的树(<=1000000),每个点有一定数量的蝴蝶,当走到一个点的时候,他周围的蝴蝶会在ti(1~3)后飞走,0时刻你到了1号节点,问最后权值最大是多少。
f[i]:表示以i这个节点为根的子树,i这个节点不能选的最优解
g[i]:表示以i这个节点为根的子树,i这个节点要选的最优解
h[i]:表示以i这个节点为根的子树,i这个节点要选,但是i的儿子不能选的最优解
状态转移:
首先f和g之间有一个关系,g=f+a[u]
h[u] = a[u] + 他的儿子的f之和
f的转移:
1.其中的一个儿子的g,其他的儿子都是f,然后根据g和f的关系,其实就是所有的f和,然后挑一个儿
子,他的蝴蝶数量
2. 先找一个节点走下去,然后再返回一个t=3的儿子补救一下,h[vi] + g[vj] + 剩下的f之和,其实就是所有儿子的f之和+h[vi]-f[vi]+aj, 那么我们只需要枚举前面的i(枚举的所有i之和其实是节点的总个数,所以复杂度不会超),然后在剩下的里面选一个最大的aj,并且这个节点的时间是3,所以直接记录一个最大值和次大值就可以了
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long LL;
const int N = 1e5+10;
const LL inf = 1<<60;
int a[N], t[N];
vector<int> son[N];
// 由于f和g之间存在上述的关系,所以这里并没有保留g,而是只有f
LL f[N], h[N];
void dfs(int u, int fa){
LL sf = 0;
int maxn = 0;
// 先求儿子f的和,并计算出来儿子a的最大值
for(auto v : son[u]){
if(v == fa) continue;
dfs(v, u);
sf += f[v];
maxn = max(maxn, a[v]);
}
// h和f的第一种情况就可以计算了
h[u] = sf + a[u];
f[u] = sf + maxn;
// 计算最大值和次大值
int ma1 = -inf, maid1 = -1;
int ma2 = -inf, maid2 = -1;
for(auto v : son[u]){
if(v == fa) continue;
if(t[v] == 3 && ma1 < a[v]){
ma2 = ma1, maid2 = maid1;
ma1 = a[v], maid1 = v;
}
else if(t[v] == 3 && ma2 < a[v]){
ma2 = a[v], maid2 = v;
}
}
// 枚举儿子,根据是否为最大值,进行不同的转移
for(auto v : son[u]){
if(v == fa) continue;
if(v == maid1) f[u] = max(f[u], sf + h[v] - f[v] + ma2);
else f[u] = max(f[u], sf + h[v] - f[v] + ma1);
}
}
int main(){
int T;
scanf("%d", &T);
while(T--){
int n;
scanf("%d", &n);
for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
for(int i = 1; i <= n; i ++) scanf("%d", &t[i]);
for(int i = 1; i <= n; i ++) son[i].clear();
for(int i = 1; i <= n - 1; i ++){
int x, y; scanf("%d %d", &x, &y);
son[x].push_back(y);
son[y].push_back(x);
}
dfs(1, -1);
printf("%lld\n", f[1] + a[1]);
}
return 0;
}
CCPC Qinhuangdao 2020 K, Kingdom's power
题目链接:http://oj.daimayuan.top/course/8/problem/297
题目大意:给定一个n(<=1e6)个点的有根树,一开始在一号节点上有无数的军队,每一秒你可以指挥一支军队到相邻的一个点,希望每个点都有军队访问过,问最少需要多少的时间
如果针对每条路径的分析的话,那么感觉是自顶向下的,不太容易用树形dp实现,树形dp一般是自底向上的
现在对某个子树进行分析
第一个问题是,访问这个子树的路径是从哪里来的,可能是根节点来的也可能是从兄弟节点跑过来的,也就是军队的七点在哪里,代价如何计算
第二问题是,访问完这个子树,军队要不要回去,
现在来分析:
1. 从兄弟过来,然后访问完兄弟又回去
2. 有若干条从上面下来的路径
访问所有儿子是有两种状态的
f[u][0]:表示u这个子树,从u下去一条路径,然后又返回了一条路径
f[u][1]:表示u这个子树,从u下午若干条路径,至少有一条路径在下面停了
f[u][0]:每个儿子都要下去再回来 (f[v][0]+2)的求和
f[u][1]:如果这个儿子是下去又回来的就是f[v][0]+2,如果只下去的话,就是f[v][1],min(f[v][0]+2,f[v][1]
)但是需要保证有路径选择f[v][1]这种情况的,不然都是f[v][0]+2了,
// 这道题卡了快读
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e6+10;
int e[N], ne[N], h[N], depth[N], idx;
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
LL f[N][2];
void dfs(int u){
// 想了好久才懂这里为什么这么初始化和更新
// f[u][1]:表示至少存在一条路径从根节点到u这个子树,采用类似背包的合并,初始化的话只有u一个节点,所以f[u][1]就是depth[u]
f[u][1] = depth[u];
f[u][0] = 0;
for(int i = h[u]; i != -1; i = ne[i]){
int v = e[i];
depth[v] = depth[u] + 1;
dfs(v);
// 这里首先保证f[u][1]和f[u][0]的更新顺序,因为f[u][1]要用到f[u][0]之前的之
// f[u][1]要保证至少存在一条路径来自根,那么就是f[u][1]+f[v][1]前面的背包和后面的背包都存在
// f[u][1] + f[v][0] + 2前面的背包不存在后面的背包存在
// f[u][0] + f[v][1] 前面的背包不存在但是后面的背包存在
f[u][1] = min({f[u][1] + f[v][1], f[u][1] + f[v][0] + 2, f[u][0] + f[v][1]});
f[u][0] += f[v][0] + 2;
}
}
int main(){
int T;
scanf("%d", &T);
for(int C = 1; C <= T; C ++){
int n;
idx = 0;
scanf("%d", &n);
for(int i = 1; i <= n; i++) h[i] = -1;
depth[1] = 0;
for(int i = 2; i <= n; i ++){
int x; scanf("%d", &x);
add(x, i);
}
dfs(1);
printf("Case #%d: %lld\n", C, min(f[1][0], f[1][1]));
}
return 0;
}
CF Round #734(Div. 3) F, Equidistant Vertices
题目链接:http://oj.daimayuan.top/course/8/problem/298
题目大意:给定一个n(100)个点的树,选择一个大小为k的子集,使得这些点之间两两之间距离相等,问方案数
k==2的话,那么任意选两个点就可以了
k>=3的话,必然是存在一个分叉,存在一个分叉点,将这k个点分到了k条边
枚举分叉点,枚举距离,然后分别进行dp即可
关键点在于观察点集发现性质
时间复杂度O(n^2k)
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 110, M = 210, mod = 1e9+7;
vector<int> son[N];
int f[N];
int ans;
int cnt;
void dfs(int u, int fa, int d){
if(d == 0){ cnt ++; return ;}
for(auto v : son[u]){
if(v == fa) continue;
dfs(v, u, d-1);
}
return ;
}
int main(){
int T;
scanf("%d", &T);
while(T--){
int n, k;
ans = 0;
scanf("%d %d", &n, &k);
for(int i = 1; i <= n; i++) son[i].clear();
for(int i = 1; i <= n-1; i ++){
int a, b; scanf("%d %d", &a, &b);
son[a].push_back(b), son[b].push_back(a);
}
if(k == 2){
printf("%lld\n",1ll * n * (n - 1) / 2 % mod);
continue;
}
for(int i = 1; i <= n; i ++){
for(int d = 1; d <= n; d ++){
// 枚举完i和d之后其实这里就是一个背包了
fill(f, f+n+1, 0);
f[0] = 1;
// 枚举物品
for(auto v : son[i]){
// 利用dfs计算物品的情况
cnt = 0; dfs(v, i, d-1);
// 枚举决策
for(int j = k; j >= 1; j --){
f[j] = (1ll * cnt * f[j-1] + f[j]) % mod;
}
};
ans = (ans + f[k] * 1ll) % mod;
}
}
printf("%d\n", ans);
}
return 0;
}