树形|换根 DP总结

树形|换根 DP总结

树形dp

树形 DP,即在树上进行的 DP。由于树固有的递归性质,树形 DP 一般都是递归进行的。

基础

树形 DP 的一般过程。

没有上司的舞会

某大学有 n n n 个职员,编号为 1 − N 1 - N 1N。他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 a i a_i ai,但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。

思路

我们设 f ( i , 0 / 1 ) f(i,0/1) f(i,0/1) 代表以 i i i 为根的子树的最优解(第二维的值 0 0 0 代表 i i i 不参加舞会的情况, 1 1 1 代表 i i i 参加舞会的情况)。
对于每个状态,都存在两种决策(其中下面的 x x x 都是 i i i 的儿子):

  1. 上司参加舞会,下属不参加
  2. 上司不参加舞会,下属可参加或不参加

根据两种状态写出dp递推式
F ( i , 1 ) = Σ F ( x , 0 ) + a i F(i,1)=\Sigma F(x,0)+a_i F(i,1)=ΣF(x,0)+ai
F ( i , 0 ) = Σ m a x ( F ( x , 1 ) , F ( x , 0 ) ) F(i,0)=\Sigma max(F(x,1),F(x,0)) F(i,0)=Σmax(F(x,1),F(x,0))

代码

树上背包

树上的背包问题,背包与树形dp结合

选课

现在有 n n n 门课程,第 i i i 门课程的学分为 a i a_i ai,每门课程有零门或一门先修课,有先修课的课程需要先学完其先修课,才能学习该课程。
一位学生要学习 m m m 门课程,求其能获得的最多学分数。
每门课最多只有一门先选课的特点,与有根树中一个点最多只有一个父亲节点的特性类似。
因此可以想到根据这一性质建树,从而所有的课程组成了一个森林结构。
新增一门0学分的课程,作为无前提课程的前提课程,整个森林变为以0为根的树。

f ( u , i , j ) f(u,i,j) f(u,i,j) 表示以 u u u 号点为根的子树中,已经遍历了 u u u 号点的前 i i i 棵子树,选了 j j j 门课程的最大学分。

转移的过程结合了树形 DP 和 背包 DP 的特点,我们枚举 u u u 点的每个子结点 v v v,同时枚举以 v v v 为根的子树选了几门课程,将子树的结果合并到 u u u 上。
x x x 为根的子树大小为 s i z x siz_x sizx 可以记因此有转移方程:
F ( u , i , j ) = m a x ( F ( u , i − 1 , j − k ) + F ( v , S v , k ) ) F(u,i,j)=max(F(u,i-1,j-k)+F(v,S_v,k)) F(u,i,j)=max(F(u,i1,jk)+F(v,Sv,k))
第二维可以滚动数组优化掉,倒序枚举j。
复杂度为O(nm)

代码

#include 
#include 
#include 
using namespace std;
int f[305][305], s[305], n, m;
vector<int> e[305];
int dfs(int u) {
  int p = 1;
  f[u][1] = s[u];
  for (auto v : e[u]) {
    int siz = dfs(v);
    for (int i = min(p, m + 1); i; i--)
      for (int j = 1; j <= siz && i + j <= m + 1; j++)
        f[u][i + j] = max(f[u][i + j], f[u][i] + f[v][j]);
    p += siz;
  }
  return p;
}
int main() {
  scanf("%d%d", &n, &m);
  for (int i = 1; i <= n; i++) {
    int k;
    scanf("%d%d", &k, &s[i]);
    e[k].push_back(i);
  }
  dfs(0);
  printf("%d", f[0][m + 1]);
  return 0;
}

换根dp

树形 DP 中的换根 DP 问题又被称为二次扫描,通常不会指定根结点,并且根结点的变化会对一些值,例如子结点深度和、点权和等产生影响。

STA-Station

思路

不妨令 u 为当前结点,v 为当前结点的子结点。首先需要用 s i s_i si 来表示以 i 为根的子树中的结点个数,并且有 s u = 1 + Σ s v s_u=1+\Sigma s_v su=1+Σsv。显然需要一次 DFS 来计算所有的 S i S_i Si,这次的 DFS 就是预处理,我们得到了以某个结点为根时其子树中的结点总数。
f u f_u fu 为以 u u u 为根时,所有结点的深度之和。
f v ⬅ f u f_v⬅f_u fvfu 可以体现换根,即以 u u u 为根转移到以 v v v 为根。显然在换根的转移过程中,以 v v v 为根或以 u u u 为根会导致其子树中的结点的深度产生改变。具体表现为:

  • 所有在 v v v 的子树上的结点深度都减少了一,那么总深度和就减少了 s v s_v sv
  • 所有不在 v v v 的子树上的结点深度都增加了一,那么总深度和就增加了 n − s v n-s_v nsv

