Foreword \text{Foreword} Foreword
《小清新》树形 dp。
其实本题没有那么(重读)恶心,但我一开始写完
x
=
0
x=0
x=0 后眼瞎没有看到
x
=
1
,
2
x=1,2
x=1,2 时也必然是最优方案(这种东西不黑体吗…)可以直接无视,还在苦苦的写强制选链的特殊情况,被狠狠的恶心到了。
这个故事告诉我们,要仔细审题!
感觉自己的做法还是挺舒服的,虽然核心转移看的有些长(也只有 40 行左右),但没有太多的分类讨论,和有些代码枚举儿子然后直接取十几行 max \max max 相比感觉更加阳间。
Solution \text{Solution} Solution
题意算比较简洁清晰了,再具体一些大概就是选出两条可重点不可重边的链,将树分割成尽可能多的连通块。
(一开始我受 希望 那个题的影响,尝试了好一会用度数和点数简洁的表示出连通块数目来简化 dp,其实是在玩泥巴,直接做就很方便。)
Definition \text{Definition} Definition
设置状态
f
x
,
i
,
d
f_{x,i,d}
fx,i,d 表示
x
x
x 节点子树内选择了
i
i
i 条已经确定的链,子树内 的联通块最大数目。
d
d
d 的定义有些复杂:
在
x
x
x 作为父亲的时候,
d
∈
[
0
,
4
]
d\in [0,4]
d∈[0,4] 表示的是
x
x
x 被删除,与
x
x
x 相连的儿子的个数,
d
=
5
d=5
d=5 表示
x
x
x 不被删除。
在
x
x
x 作为儿子的时候,
d
∈
[
0
,
1
]
d\in [0,1]
d∈[0,1] 表示
x
x
x 被删除,且与父亲相连/不相连。
d
=
5
d=5
d=5 表示
x
x
x 不被删除。
Transfer \text{Transfer} Transfer
Part 1
d
=
5
d=5
d=5,
x
x
x 不删时:
x
x
x 必然不会和儿子相连,儿子要么删了但不和父亲连(
d
=
0
d=0
d=0),要么根本不删 (
d
=
5
d=5
d=5),不删的时候由于父子都不删,两个联通块合并成一个,所以答案要减一。
f
x
,
i
,
5
+
f
s
o
n
,
j
,
5
−
1
→
f
x
,
i
+
j
,
5
f_{x,i,5}+f_{son,j,5}-1\to f_{x,i+j,5}
fx,i,5+fson,j,5−1→fx,i+j,5
f
x
,
i
,
5
+
f
s
o
n
,
j
,
0
→
f
x
,
i
+
j
,
5
f_{x,i,5}+f_{son,j,0}\to f_{x,i+j,5}
fx,i,5+fson,j,0→fx,i+j,5
d
∈
[
0
,
4
]
d\in[0,4]
d∈[0,4] 时:
x
x
x 可以和儿子相连也可以不连,不连的时候儿子是否删除都可以,对应转移分别就是:
f
x
,
i
,
d
+
f
s
o
n
,
j
,
1
→
f
x
,
i
+
j
,
d
+
1
f_{x,i,d}+f_{son,j,1}\to f_{x,i+j,d+1}
fx,i,d+fson,j,1→fx,i+j,d+1
f
x
,
i
,
d
+
f
s
o
n
,
j
,
0
→
f
x
,
i
+
j
,
d
f_{x,i,d}+f_{son,j,0}\to f_{x,i+j,d}
fx,i,d+fson,j,0→fx,i+j,d
f
x
,
i
,
d
+
f
s
o
n
,
j
,
5
→
f
x
,
i
+
j
,
d
f_{x,i,d}+f_{son,j,5}\to f_{x,i+j,d}
fx,i,d+fson,j,5→fx,i+j,d
这样
x
x
x 从所有儿子得到的转移就全完事了,接下来我们需要把
f
x
,
i
,
d
f_{x,i,d}
fx,i,d 的
d
d
d 的定义从“父亲形态”转化成“儿子形态”,以使它可以继续向父亲转移。
Part 2
d
=
5
d=5
d=5 时:
x
x
x 没选就是没选,没什么好说的。
f
x
,
i
,
5
→
f
x
,
i
,
5
f_{x,i,5}\to f_{x,i,5}
fx,i,5→fx,i,5
d
=
0
d=0
d=0 时:注意我们之前是强制让
x
x
x 被删除的,所以我们不能直接让它转移到
f
x
,
i
,
0
f_{x,i,0}
fx,i,0。为了删除它,要么使其单独一个点成链,要么让它成为链尾和父亲相连,对应就是:
f
x
,
i
,
0
→
f
x
,
i
+
1
,
0
f_{x,i,0}\to f_{x,i+1,0}
fx,i,0→fx,i+1,0
f
x
,
i
,
0
→
f
x
,
i
,
1
f_{x,i,0}\to f_{x,i,1}
fx,i,0→fx,i,1
d
=
2
/
4
d=2/4
d=2/4 时:向
x
x
x 连接的偶数条边必然两两成链:
f
x
,
i
,
d
→
f
x
,
i
+
d
/
2
,
0
f_{x,i,d}\to f_{x,i+d/2,0}
fx,i,d→fx,i+d/2,0
d
=
1
/
3
d=1/3
d=1/3 时:单出来的一条可以把
x
x
x 当成链头停止,也可以继续向父亲延伸:
f
x
,
i
,
d
→
f
x
,
i
+
d
/
2
+
1
,
0
f_{x,i,d}\to f_{x,i+d/2+1,0}
fx,i,d→fx,i+d/2+1,0
f
x
,
i
,
d
→
f
x
,
i
+
d
/
2
,
1
f_{x,i,d}\to f_{x,i+d/2,1}
fx,i,d→fx,i+d/2,1
Part 3
这样看起来就做完了?
但是你一测发现过不去样例!
仔细一看,是被两个点一条边的情况
h
a
c
k
hack
hack 了。
一般的,当一个图长成深度为 1 的菊花的时候,我们都会出问题。
我们缺少了在一个点连续摆烂的情况。
这个东西有两种解决方案:
第一种是当
x
x
x 被删除的时候,可以原地多选几条链,即:
f
x
,
i
,
0
→
f
x
,
i
+
1
,
0
f_{x,i,0}\to f_{x,i+1,0}
fx,i,0→fx,i+1,0
f
x
,
i
,
1
→
f
x
,
i
+
1
,
1
f_{x,i,1}\to f_{x,i+1,1}
fx,i,1→fx,i+1,1
第二种是求出度数最大点的度数,让答案和这个度数取个
max
\max
max。
第二种虽然看起来更加简单,跑起来也稍微快一点,但如果在考场上,本人还是建议第一种,毕竟多说多错,直接给一个合法转移,让 std::max
做决策而不是自己归纳贪心,肯定更加稳妥。
如果是做练习当然就精益求精啦。
Code \text{Code} Code
代码就简单了,把上面的转移敲出来即可。
(滚动数组无脑好用,不会互相转移出 bug,强推)
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define debug(...) fprintf(stderr,__VA_ARGS__)
#define ok debug("OK\n")
using namespace std;
const int N=1e5+100;
const int M=2e5+100;
const int inf=1e9;
inline ll read(){
ll x(0),f(1);char c=getchar();
while(!isdigit(c)) {if(c=='-')f=-1;c=getchar();}
while(isdigit(c)) {x=(x<<1)+(x<<3)+c-'0';c=getchar();}
return x*f;
}
int n;
int f[N][3][6];
int tmp[2][3][6],now,pre;
vector<int>v[N];
inline void Max(int &x,int y){
y>x?x=y:0;return;
}
void dfs(int x,int fa){
for(int to:v[x]){
if(to==fa||vis[to]) continue;
dfs(to,x);
}
now=1;pre=0;
memset(tmp,-0x3f,sizeof(tmp));
tmp[now][0][5]=1;tmp[now][0][0]=0;
for(int s:v[x]){
if(s==fa||vis[s]) continue;
swap(now,pre);
memset(tmp[now],-0x3f,sizeof(tmp[now]));
for(int i=0;i<=2;i++){
for(int j=0;i+j<=2;j++){
Max(tmp[now][i+j][5],tmp[pre][i][5]+f[s][j][5]-1);
Max(tmp[now][i+j][5],tmp[pre][i][5]+f[s][j][0]);
for(int d=0;d<=4;d++){
Max(tmp[now][i+j][d],tmp[pre][i][d]+f[s][j][0]);
Max(tmp[now][i+j][d],tmp[pre][i][d]+f[s][j][5]);
if(d+1<=4)Max(tmp[now][i+j][d+1],tmp[pre][i][d]+f[s][j][1]);
}
}
}
}
memset(f[x],-0x3f,sizeof(f[x]));
for(int i=0;i<=2;i++){
Max(f[x][i][5],tmp[now][i][5]);
if(i+1<=2) Max(f[x][i+1][0],tmp[now][i][0]);
Max(f[x][i][1],tmp[now][i][0]);
for(int j=1;j<=4;j++){
if(j&1){
if(i+j/2<=2) Max(f[x][i+j/2][1],tmp[now][i][j]);
if(i+(j+1)/2<=2) Max(f[x][i+(j+1)/2][0],tmp[now][i][j]);
}
else if(i+j/2<=2) Max(f[x][i+j/2][0],tmp[now][i][j]);
}
}
for(int i=1;i<=2;i++){
Max(f[x][i][0],f[x][i-1][0]);
Max(f[x][i][1],f[x][i-1][1]);
}
return;
}
void work0(){
for(int i=1;i<n;i++){
int x=read(),y=read();
v[x].push_back(y);v[y].push_back(x);
}
dfs(1,0);
printf("%d\n",max(f[1][2][0],f[1][2][5]));
return;
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("a.in","r",stdin);
freopen("a.out","w",stdout);
#endif
int T=read(),op=read();
while(T--){
n=read();
for(int i=1;i<=op;i++) read(),read();
work0();
for(int i=1;i<=n;i++) v[i].clear();
}
return 0;
}
/*
*/