算法刷题day53:树形DP

引言

关于这个树形 D P DP DP 啊,其实感觉就是对树进行 D F S DFS DFS 只不过有一个返回值罢了,这个返回值一般代表的就是以该结点为根的子树的一个属性,还是相当于用一个已知的状态去推未知的状态,只不过这种已知是通过递归来实现的,叶子结点的值一般都是初始值已知的,然后逐步递归到根结点,大部分还是用子结点去更新父结点的,大体的思路基本就是这样。稍有难度的题目还会加一些其它的算法,比如就是高精度,任何一类题都能加,其实还是把各个知识点都牢牢掌握了,其它的就没什么了,加油吧!


一、没有上司的舞会

标签:动态规划、树形DP

思路:这道题自己已经写烂了,估计写个五六十遍应该是有了。我们定义状态 f [ u ] [ 0 ] 、 f [ u ] [ 1 ] f[u][0]、f[u][1] f[u][0]f[u][1] 分别代表以结点 u u u 为子树,并且结点 u u u 去或者不去的最大值,最终的答案就是 m a x ( f [ r o o t ] [ 0 ] , f [ r o o t ] [ 1 ] ) max(f[root][0],f[root][1]) max(f[root][0],f[root][1]) ,然后就是递归子结点,把子结点算出来然后推出父结点,递推的方程为 f [ u ] [ 0 ] = f [ u ] [ 0 ] + ∑ m a x ( f [ j ] [ 0 ] , f [ j ] [ 1 ] ) f[u][0] = f[u][0] + \sum max(f[j][0],f[j][1]) f[u][0]=f[u][0]+max(f[j][0],f[j][1]) f [ u ] [ 1 ] = f [ u ] [ 1 ] + ∑ f [ j ] [ 0 ] f[u][1] = f[u][1] + \sum f[j][0] f[u][1]=f[u][1]+f[j][0]。这里有个技巧可以判断递归函数写的对不对,看叶子节点,如果当前的结点是叶子结点,那么 f [ u ] [ 1 ] = w [ u ] f[u][1] = w[u] f[u][1]=w[u] 是对的,然后没有子结点了,那么 f [ u ] [ 0 ] = 0 f[u][0] = 0 f[u][0]=0 ,是对的,然后看根结点,如果当前的结点是根结点,然后 f [ u ] [ 1 ] f[u][1] f[u][1] 就是自己的值加上子结点中各子结点不去 f [ j ] [ 0 ] f[j][0] f[j][0] 的值,然后 f [ u ] [ 0 ] f[u][0] f[u][0] 就是所有子结点去或者不去最大值之和,这样一看就对了,递归的顺序是在每一个结点中,先递归每个子结点,把每个子结点的两个状态计算出来,然后通过子结点去计算父结点的最值,这样所有的就通了。

题目描述:

Ural 大学有 N 名职员,编号为 1∼N。

他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。

每个职员有一个快乐指数,用整数 Hi 给出,其中 1≤i≤N。

现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。

在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。

输入格式
第一行一个整数 N。

接下来 N 行,第 i 行表示 i 号职员的快乐指数 Hi。

接下来 N−1 行,每行输入一对整数 L,K,表示 K 是 L 的直接上司。(注意一下,后一个数是前一个数的父节点,不要搞反)。

输出格式
输出最大的快乐指数。

数据范围
1≤N≤6000,−128≤Hi≤127
输入样例:
7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5
输出样例:
5

示例代码:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
typedef pair<int,int> PII;
#define x first
#define y second

const int N = 6010, M = N, INF = 0x3f3f3f3f;

int n, m;
int w[N];
int h[N], e[M], ne[M], idx;
int f[N][2];
bool has_fa[N];

