先写个考试 鸽着~
考试有树
d
p
dp
dp紫题 我回来了
每年联赛基本都有好几道题时树
d
p
dp
dp,这块一定得好好学
U
P
D
:
20201109
UPD:20201109
UPD:20201109期中考砸 回来补博客
q
a
q
qaq
qaq 先把例题发上去 后面习题一天补上
一、基本概念
树形动态规划,顾名思义,就是在“树”的数据结构上做动态规划,通过有限次地遍历树,记录相关信息,以求解问题,通常都是建立在图上的。线性的动态规划的顺序有向前向后两种方向,即顺推和逆推;而树形动态规划是建立在树上的,树中的父子关系天然就是个递归(子问题)结构,所以也相应的有两个方向。
①叶
→
→
→根,即根的子节点传递有用的信息给根,之后由根得出最优解的过程。这种方式
D
P
DP
DP的题目应用比较多。
②根
→
→
→叶,即需要取所有点作为一次根节点进行求值,此时父节点得到了整棵树的信息,只需要去除这个儿子的
D
P
DP
DP值的影响,然后再转移给这个儿子,这样就能达到根
→
→
→叶的顺序。
实现方式:树形
D
P
DP
DP是通过记忆化搜索实现的,因此采用的是递归方式。
时间复杂度:树形
D
P
DP
DP的时间复杂度基本上是
O
(
n
)
O(n)
O(n),若有附加维
m
m
m则是
O
(
n
m
)
O(nm)
O(nm)
二、经典问题
1.树的重心
定义
1
:
1:
1:找到一个点,其所有子树中最大的子树结点数最少,那么这个点就是这棵树的重心
定义
2
:
2:
2:以这个点为根,其所有的子树大小都不超过整棵树的一半,那么这个点就是这棵树的重心
S
o
l
u
t
i
o
n
:
Solution:
Solution:任选一个结点为根,把无根树变成有根树,然后设
f
i
f_i
fi表示以
i
i
i为根的子树的结点的个数,则
f
[
i
]
=
∑
j
∈
s
o
n
[
i
]
+
1
f[i]=\sum\limits_{j∈son[i]}+1
f[i]=j∈son[i]∑+1。结点
i
i
i的子树中最大的有
m
a
x
j
∈
s
o
n
[
i
]
{
f
[
j
]
}
max_{j∈son[i]}\{f[j]\}
maxj∈son[i]{f[j]}个结点,
i
i
i的祖宗有
n
−
f
[
i
]
n-f[i]
n−f[i]个结点,就能求出删去结点
i
i
i后的最大连通块了
实现:只需一次
D
F
S
DFS
DFS,在无根树转有根树的同时计算即可。
性质
1
:
1:
1:树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么他们的距离和相等。
性质
2
:
2:
2:把一棵树添加或删除一个子叶,树的重心最多只移动一条边的距离。
性质
3
:
3:
3:把两棵树通过一条边相连得到一颗新的树,那么新的树的重心在原来两个树的重心的路径上。
只求树的重心果题还是挺难找的 看这道 p o j poj poj的题吧
poj1655 Balancing Art
题面
题意:求树的重心,并输出删去重心后的最大子树结点数
我注册了个
p
o
j
poj
poj账号哈哈,
p
o
j
poj
poj好像不支持万能库??
裸题 上代码
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
#define N 20020
#define reg register
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
struct node{
int to,nxt;
}edge[N<<1];
int u,v,n,T,ans,cnt,head[N],siz[N],son[N];
inline void addedge(int u, int v){
edge[++cnt].to=v,edge[cnt].nxt=head[u],head[u]=cnt;
}
inline void superadd(int u, int v){
addedge(u,v),addedge(v,u);
}
void dfs(int u, int fa){
siz[u]=1;
for(reg int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(v==fa)continue;
dfs(v,u);
siz[u]+=siz[v],son[u]=max(son[u],siz[v]);
}
son[u]=max(son[u],n-siz[u]);
if(son[u]<son[ans])ans=u;
else if(son[u]==son[ans]&&u<ans)ans=u;
}
int main(){
read(T);
while(T--){
read(n);
memset(siz,0,sizeof siz);
memset(son,0,sizeof son);
memset(head,0,sizeof head);
cnt=ans=0,son[0]=999999999;
for(reg int i=1;i<n;i++)read(u),read(v),superadd(u,v);
dfs(1,0);
printf("%d %d\n",ans,son[ans]);
}
}
2.树的直径
这里不得不
d
i
s
s
diss
diss一下
s
b
sb
sb一本通,直径两个字一笔没提,求法就给一个
定义:一棵树中,任意两个点的最短路径的距离最大值即为树的直径
有两种求法
1.
1.
1.两次
d
f
s
:
dfs:
dfs:实质是遍历点
2.
2.
2.树形
d
p
:
dp:
dp:实质是遍历边
求法
1
:
1:
1:两次
d
f
s
dfs
dfs
(
(
(或两次
b
f
s
bfs
bfs
)
)
)
做法:先从任意一点
P
P
P开始,找到距离他最远的点
Q
Q
Q;再从
Q
Q
Q找到距离最远的点
W
W
W,
Q
W
QW
QW即为树的直径
证明:①若
P
P
P是直径上的一点,则
Q
Q
Q是直径的端点,
Q
W
QW
QW就是直径
②若
P
P
P不在直径上,反证法:
假设
Q
W
QW
QW不是直径,
A
B
AB
AB是直径,则
Q
Q
Q一定不在直径上
(
1
)
(1)
(1)当
A
B
AB
AB与
P
Q
PQ
PQ有交点时,如图(为了方便把
A
B
AB
AB和
P
Q
PQ
PQ画成直线)
我们选的
Q
Q
Q是距离
P
P
P最远的点,则
P
Q
>
P
O
+
O
A
PQ>PO+OA
PQ>PO+OA
两边同时减去
P
O
PO
PO得
O
Q
>
O
A
OQ>OA
OQ>OA
两边同时加上
O
B
OB
OB得
O
Q
+
O
B
>
A
B
OQ+OB>AB
OQ+OB>AB,与
A
B
AB
AB是直径矛盾
(
2
)
(2)
(2)当
A
B
AB
AB与
P
Q
PQ
PQ没有交点时,如图
P
Q
>
N
P
+
N
M
+
N
B
PQ>NP+NM+NB
PQ>NP+NM+NB,两边同时减去
N
P
NP
NP,得
N
Q
>
N
M
+
M
B
NQ>NM+MB
NQ>NM+MB
两边同时加上
A
M
+
M
N
AM+MN
AM+MN,得
A
M
+
M
N
+
N
Q
>
A
M
+
2
N
M
+
M
B
>
A
M
+
M
B
=
A
B
AM+MN+NQ>AM+2NM+MB>AM+MB=AB
AM+MN+NQ>AM+2NM+MB>AM+MB=AB
与
A
B
AB
AB为直径矛盾
故
Q
Q
Q在直径上,且为直径的端点,所以
Q
W
QW
QW为直径
来个板子题
FZOJ 3398 树的直径
题目描述
树的直径:树上两点之间的最大距离。
给出一个树,让你求树的直径。
输入
一个数n表示节点数,以下(n-1)行每行两个数x,y表示x与y间有边。
输出
一个整数,树的直径。
样例输入
10
2 8
7 2
2 1
1 10
2 3
3 4
4 9
3 5
3 6
样例输出
5
提示
40% n<=2000
100% n<=200000
上代码
#include<bits/stdc++.h>
using namespace std;
#define N 200020
#define reg register
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int n,u,v,p,q,cnt,dep[N],head[N];
struct node{
int to,nxt;
}edge[N<<1];
inline void addedge(int u, int v){
edge[++cnt].to=v,edge[cnt].nxt=head[u],head[u]=cnt;
}
inline void superadd(int u, int v){
addedge(u,v),addedge(v,u);
}
void dfs(int u, int fa){
dep[u]=dep[fa]+1;
for(reg int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(v==fa)continue;
dfs(v,u);
}
if(dep[u]>dep[q])q=u;
}
int main(){
read(n);
for(reg int i=1;i<n;i++)read(u),read(v),superadd(u,v);
dep[0]=-1;dfs(1,0);
p=q,q=0;
memset(dep,0,sizeof dep);
dep[0]=-1;dfs(p,0);
printf("%d\n",dep[q]);
}
求法
2
:
2:
2:树形
d
p
dp
dp
这个方法也可以适用于有权值的树
一棵有根树的最长连,可能出现红色或黄色两种情况的直径
所以要解决这个问题,我们要求出每个结点为根的子树中的最长链,取其中的最大值为该树的直径
设
d
p
1
[
u
]
dp1[u]
dp1[u]表示以
u
u
u为根的子树中,
u
u
u到叶子结点的距离最大值;
d
p
2
[
u
]
dp2[u]
dp2[u]表示以
u
u
u为根的子树中,
u
u
u到叶子节点的距离次大值
当遍历到
i
i
i的儿子
j
j
j时,
①若
d
p
1
[
j
]
+
d
i
s
[
i
]
[
j
]
>
d
p
1
[
i
]
dp1[j]+dis[i][j]>dp1[i]
dp1[j]+dis[i][j]>dp1[i],则
d
p
2
[
i
]
=
d
p
1
[
i
]
,
d
p
1
[
i
]
=
d
p
1
[
j
]
+
d
i
s
[
i
]
[
j
]
dp2[i]=dp1[i],dp1[i]=dp1[j]+dis[i][j]
dp2[i]=dp1[i],dp1[i]=dp1[j]+dis[i][j]
②若
d
p
2
[
i
]
<
d
p
1
[
j
]
+
d
i
s
[
i
]
[
j
]
<
d
p
1
[
i
]
dp2[i]<dp1[j]+dis[i][j]<dp1[i]
dp2[i]<dp1[j]+dis[i][j]<dp1[i],则
d
p
2
[
i
]
=
d
p
1
[
j
]
+
d
i
s
[
i
]
[
j
]
dp2[i]=dp1[j]+dis[i][j]
dp2[i]=dp1[j]+dis[i][j]
最后扫描所有的结点,找最大的
d
p
1
[
i
]
+
d
p
2
[
i
]
dp1[i]+dp2[i]
dp1[i]+dp2[i]的值
上代码
就不找例题了 好累 哭哭
void dfs(int u, int fa){
for(reg int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(v==fa)continue;
dfs(v,u);
if(dp1[u]<dp1[v]+edge[i].val)
dp2[u]=dp1[u],dp1[u]=dp1[v]+edge[i].val;
else if(dp2[u]<dp1[v]+edge[i].val)
dp2[u]=dp1[v]+edge[i].val;
ans=max(ans,dp1[u]+dp2[u]);
}
}
三、一本通题目
行 知识点就这么多够了 来上题吧
书里的例题分为五个题型
1.
1.
1.由根分成左子树和右子树两部分的情况
2.
2.
2.背包类树形
D
P
DP
DP
3.
3.
3.求树的最长链问题
4.
4.
4.覆盖一棵树上所有边
5.
5.
5.覆盖一棵树上的所有点
就一个一个按顺序来了
题型 1. 1. 1.二叉苹果树
#include<bits/stdc++.h>
using namespace std;
#define N 110
#define reg register
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int n,q,u,v,w,cnt,a[N],head[N],ls[N],rs[N],dp[N][N];
struct node{
int to,nxt,val;
}edge[N<<1];
inline void addedge(int u, int v, int w){
edge[++cnt].to=v,edge[cnt].val=w,edge[cnt].nxt=head[u],head[u]=cnt;
}
inline void superadd(int u, int v, int w){
addedge(u,v,w),addedge(v,u,w);
}
void build(int u, int fa){
int g=0;
for(reg int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(v==fa)continue;
if(!g)ls[u]=v,a[v]=edge[i].val,g++;
else rs[u]=v,a[v]=edge[i].val;
build(v,u);
}
}
int dfs(int u, int num){
if(!num)return 0;
if(!ls[u]&&!rs[u])return a[u];
if(dp[u][num])return dp[u][num];
for(reg int k=0;k<num;k++)
dp[u][num]=max(dp[u][num],dfs(ls[u],k)+dfs(rs[u],num-k-1)+a[u]);
return dp[u][num];
}
int main(){
read(n),read(q);
for(reg int i=1;i<n;i++)read(u),read(v),read(w),superadd(u,v,w);
build(1,0);
printf("%d\n",dfs(1,q+1));
}
题型 2. 2. 2.选课
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:树
d
p
+
dp+
dp+背包
首先,肯定要从没有先修课的课开始选,所以要从跟
0
0
0相连的边开始选。
故直接将
0
0
0设为根就可以了,结点数变为
n
+
1
n+1
n+1
设
d
p
[
u
]
[
t
]
dp[u][t]
dp[u][t]表示在
x
x
x为根的子树中选
t
t
t门课能够获得的最高学分
这就转化成了一个背包问题
倒序枚举空间
(
(
(即选课总门数
)
)
) 正序枚举物品
(
(
(即传递给子树的选课门数
)
)
)
状态转移方程:
d
p
[
u
]
[
t
]
=
m
a
x
j
=
0
t
{
d
p
[
u
]
[
t
−
j
]
+
d
p
[
v
]
[
j
]
}
dp[u][t]=max_{j=0}^{t}\{dp[u][t-j]+dp[v][j]\}
dp[u][t]=maxj=0t{dp[u][t−j]+dp[v][j]}
上代码↓
#include<bits/stdc++.h>
using namespace std;
#define N 110
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int n,m,u,cnt,head[N],val[N],dp[N][N];
struct node{
int to,nxt;
}edge[N<<1];
inline void addedge(int u, int v){
edge[++cnt].to=v,edge[cnt].nxt=head[u],head[u]=cnt;
}
inline void superadd(int u, int v){
addedge(u,v),addedge(v,u);
}
void dfs(int u, int fa){
dp[u][1]=val[u];
for(int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(v==fa)continue;
dfs(v,u);
for(int t=m;t;t--)
for(int j=0;j<t;j++)
dp[u][t]=max(dp[u][t],dp[u][t-j]+dp[v][j]);
}
}
int main(){
read(n),read(m);
for(int i=1;i<=n;i++)
read(u),read(val[i]),superadd(u,i);
m++;
dfs(0,-1);
printf("%d\n",dp[0][m]);
}
题型 3. 3. 3.数字转换
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:把可以转化的数和约数和连上边,这样就组成了一棵树。题中要求的最大变换即为树的直径,就用到了上面树的直径的例题。
这题还要判定一个数只能转化为比他小的约数和,所以我们要判断一个边是否能连上,间接地遍历了边集,所以用树形
d
p
dp
dp求直径更为简洁
(
(
(也可以用两次
d
f
s
dfs
dfs,但是上面写了两次
d
f
s
dfs
dfs的题,这道就用树形
d
p
dp
dp叭
)
)
)
P
S
:
PS:
PS:如果懒得建图一定要注意遍历顺序是从大数到小数!
上代码↓
#include<bits/stdc++.h>
using namespace std;
#define reg register
#define N 50005
int n,ans,a[N],dis1[N],dis2[N];
void dp(){
for(reg int i=n;i;i--){
if(a[i]<i){
if(dis1[i]+1>dis1[a[i]])
dis2[a[i]]=dis1[a[i]],dis1[a[i]]=dis1[i]+1;
else if(dis1[i]+1>dis2[a[i]])dis2[a[i]]=dis1[i]+1;
}
}
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
for(int j=2;j<=n/i;j++)a[i*j]+=i;
}
dp();
for(reg int i=1;i<=n;i++)
if(dis1[i]+dis2[i]>ans)ans=dis1[i]+dis2[i];
cout<<ans<<endl;
}
题型 4. 4. 4.战略游戏
题面
不懂一本通上说的什么树的最大独立集,就把他改成覆盖树上所有点了…
S
o
l
u
t
i
o
n
:
Solution:
Solution:
设
d
p
[
u
]
[
0
]
dp[u][0]
dp[u][0]表示
u
u
u点不放士兵时,以
u
u
u为根的子树最少需要的士兵数
d
p
[
u
]
[
1
]
dp[u][1]
dp[u][1]表示
u
u
u点不放士兵时,以
u
u
u为根的子树最少需要的士兵数
那么当
u
u
u点不放士兵时,因为士兵要看到所有的路,所以所有
u
u
u的儿子结点都要放士兵
即
d
p
[
u
]
[
0
]
=
∑
v
∈
s
o
n
[
u
]
d
p
[
v
]
[
1
]
dp[u][0]=\sum\limits_{v∈son[u]}{dp[v][1]}
dp[u][0]=v∈son[u]∑dp[v][1]
当
u
u
u点放士兵时,
u
u
u的儿子结点放不放士兵都可以,那么加上
d
p
[
u
]
[
1
]
dp[u][1]
dp[u][1]和
d
p
[
u
]
[
0
]
dp[u][0]
dp[u][0]中最小的就可以了
即
d
p
[
u
]
[
1
]
=
∑
v
∈
s
o
n
[
u
]
m
i
n
{
d
p
[
v
]
[
0
]
,
d
p
[
v
]
[
1
]
}
dp[u][1]=\sum\limits_{v∈son[u]}{min\{dp[v][0],dp[v][1]\}}
dp[u][1]=v∈son[u]∑min{dp[v][0],dp[v][1]}
上代码↓
#include<bits/stdc++.h>
using namespace std;
#define N 1515
#define reg register
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int u,v,n,m,cnt,head[N],dp[N][2];
struct node{
int nxt,to;
}edge[N<<1];
inline void addedge(int u, int v){
edge[++cnt].to=v,edge[cnt].nxt=head[u],head[u]=cnt;
}
inline void superadd(int u, int v){
addedge(u,v),addedge(v,u);
}
void dfs(int u, int fa){
dp[u][1]=1;
for(int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(v==fa)continue;
dfs(v,u);
dp[u][0]+=dp[v][1];
dp[u][1]+=min(dp[v][0],dp[v][1]);
}
}
int main(){
read(n);
for(reg int i=1;i<=n;i++){
read(u),read(m);
for(reg int j=1;j<=m;j++)read(v),superadd(u,v);
}
dfs(0,-1);
printf("%d\n",min(dp[0][0],dp[0][1]));
}
题型 5. 5. 5.皇宫看守
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:
这道题和上道题的区别是,上道题是覆盖所有边,这道题是覆盖所有点。
覆盖所有边要用两个状态,而覆盖所有点需要用三个状态
①
①
①在父结点安排警卫
d
p
[
u
]
[
0
]
dp[u][0]
dp[u][0]表示
u
u
u结点在父亲结点可以被看到时,以
u
u
u为根的子树最少需要安排的士兵数
②
②
②在当前结点安排警卫
d
p
[
u
]
[
1
]
dp[u][1]
dp[u][1]表示
u
u
u结点安排警卫时,以
u
u
u为根的子树需要安排的最少士兵数
③
③
③在子结点安排警卫
d
p
[
u
]
[
2
]
dp[u][2]
dp[u][2]表示
u
u
u结点能被它的至少一个子节点看到时,以
u
u
u为根的子树最少需要安排的士兵数
对于
d
p
[
u
]
[
0
]
,
dp[u][0],
dp[u][0],表示
u
u
u结点能被父亲结点看到,这时
u
u
u结点不用安排警卫,子结点要么安排警卫,要么被它的子结点看到,所以有
d
p
[
u
]
[
0
]
=
∑
v
∈
s
o
n
[
u
]
m
i
n
{
d
p
[
v
]
[
1
]
,
d
p
[
v
]
[
2
]
}
dp[u][0]=\sum\limits_{v∈son[u]}{min\{dp[v][1],dp[v][2]\}}
dp[u][0]=v∈son[u]∑min{dp[v][1],dp[v][2]}
(
P
S
.
(PS.
(PS.一本通又双叒叕写错了…
)
)
)
对于
d
p
[
u
]
[
1
]
,
dp[u][1],
dp[u][1],表示
u
u
u结点安排了警卫,
u
u
u的儿子
v
v
v可以安排警卫,也可以被子结点看守,还可以被它的父结点
u
u
u看守,所以有
d
p
[
u
]
[
1
]
=
c
o
s
t
[
u
]
+
∑
v
∈
s
o
n
[
u
]
m
i
n
{
d
p
[
v
]
[
0
]
,
d
p
[
v
]
[
1
]
,
d
p
[
v
]
[
2
]
}
dp[u][1]=cost[u]+\sum\limits_{v∈son[u]}{min\{dp[v][0],dp[v][1],dp[v][2]\}}
dp[u][1]=cost[u]+v∈son[u]∑min{dp[v][0],dp[v][1],dp[v][2]}
P
S
.
PS.
PS.不要忘记加上自身的花费
c
o
s
t
[
u
]
cost[u]
cost[u]
对于
d
p
[
u
]
[
1
]
,
dp[u][1],
dp[u][1],表示
u
u
u能被至少一个子结点看到,此时
u
u
u结点没有被安排警卫,子结点需要安排警卫或被它的后代看到,所以我们要比较这两个方案哪个更优
但问题来了,如果所有的子结点都是被后代看到更优,那么就没有结点看到
u
u
u结点了,这就是为什么我加粗了至少一个,这时候我们就要判断用哪个子结点安排警卫最优
所以当所有的
d
p
[
v
]
[
1
]
>
d
p
[
v
]
[
2
]
dp[v][1]>dp[v][2]
dp[v][1]>dp[v][2]时,维护一个
r
=
m
i
n
v
∈
s
o
n
[
u
]
{
d
p
[
v
]
[
1
]
−
d
p
[
v
]
[
2
]
}
r=min_{v∈son[u]}\{dp[v][1]-dp[v][2]\}
r=minv∈son[u]{dp[v][1]−dp[v][2]},最后总结果加上
r
r
r的意义是使一个子结点安排警卫来看守
u
u
u结点
所以有
d
p
[
u
]
[
2
]
=
r
+
∑
v
∈
s
o
n
[
u
]
m
i
n
{
d
p
[
v
]
[
1
]
,
d
p
[
v
]
[
2
]
}
dp[u][2]=r+\sum\limits_{v∈son[u]}{min\{dp[v][1],dp[v][2]\}}
dp[u][2]=r+v∈son[u]∑min{dp[v][1],dp[v][2]}
其中
r
=
m
i
n
{
d
p
[
v
]
[
1
]
−
m
i
n
{
d
p
[
v
]
[
1
]
,
d
p
[
v
]
[
2
]
}
}
r=min\{dp[v][1]-min\{dp[v][1],dp[v][2]\}\}
r=min{dp[v][1]−min{dp[v][1],dp[v][2]}}
P
S
.
PS.
PS.这样写
r
r
r的意思是,如果子结点有安排警卫的,那么
r
=
0
r=0
r=0表示不用补子结点
如果都没有 那么
r
=
d
p
[
v
]
[
1
]
−
d
p
[
v
]
[
2
]
r=dp[v][1]-dp[v][2]
r=dp[v][1]−dp[v][2]表示让
v
v
v结点从被子结点看到改为自己安排警卫
我这块理解了很长时间 所以就多码点字 哈哈
上代码↓
#include<bits/stdc++.h>
using namespace std;
#define N 1515
#define reg register
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int n,m,u,v,cnt,head[N],dp[N][3],c[N];
struct node{
int to,nxt;
}edge[N<<1];
inline void addedge(int u, int v){
edge[++cnt].to=v,edge[cnt].nxt=head[u],head[u]=cnt;
}
inline void superadd(int u, int v){
addedge(u,v),addedge(v,u);
}
void dfs(int u, int fa){
int r=2e9;dp[u][1]=c[u];
for(reg int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(v==fa)continue;
dfs(v,u);
dp[u][0]+=min(dp[v][1],dp[v][2]);
dp[u][1]+=min(dp[v][0],min(dp[v][1],dp[v][2]));
dp[u][2]+=min(dp[v][1],dp[v][2]),r=min(r,dp[v][1]-min(dp[v][1],dp[v][2]));
}
dp[u][2]+=r;
}
int main(){
read(n);
for(reg int i=1;i<=n;i++){
read(u),read(c[u]),read(m);
for(reg int j=1;j<=m;j++)read(v),superadd(u,v);
}
dfs(1,0);
printf("%d\n",min(dp[1][1],dp[1][2]));
}
后面就是练习题了
加分二叉树
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:数据范围这么小忍不住用区间
d
p
dp
dp
#include<bits/stdc++.h>
using namespace std;
#define N 50
typedef long long ll;
int n,root[N][N];
ll dp[N][N],ans;
inline void read(ll &x){
ll s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
void print(int l, int r){
if(l>r)return ;
printf("%d ",root[l][r]);
if(l==r)return ;
print(l,root[l][r]-1),print(root[l][r]+1,r);
}
int main(){
cin>>n;
for(int i=1;i<=n;i++)read(dp[i][i]),root[i][i]=i;
for(int l=1;l<n;l++){
for(int i=1,j=l+1;j<=n;i++,j++){
dp[i][j]=dp[i+1][j]+dp[i][i];
root[i][j]=i;
for(int k=i+1;k<j;k++){
ll now=dp[i][k-1]*dp[k+1][j]+dp[k][k];
if(dp[i][j]<now)dp[i][j]=now,root[i][j]=k;
}
}
}
printf("%lld\n",dp[1][n]);
print(1,n);
}
旅游规划
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:这道题可能有多条直径,而且要求出所有在直径上的点,所以用树形
d
p
dp
dp求树的直径
还是往常的直径的长度
l
e
n
=
m
a
x
{
d
i
s
1
[
i
]
len=max\{dis1[i]
len=max{dis1[i]和
d
i
s
[
2
]
}
dis[2]\}
dis[2]}
但是问题来了,如果一个点
u
u
u在直径上,但他只有两条边,一条连向父亲
f
a
fa
fa,一条连向儿子
v
v
v,那么他的
d
i
s
2
dis2
dis2值会为
0
0
0,但实际上他也在直径上
这个问题的出现的原因是因为我们把
d
f
s
dfs
dfs设成单向,不让一个点去重复遍历他的父亲结点,因为他遍历不了父亲结点,所以才导致
d
i
s
2
dis2
dis2值为
0
0
0,故我们需要求出此节点在父亲结点方向链长的最大值
如何解决这个问题呢
我们设置两个数组
s
o
n
1
[
i
]
=
v
son1[i]=v
son1[i]=v表示
i
i
i点的
d
i
s
1
[
i
]
dis1[i]
dis1[i]值是从
v
v
v点转移的,
m
o
v
e
[
i
]
move[i]
move[i]表示
i
i
i点在父亲结点方向的链长最大值
分两种情况
1.
1.
1.如果
s
o
n
[
f
a
]
=
u
son[fa]=u
son[fa]=u:说明
u
u
u就在
f
a
fa
fa求得
d
i
s
1
dis1
dis1值的链中,转移
m
o
v
e
[
u
]
move[u]
move[u]不能从
d
i
s
1
dis1
dis1中转移。那么又有两种情况,
f
a
fa
fa有
d
i
s
2
dis2
dis2和没有
d
i
s
2
dis2
dis2,所以
m
o
v
e
[
u
]
=
m
a
x
{
d
i
s
2
[
f
a
]
,
m
o
v
e
[
f
a
]
}
move[u]=max\{dis2[fa],move[fa]\}
move[u]=max{dis2[fa],move[fa]}
2.
2.
2.如果
s
o
n
[
f
a
]
≠
u
son[fa]\neq u
son[fa]=u:这时候转移可以从
d
i
s
1
dis1
dis1转移了,所以
m
o
v
e
[
u
]
=
m
a
x
{
d
i
s
1
[
f
a
]
,
m
o
a
v
[
f
a
]
}
+
1
move[u]=max\{dis1[fa],moav[fa]\}+1
move[u]=max{dis1[fa],moav[fa]}+1
最后判断
m
[
i
]
+
d
i
s
1
[
i
]
=
l
e
n
m[i]+dis1[i]=len
m[i]+dis1[i]=len或
d
i
s
1
[
i
]
+
d
i
s
2
[
i
]
=
l
e
n
dis1[i]+dis2[i]=len
dis1[i]+dis2[i]=len时说明
i
i
i在直径上
上代码
#include<bits/stdc++.h>
using namespace std;
#define N 200020
#define reg register
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int n,u,v,ans,cnt,head[N],dis1[N],dis2[N],son1[N],m[N];
struct node{
int to,nxt;
}edge[N<<1];
inline void addedge(int u, int v){
edge[++cnt].to=v,edge[cnt].nxt=head[u],head[u]=cnt;
}
inline void superadd(int u, int v){
addedge(u,v),addedge(v,u);
}
void dfs(int u, int fa){
for(reg int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(v==fa)continue;
dfs(v,u);
if(dis1[u]<dis1[v]+1)dis2[u]=dis1[u],dis1[u]=dis1[v]+1,son1[u]=v;
else if(dis2[u]<dis1[v]+1)dis2[u]=dis1[v]+1;
}
ans=max(ans,dis1[u]+dis2[u]);
}
void df5(int u, int fa){
if(u!=1){
if(son1[fa]==u)m[u]=max(dis2[fa],m[fa])+1;
else m[u]=max(dis1[fa],m[fa])+1;
}
for(reg int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(v!=fa)df5(v,u);
}
}
int main(){
read(n);
for(reg int i=1;i<n;i++)read(u),read(v),superadd(++u,++v);
dfs(1,0);df5(1,0);
for(reg int i=1;i<=n;i++)if(m[i]+dis1[i]==ans||dis1[i]+dis2[i]==ans)printf("%d\n",i-1);
}
周年纪念晚会
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:和没有上司的舞会一样
d
p
[
u
]
[
0
]
dp[u][0]
dp[u][0]表示
u
u
u不去,
d
p
[
u
]
[
1
]
dp[u][1]
dp[u][1]表示
u
u
u去
那么
d
p
[
u
]
[
0
]
=
∑
v
∈
s
o
n
[
u
]
m
i
n
{
d
p
[
v
]
[
0
]
,
d
p
[
v
]
[
1
]
}
dp[u][0]=\sum\limits_{v∈son[u]}min\{dp[v][0],dp[v][1]\}
dp[u][0]=v∈son[u]∑min{dp[v][0],dp[v][1]}
d
p
[
u
]
[
1
]
=
∑
v
∈
s
o
n
[
u
]
d
p
[
v
]
[
0
]
dp[u][1]=\sum\limits_{v∈son[u]}dp[v][0]
dp[u][1]=v∈son[u]∑dp[v][0]
代码↓
#include<bits/stdc++.h>
using namespace std;
#define N 6060
#define reg register
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int n,u,v,cnt,head[N],dp[N][2],a[N];
struct node{
int to,nxt;
}edge[N<<1];
inline void addedge(int u, int v){
edge[++cnt].to=v,edge[cnt].nxt=head[u],head[u]=cnt;
}
inline void superadd(int u, int v){
addedge(u,v),addedge(v,u);
}
void dfs(int u, int fa){
dp[u][1]=a[u];
for(reg int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(v==fa)continue;
dfs(v,u);
dp[u][0]+=max(dp[v][0]+dp[v][1]);
dp[u][1]+=dp[v][0];
}
}
int main(){
read(n);
for(reg int i=1;i<=n;i++)read(a[i]);
while(1){
read(u),read(v);
if(u==0&&v==0)break;
superadd(u,v);
}
dfs(1,0);
printf("%d\n",max(dp[1][0],dp[1][1]));
}
叶子的颜色 ( C Q O I 2009 (CQOI2009 (CQOI2009叶子的染色 ) ) )
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:首先一个结点有三种状态:
1.
1.
1.黑色
2.
2.
2.白色
3.
3.
3.无色
我们可以发现,叶子结点的染色方案只与上一个有色结点有关
这句话有两个意思
1.
1.
1.无色结点与答案无关,因为它做不出任何贡献
2.
2.
2.根结点与答案无关,因为答案只与叶子结点的上一个有色结点有关
设
d
p
[
u
]
[
0
]
dp[u][0]
dp[u][0]为
u
u
u结点染成黑色时,以
u
u
u为根的子树需要的最少代价
d
p
[
u
]
[
1
]
dp[u][1]
dp[u][1]为
u
u
u结点染成白色时,以
u
u
u为根的子树需要的最少代价
那么
d
p
[
u
]
[
0
]
=
1
+
∑
v
∈
s
o
n
[
u
]
m
i
n
{
d
p
[
v
]
[
0
]
−
1
,
d
p
[
v
]
[
1
]
}
dp[u][0]=1+\sum\limits_{v∈son[u]}min\{dp[v][0]-1,dp[v][1]\}
dp[u][0]=1+v∈son[u]∑min{dp[v][0]−1,dp[v][1]}
d
p
[
u
]
[
1
]
=
1
+
∑
v
∈
s
o
n
m
i
n
{
d
p
[
v
]
[
0
]
,
d
p
[
v
]
[
1
]
−
1
}
dp[u][1]=1+\sum\limits_{v∈son}min\{dp[v][0],dp[v][1]-1\}
dp[u][1]=1+v∈son∑min{dp[v][0],dp[v][1]−1}
初始条件
d
p
[
u
]
[
c
[
u
]
]
=
1
,
d
p
[
u
]
[
!
c
[
u
]
]
=
i
n
f
u
∈
[
1
,
n
]
dp[u][c[u]]=1,dp[u][!c[u]]=inf\,\,\,\,\,\,\,\,\,\,\,\,\,u∈[1,n]
dp[u][c[u]]=1,dp[u][!c[u]]=infu∈[1,n]
d
p
[
u
]
[
0
]
=
1
,
d
p
[
u
]
[
1
]
=
1
u
∈
[
n
+
1
,
m
]
dp[u][0]=1,dp[u][1]=1\,\,\,\,\,\,\,\,\,\,\,\,\,u∈[n+1,m]
dp[u][0]=1,dp[u][1]=1u∈[n+1,m]
上代码↓
#include<bits/stdc++.h>
using namespace std;
#define N 10010
#define reg register
#define inf 2000000000
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int n,m,u,v,cnt,head[N],dp[N][2],c[N];
struct node{
int to,nxt;
}edge[N<<1];
inline void addedge(int u, int v){
edge[++cnt].to=v,edge[cnt].nxt=head[u],head[u]=cnt;
}
inline void superadd(int u, int v){
addedge(u,v),addedge(v,u);
}
void dfs(int u, int fa){
for(reg int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(v==fa)continue;
dfs(v,u);
dp[u][0]+=min(dp[v][0]-1,dp[v][1]);
dp[u][1]+=min(dp[v][0],dp[v][1]-1);
}
}
int main(){
read(m),read(n);
for(reg int i=1;i<=n;i++)read(c[i]),dp[i][c[i]]=1,dp[i][!c[i]]=inf;
for(reg int i=1;i<m;i++)read(u),read(v),superadd(u,v);
for(reg int i=1+n;i<=m;i++)dp[i][0]=dp[i][1]=1;
dfs(n+1,0);
printf("%d\n",min(dp[n+1][0],dp[n+1][1]));
}
骑士
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:模型是没有上司的舞会的模型,只不过把每个骑士和他烦的骑士连上边,是颗有
n
n
n个结点
n
n
n条边的基环树
那么我们找到环,把环中的一条边断开,那么就变成了一棵树,再对断开的两个结点分别做根进行树
d
p
dp
dp。因为断开的两个结点也是有条件约束的,就强制在一个结点遍历时不遍历另一个结点。
树中会有很多联通块,对每个联通块都进行一次找环的操作,所有答案加起来就是最后的答案
实现时要注意:
1.
1.
1.边要连单向边,并存一个
f
f
f数组指向边的反方向,来存储上一个结点的信息
2.
2.
2.环只有一个,联通块不是一个,要对每个联通块都求一遍环
3.
a
n
s
3.ans
3.ans要开
l
o
n
g
l
o
n
g
long\,\,\,\,long
longlong
上代码↓
#include<bits/stdc++.h>
using namespace std;
#define reg register
#define N 1000100
typedef long long ll;
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
ll ans,dp[N][2];
bool vis[N];
int n,u,v,rt,cnt,head[N],c[N],f[N];
struct node{
int to,nxt;
}edge[N];
inline void addedge(int u, int v){
edge[++cnt].to=v,edge[cnt].nxt=head[u],head[u]=cnt;
}
void dfs(int u){
vis[u]=1;
dp[u][0]=0,dp[u][1]=c[u];
for(reg int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(v==rt)continue;
dfs(v);
dp[u][0]+=max(dp[v][1],dp[v][0]);
dp[u][1]+=dp[v][0];
}
}
inline void judge(int x){
rt=x,vis[x]=1;
while(!vis[f[rt]])rt=f[rt],vis[rt]=true;
dfs(rt);
ll p=dp[rt][0];
vis[rt]=true,rt=f[rt];
dfs(rt);
ll q=dp[rt][0];
ans+=max(p,q);
}
int main(){
read(n);
for(reg int i=1;i<=n;i++)read(c[i]),read(v),addedge(v,i),f[i]=v;
for(reg int i=1;i<=n;i++)if(!vis[i])judge(i);
printf("%lld\n",ans);
}
四、总结
讲个故事
C
S
P
2019
CSP2019
CSP2019前在家跟同学语音刷题,突然发现好多蓝树
d
p
dp
dp题都变成了绿色,我就跟同学提了嘴是不是出题组为了出树
d
p
dp
dp才把标签弄简单的,结果没想到三个树
d
p
dp
dp
总的来说,树形
d
p
dp
dp是一个能解决许多树上问题的一个优秀高效算法,在联赛中也屡见不鲜。
本人
Q
Q
:
407694747
QQ:407694747
QQ:407694747,欢迎各位大佬一起来讨论