C++树形dp经验总结(换根dp,点覆盖问题,含例题City Upgrading)

最近几天一直在看树形dp的相关问题,并写了几道经典的模板例题,这里来稍微总结一下树形dp。

首先顾名思义,树形dp就是指题意要求,在树形的结构上的动态规划问题,转移方程通常就是从树的父节点和子节点之间进行转移,所以核心的状态转移部分是在dfs深度搜索中实现,下面是两个常见的树形dp问题类型。

一是换根dp,这个说是树形dp的类型,倒不如说是树形dp的具体方法的一种,通常见于求树上某个结点的什么属性要最大,然后这个属性又需要我们从这个结点出发遍历一次树才能得到。从暴力的角度思考,枚举每个结点并搜索一次,复杂度就是O(n²),一般是会超时的(如果题目是想你用树形dp做的话),这时候我们就得考虑用树形dp,也就是如何用一次搜索的结果,来得到每个结点的对应答案。

经典的例题比如STA-Station,求一棵树中,以哪个结点作为根,其他所有结点的深度之和最大。单独一个点的深度之和用dfs或者bfs一下就很容易可以得到,但是想要求以哪个结点为根得到的答案最大,没法枚举每一个结点都跑一遍搜索。但是我们可以通过记录结点的一些信息,来递推得到这个结果。需要记录的信息包括,以初始的某个结点为根跑完dfs后,每个结点的深度,每个结点的子树的大小(包括本身的结点个数),以及以该结点为根时的其他结点深度和。具体的推导过程就不放这了,大概可以自己试着推推看,核心利用根从父节点变为子节点时,原先子节点的子树的深度统统减一,父节点的其他子树的深度加一,利用这个关系可以得到递推式。然后再跑一遍dfs,从父节点开始向子节点不断更新答案数组。下面放放我的代码:

#include<bits/stdc++.h>
#include<queue>
#define db double
#define ll long long
#define ui unsigned int
using namespace std;
const int maxn = 1e6+10;
vector<int>G[maxn];
ll dep[maxn],size[maxn],f[maxn],n;
int read() {
	int x = 0, f = 0, ch = getchar();
	while (!isdigit(ch)) { if (ch == '-')f = 1;ch = getchar(); }
	while (isdigit(ch)) { x = x * 10 + ch - '0';ch = getchar(); }
	return f ? -x : x;
}
void dfs1(int u,int fa){
	for(auto v:G[u]){
		if(v==fa)continue;
		dep[v]=dep[u]+1;
		dfs1(v,u);
		size[u]+=size[v];
		f[u]+=f[v];
	}
	size[u]++;
	f[u]+=dep[u];
}
void dfs2(int u,int fa){
	for(auto v:G[u]){
		if(v==fa)continue;
		f[v]=n-2*size[v]+f[u];
		dfs2(v,u);
	}
}
int main() {
	 n=read();
	for(int i=0;i<n-1;i++){
		int u=read(),v=read();
		G[u].push_back(v);
		G[v].push_back(u); 
	}
	dfs1(1,0);
	dfs2(1,0);
	ll pos=0,maxx=0;
	for(int i=1;i<=n;i++){
		if(f[i]>maxx)
			maxx=f[i],pos=i;
	}
	cout<<pos<<endl;
	return 0;
}

二是点覆盖问题,之所以特地强调这个,主要是因为昨天的杭电多校里面有一道这样的模板题签到,我们队伍之前都还没学树形dp,导致这个签到题没有签出来,非常可惜,所以后来果断恶补了一下这题。回到正题,下面介绍一下点覆盖问题。

点覆盖问题的题意一般就是,在一个树上选择一些结点,每个结点可以点亮相邻的边,问点亮整棵树的边需要的最少结点花费(结点一般有个权值)。这种类型的题就是很模板的树形dp点覆盖问题,比如保安站岗,下面是这题的题目介绍:

五一来临,某地下超市为了便于疏通和指挥密集的人员和车辆,以免造成超市内的混乱和拥挤,准备临时从外单位调用部分保安来维持交通秩序。

已知整个地下超市的所有通道呈一棵树的形状;某些通道之间可以互相望见。总经理要求所有通道的每个端点(树的顶点)都要有人全天候看守,在不同的通道端点安排保安所需的费用不同。

一个保安一旦站在某个通道的其中一个端点,那么他除了能看守住他所站的那个端点,也能看到这个通道的另一个端点,所以一个保安可能同时能看守住多个端点(树的结点),因此没有必要在每个通道的端点都安排保安。

编程任务:

请你帮助超市经理策划安排,在能看守全部通道端点的前提下,使得花费的经费最少。

这种类型的题,通常做法就是设置一个二维的dp数组dp[i][3],前面的i表示第i个结点,后面的3标记这个结点被点亮的方式,有自身点亮、父节点点亮、子节点点亮三种。这个dp的具体含义就是第i个节点以j方式被点亮后,包括子节点也全都点亮所需要的最少花费。

下面我们来考虑一下转移方程,由父节点覆盖的话,说明当前节点没有选择,那么它的子节点只能通过子节点的自己覆盖或者子节点的子节点覆盖,取最小值累加即可。由本身覆盖的话,子节点肯定能够覆盖到,所以任意取子节点的覆盖方式中的最小值累加。重点在于儿子覆盖,由于当前结点由儿子覆盖,那么对于儿子,要么是儿子的儿子覆盖,要么是儿子本身覆盖。假如所有的儿子都是由孙子覆盖,那么之前的结点本身就做不到由儿子覆盖矛盾,所以至少得有一个儿子是本身覆盖,所以同时维护一个变量记录至少一个儿子本身覆盖减去让孙子覆盖的差值,标记如果所有的儿子都由孙子覆盖,则本身加上这个差值。理解了这个逻辑之后,明确了转移方程,那么树形dp的代码很快就可以打完了。

