Dynamic Programming从入门到放弃 —— 第三章:区间DP + 树形DP(Acwing石子合并+Acwing没有上司的舞会)

区间DP

区间DP祖宗题开始区间DP

原题链接 Acwing 282 石子合并

分析
对于本题,假设使用一维数组来设计状态和状态转移方程。如果使用正推的思想,不妨设dp[i]表示从第1堆石头合并到第i堆石头的最小花费。设石堆的质量依次为1、4、2、5、3,那么:

  • dp[1]表示从第1堆石头合并到第1堆石头,dp[1] = 0
  • dp[2]表示从第1堆石头合并到第2堆石头,dp[2] = 1 + 4 = 5
  • 当递推到dp[3]的时候,表示从第1堆石头合并到第3堆石头,如果先合并前两堆石头,再合并第3堆,dp[3]尚且可以表示为dp[2] + a[3],但如果先合并第2、3堆石头,最后与第1堆石头合并,很显然,表示dp[3]就显得极其麻烦了
  • 由此得到结论,一维数组绝不是解决问题的最好方法

正确的设计
1、设计状态
dp[i][j]:表示合并第i堆到第j堆石子的最小花费

2、状态转移方程:
① 当i == j

当i == j时,dp[i][j] = 0//合并第i堆到第i堆,即不用合并

② 当i != j时,选取一个中间值k,将区间[i,j]分为了两个部分,分别为区间[i,k]和区间[k + 1,j]dp[i][j]等于合并左半区间所需要的代价与合并右半区间所需要的代价之和的最小值,再加上最后一步合并左右两个区间所需要的代价
在这里插入图片描述
当然,上图中的k放在了2与5之间,其实,k也可以放在1与4、4与2、5与3之间。即k需要枚举。

状态转移:

dp[i][j] = Min{dp[i][k] + dp[k + 1][j] + sum(i,j)}(i <= k <= j-1) 

其中,sum(i,j)表示第i堆石头到第j堆石头的质量之和,用前缀和表示

思考:k为什么要小于等j - 1???
答:若k == j,则k + 1 > j,与题意矛盾

举例:
对于上图,石堆的质量分别为1、4、2、5、3。

  • 当区间长度为1的时候,dp[i][j] = 0;
  • 当区间长度为2的时候
    dp[1][2] = 5,dp[2][3] = 6,dp[3][4] = 7,dp[4][5] = 8
  • 当区间长度为3的时候,如何计算dp[1][3]?

在计算dp[1][3](合并前3堆石头需要的最小代价)的时候,需要枚举k的位置。

  • k = 1,即第1堆石头单独作为一堆,然后将2、3两堆石头合并,【1,4,2】变为【1,6】,最后再合并一次,得到【7】,这种方法需要的代价为6 + 7 = 13。
  • k = 2,即前2堆石头作为一堆,然后与第3堆石头合并,【1,4,2】变为【5,2】,最后再合并一次,得到【7】,这种方法需要的代价为5 + 7 = 12。
  • 比较k = 1k = 2时计算出的dp[1][3],取其最小值,不难发现dp[1][3] = 12
  • 通过以上思路,不断枚举区间长度,区间起点,区间的分割点,就可以得到最后的dp[1][n],代表合并第1堆到第n堆石头所需要的最小代价。

区间DP模板:
时间复杂度:O(n^3),数据范围n <= 300

for (int len = 2;len <= n;len++)//用来枚举区间长度(长度为1 -> 合并1堆石子 -> 代价为0)
{
    for (int l = 1;l <= n;l++)//枚举区间起点
    {
        int r = l + len - 1;//计算区间终点(起点与长度不同,终点不同)
        if (r > n) break;//终点越界,结束
        for (int k = l;k <= r;k++)//枚举分割点
        {
            //大问题通过小问题解决
            dp[l][r] = min(dp[l][r],dp[l][k] + dp[k + 1][r] + w[l][r]);
        }
    }
}

区间DP核心思想
在区间上进行动态规划,从小区间推导大区间,先在小区间进行DP得到小区间的最优解,然后通过合并小区间的最优解,进而求出大区间的最优解。先算区间长度为1的情况,再算区间长度为2的情况… …再算区间长为len的情况。

区间DP小结:
① 阶段的划分:区间长度(先计算出区间长度为1的最优解,通过这个最优解计算出所有区间长度为2的最优解…进而得到区间长度为n的最优解)
② 状态的表示:枚举起点,起点不同,状态不同
③ 决策的实现:枚举分割点

Acwing 282 AC代码

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 310;
const int inf = 0x3f;
int a[N];//用来存储石堆的质量
int preSum[N];//preSum[i]表示前i堆的石堆质量之和
int dp[N][N];//dp[i][j]表示合并第i到第j堆需要的最小代价

