树形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小结的主要内容,如果未能解决你的问题,请参考以下文章
动态规划_计数类dp_数位统计dp_状态压缩dp_树形dp_记忆化搜索