树形dp
• 树型dp一般先算子树然后进行合并,在实现上与树的后序遍历(这个说法并不准确,因为其实很多都不是二叉树)类似——遍历子树,遍历完之后把子树的值合并给父亲。
• 大部分的树形dp都是利用递归,先确定叶子结点的特征,然后推上去,即状态转移方程一般都是有该点的某些特征 = 其子节点的一些特征
• 有的树形dp以哪个点为根节点无所谓,有的有所谓。
引入
• 给你一棵n个点的树(1号点为根节点),求以点i为根的子树的大小
• f[i]以点i为根的子树的点的个数
• f[i] = 1+Σf[k] (k是i的儿子)
void dfs(i )
{
if(i是叶子节点) f[i] = 1, 返回;
for (k 是i的儿子)
{
dfs(k);
f[i]+=f[k];
}
f[i]+=1;
}
例题
NC15033 小G有一个大树
题意:
给你一棵树,让你找一个点,把这个点删除之后,树分成了好几块,把每块的边权和求出来,要求这些边权和中最大的那块最小。输出删除那个点,已经最大的那个块的边权和。(如果边权和相等,那么输出编号小的那个点)。
分析:
首先,对于这个题,那个点是根节点是无所谓的,所以我们dfs可以默认从1开始,并假设上一个点为0,然后dfs大体过程与引入中的例子差不多。
状态转移方程dp[now] += dp[tree[now][i]];
AC代码:
#include <bits/stdc++.h>
using namespace std;
int n;
int u, v, x = (int)1e9, mx = (int)1e9;
int dp[1005];
vector<int> tree[1005];
void dfs(int now, int pre)
{
dp[now] = 1;
int m = 0;
for(int i = 0; i < tree[now].size(); ++i)
{
if(tree[now][i] == pre) continue;
dfs(tree[now][i], now);
dp[now] += dp[tree[now][i]];
m = max(m, dp[tree[now][i]]);
}
m = max(m, n - dp[now]);
if(m < mx)
{
x = now;
mx = m;
}
if(m == mx)
{
x = min(x, now);
}
}
int main()
{
scanf("%d", &n);
for(int i = 1; i < n; ++i)
{
scanf("%d%d", &u, &v);
tree[u].push_back(v);
tree[v].push_back(u);
}
dfs(1, 0);
printf("%d %d\n", x, mx);
return 0;
}
NC51178 没有上司的舞会 (最大独立集)
题意:
公司中员工的关系可以用一颗树来表示,一个点的父节点是这个点的直接上司,每一个点都有一个happy值,每个人都不想和他的直接上司一起参加舞会。即一个点参加舞会,那么它的直接下属都不会参加舞会。问你参加这场舞会的人happy值总和最大是多少
分析:
这个状态需要是二维的,dp[i][1/0],代表第i个员工参加/不参加时以点i为根节点的子树中所有点的happy值和的最大值,然后由其子节点更新值即可。
//状态转移方程
dp[now][0] += max(dp[to][1], dp[to][0]);
dp[now][1] += dp[to][0];
AC代码:
#include <bits/stdc++.h>
using namespace std;
int n;
int ar[6005];
int dp[6005][5];
int u, v;
vector<int> tree[6005];
void dfs(int now, int pre)
{
dp[now][1] = ar[now];
for(int i = 0; i < tree[now].size(); ++i)
{
int to = tree[now][i];
//这个to很明显是与当前结点相连的一个结点,他有可能是它的父节点也有可能是子节点。
//所以特判一下。
//然后这个to变量,如果定义在了全局就wa了???
//必须是没跑一次循环定义一次???可能好像大概是因为递归会多次调用
//然后这个全局变量就会出现一些奇怪的东西??
if(to == pre) continue;
dfs(to, now);
dp[now][0] += max(dp[to][1], dp[to][0]);
dp[now][1] += dp[to][0];
}
}
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; ++i) scanf("%d", &ar[i]);
for(int i = 1; i < n; ++i)
{
scanf("%d%d", &u, &v);
tree[u].push_back(v);
tree[v].push_back(u);
}
scanf("%d%d", &u, &v);
dfs(1, 0);
printf("%d\n", max(dp[1][0], dp[1][1]));
return 0;
}
POJ1463 NC106060 Strategic game (树的最小点覆盖)
题意:
有一个城堡,用一颗树表示,这个城堡有n条边,n + 1个点,如果一个点上有士兵,那么与这个点相连的边就会被这个士兵看住,问你最少需要几个兵就可以把这个城堡看住。
分析:
状态也比较简单,还是dp[i][0/1]
代表点i没有/有放一个士兵。
然后由其子节点更新它即可。
//状态转移方程
dp[root][0] += dp[to][1];
dp[root][1] += min(dp[to][1], dp[to][0]);
AC代码:
#include <cstdio>
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
int n;
int a, num, b;
vector<int> tree[1505];
int dp[1505][5];
bool boo[1505];
int root;
//注意这三个容器都需要清空的,然后我的写法是在操作的过程中偷偷的用完就清空了
//没有单独写memset
void dfs(int root)
{
dp[root][1] = 1;
dp[root][0] = 0;
boo[root] = false;
for(int i = 0; i < tree[root].size(); ++i)
{
int to = tree[root][i];
dfs(to);
dp[root][0] += dp[to][1];
dp[root][1] += min(dp[to][1], dp[to][0]);
}
}
int main()
{
while(~scanf("%d", &n))
{
for(int i = 1; i <= n; ++i)
{
scanf("%d:(%d)", &a, &num);
tree[a].clear();
for(int j = 1; j <= num; ++j)
{
scanf("%d", &b);
tree[a].push_back(b);
boo[b] = true;
}
}
for(int i = 0; i < n; ++i)
{
if(!boo[i])
{
root = i;
break;
}
}
dfs(root);
printf("%d\n", min(dp[root][1], dp[root][0]));
}
return 0;
}
NC24953 Cell Phone Network(树的最小支配集)
题意:
跟上一个题很像,也是给你一颗树,让你想办法覆盖,这个题需要覆盖的是所有的点,当这个点被覆盖或者与这个点直接相连的点被覆盖时就算被覆盖。问你最少需要覆盖几个点。
分析:
这个状态就稍微复杂了一扭扭,一个点被覆盖有三种方法,一是自己覆盖,二是由它的任意一个儿子覆盖,三是由他爹覆盖。
因此,我们用dp[i][0/1/2]
表示这个点的覆盖方式对应上面的一二三。然后考虑如何用子节点来更新它的值。
首先,dp[i][0]
表示自己覆盖,那么他儿子的状态怎么样都可以,所以直接在他每个儿子的三种状态中取min。
其次,dp[i][1]
表示有它的任意一个儿子覆盖,那么它的值一定不可能由dp[to][2]
转移得到,而他只需要一个儿子能覆盖即可,那么我们还是取min(dp[to][0], dp[to][1])
,然后判断这它所有儿子中有没有选了比不选划算的,如果有太棒了,没有的话说明我们需要让他一个儿子变成选的,那就要考虑变哪一个,很明显应该边dp[0] - dp[1]
最小的那个。
最后,对于dp[i][2]
的那可以由dp[to][0]和dp[to][1]
转移过来,那自然还是取min。
然后,考虑一些边界条件,比如dp[root][2]
没有爹,所以应该为inf,对于叶子结点,他没有儿子,所以dp[now][1]
应该也是inf。
AC代码:
#include <bits/stdc++.h>
using namespace std;
int n;
int u, v;
vector<int> tree[10005];
int dp[10005][5];
const int inf = 0x3f3f3f3f;
void dfs(int now, int pre)
{
bool have_son = false;
bool need_add = true;
dp[now][0] = 1;
int mi = inf;
for(int i = 0; i < tree[now].size(); ++i)
{
int to = tree[now][i];
if(to == pre) continue;
have_son = true;
dfs(to, now);
dp[now][0] += min(dp[to][0], min(dp[to][1], dp[to][2]));
if(dp[to][0] <= dp[to][1]) need_add = false;
mi = min(mi, dp[to][0] - dp[to][1]);
dp[now][1] += min(dp[to][0], dp[to][1]);
if(now == 1) dp[now][2] = inf;
else dp[now][2] += min(dp[to][1], dp[to][0]);
}
if(need_add) dp[now][1] += mi;
if(!have_son) dp[now][1] = inf;
}
int main()
{
scanf("%d", &n);
for(int i = 1; i < n; ++i)
{
scanf("%d%d", &u, &v);
tree[u].push_back(v);
tree[v].push_back(u);
}
dfs(1, 0);
printf("%d\n", min(dp[1][0], dp[1][1]));
return 0;
}
NC50505 二叉苹果树
题意:
有一颗二叉苹果树,权值在边上,现在需要剪枝,告诉你要保留几个枝,问你这棵树剪完枝之后权值和最大是多少
分析:
首先,权值在边上不好处理,因为我们动态规划中的状态dp[i][j]
,这个i一般代表点,那么我们可以把权值下移,保留q枝变成保留q + 1个点。
那么,状态dp[i][j]
就表示以点i为根节点的子树共有j个结点时权值最大应该是多少,他肯定是由它的儿子转移过来,而他只有两个儿子儿子,那么我们只需要枚举以它的左儿子为根的子树有几个点然后取max更新即可。
AC代码:
#include <bits/stdc++.h>
using namespace std;
int n, q;
int u, v, w;
struct node
{
int to, le, ne;
} edge[205];
struct node1
{
int w, l, r;
} tree[105];
int tot;
int head[105];
int dp[105][105];
bool vis[105];
void addedge(int u, int v, int w)
{
edge[++tot].to = v;
edge[tot].le = w;
edge[tot].ne = head[u];
head[u] = tot;
}
void build(int root)
{
vis[root] = true;
for(int i = head[root]; i ; i = edge[i].ne)
{
int y = edge[i].to;
if(!vis[y])
{
if(!tree[root].l)
{
tree[root].l = y;
tree[y].w = edge[i].le;
}
else
{
tree[root].r = y;
tree[y].w = edge[i].le;
}
build(y);
}
}
}
void dfs(int root)
{
dp[root][1] = tree[root].w;
int le = tree[root].l, ri = tree[root].r;
if(!le) return ;
dfs(le);
dfs(ri);
for(int i = 2; i <= q; ++i)
{
for(int k = 0; k <= i - 1; ++k)
dp[root][i] = max(dp[root][i], dp[le][k] + dp[ri][i - 1 - k]);
dp[root][i] += tree[root].w;
}
}
int main()
{
scanf("%d%d", &n, &q);
++q;
for(int i = 1; i < n; ++i)
{
scanf("%d%d%d", &u, &v, &w);
addedge(u, v, w);
addedge(v, u, w);
}
build(1);
dfs(1);
printf("%d\n", dp[1][q]);
return 0;
}
拓展:
如果不是二叉树,是多叉树呢?
多叉树的话解决方法是树上背包,如下。
树上背包
P2014 [CTSC1997]选课
分析:
把这个关系搞出来,是一棵树,要自己加一个结点0,代表根节点
然后,普通背包理解了的话再稍微结合一下树形dp就懂了
dp[i][k]
代表以i为根节点的子树有k个后代的最大值
状态转移方程
dp[root][k] = max(dp[root][k], dp[root][k - j] + dp[v][j - 1] + w);
关键代码:
for(int i = head[root]; i; i = edge[i].ne)
{
v = edge[i].to;
w = edge[i].len;
dfs(v);
for(int k = m; k; --k)//与背包相同,从后向前枚举
{
//j表示新加进来几个结点,那很明显此时dp[root][k]来自两部分
//一部分是本来的k-j,另一部分是新加进来的j个结点,然后理解一下最后这个加w就可以了(状态含义)
for(int j = 1; j <= k; ++j)
{
dp[root][k] = max(dp[root][k], dp[root][k - j] + dp[v][j - 1] + w);
}
}
}
AC代码:
复杂度O(nm^2)
#include <bits/stdc++.h>
using namespace std;
int ar[305];
struct node
{
int to, len, ne;
}edge[1005];
int head[305];
int n, m, k, s, tot;
int dp[305][305];
void addedge(int u, int v, int w)
{
edge[++tot].to = v;
edge[tot].len = w;
edge[tot].ne = head[u];
head[u] = tot;
}
void dfs(int root)
{
int v, w, son;
for(int i = head[root]; i; i = edge[i].ne)
{
v = edge[i].to;
w = edge[i].len;
dfs(v);
for(int k = m; k; --k)
{
for(int j = 1; j <= k; ++j)
{
dp[root][k] = max(dp[root][k], dp[root][k - j] + dp[v][j - 1] + w);
}
}
}
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; ++i)
{
scanf("%d%d", &k, &s);
addedge(k, i, s);
}
dfs(0);
printf("%d\n", dp[0][m]);
return 0;
}
树的直径
NC202475树上子链(树的直径)
分析:
首先树的直径肯定是从根节点向下到叶子节点一条最长的,然后再在不经过最长路径的情况下早找一条从根节点到叶子结点最长的路径,然后路径和即直径,但是有一个要注意的点就是根节点的权值不要加两次!!!
一. 我们采用动态规划的思想
dp[u][0/1] 第二维取0表示从结点u的所有子节点出发向下的叶子节点最长的路径,取1表示不经过刚才的路径的最长的路径
在规划的过程中,dp[u][0/1]的值均不包括结点u的权值,对u结点规划结束后将dp[u][0] + w[u]
,dp[u][1]
则不需要(避免父节点权值加两次),并且对于上一层,只有dp[u][0]会影响到,dp[u][1]的值不会对上一层造成影响。
状态转移方程
if(dp[to][0] > dp[u][0])
{
dp[u][1] = dp[u][0];
dp[u][0] = dp[to][0];
}
else if(dp[to][0] > dp[u][1]) dp[u][1] = dp[to][0];
当时的疑问:1. dp[u][0]和dp[u][1]会不会有重复的结点
2.错误代码
if(dp[to][0] + w > dp[u][0])
{
dp[u][1] = dp[u][0];
dp[u][0] = dp[to][0] + w;
}
else if(dp[to][0] + w > dp[u][1]) dp[u][1] = dp[to][0] + w;
二. 两次dfs
先以任意一个点为起点,dfs得到所有的点到这个点的距离,其中最大那个点(设为p)一定是直径的一个端点
再以最大那个点为起点,dfs相同的操作,距p最远的那个点假设是q,则pq为直径。
证明见dalao的文章:https://zhuanlan.zhihu.com/p/115966044
AC代码:
一. 树形dp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll inf = -1e18;
int n;
ll ar[100005];
vector<int> tree[100005];
int u, v;
ll dp[100005][3];
ll ans = inf;
void dfs(int u, int pre)
{
dp[u][1] = dp[u][0] = 0;
for(int i = 0; i < tree[u].size(); ++i)
{
int to = tree[u][i];
if(to == pre) continue;
dfs(to, u);
if(dp[to][0] > dp[u][0])
{
dp[u][1] = dp[u][0];
dp[u][0] = dp[to][0];
}
else if(dp[to][0] > dp[u][1]) dp[u][1] = dp[to][0];
}
dp[u][0] += ar[u];
ans = max(ans, dp[u][0] + dp[u][1]);
}
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; ++i) scanf("%lld", &ar[i]);
for(int i = 1; i < n; ++i)
{
scanf("%d%d", &u, &v);
tree[u].push_back(v);
tree[v].push_back(u);
}
dfs(1, 0);
printf("%lld\n", ans);
return 0;
}
二. 两次dfs
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll mx = -1e18, ans = -1e18;
ll ar[100005];
ll dis[100005];
bool vis[100005];
vector<int> tree[100005];
int n, u, v, p, q;
void dfs(int u)
{
for(int i = 0; i < tree[u].size(); ++i)
{
int to = tree[u][i];
if(!vis[to])
{
vis[to] = true;
dis[to] = dis[u] + ar[to];
dfs(to);
}
}
}
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; ++i) scanf("%lld", &ar[i]);
for(int i = 1; i < n; ++i)
{
scanf("%d%d", &u, &v);
tree[u].push_back(v);
tree[v].push_back(u);
}
dis[1] = ar[1];
vis[1] = true;
dfs(1);
for(int i = 1; i <= n; ++i)
{
//cout << dis[i] << ' ';
if(dis[i] > mx)
{
mx = dis[i];
p = i;
}
dis[i] = vis[i] = 0;
}
//cout << '\n' << p << ' ' << mx << '\n';
dis[p] = ar[p];
vis[p] = true;
dfs(p);
for(int i = 1; i <= n; ++i)
{
//cout << dis[i] << ' ';
if(dis[i] > ans)
{
ans = dis[i];
q = i;
}
}
//cout << '\n' << q << ' ';
printf("%lld\n", ans);
return 0;
}