int main()
{
    memset(dp,inf,sizeof dp);//取最小值的时候将其初始化为无穷大
    int n;
    cin >> n;
    for (int i = 1;i <= n;i++) 
    {
        cin >> a[i];
        preSum[i] = preSum[i - 1] + a[i];//前缀和
        dp[i][i] = 0;//初始化dp数组,表示区间长度为1的情况
    }
    
    //区间dp模板
    for (int len = 2;len <= n;len++)//用来枚举区间长度(长度为1 -> 合并1堆石子 -> 代价为0)
    {
        for (int l = 1;l <= n;l++)//枚举区间起点
        {
            int r = l + len - 1;//计算区间终点(起点与长度不同,终点不同)
            if (r > n) break;//终点越界,结束
            for (int k = l;k <= r;k++)//枚举分割点
            {
                //大问题通过小问题解决
                //preSum[r] - preSum[l - 1]表示区间[l,r]的石堆的质量之和
                dp[l][r] = min(dp[l][r],dp[l][k] + dp[k + 1][r] + preSum[r] - preSum[l - 1]);
            }
        }
    }
    cout << dp[1][n];
    return 0;
}
树形DP

与区间DP一样,以一道例题开始树形DP

原题链接 Acwing 285 没有上司的舞会

题意分析:
根据题意,1、2是3的直接下属,6、7是4的直接下属,3、4是5的直接下属。故画出如下的一棵二叉树:
在这里插入图片描述
状态设计与状态转移方程:

  • dp[u][0]表示在u不参加的情况下,以u为根结点的子树的欢乐值之和的最大值。
  • dp[u][1]表示在u参加的情况下,以u为根结点的子树的欢乐值之和的最大值。
  • 当结点u参加时,dp[u][1]等于结点u的欢乐值happy[u]、u的左结点不参加的情况下左子树的最大欢乐值dp[u -> lchild][0]、u的右结点不参加的情况下右子树的最大欢乐值dp[u -> rchild][0]三者之和,即:
dp[u][1] = happy[u] + dp[u -> lchild][0] + dp[u -> rchild][0]
  • 需要注意的是,当一个P结点的直接上司Q没有去的时候,该结点即可以去,也可以不去,有两种选择。也就是说,当结点u没有去的时候,u的左孩子去或者不去两种情况中的较大值max1、u的右孩子去或者不去两种情况中的较大值max2,dp[u][0] = max1 + max2,即:
dp[u][0] = max(dp[u -> lchild][0],dp[u -> lchild][1]) + max(dp[u -> rchild][0],dp[r -> lchild][1])
  • 最后的答案为max{dp[root][0],dp[root][1]}

没有上司的舞会 AC代码:

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int n;
const int N = 10000;
vector<int> v[N];//v[i]存放i的孩子
int happy[N];//用来存储每个结点的欢乐值
int dp[N][2];
//f[i][0]:根结点i没去的情况下欢乐值之和的最大值
//f[i][1]:根结点i去了舞会的情况下欢乐值之和的最大值
int vis[N];//vis[i]=0表示i为父亲结点,vis[i]=1表示i为孩子结点

void dfs(int x)//求以x为根结点的最优解
{
    dp[x][0] = 0;//结点x没去舞会
    dp[x][1] = happy[x];//结点x去了舞会
    for (int i = 0;i < v[x].size();i++)//找x的孩子结点,v[x].size()代表x结点的孩子个数
    {
        int y = v[x][i];//y就是x的孩子
        dfs(y);
        dp[x][0] += max(dp[y][0],dp[y][1]);//长官没去舞会,其下属可以去也可以不去,取最优解
        dp[x][1] += dp[y][0];//长官去了舞会,其下属不去舞会
    }
}

int main()
{
    cin >> n;
    for (int i = 1;i <= n;i++) cin >> happy[i];
    for (int i = 1;i <= n - 1;i++)
    {
        int l,k;
        cin >> l >> k;
        vis[l] = 1;//表示l是孩子结点
        v[k].push_back(l);//k的孩子是l 
    }
    //查找根结点
    int root = -1;
    for (int i = 1;i <= n;i++)
        if (vis[i] == 0)
        {
            root = i;
            break;//一棵树只有一个根结点
        }
        
    dfs(root);//从根结点开始搜索结果
    cout << max(dp[root][0],dp[root][1]);
    return 0;
}
对于vector的语法补充:
  • vector<int> v[N];//定义的是一个二维数组(可以连续push多个k的孩子节点,即k可以对应多个孩子,如L1、L2、L3)
  • v[x][i]v[x][i + 1]表示x节点的两个孩子节点
vector<int> v[N]vector<int> v(N)的区别:
  • 前者有N个vector,暂时没存储结点,等建树的时候push结点
  • 后者是一个变量vector,里面开了N个空间

前者的存储方式可以联想链表,如图所示
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值