ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

树形动态规划

2021-07-24 23:34:42  阅读:157  来源: 互联网

标签:结点 head int 树形 cnt edge 动态 规划 dp


算法简介

树形动态规划,亦称树形 \(dp\),指在一个树形结构上进行的动态规划。通常题目会给出一棵多叉树或若干条链,要求选出某些结点,使得它们之间满足某种最优解性质。

这种动态规划最大的特点便是 采用递归函数实现,类似于记忆化搜索的形式,既可以像记忆化搜索一样,令 dfs 函数返回某个特定的值;也可以用一个数组来保存动态规划的值,然后在上一层递归函数里调用。

树形 \(dp\) 的思想大致可以描述为一个状态:\(dp_{i, j, k}\),意为在结点 \(i\) 的子树内选出 \(j\) 个结点,且:

  • 若 \(k = 0\),则 \(dp_{i, j, k}\) 表示不选中结点 \(i\) 的最优解

  • 若 \(k = 1\),则 \(dp_{i, j, k}\) 表示选中结点 \(i\) 的最优解

如果是 树上背包 问题,状态则要相应地变更为:\(dp_{i, j}\) 表示从 \(i\) 的子树内选出 \(j\) 个结点的最优解。大多数模板题都可以通过这两个状态或其变式来解决。

例题选讲

树的最大独立集

例题链接

给出一棵树,要求从树中选出若干个 不相邻 的结点,使得其满足所有选出结点的点权之和最大。

这种类型的问题可以用第一类状态来解决:设 \(dp_{i, 0}\) 表示从 \(i\) 的子树最大独立集点权之和,且该最大独立集不包含结点 \(i\);\(dp_{i, 1}\) 同样表示 \(i\) 的子树最大独立集点权之和,但是该最大独立集包含结点 \(i\)。

显然,如果不选中结点 \(i\),则 \(i\) 的所有儿子的最大独立集一定会被选中,故而 \(dp_{i, 0} = \sum\limits max\{dp_{j, 0}, dp_{j, 1}\}, (u, v) \in E\);若选中结点 \(i\),则 \(i\) 的所有儿子一定不能被选中,所以 \(dp_{i, 1} = \sum\limits dp_{j, 0} + w_i\)。

筛选出唯一的根节点 \(r\),则最终答案为 \(max\{ dp_{r, 0}, dp_{r, 1} \}\)。

参考代码

#include <cstdio>
#include <algorithm>
using namespace std;
#define maxn 6005
#define maxm 12005
#define inf 0x3f3f3f3f

struct node
{
	int to, nxt;
} edge[maxm];

int n, cnt;
int head[maxn], r[maxn], dp[maxn];

void add_edge(int u, int v)
{
	cnt++;
	edge[cnt].to = v;
	edge[cnt].nxt = head[u];
	head[u] = cnt;
}

int dfs(int u, int fa)
{
	int sum1 = 0, sum2 = 0;
	for (int i = head[u]; i; i = edge[i].nxt)
	{
		if (edge[i].to != fa)
		{
			sum1 += dfs(edge[i].to, u);
			sum2 += dp[edge[i].to];
		}
	}
	dp[u] = max(sum1 + r[u], sum2);
	return sum2;
}

int main()
{
	int u, v, ans = -inf;
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
		scanf("%d", &r[i]);
	for (int i = 1; i <= n - 1; i++)
	{
		scanf("%d%d", &u, &v);
		add_edge(u, v);
		add_edge(v, u);
	}
	dfs(1, 0);
	for (int i = 1; i <= n; i++)
		ans = max(ans, dp[i]);
	printf("%d\n", ans);
	return 0;	
} 

树上背包

例题链接

给出若干条链,假如要选中结点 \(i\),则必须先选中结点 \(i\) 的前驱结点 \(p\)。试选出 \(m\) 个结点,使得这 \(m\) 个结点的点权总和最大。

因为这个图可能不连通,所以我们得想办法把这个图 转化成一棵树。可以使用 虚拟结点 的起脚,虚拟出一个结点 \(0\),这个结点是所有没有真实前驱结点的结点的前驱结点。这样,这个不连通图就被转化成了一棵树。

现在,直接按照树上 \(01\) 背包的状态转移方程求解即可。枚举 \(i\) 的子树内选的结点个数 \(j\) 和 \(i\) 的子结点 \(v\) 的子树内选的结点个数 \(k\)。若 \(1 \leq k < j \leq size_i\),则 \(dp_{i, j} = max\{ dp_{i, j}, dp_{i, j - k} + dp_{v, k} \}\)。特殊地,因为选中一个结点必须要选中其前驱结点,而假如只能选一个结点,则其子树内其他结点都不能选(若选中该结点,则没选中其前驱结点),所以只能选中该结点 \(i\) 本身,即边界条件为 \(dp_{i, 1} = w_i\)。

注意,因为虚拟了一个结点 \(0\),所以必须要多选中一个虚拟结点 \(0\),也就是求解使用的 \(m\) 比数据输入的 \(m\) 要大 \(1\)。

参考代码

#include <cstdio>
#include <algorithm>
using namespace std;
#define maxn 305
#define maxm 305

struct node
{
	int to, nxt;
} edge[maxm];

int n, m, cnt;
int head[maxn], w[maxn], dp[maxn][maxn];

void add_edge(int u, int v)
{
	cnt++;
	edge[cnt].to = v;
	edge[cnt].nxt = head[u];
	head[u] = cnt;
}

void dfs(int u)
{
	dp[u][1] = w[u];
	for (int i = head[u]; i; i = edge[i].nxt)
	{
		dfs(edge[i].to);
		for (int j = m; j; j--)
			for (int k = j - 1; k; k--)
				dp[u][j] = max(dp[u][j], dp[u][j - k] + dp[edge[i].to][k]);
	}
}