由此地递推方程:
f v = f u + n − s v = f u + n − 2 × s v f_v=f_u+n-s_v=f_u+n-2 × s_v fv=fu+nsv=fu+n2×sv

在第二次 DFS 遍历整棵树并状态转移 f v = f u + n − 2 ∗ s v f_v=f_u+n-2*s_v fv=fu+n2sv,那么就能求出以每个结点为根时的深度和了。

代码

#include 
using namespace std;
int head[1000010 << 1], tot;
long long n, size[1000010], dep[1000010];
long long f[1000010];
struct node {
  int to, next;
} e[1000010 << 1];
void add(int u, int v) {  // 建图
  e[++tot] = node{v, head[u]};
  head[u] = tot;
}
void dfs(int u, int fa) {  // 预处理dfs
  size[u] = 1;
  dep[u] = dep[fa] + 1;
  for (int i = head[u]; i; i = e[i].next) {
    int v = e[i].to;
    if (v != fa) {
      dfs(v, u);
      size[u] += size[v];
    }
  }
}
void get_ans(int u, int fa) {  // 第二次dfs换根dp
  for (int i = head[u]; i; i = e[i].next) {
    int v = e[i].to;
    if (v != fa) {
      f[v] = f[u] - size[v] * 2 + n;
      get_ans(v, u);
    }
  }
}
int main() {
  scanf("%lld", &n);
  int u, v;
  for (int i = 1; i <= n - 1; i++) {
    scanf("%d %d", &u, &v);
    add(u, v);
    add(v, u);
  }
  dfs(1, 1);
  for (int i = 1; i <= n; i++) f[1] += dep[i];
  get_ans(1, 1);
  long long int ans = -1;
  int id;
  for (int i = 1; i <= n; i++) {
    if (f[i] > ans) {
      ans = f[i];
      id = i;
    }
  }
  printf("%d\n", id);
}

树的直径与重心

树的直径

给定一棵树,
树中每条边都有一个权值,
树中两点之间的距离定义为连接两点的路径边权之和。
树中最远的两个节点之间的距离被称为树的直径,
连接这两个点的路径被称为树的最长链。
————————————————————————————————
树的直径求法:双dfs或树形dp

双dfs

考虑贪心策略,
对于树上的一个随机的点 W W W ,
我们找到离他最远的 P P P ,
找到离 P P P 距离最远的点 Q Q Q ,
P Q PQ PQ 的距离即为我们要求的直径。
树形|换根 DP总结_第1张图片
如图,假设五号点为 W W W
找到离他距离最远的点 4 ( P ) 4(P) 4(P)
再找到距离 P P P 最远的点 6 ( Q ) 6(Q) 6(Q)
P Q PQ PQ 的距离即为直径。

代码

#include
#define N 200005
using namespace std;
int n,m;
struct edge{
	int to,nxt,w;
}e[N];
int tot;
int ans,pos;
int head[N],dis[N];
void add(int u,int v,int w){
	e[++tot]={v,head[u],w},head[u]=tot;
}
void dfs(int me,int dad){
	if(ans<dis[me])ans=dis[me],pos=me;
	for(int i=head[me];i;i=e[i].nxt){
		int son=e[i].to;
		if(son==dad)continue;
		dis[son]=dis[son]+e[i].w;
		dfs(son,me);
	}
}
int main(){
	cin>>n;
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		add(u,v,1),add(v,u,1);
	}
	dis[1]=0;
	dfs(1,0);//第一次dfs,随便找一个点。
	ans=0;dis[pos]=0,dfs(pos,0);//第二次dfs
	cout<<ans<<endl;
}

树形dp

考虑 f [ i ] f[i] f[i] i i i 为根的子树中和从 i i i 出发的最长长度
考虑以 u u u 为根的子树
f [ u ] = m a x ( f [ u ] , f [ v ] + e [ i ] . w ) f[u] = max(f[u],f[v]+e[i].w) f[u]=max(f[u],f[v]+e[i].w)
最长长度就是两条链的和。

或者说,我们原来找到了一条 f [ u ] f[u] f[u] 的链

现在我们新找到了一条由 v v v 节点继承的链

这两条链长度之和的最大值即为 a n s ans ans
所以
a n s = m a x ( a n s , f [ u ] + f [ v ] + e [ i ] . w ) ans=max(ans,f[u]+f[v]+e[i].w) ans=max(ans,f[u]+f[v]+e[i].w)

PS: 要写在 f [ u ] f[u] f[u] 的转移之前

代码

void dfs(int me,int dad)
{
    f[me]=0;
    for(int i=head[me];~i;i=e[i].nxt)
    {
        int son=e[i].to;
        if(son==dad)continue;
        dfs(son,me);
        ans=max(ans,f[me]+f[son]+e[i].w);
        f[me]=max(f[me],f[son]+e[i].w);
    }
}