void add(int a, int b)
{
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

int dfs(int u)  // 这道题不用怕递归回去,因为只建了父->子的边
{
	f[u][1] = w[u];
	
	for(int i = h[u]; i != -1; i = ne[i])
	{
		int j = e[i];
		dfs(j);  // 计算子结点 
		 
		f[u][0] += max(f[j][0], f[j][1]);
		f[u][1] += f[j][0];
	}
}

int main()
{
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	
	memset(h, -1, sizeof h);
	cin >> n;
	for(int i = 1; i <= n; ++i) cin >> w[i];
	
	for(int i = 0; i < n - 1; ++i)
	{
		int a, b; cin >> a >> b;
		add(b,a);  // 已知父子关系 
		has_fa[a] = true;
	}
	
	int root = 1;
	while(has_fa[root]) root++;
	
	dfs(root);
	
	cout << max(f[root][0], f[root][1]) << endl;
	
	return 0;
}

二、树的重心

标签:dfs、树形DP

思路:需要计算去除掉一个点的剩余连通块的最大值,我们可以用递归向下来求出每个分支的结点数,求最大,然后用总的结点数减去包含自己在内的所有分支的总结点数,就是该点向上走的那部分连通块的结点数,然后取最小值即可。还是拿子结点递归回来的信息,然后处理最值。按照上一题讲的思路看一看,如果当前结点是叶子结点的话,没有分支,然后总和是 s u m = 1 sum = 1 sum=1 ,最大的连通块数就是 n − s u m n - sum nsum ,然后取最小,然后返回 s u m sum sum ,是对的。然后如果该结点是根结点的话,然后总结点数就是自己加上每个结点的数量,然后 s i z e size size 在其中也是取最大,然后向上的连通块就是 n n n 减去向下的总和,取最小,也是对的。

题目描述:

给定一颗树,树中包含 n 个结点(编号 1∼n)和 n−1 条无向边。

请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。

重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。

输入格式
第一行包含整数 n,表示树的结点数。

接下来 n−1 行,每行包含两个整数 a 和 b,表示点 a 和点 b 之间存在一条边。

输出格式
输出一个整数 m,表示将重心删除后,剩余各个连通块中点数的最大值。

数据范围
1≤n≤105
输入样例
9
1 2
1 7
1 4
2 8
2 5
4 3
3 9
4 6
输出样例:
4

示例代码:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
typedef pair<int,int> PII;
#define x first
#define y second

const int N = 1e5+10, M = N * 2, INF = 0x3f3f3f3f;

int n, m;
int h[N], e[M], ne[M], idx;
int ans = 2e9;

void add(int a, int b)
{
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

int dfs(int u, int fa)  // 计算以u为根结点且向下走的结点数 
{
	int sum = 1, size = 0;  // 包括u在内的向下走的总结点数,每个分支的最大结点数 
	for(int i = h[u]; i != -1; i = ne[i])
	{
		int j = e[i];
		if(j == fa) continue;  // 说明往上走了
		
		int d = dfs(j,u);  // 计算每个以子结点为根的向下的结点数	
		size = max(size, d);
		sum += d;
	}
	
	size = max(size, n - sum); // 计算向上的结点数
	ans = min(ans, size);
	return sum; 
}

int main()
{
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	
	memset(h, -1, sizeof h);
	cin >> n;
	for(int i = 0; i < n - 1; ++i)
	{
		int a, b; cin >> a >> b;
		add(a,b), add(b,a);  // 由于没有父子关系需要建无向边 
	}
	
	dfs(1,-1);  // 由于是无向边防止往回递归需要加上父结点 
	
	cout << ans << endl;
	
	return 0;
}

三、树的最长路径

标签:树的深度优先遍历、DP、树形DP、树的直径

思路:其实就是枚举每一个点,如果是直径的话,肯定会将某一个点当作根,然后直径就是这个点向下的第一、第二长的路径之和。我们枚举每一个点,求出该点向下的第一、第二长路径之和,虽然边权有负值,但是我不走或者没有边了就是 0 0 0 ,所以最开始初始化为 0 0 0 就行了,然后就是递归每一个子结点的最长路径了,就是从子结点推父结点,然后加上连边,求第一大和第二大。因为如果是向上走是最长的话,那么肯定会存在一个该点向上走的点作为根结点出现,这个点也只是那个点递归下来的,也不是最大值。然后最后取第一、第二大边的最大值即可。分析的话,就是还是遍历每个子结点,然后先递归出每个子结点的值,然后计算以该结点为根结点的第一、第二大值,最后求出极值。如果该点是叶子,那么向下没有边,和就是 0 0 0 ,如果是根结点那就是刚才分析过了,并且不会存在向上的边是直径,因为肯定会存在它的祖先向下包含了它的向上的那条边。

题目描述:

给定一棵树,树中包含 n 个结点(编号1~n)和 n−1 条无向边,每条边都有一个权值。

现在请你找到树中的一条最长路径。

换句话说,要找到一条路径,使得使得路径两端的点的距离最远。

注意:路径中可以只包含一个点。

输入格式
第一行包含整数 n。

接下来 n−1 行,每行包含三个整数 ai,bi,ci,表示点 ai 和 bi 之间存在一条权值为 ci 的边。

输出格式
输出一个整数,表示树的最长路径的长度。

数据范围
1≤n≤10000,1≤ai,bi≤n,−105≤ci≤105
输入样例:
6
5 1 6
1 4 5
6 3 9
2 6 8
6 1 7
输出样例:
22

示例代码:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
typedef pair<int,int> PII;
#define x first
#define y second

const int N = 1e4+10, M = N * 2, INF = 0x3f3f3f3f;

int n, m;
int h[N], e[M], w[M], ne[M], idx;
int ans = -2e9;

void add(int a, int b, int c)
{
	e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

int dfs(int u, int fa)
{
	int d1 = 0, d2 = 0;
	
	for(int i = h[u]; i != -1; i = ne[i])
	{
		int j = e[i];
		if(j == fa) continue;
		
		int d = dfs(j,u) + w[i];
		if(d > d1) d2 = d1, d1 = d;
		else if(d > d2) d2 = d;
	}
	
	ans = max(ans, d1 + d2);
	return d1;
}

int main()
{
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	
	memset(h, -1, sizeof h);
	cin >> n;
	for(int i = 0; i < n - 1; ++i)
	{
		int a, b, c; cin >> a >> b >> c;
		add(a,b,c), add(b,a,c);
	}
	
	dfs(1,-1);
	
	cout << ans << endl;
	
	return 0;
}

四、树的中心

标签:树的深度优先遍历、DP、树形DP、换根DP

思路:整体就是找到所有点的最远距离,然后取最小值就行了。一个点的最远距离要么是该点向下走的最远距离,这个可以用上一题的思路,找到 d 1 [ u ] d1[u] d1[u] 就行了,要么就是该点向上走的的最远距离,这个就是从根结点出发,我们假设 1 1 1 号点为根结点,然后建立关系,然后就是向下遍历每一个结点,子结点的向上的最大值,如果当前结点不是父结点向下的最大值,那么 u p [ j ] = d 1 [ u ] + w [ i ] up[j] = d1[u] + w[i] up[j]=d1[u]+w[i] ,如果是父结点向下的最大值,那么 u p [ j ] = d 2 [ u ] + w [ i ] up[j] = d2[u] + w[i] up[j]=d2[u]+w[i] ,就是取第二大就行了,这样就不会跟最大的重合了,当然 u p [ r o o t ] = 0 up[root] = 0 up[root]=0 ,就相当于是拿父结点去更新子结点,子结点向上的最大值其实就是父结点不包括该点向下的最大值加上当前的边。然后就需要求出每个点向下的第一大和第二大值,然后需要判断出该点向下的最大值的子结点是谁,这样再求向上最大值的时候,就需要判断子结点是不是向下的最大值。这个是父推子,然后具体细节见代码。

题目描述:

给定一棵树,树中包含 n 个结点(编号1~n)和 n−1 条无向边,每条边都有一个权值。

请你在树中找到一个点,使得该点到树中其他结点的最远距离最近。

输入格式
第一行包含整数 n。

接下来 n−1 行,每行包含三个整数 ai,bi,ci,表示点 ai 和 bi 之间存在一条权值为 ci 的边。

输出格式
输出一个整数,表示所求点到树中其他结点的最远距离。

数据范围
1≤n≤10000,1≤ai,bi≤n,1≤ci≤105
输入样例:
5 
2 1 1 
3 2 1 
4 3 1 
5 1 1
输出样例:
2

示例代码:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
typedef pair<int,int> PII;
#define x first
#define y second

const int N = 1e4+10, M = N * 2, INF = 0x3f3f3f3f;

int n, m;
int h[N], e[M], w[M], ne[M], idx;
int d1[N], d2[N], p1[N], up[N];
bool is_leaf[N];

void add(int a, int b, int c)
{
	e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

int dfs_d(int u, int fa)
{
	d1[u] = d2[u] = -INF;
	
	for(int i = h[u]; i != -1; i = ne[i])
	{
		int j = e[i];
		if(j == fa) continue;
		
		int d = dfs_d(j,u) + w[i];
		if(d >= d1[u]) 
		{
			d2[u] = d1[u];
			d1[u] = d, p1[u] = j;
		}
		else if(d > d2[u])
		{
			d2[u] = d;
		}
	}
	
	if(d1[u] == -INF)
	{
		d1[u] = d2[u] = 0;
		is_leaf[u] = true;
	}
	return d1[u];
}

void dfs_u(int u, int fa)
{
	for(int i = h[u]; i != -1; i = ne[i])
	{
		int j = e[i];
		if(j == fa) continue;  // 这个树由于向下的方向以及树的结构是以1为根结点,所以要对应上
		
		//根结点向上的最大值是0,我们定义根结点
		// 子结点向上的最大值,就是父结点向下的最大值(不包括该点的那条路径)
		if(j == p1[u]) up[j] = max(up[u], d2[u]) + w[i];
		else up[j] = max(up[u], d1[u]) + w[i];
		
		dfs_u(j,u);	  // 然后继续推其它的子结点
	} 
}

int main()
{
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	
	memset(h, -1, sizeof h);
	cin >> n;
	for(int i = 0; i < n - 1; ++i)
	{
		int a, b, c; cin >> a >> b >> c;
		add(a,b,c), add(b,a,c);
	}
	
	dfs_d(1,-1);
	dfs_u(1,-1);
	
	int res = d1[1];
	for(int i = 2; i <= n; ++i)
	{
		if(is_leaf[i]) res = min(res, up[i]);
		else res = min(res, max(d1[i], up[i]));
	}
	
	cout << res << endl;
	
	return 0;
}

五、数字转换

标签:数论、DP、树形DP

思路:整体思路就是给可以进行转换的数进行连边,然后对每一棵树进行一次,树的最长路径,然后找最大值即可。至于怎么建树,可以先求出每个数的约数之和,然后进行连边,连边应该是约数和向约数连边,怎么求 1 ∼ n 1\sim n 1n 中所有数的约数之和,可以直接枚举 1 ∼ n 1\sim n 1n ,然后枚举他们的倍数,只要 i ∗ j ≤ n i * j \leq n ijn 即可,然后加上该数的约数 i i i 就行了。最后遍历每个子树,求最值即可。

题目描述:

如果一个数 x 的约数之和 y(不包括他本身)比他本身小,那么 x 可以变成 y,y 也可以变成 x。

例如,4 可以变为 3,1 可以变为 7。

限定所有数字变换在不超过 n 的正整数范围内进行,求不断进行数字变换且不出现重复数字的最多变换步数。

输入格式
输入一个正整数 n。

输出格式
输出不断进行数字变换且不出现重复数字的最多变换步数。

数据范围
1≤n≤50000
输入样例:
7
输出样例:
3
样例解释
一种方案为:4→3→1→7。

示例代码:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
typedef pair<int,int> PII;
#define x first
#define y second

const int N = 5e4+10, M = N * 2, INF = 0x3f3f3f3f;

int n, m;
int h[N], e[M], ne[M], idx;
bool st[N];
LL sum[N];
int ans;

void add(int a, int b)
{
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

int dfs(int u)
{
    st[u] = true;
	int d1 = 0, d2 = 0;
	
	for(int i = h[u]; i != -1; i = ne[i])
	{
		int j = e[i];
		if(st[j]) continue;
		int d = dfs(j) + 1;
		
		if(d > d1) d2 = d1, d1 = d;
		else if(d > d2) d2 = d;
	}
	
	ans = max(ans, d1+d2);
	return d1;
}

int main()
{
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	
	memset(h, -1, sizeof h); 
	cin >> n;
	
	for(int i = 1; i <= n; ++i)  // 枚举i的倍数是谁在不超过n时 
	{
		// 这样写的好处是不会暴int
		for(int j = 2; i * j <= n; ++j)  // 倍数 不包括自己 
		{
			sum[i*j] += i;
		}
	}
	
	for(int i = 1; i <= n; ++i)
	{
		if(sum[i] < i)
		{
			add(sum[i], i);  // 一个数只有一个约数和,多个约数和为一个数,所以应该是约数和向约数连边
		}
	}
	
	for(int i = 1; i <= n; ++i)
	{
	    if(!st[i])  // 因为可能有多个子树
	    {
	        dfs(i);
	    }
	}
	
	cout << ans << endl; 
	
	return 0;
}
  • 16
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
树形动态规划(Tree DP)是一种解决树状结构问题的算法思想。它利用了树这种特殊的数据结构的性质进行求解,常用来解决树的最优路径、最大值、最小值等类型的问题。 在夜深人静的时候写算法,我通常会采用以下步骤来完成树形dp的实现: 第一步是定义状态。我们首先需要确定问题的状态表示方式。对于树形dp来说,常用的状态表示方式是以节点为单位进行表示。我们可以定义dp[i]表示以节点i为根的子树的某种性质,比如最大路径和、最长路径长度等。 第二步是确定状态转移方程。根据问题的特点,我们需要找到状态之间的关系,从而确定状态转移方程。在树形dp中,转移方程常常与节点的子节点相关联。我们可以通过遍历节点的子节点,利用它们的状态来更新当前节点的状态,从而得到新的状态。 第三步是确定初始条件。在动态规划中,我们需要确定初始状态的值。对于树形dp来说,我们可以选择将叶节点作为初始状态,然后逐步向上更新,最终得到整棵树的最优解。 第四步是确定计算顺序。树形dp的计算通常是从根节点开始,自顶向下逐步计算,直到达到叶节点。因为树形dp的计算过程中需要利用到子节点的状态来更新当前节点的状态,所以必须按照计算顺序进行。 夜深人静时,写算法树形dp是相对较复杂的算法,需要仔细思考问题的状态表示方式,转移方程以及初始条件。在实现过程中,可以采用递归的方式进行代码编写,或者利用栈等数据结构进行迭代实现。 总的来说,夜深人静写算法树形dp需要耐心和细心,经过思考和实践,才能顺利解决树状结构问题。但是,一旦理解并掌握了树形dp的思想和方法,就能够高效地解决各种树形结构问题,提升算法的效率和准确性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lijiachang030718

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值