int main()
{
	int k;
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++)
	{
		scanf("%d%d", &k, &w[i]);
		add_edge(k, i);
	}
	m++;
	dfs(0);
	printf("%d\n", dp[0][m]);
	return 0;
}

树的直径

概念

树的直径,指树上最长的简单路径。

简单路径指不重复经过同一个点的路径。

算法

在树上任意找到一个点u,找到距离u最远的顶点x,再找到距离x最远的顶点yxy之间的简单路径即为树的直径。

具体实现可以使用\(dfs\)。

算法证明

模板

#include <cstdio>
using namespace std;
#define maxn 100005
#define maxm 200005

struct node {
	int to, nxt;
}edge[maxm];

int n, cnt;
int maxlen, point;
int head[maxn];

void add_edge(int u, int v) {
	cnt++;
	edge[cnt].to = v;
	edge[cnt].nxt = head[u];
	head[u] = cnt;
}

void dfs(int u, int pre, int step) {
	if (step > maxlen) {
		maxlen = step;
		point = u;
	}
	for (int i = head[u]; i; i = edge[i].nxt) {
		if (edge[i].to != pre) {
			dfs(edge[i].to, u, step + 1);
		}
	}
}

int diameter() {
	maxlen = -1;
	dfs(1, 0, 0);
	maxlen = -1;
	dfs(point, 0, 0);
	return maxlen;
}

int main() {
	int u, v;
	scanf("%d", &n);
	for (int i = 1; i <= n - 1; i++) {
		scanf("%d%d", &u, &v);
		add_edge(u, v);
		add_edge(v, u);
	}
	printf("%d\n", diameter());
	return 0;
}

变式

题目大意:给定一棵有\(n\)个节点的树,试求每个节点\(i\)到距离其最远的节点\(j\)的距离。

因为距离点\(i\)最远的节点一定是该树直径的端点,故而可以进行\(3\)次\(dfs\):

  1. 求出该树直径的一个端点\(x\)。

  2. 更新\(x\)到节点\(i\)的距离,同时找出直径的另一端点\(j\)。

  3. 更新\(j\)到节点\(i\)的距离。

参考代码

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 10005
#define maxm 20005

struct node
{
	int to, nxt, w;
}edge[maxm];

int n, cnt;
int maxlen, point;
int head[maxn], dis[maxn];

void add_edge(int u, int v, int w)
{
	cnt++;
	edge[cnt].to = v;
	edge[cnt].w = w;
	edge[cnt].nxt = head[u];
	head[u] = cnt;
}

void dfs(int u, int pre, int step)
{
	if (step >= maxlen)
	{
		maxlen = step;
		point = u;
	}
	for (int i = head[u]; i; i = edge[i].nxt)
	{
		if (edge[i].to != pre)
		{
			dis[edge[i].to] = max(dis[edge[i].to], step + edge[i].w);
			dfs(edge[i].to, u, step + edge[i].w);
		}
	}
}

void diameter()
{
	maxlen = -1;
	dfs(1, 0, 0);
	maxlen = -1;
	dfs(point, 0, 0);
	maxlen = -1;
	dfs(point, 0, 0);
}

int main()
{
	int u, v, w;
	scanf("%d", &n);
	for (int i = 1; i <= n - 1; i++)
	{
		scanf("%d%d%d", &u, &v, &w);
		add_edge(u, v, w);
		add_edge(v, u, w);
	}
	diameter();
	for (int i = 1; i <= n; i++)
		printf("%d ", dis[i]);
	printf("\n");
	return 0;
}

树的重心

概念

树的重心,也叫做树的质心。整颗树以它的重心为根时,它的所有子树中最大的子树节点数最小,即删去重心后,生成的多棵树会尽量平均

树上的所有结点到重心的距离之和一定是最短的。

算法

在树中任意选一个点u,规定以u为根的有根树统一称为树A。从u开始\(dfs\),沿路统计出整棵树的所有子树和节点数最大的树A。此时,整棵树除树A外的部分也是无根树u的一个子树。求出每个点的子树的最大节点数,再取最小值,即可得到树的重心。

一棵树最多只有两个重心。

模板

#include <cstdio>
#include <algorithm>
using namespace std;
#define maxn 50005
#define maxm 100005
#define inf 0x3f3f3f3f

struct node
{
	int to, nxt;
} edge[maxm];

int n, cnt, val = inf;
int head[maxn], f[maxn], size[maxn];

void add_edge(int u, int v)
{
	cnt++;
	edge[cnt].to = v;
	edge[cnt].nxt = head[u];
	head[u] = cnt;
}

void dfs(int u, int fa)
{
	size[u] = 1;
	for (int i = head[u]; i; i = edge[i].nxt)
	{
		if (edge[i].to != fa)
		{
			dfs(edge[i].to, u);
			size[u] += size[edge[i].to];
			f[u] = max(f[u], size[edge[i].to]);
		}
	}
	f[u] = max(f[u], n - size[u]);
	val = min(val, f[u]);
}

int main()
{
	int u, v;
	scanf("%d", &n);
	for (int i = 1; i <= n - 1; i++)
	{
		scanf("%d%d", &u, &v);
		add_edge(u, v);
		add_edge(v, u);
	}
	dfs(1, 0);
	for (int i = 1; i <= n; i++)
		if (f[i] == val)
			printf("%d ", i);
	puts("");
	return 0;
}

标签:结点,head,int,树形,cnt,edge,动态,规划,dp
来源: https://www.cnblogs.com/Ling-Lover/p/shu-xing-dong-tai-gui-hua.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有