因为树形 d p dp dp 的做法不需要依赖于 x + y > x ( y ∈ R + ) x+y>x(y \in R^+) x+y>x(yR+) 的性质,所以边权可以为负。

树的重心

考虑一个点,以它为根的树中,最大的子树节点数最少,我们把这个点称为树的重心。
例:下图中重心为 1 1 1 2 2 2
树形|换根 DP总结_第2张图片

树形dp

求解树的重心的时候,我们通常会采用树形 d p dp dp

我们用 s [ i ] s[i] s[i] 代表以 i i i 为根的子树节点数

f [ i ] f[i] f[i] 代表以 i i i 为根的子树中最大的子树节点个数

显然, f [ u ] = m a x ( f [ u ] , s [ v ] ) f[u]=max(f[u],s[v]) f[u]=max(f[u],s[v])
但是我们求重心的时候,是以 u u u 为根。

2 2 2 号节点的父亲变为儿子树形|换根 DP总结_第3张图片
所以最后统计 f [ u ] f[u] f[u] 的时候,还要记得统计 n − s [ u ] n-s[u] ns[u] (即以原来父亲为根的子树的节点数)

代码

void dfs(int me,int dad)
{
    s[me]=1,f[me]=0;
    for(int i=head[me];i;i=e[i].nxt)
    {
        int son=e[i].to;
        if(son==dad)continue;
        dfs(son,dad);
        s[me]+=s[son];
        f[me]=max(f[me],s[son]); 
    }
    f[me]=max(f[me],n-s[me]);
}

性质总结

  1. 以重心为根,所有的子树的大小都不超过整个树大小的一半。
  2. 树的重心最多有两个。
  3. 树的重心到其他节点的距离是最小的。
  4. 把一个树添加或删除一个叶子,那么它的重心最多只移动一条边的距离。

CSP-S 2019 树的重心

#include
using namespace std;
#define rd(x) cin>>x
#define ll long long
#define pb push_back
#define print(x) cout<<x<<endl;
const int N = 3e5 + 7;
int n, rt, s[N], g[N], u, v, z[N];
vector<int> e[N];
ll ans, c1[N], c2[N];

inline void add(ll *c, int x, int k) {
	++x;
	while (x <= n + 1) c[x] += k, x += x & -x;
}

inline ll ask(ll *c, int x) {
	++x;
	ll k = 0;
	while (x) k += c[x], x -= x & -x;
	return k;
}

void dfs1(int x, int f) {
	s[x] = 1, g[x] = 0;
	bool fg = 1;
	for (int i = 0; i < e[x].size(); i++) {
		int y = e[x][i];
		if (y == f) continue;
		dfs1(y, x);
		s[x] += s[y];
		g[x] = max(g[x], s[y]);
		if (s[y] > (n >> 1)) fg = 0;
	}
	if (n - s[x] > (n >> 1)) fg = 0;
	if (fg) rt = x;
}

void dfs2(int x, int f) {
	add(c1, s[f], -1);
	add(c1, n - s[x], 1);
	if (x ^ rt) {
		ans += x * ask(c1, n - 2 * g[x]);
		ans -= x * ask(c1, n - 2 * s[x] - 1);
		ans += x * ask(c2, n - 2 * g[x]);
		ans -= x * ask(c2, n - 2 * s[x] - 1);
		if (!z[x] && z[f]) z[x] = 1;
		ans += rt * (s[x] <= n - 2 * s[z[x] ? v : u]);
	}
	add(c2, s[x], 1);
	for (int i = 0; i < e[x].size(); i++) {
		int y = e[x][i];
		if (y == f) continue;
		dfs2(y, x);
	}
	add(c1, s[f], 1);
	add(c1, n - s[x], -1);
	if (x ^ rt) {
		ans -= x * ask(c2, n - 2 * g[x]);
		ans += x * ask(c2, n - 2 * s[x] - 1);
	}
}

inline void solve() {
	rd(n);
	for (int i = 1; i <= n; i++) e[i].clear();
	for (int i = 1, x, y; i < n; i++) rd(x), rd(y), e[x].pb(y), e[y].pb(x);
	ans = 0;
	dfs1(1, 0);
	dfs1(rt, 0);
	u = v = 0;
	for (int i = 0; i < e[rt].size(); i++) {
		int x = e[rt][i];
		if (s[x] > s[v]) v = x;
		if (s[v] > s[u]) swap(u, v);
	}
	for (int i = 1; i <= n + 1; i++) c1[i] = c2[i] = 0;
	for (int i = 0; i <= n; i++) add(c1, s[i], 1), z[i] = 0;
	z[u] = 1;
	dfs2(rt, 0);
	print(ans);
}

int main() {
	int T;
	rd(T);
	while (T--) solve();
	return 0;
}

主要是统计 x x x 为重心的次数

你可能感兴趣的:(c++,dp,算法,动态规划,深度优先)