区间DP详解

1、acwing 282 石子合并 区间dp基础题

设有 N 堆石子排成一排,其编号为 1,2,3,…,N。

每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。

例如有 4 堆石子分别为 1 3 5 2, 我们可以先合并 1、2堆,代价为 1+3=4,得到 4 5 2

第二步是先合并 2,3 堆,则代价为 5+2=7,得到 4 7

最后一次合并代价为 4+7=11,总代价为 4+7+11=22。

问题是:找出一种合理的方法,使总的代价最小,输出最小代价。

思考方式:闫氏DP分析法
核心思想:考虑最后一次合并的状态转换:必定是由左边连续的一部分 + 右边连续的一部分
1、 d p [ i ] [ j ] dp[i][j] dp[i][j]: 表示将区间 i 到 j 的石子合并成一堆的最小值

集合:区间i到j的石子的合并方案,属性:min

2、状态转移方程:
d p [ i ] [ j ] = d p [ i ] [ k ] + d p [ k + 1 ] [ j ] + ∑ i j a [ i ] dp[i][j] = dp[i][k] + dp[k+1][j] + \sum_{i}^{j}a[i] dp[i][j]=dp[i][k]+dp[k+1][j]+ija[i]
其中连续的区间和: s u m [ j ] − s u m [ i − 1 ] sum[j] - sum[i-1] sum[j]sum[i1], 前缀和预处理
3、区间DP的初始化:
  • 所有 d p [ i ] [ j ] dp[i][j] dp[i][j] = INF, i != j
  • d p [ i ] [ i ] = 0 dp[i][i] = 0 dp[i][i]=0, 区间为1的合并消耗的都为0
4、枚举方式:复杂度 O(n^3)
  • 枚举len的区间长度:一般从2到n
    • 枚举左端点 i 从1开始, 右端点:j = i + len - 1 , 并且保证 j <= n
      • 枚举 k 值 ,在 i 到 j 之间
AC代码:
#include <bits/stdc++.h>
using namespace std;

int n,a[310],sum[310],dp[310][310];
//dp[i][j]:合并从i到j的石子的区间最值

int main(){
    cin>>n;
    for (int i = 1; i <= n; i ++ ) cin>>a[i], sum[i] = sum[i-1] + a[i];
    memset(dp, 0x3f, sizeof(dp));
    for (int i = 1; i <= n; i ++ ) dp[i][i] = 0;  //区间为 len = i - i + 1
    
    for(int len = 2; len<=n ;len++){
        for (int i = 1; i+len-1 <= n; i ++ ){
            int j = i + len - 1;
            for(int k = i; k<=j; k++){
                dp[i][j] = min(dp[i][j] , dp[i][k] + dp[k+1][j] + sum[j] - sum[i-1]);
            }
        }
    }
    
    cout<<dp[1][n];
}

2、AcWing 1068. 环形石子合并 环形DP问题

将 n 堆石子绕圆形操场排放,现要将石子有序地合并成一堆。

规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。

请编写一个程序,读入堆数 n 及每堆的石子数,并进行如下计算:

  • 选择一种合并石子的方案,使得做 n−1 次合并得分总和最小。
  • 选择一种合并石子的方案,使得做 n−1 次合并得分总和最大。
输入样例:
4
4 5 9 4
输出样例:
43
54
思考方式:尽可能转化为链式的思路(加长一段)

在这里插入图片描述

加长一段后:所有可能的环形合并石子的方式都映射在新的链式问题上了
最后只需要遍历一遍,即可得到最后的值,复杂度: O(n^3)
AC代码:
#include <bits/stdc++.h>
using namespace std;

const int N = 410;

int n,f[N][N], g[N][N], a[N], sum[N];    //f:min  , g:max

