树形DP小结

Posted 佐世保镇守府

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树形DP小结相关的知识,希望对你有一定的参考价值。

\\(\\large\\texttt{Warning:}\\) 此篇博客中的代码是本人在 \\(2019\\) 年到 \\(2021\\) 年间断断续续写的,所以码风有较大的差异 ,后期会更改代码。

树形DP

只要你学会了树,还学会了 \\(dp\\) ,那么你就学会了树形 \\(dp\\)\\(By\\) 某不愿透露姓名的教练

  • 什么是树形DP

    树形 \\(DP\\),就是在“树”的数据结构上的动态规划,一般状态转移都是和子树相关,且能与线段树等数据结构相结合。

    因为其具有传递性,就对于具有一定规律的树上问题求解起到了很大帮助。


  • 常见的题型
    • 子树和计数:

      这类问题主要是统计子树和,通过加减一些子树满足题目中要求的某些性质

      例如 \\(CF767C、Luogu\\ P1122\\)

    • 树上背包问题:

      这类问题就是让你求在树上选一些点满足价值最大的问题,一般都可以设 \\(\\large f_{i,j}\\) 表示 \\(i\\) 这颗子树选 \\(j\\) 个点的最优解。

      例如 \\(Luogu\\ P1272\\ P1273\\)

    • 花费最少的费用覆盖所有点:

      这类问题是父亲与孩子有联系的题。基本有两种类型:

      • 选父亲必须不能选孩子(强制)

      • 选父亲可以不用选孩子(不强制)

      例如 \\(UVA\\ 1220\\)(类型1)、\\(Luogu\\ P2458\\)(类型2)

    • 树上统计方案数:

      这类问题就是给你一个条件,问你有多少个点的集合满足这样的条件。这类题主要运用乘法原理,控制一个点不动,看他能做多少贡献

    • 与多种算法结合

      这类问题就只能根据题目分析,听天由命了\\(\\cdots\\cdots\\)


  • 例题:
    • \\(Luogu\\ P2015\\) 二叉苹果树:

      这道题属于常见题型中的树上背包问题,可以将其作为模板题。

      这道题还有一个隐含的条件,当某条边被保留下来时,从根节点到这条边的路径上的所有边也都必须保留下来。

      所以,我们可以很容易定义我们的 \\(dp\\) 状态。令 \\(\\large f_{i,j}\\) 表示在 \\(i\\) 子树中保留 \\(j\\) 条边能够得到的最大苹果树。

      那么,状态转移方程就显而易见了:\\(\\large\\mathcal{ f_{u,i}=\\max(f_{u,i},f_{u,i-j-1}+f_{v,j}+Apple_{u,v})}\\) ,其中 \\(v\\)\\(u\\) 的子节点,\\(\\mathcal{Apple_{u,v}}\\) 表示 \\(u \\rightarrow v\\) 这条边上的苹果数。

      注意: 由于这是一个 \\(0/1\\) 背包,所以 \\(i、j\\) 需要倒序遍历。

      代码:

      点击查看代码
      // =============================================
      // 暁の水平线に胜利を刻むのです!
      // Author: 佐世保の时雨
      // Blog: https://www.cnblogs.com/SasebonoShigure
      // =============================================
      // 此代码为2019年码风
      #include <cstdio>
      #include <vector> 
      #include <algorithm>
      using namespace std;
      const int MAXN = 110;
      typedef pair<int, int> T;
      vector <T> Tree[MAXN];
      int n, q, DP[MAXN][MAXN];
      bool Visited[MAXN];
      void DFS(int Father, int Node) {
      	for (int i = 0; i < Tree[Node].size(); i ++) {
      		T Son = Tree[Node][i];
      		if (Son.first != Father and Visited[Son.first] == false) {
      			DFS(Node, Son.first);
      			for (int j = q; j > 0; j --) {
      				for (int k = j - 1; k >= 0; k --) {
      					DP[Node][j] = max(DP[Node][j], Son.second + DP[Son.first][k] + DP[Node][j - k - 1]);
      				}
      			}
      		}
      	}
      
      	return ;
      }
      int main () {
      	scanf ("%d %d", &n, &q);
      
      	for (int i = 1; i < n; i ++) {
      		int u, v, w;
      		scanf ("%d %d %d", &u, &v, &w);
      		Tree[u].push_back(make_pair(v, w));
      		Tree[v].push_back(make_pair(u, w));
      	}
      
      	DFS(1, 1);
      
      	printf ("%d\\n", DP[1][q]);
      	return 0;
      }
      
    • \\(Luogu\\ P4516\\ [JSOI2018]\\)潜入行动:

      这道题也是一道书上背包的简单好题,我们可以定义 \\(\\large f_{Root,i,0/1,0/1}\\) 表示在 \\(Root\\) 的子树中放置了 \\(i\\) 个监听设备,\\(Root\\) 是否放置监听设备,\\(Root\\) 是否被监听的方案数。

      经过简单的推理,我们可以得出状态转移方程:(推出状态转移方程的过程之后补)

      其实也不是很长,对吧

      \\(\\large{ f_{Root,i+j,0,0}=\\sum f_{Root,i,0,0}\\times f_{v,j,0,1}\\\\ f_{Root,i+j,1,0}=\\sum f_{Root,i,0,0}\\times (f_{v,j,0,0}+f_{v,j,0,1})\\\\ f_{Root,i+j,0,1}=\\sum f_{Root,i,0,1}\\times (f_{v,j,0,1}+f_{v,j,1,1})+f_{Root,i,0,0}\\times f_{v,j,1,1}\\\\ f_{Root,i+j,1,1}=\\sum f_{Root,i,1,0}\\times (f_{v,j,1,0}+f_{v,j,1,1})+f_{Root,i,1,1}\\times (f_{v,j,0,0}+f_{v,j,0,1}+f_{v,j,1,0}+f_{v,j,1,1})\\\\ }\\)

      代码:

      点击查看代码
      // =============================================
      // 暁の水平线に胜利を刻むのです!
      // Author: 佐世保の时雨
      // Blog: https://www.cnblogs.com/SasebonoShigure
      // =============================================
      
      // 此代码为2019年码风
      #include <cstdio>
      #include <vector>
      #include <cstring>
      
      using namespace std;
      
      const int MAXN = 100005;
      const long long MOD = 1000000007ll;
      
      vector<int> Tree[MAXN];
      
      int Read() {
      	int x = 0, f = 0;
      	char c = getchar ();
      
      	while (c > \'9\' or c < \'0\') {
      		if (c == \'-\') {
      			f = 1;
      		}
      
      		c = getchar ();
      	}
      
      	while (c >= \'0\' and c <= \'9\') {
      		x = (x << 1) + (x << 3) + (c ^ 48);
      		c = getchar ();
      	}
      
      	return (f == 1) ? -x : x;
      }
      
      void Write(const int &x) {
      	if (x < 0) {
      		putchar(\'-\');
      		Write(-x);
      	}
      
      	if (x > 10) {
      		Write(x / 10);
      	}
      
      	putchar(x % 10 + 48);
      
      	return ; 
      }
      
      int n, k, u, v, DP[MAXN][105][2][2], DP2[105][2][2], Size[MAXN];
      
      void DFS(long long Node,long long Father) {
      	DP[Node][0][0][0] = DP[Node][1][1][0] = Size[Node] = 1;
      
      	for (int i = 0; i < Tree[Node].size(); i ++ ) {
      		int Child = Tree[Node][i];
      
      		if(Child == Father) {
      			continue;
      		}
      
      		DFS(Child, Node);
      		memset(DP2, 0, sizeof DP2);
      
      		for (int j = 0; j <= k and j <= Size[Node]; j ++ ) {
      			for (int l = 0; l <= k - j and l <= Size[Child]; l ++ ) {
      				DP2[j + l][0][0] = (DP2[j + l][0][0] + (((long long)DP[Node][j][0][0] * DP[Child][l][0][1]) % MOD)) % MOD;
      				DP2[j + l][1][0] = (DP2[j + l][1][0] + (((long long)DP[Node][j][1][0] * DP[Child][l][0][1] + (long long)DP[Node][j][1][0] * DP[Child][l][0][0]) % MOD)) % MOD;
      				DP2[j + l][0][1] = (DP2[j + l][0][1] + (((long long)DP[Node][j][0][1] * DP[Child][l][0][1] + (long long)DP[Node][j][0][1] * DP[Child][l][1][1] + (long long)DP[Node][j][0][0] * DP[Child][l][1][1]) % MOD)) % MOD;
      				DP2[j + l][1][1] = (DP2[j + l][1][1] + (((long long)DP[Node][j][1][1] * DP[Child][l][0][0] + (long long)DP[Node][j][1][1] * DP[Child][l][0][1] + (long long)DP[Node][j][1][1] * DP[Child][l][1][1] + (long long)DP[Node][j][1][0] * (long long)DP[Child][l][1][0] + (long long)DP[Node][j][1][0] * DP[Child][l][1][1] + (long long)DP[Node][j][1][1] * DP[Child][l][1][0]) % MOD)) % MOD;
      			}
      		}
      
      		Size[Node] += Size[Child];
      		memcpy (DP[Node], DP2, sizeof DP2);
      	}
      
      	return ;
      }
      
      int main() {
      	n = Read();
      	k = Read();
      
      	for (int i = 1; i < n; i ++ ) {
      		u = Read();
      		v = Read();
      
      		Tree[u].push_back(v);
      		Tree[v].push_back(u);
      	}
      
      	DFS(1, 0);
      
      	printf ("%lld\\n", (DP[1][k][0][1] + DP[1][k][1][1]) % MOD);
      
      	return 0;
      }
      
      

      别喷我的压行啊!!!

树形dp小结

做了好久的树形DP(大雾) ,(noip)csp-s考了好多树形DP
树形DP基本分为这几种

1.最大独立集(没有上司的舞会)

经典树形DP
(dp[i][0/1]) 表示i这个点选与不选。
(dp[u][0] += dp[v][1];)
(dp[u][1] += max(dp[v][0],dp[v][1]);)

2.最小点覆盖(战略游戏)

状态与上面一样
(dp[u][0] += dp[v][1];)
(dp[u][1] += min(dp[v][0],dp[v][1]);)

3.最小支配集(SDOI保安站岗)

(dp[i][0/1/2]) 表示这个点被自己覆盖,被儿子覆盖,被父亲覆盖
(dp[u][0] += min(dp[v][0],dp[v][1],dp[v][2]);)
(dp[u][1] += min(dp[v][1],dp[v][0]);)
(注:如果u所有的儿子v的dp[v][1] < dp[v][0] 强制选一个最小的dp[v][0]);
(dp[u][2] += min(dp[v][0],dp[v][1]);)

   int dt=1e9+7,cnt=0;f[u][1]=pa[u];
    for(int p=last[u];p;p=pre[p])
    {
        int v=other[p];
        if(v==fa)continue;
        dfs(v,u);
    }
    for(int p=last[u];p;p=pre[p])
    {
        int v=other[p];
        if(v==fa)continue;
        f[u][1]+=min(min(f[v][2],f[v][1]),f[v][0]);
        f[u][2]+=min(f[v][1],f[v][0]);
        if(f[v][1]<f[v][0])cnt = 1;
        else dt=min(dt,f[v][1]-f[v][0]);
        f[u][0]+=min(f[v][1],f[v][0]);
    }
    if(cnt==0)
    f[u][0]+=dt;

4.Computer

求树上每个点到最远点的距离
(dp[i][0/1/2]) 分别表示子树内最长链,次长链,子树外最长链

inline void dfs1(int x,int fa)
{
    int fis = 0 ,sec = 0;
    for(int p = last[x];p;p = pre[p])
    {
        int v = other[p];
        if(v == fa)continue;
        dfs1(v,x);
        int tmp = dp[v][0] + len[p];
        if(tmp >= fis)
        sec = fis,fis = tmp;
        else if(tmp > sec)
        sec = tmp;
    }
    dp[x][0] = fis;dp[x][1] = sec;
}
inline void dfs2(int x,int fa)
{
    for(int p = last[x];p;p = pre[p])
    {
        int v = other[p];
        if(v == fa)continue;
        if(dp[v][0] == dp[x][0] - len[p])
        dp[v][2] = len[p] + max(dp[x][1],dp[x][2]);
        else 
        dp[v][2] = len[p] + max(dp[x][2],dp[x][0]);
        dfs2(v,x);
    }
}

5.树上背包(选课,有线电视网)

(dp[i][j]) 表示子树i中选了j门课
$ dp[u][j] = max(dp[u][j],dp[v][j-k] + dp[u][k]) $

   for(int p=last[u];p;p=pre[p])
    {
        int v=other[p];
        for(int i=m+1;i>=1;i--)
        {
            for(int j=i;j>=1;j--)
            if(f[u][j]>=0&&f[v][i-j]>=0)
            f[u][i]=max(f[u][i],f[u][j]+f[v][i-j]);
        }
    }

复杂度(O(nm^{2}))
用dfn序优化之后就是(O(nm))
树上背包基本都是这个套路
特别的像HAOI树上染色一题

     for(int i = min(siz[u],k) ; i >= 0 ;i--)
        {
            for(int j = 0; j <= min(siz[v],i);j++)
            {
                if(f[u][i-j] == -1)continue;
                ll val = (ll)(k-j)*j*len[p] + (ll)(siz[v]-j)*(n-k-siz[v]+j)*len[p];
                f[u][i] = max(f[u][i],f[v][j] + f[u][i-j] + val);
            }
        }

取min的两个位置要特别注意,如果不取min的话复杂度就是错的,这其中的道理不好分析,但这样做完之后,复杂度就是(O(n^{2}))

总结

树形DP多数分为(dp[i][0/1]),和背包几种类型,主要考虑子树中对当前点的贡献,或者子树外对当前点的贡献,来写出方程。

以上是关于树形DP小结的主要内容,如果未能解决你的问题,请参考以下文章

本周小结(未完工)

2016集训测试赛(二十五)小结

使用elementui树形控件写项目小结

动态规划_计数类dp_数位统计dp_状态压缩dp_树形dp_记忆化搜索

Starship Troopers(HDU 1011 树形DP)

HDU1520 Anniversary party(树形dp入门题)