ICode9

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

树上DP模版题 重建道路

2021-07-25 20:01:41  阅读:167  来源: 互联网

标签:树上 int 模版 sum 子树 DP now 节点 dp


树上DP

模版题重建道路

https://www.luogu.com.cn/problem/P1272

题意:

有n个点,n-1条边 , 求最少去掉多少条边会使使一颗含有P个点的子树和其他节点分离。

分析:

通过读题我们可以知道这个一颗树,含有根节点,无环,那么我们可以从上往下或从下往上的来思考。

我们可以维护一个二维数组dp [i] [j] ,i表示子树的根节点,j表示子树的节点数量, dp [i] [j] 表示获得节点数量为j且根节点为i的子树所需要删除的边的个数。

对于一个子树的根节点s,该子树含有n个点,我们用c(vi)表示在以s的子节点vi为根的子树内保留的点的数量。

那么有
n = ∑ c ( v i ) + 1   - - - - - - - - - - - 1 n = \sum c(v_i)+1 -----------1 n=∑c(vi​)+1 -----------1
后面的加1是因为要包括根节点。

同时我们用d(vi) 表示在以s的子节点vi为根的子树内删除的边的数量

那么有
d p [ s ] [ n ] = ∑ d ( v i )   - - - - - - - - 2 dp[s][n] = \sum d(v_i) --------2 dp[s][n]=∑d(vi​) --------2
因此,我们要做的就是在子树的根节点s上找到一种方式在满足1式的同时使2式最小。

这个就类似与背包问题。装满一定容量的背包使价值最大。那么类比1个背包的n个物件,这里有n个节点,也就是n颗子树,每个子树n_i含有k个子节点。

那么背包问题是 dp[i] [j]

同理以第n_i个节点为根的子树来说有 dp[n_i] [i] [j] , n_i表示根节点的编号,i表示这个根节点的第i个子树,j表示以n_i为节点的子树在第i个节点的子树上保留了j个点,dp[n_i] [i] [j]表示删除了多少条边。

为了和上面的dp [i] [j]保持一致,这里统一用 dp [i] [k] [j] , i 表示根节点的编号, k 表示这个根节点下的第 k个子树 , j表示在 第k个子树下保留了 j个节点 。 dp[i] [k] [j]表示删除了多少条边。

那么状态转移方程式就可以写出来了:
d p [ i ] [ k ] [ j ] = m i n ( d p [ i ] [ k − 1 ] [ j ] + 1 , d p [ i ] [ k − 1 ] [ j − s k ] + d p [ k ] [ s k ] ) dp[i][k][j] = min(dp[i][k-1][j]+1 , dp[i][k-1][j-s_k] + dp[k][s_k]) dp[i][k][j]=min(dp[i][k−1][j]+1,dp[i][k−1][j−sk​]+dp[k][sk​])
解释:

dp[i] [k] [j]

以i为根节点的子树,它在第k个子节点生成的子树上保留j个点 ,要删除的边数

dp[i] [k-1] [j]

以i为根节点的子树,它在第k个子节点生成的子树上保留j个点 ,要删除的边数 。

这里为什么要加1呢, 类比背包问题,背包问题dp[i] [j] 可以等于 dp[i-1] [j] 表示第i件我不拿,这里也是一个意思,表示我不从第k个子树里保留节点 ,那么我就要把 i 和k之间的边断开,因此要加1

dp[i] [k-1] [j-s_k] + dp[k] [s_k]

这里的s_k是一个枚举量,总体的意思是,我从第k-1颗子树里取j-s_k 个点 并且从 第k颗子树里取s_k 个点凑出j个点。删除的边数也是两边凑出来。

同时我们可以从方程中看出第i个子树的状态要从它的子树转移来,因此整颗树要从低往上dp。

核心代码