int main()
{
    cin>>n;
    for (int i = 1; i <= n; i ++ ) cin>>a[i], sum[i] = sum[i-1] + a[i];
    for(int i=n+1; i<=2*n; i++) sum[i] = sum[i-1] + a[i-n];
    memset(f,0x3f,sizeof(f));
    memset(g,-0x3f,sizeof(g));
    for (int i = 1; i <= 2*n; i ++ ) f[i][i] = g[i][i] = 0;
    
    for(int len = 2; len<=n; len++){
        for (int i = 1; i+len-1 <= 2*n; i ++ ){
            int j = i + len - 1;
            for(int k=i;k<=j;k++){
                f[i][j] = min(f[i][j] , f[i][k] + f[k+1][j] + sum[j] - sum[i-1]);
                g[i][j] = max(g[i][j] , g[i][k] + g[k+1][j] + sum[j] - sum[i-1]);
            }
        }
    }
    
    int minn = 0x3f3f3f, maxn = -0x3f3f3f;
    
    for (int i = 1; i <= n; i ++ ){
        minn = min(minn , f[i][i+n-1]);
        maxn = max(maxn , g[i][i+n-1]);
    }
    
    cout<<minn<<endl;
    cout<<maxn<<endl;
}

3、Acwing 320 能量项链 环形DP

在 Mars 星球上,每个 Mars 人都随身佩带着一串能量项链,在项链上有 N 颗能量珠。

能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。

并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。

因为只有这样,通过吸盘(吸盘是 Mars 人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。

如果前一颗能量珠的头标记为 m,尾标记为 r,后一颗能量珠的头标记为 r,尾标记为 n,则聚合后释放的能量为 m×r×n(Mars 单位),新产生的珠子的头标记为 m,尾标记为 n。

需要时,Mars 人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。

显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。

例如:设 N=4,4 颗珠子的头标记与尾标记依次为 (2,3)(3,5)(5,10)(10,2)。

我们用记号 ⊕ 表示两颗珠子的聚合操作,(j⊕k)表示第 j,k 两颗珠子聚合后所释放的能量。则

第 4、1两颗珠子聚合后释放的能量为:(4⊕1)=10×2×3=60。

这一串项链可以得到最优值的一个聚合顺序所释放的总能量为 ((4⊕1)⊕2)⊕3)=10×2×3+10×3×5+10×5×10=710。

解法:同第二题

状态转移方程稍微修改一下:

f[i][j] : 区间i到j石子最大能产生的能量
f[i][j] = max(f[i][k] + f[k+1][j] + a[i].l * a[k].r * a[j].r , f[i][j]);
AC代码:
#include <bits/stdc++.h>
#define ll long long
using namespace std;

const int N = 210;

struct node{
    ll l,r;
}a[N];
ll n;
ll f[N][N];    //区间i到j石子最大能产生的能量

// f[i][j] = max(f[i][k] + f[k+1][j]  + a[i].l * a[k].r * a[j].r , f[i][j]); 

int main(){
    cin>>n;
    for (int i = 1; i <= n; i ++ ) cin>>a[i].l;
    for (int i = 1; i <= n; i ++ ) a[i].r = a[i+1].l;
    a[n].r = a[1].l;
    for (int i = n+1; i <= n*2; i ++ ) a[i].l = a[i-n].l , a[i].r = a[i-n].r;
    //初始化
    memset(f, -0x3f3f3f3f , sizeof(f));
    for (int i = 1; i <= n*2; i ++ ) f[i][i] = 0;
    
    for(int len = 2; len <= n; len++){
        for(int i=1; i + len - 1 <= 2*n; i++){
            int j = i + len - 1;
            for(int k = i; k<=j ;k++){
                f[i][j] = max(f[i][k] + f[k+1][j]  + a[i].l * a[k].r * a[j].r , f[i][j]); 
            }
        }
    }
    
    ll maxn = -0x3f3f3f3f;
    for (int i = 1; i <= n; i ++ ) maxn = max(maxn, f[i][i+n-1]);
    cout<<maxn<<endl;
    
}

4、acwing 479 加法二叉树 中序遍历区间DP

设一个 n 个节点的二叉树 tree 的中序遍历为(1,2,3,…,n),其中数字 1,2,3,…,n 为节点编号。

每个节点都有一个分数(均为正整数),记第 i 个节点的分数为 di,tree 及它的每个子树都有一个加分,任一棵子树 subtree(也包含 tree 本身)的加分计算方法如下:

subtree的左子树的加分 × subtree的右子树的加分 + subtree的根的分数

若某个子树为空,规定其加分为 1。

叶子的加分就是叶节点本身的分数,不考虑它的空子树。

试求一棵符合中序遍历为(1,2,3,…,n)且加分最高的二叉树 tree。

要求输出:

(1)tree的最高加分

(2)tree的前序遍历

思路:求一个树根的分数 = max(左子树*右子树+自己分数)
尝试用区间dp来解:前提是已知中序遍历:左-根-右
d p [ i ] [ j ] dp[i][j] dp[i][j]:从第i个结点到第j个结点的最大得分

状态转移方程:k为root时

d p [ i ] [ j ] = m a x (   d p [ i ] [ j ]   ,   d p [ i ] [ k − 1 ] ∗ d p [ k + 1 ] [ j ] + w [ k ]   ) dp[i][j] = max( \ dp[i][j] \ , \ dp[i][k-1] * dp[k+1][j] + w[k] \ ) dp[i][j]=max( dp[i][j] , dp[i][k1]dp[k+1][j]+w[k] )

重点注意下:初始化

d p [ i ] [ i ] = w [ i ]   ,   r o o t [ i ] [ i ] = i dp[i][i] = w[i] \ ,\ root[i][i] = i dp[i][i]=w[i] , root[i][i]=i

AC代码:
#include <bits/stdc++.h>
using namespace std;

const int N = 40;

int dp[N][N] , root[N][N];
int w[N],n;
vector<int> v;

void dfs(int l, int r){
	if(l > r) return;
    int k = root[l][r];
    v.push_back(k);
    if(k!=l) dfs(l, k-1);
    if(k!=r) dfs(k+1, r);
}

int main(){
    cin>>n;
    for (int i = 1; i <= n; i ++ ) cin>>w[i];
    for (int i = 1; i <= n; i ++ ) dp[i][i] = w[i], root[i][i] = i;
    for(int len = 2; len <= n; len ++){
        for (int i = 1; i+len-1 <= n; i ++ ){
            int j = i + len - 1;
            for(int k=i; k<=j ;k++){
                int left = (k==i) ? 1 : dp[i][k-1];
                int right = (k==j) ? 1 : dp[k+1][j];
                int x = left * right + w[k];
                if(dp[i][j] < x ){
                    dp[i][j] = x;
                    root[i][j] = k;
                }
            }
        }
    }
    
    dfs(1,n);    
    cout<<dp[1][n]<<endl;
     for (int i = 0; i < v.size(); i ++ ) cout<<v[i]<<" ";
}

补题:2021蓝桥杯国赛C:最小权值

拖了很久了,很可惜,当时一直考虑完全二叉树各类性质,应该用DP的思想去解决!

题面:对于一棵有根二叉树 T,小蓝定义这棵树中结点的权值 W(T) 如下:

空子树的权值为 0

如果一个结点 v 有左子树 L, 右子树 R,分别有 C(L) 和 C(R) 个结点,则

  • W ( v ) = 1 + 2 W ( L ) + 3 W ( R ) + ( C ( L ) ) 2 C ( R ) W(v) = 1 + 2W(L) + 3W(R) + (C(L))^2 C(R) W(v)=1+2W(L)+3W(R)+(C(L))2C(R)

树的权值定义为树的根结点的权值。

小蓝想知道,对于一棵有 2021 个结点的二叉树,树的权值最小可能是多少?

#include <bits/stdc++.h>
#define ll long long
using namespace std;

ll dp[5000];

//dp[i]:结点个数为i的树,根节点的最小权值
//dp[i] = min(dp[i] , 1 + 2*dp[j] + 3*dp[i-j] + j*j*(i-j)); 

int main(){
	memset(dp, 0x3f3f3f3f, sizeof(dp));
	dp[0] = 0;
	for(int i=1;i<=2021;i++){
		for(int j=0;j<i;j++){
			dp[i] = min(dp[i] , 1 + 2*dp[j] + 3*dp[i-j-1] + j*j*(i-j-1)); 
		}
	}
	cout<<dp[2021]<<endl;
} 

运行结果:2653631372

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值