树形DP
树形dp
summary
树形dp的主要实现形式是dfs,在dfs中dp,主要的实现形式是\(dp[i] [j] [0/1]\),i 是以 i 为根的子树,j 是表示在以 i 为根的子树中选择 j 个子节点,0表示这个节点不选,1表示选择这个节点。有的时候 j 或0/1这一维可以压掉
选择节点/边类
没有上司的舞会
三种状态
1、上司去,而剩下的这个上司的职员就不会去。
2、不去,而那个职员去
3、上司不去,而那个职员也不去。
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
const int N =6010;
int n,st,r[N],f[N][2];//0不 1 go
vector< int >g[N];
bool vis[N];
void dfs(int x,int fa){
f[x][0]=0;f[x][1]=r[x];
int siz=g[x].size();
for(int i=0;i<siz;i++){
int y=g[x][i];
if(y==fa)continue;
dfs(y,x);
f[x][0]+=max(f[y][0],f[y][1]);
f[x][1]+=f[y][0];
}
}
int main(){
n=read();
for(int i=1;i<=n;i++)r[i]=read();
for(int i=1,x,y;i<n;i++){
x=read();
y=read();
vis[x]=1;
g[y].push_back(x);
}
for(int i=1;i<=n;i++){
if(!vis[i]){
st=i,dfs(i,0);break;
}
}
printf("%d\n",max(f[st][0],f[st][1]));
return 0;
}
最大子树和
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
#define N 16050
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
int f[N];
int n,ans;
vector< int >g[N];
void dfs(int x,int fa){
for(int i=0;i<g[x].size();i++){
int y=g[x][i];
if(y==fa)continue;
dfs(y,x);
f[x]+=max(f[y],0);
}
ans=max(ans,f[x]);
}
int main(){
n=read();
for(int i=1;i<=n;i++)f[i]=read();
for(int i=1,x,y;i<n;i++){
x=read();y=read();
g[x].push_back(y);
g[y].push_back(x);
}
dfs(1,0);
printf("%d\n",ans);
return 0;
}
战略游戏
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
const int N = 2000;
int n,ans,f[N][2];
vector< int >g[N];
bool vis[N];
void dfs(int x,int fa){
f[x][1]=1;f[x][0]=0;
for(int i=0;i<g[x].size();i++){
int y=g[x][i];
if(y==fa)continue;
dfs(y,x);
f[x][0]+=f[y][1];
f[x][1]+=min(f[y][1],f[y][0]);
}
}
int main(){
n=read();
for(int i=1,x,y,num;i<=n;i++){
x=read();num=read();
while(num--){
y=read();
g[x].push_back(y);
g[y].push_back(x);
}
}
dfs(0,-1);
printf("%d",min(f[0][0],f[0][1]));
return 0;
}
消防局的设立
贪心
树上的最小点覆盖
按照反方向的bfs序(从叶子到根)来进行贪心.每检查一个结点,覆盖他的爷爷,然后对爷爷的儿孙打标记
dfs序
2 4 5 7 6 8 3
bfs序:是一种层次遍历算法
2 3 4 5 6 7 8
用queue维护bfs序的同时维护一个栈——得到反向bfs序
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<vector>
#include<stack>
#define N 10005
using namespace std;
int n,m;
int fa[N],cnt,ans=0,dad;
bool vis[N];
vector< int > g[N];
queue <int> q;
stack <int> s;
void dfs(int x,int dis){
if (dis>2) return;
vis[x]=1;
for(int i=0;i<g[x].size();i++)
dfs(g[x][i],dis+1);
}
int main(){
scanf("%d",&n);
for(int i=2,x;i<=n;i++){
scanf("%d",&fa[i]);
g[i].push_back(fa[i]);
g[fa[i]].push_back(i);
}
q.push(1);
s.push(1);
while (!q.empty()){
int x=q.front();
q.pop();
for (int i=0;i<g[x].size();i++) {
int y=g[x][i];
if (y==fa[x]) continue;
q.push(y);
s.push(y);
}
}
while(!s.empty()){
int x=s.top();s.pop();
if(!vis[x]){
++ans;
dfs(fa[fa[x]],0);
}
}
printf("%d",ans);
return 0;
}
保安站岗
(以下全部都是对于要覆盖任意一个以x为根的子树来说的)
(其中我们设y节点为y的儿子,fa为x的父亲)
1.x节点被自己覆盖,即选择x点来覆盖x点—— f[x] [0]
2.x节点被儿子y覆盖,即选择y点来覆盖x点 —— f[x] [1]
3.x节点被父亲fa覆盖,即选择fa点来覆盖x点 —— f[x] [2]
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
const int N = 3055;
int n,m,w[N],f[N][3],add=0x3f3f3f3f;
vector<int >g[N];
inline void dfs(int x,int from){
int sz=g[x].size();
bool add=true;
int minn=0x3f3f3f3f;
for(int i=0;i<sz;++i){
int y=g[x][i];
if(y==from)continue;
dfs(y,x);
int t=min(f[y][1],f[y][0]);
f[x][0]+=min(t,f[y][2]);
f[x][2]+=t;
f[x][1]+=t;
minn=min(minn,f[y][0]-f[y][1]);
if(f[y][0]<=f[y][1])add=false;
}
f[x][0] += w[x];
if(add) f[x][1] += minn;
}
int main(){
n=read();
for(int i=1,x,y;i<=n;i++){
x=read();w[x]=read();m=read();
while(m--){
y=read();
g[x].push_back(y);
g[y].push_back(x);
}
}
dfs(1,0);
printf("%d\n",min(f[1][0],f[1][1]));
return 0;
}
联合权值
树形dp
距离差2的话,要么是祖父和儿子,要么是两个儿子之间
维护联合权值最大值只需记录每个点儿子的最大值,次大值
儿子之间联合权值之和等于权值和的平方减去权值的平方和( son中w总和,平方一下,再减去son[i]与son[i](自己配自己)这样不合法的情况即可 )
儿子和祖父就直接 w相乘再乘2
#include <vector>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N=200005;
const int md=10007;
inline int read() {
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return f*x;
}
typedef long long ll;
inline void plu(ll &x,ll y){x+=y,x>=md&&(x-=md);}
ll n,w[N];
ll ans1,ans2;
int hd[N],to[N<<1],nxt[N<<1],tot;
inline void add(int x,int y) {
to[++tot]=y;nxt[tot]=hd[x];hd[x]=tot;
}
void dfs(int x,int fa,int g) {
ll fir=0,sec=0,sum1=0,sum2=0;//1是和的平方,2平方的和
for(int i=hd[x];i;i=nxt[i]) {
int y=to[i];
if(y==fa) continue;
plu(sum1,w[y]);
plu(sum2,w[y]*w[y]%md);
if(w[y]>fir)
sec=fir,fir=w[y];
else if(w[y]>sec)
sec=w[y];
dfs(y,x,fa);
}
ans1=max(ans1,max(fir*sec,w[g]*w[x]));
plu(ans2,((sum1*sum1%md-sum2+md)%md+(w[x]*w[g]*2)%md)%md);
}
int main() {
n=read();
int x,y;
for(int i=1;i<n;i++) {
x=read();y=read();
add(x,y),add(y,x);
}
for(int i=1;i<=n;i++) w[i]=read();
dfs(1,0,0);
printf("%lld %lld\n",ans1,ans2);
return 0;
}
CF633F The Chocolate Spree
https://www.cnblogs.com/zwfymqz/p/9759346.html
#include <vector>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define int long long
inline int read() {
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
const int N=100006;
inline void Max(int &x,int y) {if(x<y)x=y;}
int n,ans,w[N];
vector<int> G[N];
inline void add(int x,int y) {
G[x].push_back(y);G[y].push_back(x);
}
int f[N][2],g[N],d[N],h[N];
//f[x][0]以x为根的子树选两条,1选1条
void dfs(int x,int fa) {
f[x][0]=f[x][1]=g[x]=d[x]=w[x];
for(auto y:G[x]) {
if(y==fa) continue;
dfs(y,x);
Max(f[x][0],f[y][0]);
Max(f[x][0],f[y][1]+f[x][1]);
Max(f[x][0],d[y]+g[x]);
Max(f[x][0],d[x]+g[y]);
Max(f[x][1],f[y][1]);
Max(f[x][1],d[y]+d[x]);
Max(g[x],g[y]+w[x]);
Max(g[x],d[x]+f[y][1]);
Max(g[x],d[y]+w[x]+h[x]);
Max(h[x],f[y][1]);
Max(d[x],w[x]+d[y]);
}
}
signed main() {
n=read();
for(int i=1;i<=n;i++) w[i]=read();
for(int i=1;i<n;i++)
add(read(),read());
dfs(1,0);
printf("%lld\n",f[1][0]);
return 0;
}
树形背包类
选课
类似于有依赖的背包
设f(i)(j)表示在以 i 为根的子树中,选择 j 个点并且一定选择 i 的最大价值。
这里以0节点为开始的根
把选课前需要学的课与其连边(若没有要学的和0连边)
很明显,f(i)(1)就是其自己学分
转移类似01背包(倒序)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#define N 1005
using namespace std;
int n,m,f[N][N];
vector<int>g[N];
int maxx(int x,int y){
return x>y?x:y;
}
void dfs(int x){
int siz=g[x].size();
for(int i=0;i<siz;i++){
int y=g[x][i];dfs(y);
for(int j=m+1;j>=1;j--)
for(int k=0;k<j;k++)
f[x][j]=maxx(f[x][j],f[y][k]+f[x][j-k]);
}
}
int main(){
scanf("%d%d",&n,&m) ;
for(int i=1,x;i<=n;i++){
scanf("%d",&x);
scanf("%d",&f[i][1]);
g[x].push_back(i);
}
dfs(0);
printf("%d\n",f[0][m+1]);
return 0;
}
之后模拟赛的t4用另解的思想可过,第一个会TLE
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N=1005;
inline int read() {
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return f*x;
}
int n,m;
int f[N][N];
int to[N<<1],nxt[N<<1],hd[N],tot,fa[N];
inline void add(int x,int y) {
to[++tot]=y;nxt[tot]=hd[x];hd[x]=tot;
}
int s[N],rt;
int dfn_cnt,rev[N],siz[N];//反dfn序
int dfs(int x) {
int size=1;
for(int i=hd[x];i;i=nxt[i])
size+=dfs(to[i]);
rev[++dfn_cnt]=x;siz[dfn_cnt]=size;
return size;
}
int main() {
n=read();m=read();
for(int i=1;i<=n;i++) {
int k=read();s[i]=read();
if(k) add(k,i);
else add(rt,i);
}
for(int i=hd[rt];i;i=nxt[i])
dfs(to[i]);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
f[i][j]=max(f[i-siz[i]][j],f[i-1][j-1]+s[rev[i]]);
printf("%d\n",f[n][m]);
return 0;
}
二叉苹果树
一棵二叉树,边上有苹果,给定需要保留的边数量,求出最多能留住多少苹果。
维护子树大小siz[ ]
为什么是f[u] [i-j-1]而不是f[u] [i-j]
为前文提到了,保留一条边必须保留从根节点到这条边路径上的所有边,那么如果你想从u的子节点v的子树上留边的话,也要留下u,v之间的连边
那么取值范围k为什么要小于等于i-1而不是i呢?
同上,因为要保留u,v连边
对了,别忘了i,j要倒序枚举因为这是01背包
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<utility>
using namespace std;
#define N 105
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
int f[N][N],siz[N];
int n,m;
vector< pair<int,int> >g[N];
void dfs(int x,int fa){
for(int i=0;i<g[x].size();i++){
int y=g[x][i].first;
if(y==fa)continue;
dfs(y,x);
siz[x]+=siz[y]+1;
for(int j=min(siz[x],m);j>0;j--)
for(int k=min(j-1,siz[y]);k>=0;k--)
f[x][j]=max(f[x][j],f[x][j-k-1]+f[y][k]+g[x][i].second);
}
}
int main(){
n=read();m=read();
for(int i=1,x,y,z;i<n;i++){
x=read();y=read();z=read();
g[x].push_back(make_pair(y,z));
g[y].push_back(make_pair(x,z));
}
dfs(1,-1);
printf("%d\n",f[1][m]);
return 0;
}
时态同步
模拟:mx【x】 以x为根的到终点的路径长,然后ans统计做差就好
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
typedef long long LL;
const int N = 1000005;
LL ans,mx[N];
int n,s;
vector< pair<int,LL> >g[N];
bool vis[N];
void dfs(int x,int fa){
for(int i=0;i<g[x].size();i++){
int y=g[x][i].first;
if(y!=fa){
dfs(y,x);
mx[x]=max(mx[x],mx[y]+g[x][i].second);
}
}
for(int i=0;i<g[x].size();i++)
if(g[x][i].first!=fa)ans+=mx[x]-(g[x][i].second+mx[g[x][i].first]);
}
int main(){
n=read();s=read();
for(int i=1,x,y,z;i<n;i++){
x=read();y=read();z=read();
g[x].push_back(make_pair(y,z));
g[y].push_back(make_pair(x,z));
}
dfs(s,0);
printf("%lld\n",ans);
return 0;
}
[HAOI2010]软件安装
#include <iostream>
#include <cstdio>
#include <cctype>
#include <vector>
using namespace std;
const int N=105;
const int M=505;
inline int read() {
int x=0;int f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
int n,m;
int st[N],tp,dfn[N],low[N],dfn_cnt,col[N],co_cnt;
bool vis[N];
int sw[N],w[N],sv[N],v[N],in[N],d[N];
vector<int> G[N];
void tarjan(int x) {
vis[x]=1;
st[++tp]=x;
dfn[x]=low[x]=++dfn_cnt;
for(auto y:G[x]) {
if(!dfn[y]) {
tarjan(y);
low[x]=min(low[x],low[y]);
} else if(vis[y])
low[x]=min(low[x],dfn[y]);
}
if(dfn[x]==low[x]) {
co_cnt++;
while(st[tp+1]!=x) {
col[st[tp]]=co_cnt;
sw[co_cnt]+=w[st[tp]];
sv[co_cnt]+=v[st[tp]];
vis[st[tp--]]=0;
}
}
}
int hd[N],to[N*N],nxt[N*N],tot;
inline void add(int x,int y) {
to[++tot]=y;nxt[tot]=hd[x];hd[x]=tot;
}
int f[N][M];
void dfs(int x) {
for(int i=sw[x];i<=m;i++)
f[x][i]=sv[x];
for(int i=hd[x];i;i=nxt[i]) {
dfs(to[i]);
for(int j=m;j>=sw[x];j--)
for(int k=0;k<=j-sw[x];k++)
f[x][j]=max(f[x][j],f[x][j-k]+f[to[i]][k]);
}
}
int main()
{
n=read(),m=read();
for(int i=1;i<=n;i++)
w[i]=read();
for(int i=1;i<=n;i++)
v[i]=read();
for(int i=1;i<=n;i++) {
d[i]=read();
if(d[i]) G[d[i]].push_back(i);
}
for(int i=1;i<=n;i++)
if(!dfn[i])
tarjan(i);
for(int i=1;i<=n;i++)
if(d[i]&&col[d[i]]!=col[i])
add(col[d[i]],col[i]),in[col[i]]++;
for(int i=1;i<=co_cnt;i++)
if(!in[i]) add(n+1,i);
dfs(n+1);
printf("%d\n",f[n+1][m]);
return 0;
}
题:
https://www.luogu.com.cn/problem/P1272
https://www.luogu.com.cn/blog/nofind/p3761-tjoi2017-cheng-shi-shu-xing-dp
https://www.luogu.com.cn/problem/P3647