DP on tree

树形DP

本质上是思想,主要原理是把dp放到树上。解决的是基于树上的问题,也有时候会转化原本的方程到树上解决。

特征:考虑一些结点选取时的父子关系。如选择一个结点必须选其父亲,或者一个点和其相邻的点不能同时选。

两种模型:

  • 选择节点类: f i , 0 = f j , 1 , f i , 1 = max ⁡ { f j , 0 , f j , 1 } f_{i,0}=f_{j,1},f_{i,1}=\max\{f_{j,0},f_{j,1}\} fi,0=fj,1,fi,1=max{fj,0,fj,1},主要处理树上某个节点是否选。
  • 树形背包类: f v , k = f u , k + v a l , f u , k = max ⁡ { f v , k , f v , k − 1 } f_{v,k}=f_{u,k}+val,f_{u,k}=\max\{f_{v,k},f_{v,k-1}\} fv,k=fu,k+val,fu,k=max{fv,k,fv,k1}

例题:树上最小点覆盖

给定一颗 n n n 个节点的树 T T T,要求选择一个点的集合 S S S,使得树上的每一条边都与其中至少一个点相连,同时尽量让集合大小最小。 n ≤ 1 0 6 n\le 10^6 n106

(用点覆盖边)

f i , 0 / 1 f_{i,0/1} fi,0/1 表示 i i i 子树在 i i i 点选/不选的情形下,至少要选多少结点。则有

f u , 1 = 1 + ∑ v ∈ s o n ( u ) min ⁡ { f v , 0 , f v , 1 } f_{u,1}=1+\sum\limits_{v\in son(u)}\min\{f_{v,0},f_{v,1}\} fu,1=1+vson(u)min{fv,0,fv,1}

f u , 0 = ∑ v ∈ s o n ( u ) f v , 1 f_{u,0}=\sum\limits_{v\in son(u)}f_{v,1} fu,0=vson(u)fv,1

树形背包类DP

在树上选一个包含根的连通块,或背包存在依赖关系(选父才能选子),或需要知道每个点的子树中选了多少。

  • 类似选择节点类DP,在 DFS 时合并状态。叫做基于 DFS 合并
  • 将树转为 DFS 序,随后在序列上做操作,称为 DFS 序合并。会丢失一些树上信息,最经典的情况是选择一个点父亲必须选。

P2014 选课

有依赖关系,最多选 m m m 个,最大化收获。

f u , i , j f_{u,i,j} fu,i,j 表示在以 u u u 为根的子树中,已经遍历了前 i i i 棵树,选了 j j j 门课程的最大学分。转移和普通背包类似,枚举 u u u 的子节点 v v v,同时枚举以 v v v 为根的子树选了几门课。则有转移

f u , i , j = max ⁡ k ≤ min ⁡ { j , s z v } { f u , i − 1 , j − k + f v , s v , k } f_{u,i,j}=\max\limits_{k\le\min\{j,sz_v\}}\{f_{u,i-1,j-k}+f_{v,s_v,k}\} fu,i,j=kmin{j,szv}max{fu,i1,jk+fv,sv,k}

发现,可以用滚动数组。

具体的把 i i i 这维度滚动掉即可。注意初始化 f u , 1 = a u f_{u,1}=a_u fu,1=au

d p u , i dp_{u,i} dpu,i 表示在 u u u 子树里选了 i i i 节课,最大的收益。

存在转移:

∀ v ∈ s o n ( u ) , ∀ i ∈ [ m ∼ 1 ] , d p u , i ← max ⁡ j ∈ [ 0 , i ) { d p u , i − j + d p v , j } \forall v\in son(u),\forall i\in[m\sim1],dp_{u,i}\gets\max\limits_{j\in[0,i)}\{dp_{u,i-j}+dp_{v,j}\} vson(u),i[m1],dpu,ij[0,i)max{dpu,ij+dpv,j}

Cell Phone Network G

用点覆盖周围所有点。

f i , 0 f_{i,0} fi,0 表示选点 i i i,且以 i i i 为根的子树每个点都被覆盖的最少选点数。

f i , 1 f_{i,1} fi,1 表示不选点 i i i,且以 i i i 被儿子覆盖的。

f i , 2 f_{i,2} fi,2 表示不选 i i i i i i 未被儿子覆盖

#include<bits/stdc++.h>
using namespace std;
#define pb push_back
int n,a,b,f[10001][3];
vector<int>G[10001];
void dfs(int u,int fa){
  bool t=1;
  int p=0x3f3f3f3f;
  f[u][0]=1,f[u][1]=f[u][2]=0;
  for(int v:G[u]) if(v!=fa){
    dfs(v,u);
    f[u][0]+=min({f[v][0],f[v][1],f[v][2]});
    f[u][1]+=min({f[v][0],f[v][2]});
    if(f[v][0]<=f[v][2]) f[u][2]+=f[v][0],t=0;
    else f[u][2]+=f[v][2],p=min(p,f[v][0]-f[v][2]);
  } f[u][2]+=p*t;
}
main(){
  cin>>n;
  for(int i=1;i<n;++i)
    cin>>a>>b,G[a].pb(b),G[b].pb(a);
  dfs(1,0),cout<<min(f[1][0],f[1][2]);
}
         

树上背包,时间 O ( n m ) O(nm) O(nm),空间 O ( n × d e p ) O(n\times dep) O(n×dep)

Road Improvement

从首都到任意叶子都最多有一条不好道路,问对于每个首都的翻新方案数。(所有道路最初都是不好的)

显然的换根DP。第一遍,设 f u f_u fu 表示 u u u 子树的方案数。则 f 叶子 = 1 f_{叶子}=1 f叶子=1 f u = ∏ v ∈ s o n ( u ) ( f v + 1 ) f_u=\prod\limits_{v\in son(u)}(f_v+1) fu=vson(u)(fv+1)

#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define siz int(G[u].size())
const int p=1e9+7,N=2e5+1;
vector<int>G[N];
int n,f[N],a[N];
void pre(int u,int fa){
  f[u]=1; 
  for(int v:G[u]) if(v!=fa) 
    pre(v,u),f[u]=1ll*f[u]*(f[v]+1)%p;
}
void dfs(int u,int fa){
  a[u]=f[u];
  vector<int> Pre={1},Suf;
  for(int v:G[u]) 
    Pre.pb(f[v]+1),Suf.pb(f[v]+1);
  Suf.pb(1);
  for(int i=1;i<Pre.size();++i) 
    Pre[i]=1ll*Pre[i-1]*Pre[i]%p;
  for(int i=Suf.size()-2;~i;--i) 
    Suf[i]=1ll*Suf[i+1]*Suf[i]%p;
  for(int i=0;i<G[u].size();++i){
    int v=G[u][i]; 
    if(v!=fa){
      f[u]=1ll*Pre[i]%p*Suf[i+1]%p; //把 v 子树去掉
      //f[u]=1ll*f[u]*inv(f[v]+1)%p;
      f[v]=1ll*f[v]*(f[u]+1)%p; // u 作为 v 的儿子
      dfs(v,u);
    }                              
  }
}
main(){
  cin.tie(0)->sync_with_stdio(0);
  cin>>n;
  for(int i=2,x;i<=n;++i)
    cin>>x,G[i].pb(x),G[x].pb(i);
  pre(1,0),dfs(1,0);
  for(int i=1;i<=n;++i)
    cout<<a[i]<<" \n"[i==n];  
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值