(PS:这里面父节点和自身覆盖的转移方程都很好推,子节点覆盖的情况比较复杂,需要像上面那样特别判断一下,看不懂的话可以找找其他博客,下面我放放我这题的代码)

#include<bits/stdc++.h>
#include<queue>
#define db double
#define ll long long
#define ui unsigned int
using namespace std;
const int maxn = 1e4+10;
vector<int>G[maxn];
ll arr[maxn];
ll dp[maxn][3];//0代表自己覆盖,1代表儿子覆盖,2代表父亲覆盖 
int read() {
	int x = 0, f = 0, ch = getchar();
	while (!isdigit(ch)) { if (ch == '-')f = 1;ch = getchar(); }
	while (isdigit(ch)) { x = x * 10 + ch - '0';ch = getchar(); }
	return f ? -x : x;
}
void dfs(int u,int fa){
	dp[u][0]=arr[u];
	int flag=0;
	ll tem=1e9+7;
	for(auto v:G[u]){
		if(v==fa) continue;
		dfs(v,u);
		dp[u][0]+=min(dp[v][0],min(dp[v][1],dp[v][2]));
		dp[u][2]+=min(dp[v][0],dp[v][1]);
		dp[u][1]+=min(dp[v][0],dp[v][1]);
		if(dp[v][0]<dp[v][1])
			flag=1;//至少有一个儿子是自己点灯
		tem=min(tem,dp[v][0]-dp[v][1]); 
	}
	if(!flag) dp[u][1]+=tem;
}
int main() {
	int n=read();
	for(int i=1;i<=n;i++){
		int u=read();
		arr[u]=read();
		int m=read();
		for(int i=0;i<m;i++){
			int v=read();
			G[u].push_back(v);
			G[v].push_back(u);  
		}
	}
	dfs(1,1);
	cout<<min(dp[1][0],dp[1][1])<<endl;
	return 0;
}

 知道了这个模板之后,稍微改改就可以过杭电的那道题了,我这里也顺便把那道题放出来一下。

City Upgrading

Problem Description:

The city where crazyzhk resides is structured as a tree. On a certain day, the city's network needs to be upgraded. To achieve this goal, routers need to be deployed. Each router covers the node it is placed on and its neighboring nodes. There is a cost ai associated with placing a router at each node. The question is: How can the routers be deployed at minimum cost to ensure that every node is covered?

Input:

The input consists of multiple test cases. The first line contains a single integer t(1≤t≤1000) — the number of test cases. Description of the test cases follows.

The first line of each test case contains two integers n (1≤n≤10e5) — the number of the vertices in the given tree.

The second line of each case are n integers ai(1≤ai≤1e5),denoting the cost of setting up a router at each node.

Each of the next n−1 lines contains two integers u and v (1≤u,v≤n, u≠v) meaning that there is an edge between vertices u and v in the tree.

The data guarantees that the sum of n will not exceed 2⋅e5 

Output:

For each test case print a single integer ——the minimum cost to ensure that every node is covered 

Sample Input:

2 7

13 20 1 20 6 9 8

1 2

1 3

2 4

2 5

3 6

5 7

4

1 17 13 4

1 2

1 3

3 4 

Sample Output: 

27

5

#include<iostream>
#include<vector>
#include<cstring>
#include<queue>
#define db double
#define ll long long
#define ui unsigned int
using namespace std;
const int maxn = 1e5+10;
vector<int>G[maxn];
ll arr[maxn];
ll dp[maxn][3];//0代表自己覆盖,1代表儿子覆盖,2代表父亲覆盖 
int read() {
    int x = 0, f = 0, ch = getchar();
    while (!isdigit(ch)) { if (ch == '-')f = 1;ch = getchar(); }
    while (isdigit(ch)) { x = x * 10 + ch - '0';ch = getchar(); }
    return f ? -x : x;
}
void dfs(int u,int fa){
    dp[u][0]=arr[u];
    int flag=0;
    ll tem=1e9+7;
    for(auto v:G[u]){
        if(v==fa) continue;
        dfs(v,u);
        dp[u][0]+=min(dp[v][0],min(dp[v][1],dp[v][2]));
        dp[u][2]+=min(dp[v][0],dp[v][1]);
        dp[u][1]+=min(dp[v][0],dp[v][1]);
        if(dp[v][0]<dp[v][1])
            flag=1;//至少有一个儿子是自己点灯
        tem=min(tem,dp[v][0]-dp[v][1]); 
    }
    if(!flag) dp[u][1]+=tem;
}
int main() {
    int t=read();
    while(t--){
        int n=read();
        for(int i=1;i<=n;i++)
            arr[i]=read();
        for(int i=1;i<n;i++){
            int u=read(),v=read();
            G[u].push_back(v);
            G[v].push_back(u);  
        }
        dfs(1,1);
        cout<<min(dp[1][0],dp[1][1])<<endl;
        memset(dp,0,sizeof(dp));
        for(int i=1;i<=n;i++) G[i].clear();
    }
    return 0;
}

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值