void dfs(int now ,int fa){
	sum[now] = 1 ,dp[now][0][1] = 0;
	//sum[now]表示子树的节点数量,一开始只有根节点,所以1个
	//dp[now][0][1] 表示dp过程的初始量,没有任何实际意义是一个过程量,它和只保留1个点要删除的边的数量不是一回事,这个应该是dp[now][cnt[now]][1] ,其中cnt[now]表示当前节点的子节点的数量
	int k = 1;//第k颗子树
	for(int i=0;i<edge[now].size();i++){
		int to = edge[now][i];
		if(to==fa)continue;
		dfs(to,now);
		sum[now]+=sum[to]; //累加子树的节点数量
		for(int j = sum[now];j>0;j--){
			dp[i][k][j] = dp[i][k-1][j]+1;//状态转移前者
			for(int h =0;h<=min(sum[to],j-1);h++){
				dp[i][k][j] = min(dp[i][k][j] , dp[i][k-1][j-h] +dp[to][cnt[to]][h]);
				//状态转移后者
			}
		}
		k++;
	}
}

优化部分

既然树上dp可以类比背包问题,那么一定可以像背包减少一维,这里就可以减掉第二维,采用滚动数组的方式

void dfs(int now, int p)
{
  sum[now] = 1, dp[now][1] = 0;
  for (int i = 0; i < ve[now].size(); i++)
  {
    int to = ve[now][i];
    if (to == p)
      continue;
    dfs2(to, now);
    // debug(now);
    sum[now] += sum[to];
    // debug(sum[now]);
    for (int j = sum[now]; j >0; j--)
    {
      dp[now][j]++;//k被滚动掉了
      for (int k = 1; k <= min(sum[to], j - 1); k++)
      {
        dp[now][j] = min(dp[now][j], dp[now][j - k] + dp[to][k]);
      }
    }
  }
}

完整的代码

#include <algorithm>
#include <cstring>
#include <iostream>
#include <stdlib.h>
#include <vector>
using namespace std;
typedef long long ll;
#define debug(x) cout << #x << " :" << x << endl;
const int maxn = 1e3;
const int INF = 1e9;
int n, p;
vector<int> ve[maxn];
int ru[maxn];
int sum[maxn];
int dp[maxn][maxn];

void dfs2(int now, int p)
{
  sum[now] = 1, dp[now][1] = 0;
  for (int i = 0; i < ve[now].size(); i++)
  {
    int to = ve[now][i];
    if (to == p)
      continue;
    dfs2(to, now);
    // debug(now);
    sum[now] += sum[to];
    // debug(sum[now]);
    for (int j = sum[now]; j >0; j--)
    {
      dp[now][j]++;
      for (int k = 1; k <= min(sum[to], j - 1); k++)
      {
        dp[now][j] = min(dp[now][j], dp[now][j - k] + dp[to][k]);
      }
    }
  }
}
int main()
{
  std::ios::sync_with_stdio(false);
  std::cin.tie(0);
  int _ = 1;
  //cin >> _;
  while (_--)
  {

    cin >> n >> p;
    int t1, t2;
    for (int i = 1; i <= n; i++)
    {
      for (int j = 1; j <= n; j++)
      {
        dp[i][j] = INF;
      }
    }
    for (int i = 1; i < n; i++)
    {
      cin >> t1 >> t2;
      ve[t1].push_back(t2);
      ve[t2].push_back(t1);
      ru[t2]++;
    }
    int root = 0;
    for (int i = 1; i <= n; i++)
    {
      if (!ru[i])
      {
        root = i;
        break;
      }
    }
    // dfs1(1, 0);
    // init();
    dfs2(1, 0);
    int ans = dp[1][p];
    for (int i = 2; i <= n; i++)
    {
      ans = min(ans, dp[i][p] + 1);
    }
    cout << ans << endl;
  }
}

标签:树上,int,模版,sum,子树,DP,now,节点,dp
来源: https://blog.csdn.net/yin101_/article/details/119086640

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

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

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

ICode9版权所有