032动态规划之区间DP——算法备赛

区间DP

区间DP是常见的应用场景。区间DP也是线性DP,它把区间当成DP的阶段,用区间的两个端点描述状态和处理状态的转移。

区间DP的主要思想是先在小区间得到最优解,然后合并 到大区间,合并的操作一般是两个相邻子区间的合并,往往需要遍历一个k,找到最优解的中间点 大区间 i,j --> i,k k+1,j

石子合并

问题描述

n堆石子排成一排,每堆石子有一定的数量。将n堆石子合并成一堆,每次只能合并相邻的两堆石子,合并的花费为这两堆石子的总数。进过n-1次合并后成为一堆,求最小总花费。

思路分析

定义dp[i][j]为合并第 i 堆至第 j 堆的最小花费。

状态转移方程为 dp[i][j] = min{ dp[i][k] + dp[k+1][j] + w[i][j] } i<=k<j

w[i][j] 表示从第 i 堆至第 j 堆石子的总数。

代码

cpp 复制代码
//初始dp都为0
for(int i=2;i<=n;i++){
    for(int j=i-1;j>=1;j--){
        dp[j][i]=INT_MAX;
        for(int k=j;k<i;k++){
			dp[j][i] = min(dp[j][i],dp[j][k]+dp[k+1][i] + w[j][i]); 
        }  //区间和可以利用前缀和来实现 w[j][i]=sum[i]-sum[i-1];
    }
}

另一种写法,它的i,j,k都是递增的,更易于理解。

代码

cpp 复制代码
for(int len=2;len<=n;len++){
    for(int i=1;i<n-len;i++){  //len为区间长度 i为左端点,j为右端点
		int j=i+len-1;
        dp[i][j]=INT_MAX;
        for(int k=i;k<j;k++){
			dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j] + w[i][j]);  
        }  //或者把+w[i][j]操作放在循环外
    }
}

上述代码的复杂度为 O(n3) ,不太好。区间DP常常利用四边形不等式优化。

🚀四边形不等式优化

用一个数组s[i][j]记录i~j的最优分割点,在下一次循环中可利用其减小 k 的寻找范围

代码

cpp 复制代码
for(int i=1;i<=n;i++) s[i][i]=i;
for(int len=2;len<=n;i++){
	for(int i=1;i<n-len;i++){
		int j=i+len-1;
        dp[i][j]=INT_MAX;
        for(int k=s[i][j-1];k<=s[i+1][j];k++){  //缩小寻找区间。
			if(dp[i][j]>dp[i][k]+dp[k+1][j]){
                dp[i][j]=[i][k]+dp[k+1][j];
                s[i][j]=k;  //更新最佳分割点。
            }
        }
        dp[i][j]+=w[i][j];
    }
}

复杂度由O(n3)降到O(n2)证明略

图示

四边形不等式的定义1 :设w是定义在整数集合的二元函数,对于任意的 i<=i'<=j<=j',

如果有w(i,j)+w(i',j')<= w(i,j')+w(i',j),

则称w满足四边形不等式。

图示可理解为四边形不等式

对角线长度 ij+i'j'大于平行线长度 ij'+i'j

定义1的特例是定义2

四边形不等式的定义2 :对于整数 i<i+1<=j<j+1 如果有w(i,j)+w(i+1,j+1)<= w(i,j+1)+w(i+1,j)

则称w满足四边形不等式。

单调性 :如果对任意整数 i<=i'<=j<=j' ,有w( i , j' )>=w( i' , j ),称w具有单调性。

对于wij满足四边形不等式和单调性的,可以采用四边形不等式定理优化dp,时间复杂度为O(n^2);

定理1dpij=min(dpij,dpik+dpk+1j + wij) ,如果wij满足四边形不等式和单调性,则dpij也满足四边形不等式

定理2记sij=k 为dpij取得最优时的k,如果dpij满足四边形不等式,那么有 sij-1<=sij<=si+1j

证明过程复杂,这里就不赘述了,详情参阅《算法竞赛》--罗勇军 郭卫斌著 第5章10.4节。

戳气球

问题描述

n 个气球,编号为0n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。

现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 分数。 这里的 i - 1i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1i + 1 超出了数组的边界,那么就当它是一个数字为 1 的气球。

求所能获得分数的最大数量。

思路分析

定义f[i][j]表示戳破[i...j]区间的气球所能获得的最大分数,

状态转移方程为dp[i][j] = max{ d[i][k-1]+d[k+1][j]+nums[i-1]*nums[k]*nums[j+1] } i<=k<j

因为数组边界问题,应在原数组前后各插入一个 1 以方便计算。

注意:在计算dp[i][j]之前d[i][k-1]d[k+1][j]都要先算出,因此可以设计左边界i从右往左遍历,右边界j从左往右遍历,

当i>j时,dp[i][j]等于0

代码

cpp 复制代码
int maxCoins(vector<int>& nums) {
        int f[310][310];
        // f[i][j] 代表戳破以 i , j 为边界的所有气球获得的最大价值
        memset(f,0,sizeof f);
    
        vector<int> cur;
        cur.push_back(1);
        cur.insert(cur.end(),nums.begin(),nums.end());
        cur.push_back(1);
    
        int n = nums.size();
        if(n==0) return 0;
        for(int i=n;i>0;i--){
            for(int j=i;j<=n;j++){
                for(int k=i;k<=j;k++){
                    f[i][j]=max(f[i][j],f[i][k-1]+f[k+1][j]+cur[i-1]*cur[k]*cur[j+1]);
                }
            }
        }
        return f[1][n];
    }

数位翻转

蓝桥杯2024年国赛题

问题描述

小明创造了一个函数f(x)用来翻转x的二进制的数位(无前导0)。比如f(11)=13,小明随机出了一个长度为n的整数数组{a1,a2,,...an},他想知道在这个数组中最多选m个不相交的区间,将这些区间内的二进制数位翻转(将ai变为f(ai))后,整个数组的最大和是多少?

原题链接

思路分析

首先将数组中的所有数都预处理,计算出一个d数组,di存储的是f[ai]-ai

原问题转化一个子问题:从di中选最多m个不相交的区间,这些区间内所有数的和最大为多少?该问题的值再加上sum(sum为a数组的总和)就是原问题答案。

现在来思考这个子问题:如果贪心或者模拟来求具体是哪几个区间是不容易的,因为区间不能重叠,要考虑的细节很多。可以使用动态规划来解决。

定义dp[i][j]表示前 i 个数取最多 j 个不相交的区间的最大总和。枚举到i时,计算dp[i][j],有两种选择:

  • 不选最后一个下标i的后缀区间,dp[i][j]=dp[i-1][j]
  • 选最后一个下标i的后缀区间,dp[i][j]=max(dp[k-1][j-1]+suffix(k,i)suffix(k,i)表示d的子数组[k,i]的区间和。

状态转移方程为:dp[i][j]=max(dp[i-1][j],max(dp[k-1][j-1]+suffix(k,i)))

时间复杂度O(mn^2)

代码

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
ll getDiff(int x) {
    int p = x;
    int tr = 0;
    while (x) {
        tr <<= 1;
        if (x & 1) tr |= 1;
        x >>= 1;
    }
    return tr - p;
}

int main()
{
    int n, m;
    cin >> n >> m;
    vector<ll>d(n + 1);
    vector<vector<ll>>dp(n + 1, vector<ll>(m+1));  //dp[i][j]表示前i个数取最多j个不相交的区间的最大总和
    ll sum = 0;
    for (int i = 1; i <= n; i++) {
        int t; cin >> t;
        sum += t;
        d[i] = getDiff(t);
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) dp[i][j] = dp[i - 1][j];  //不选从i开始的后缀子数组
        for (int j = 1; j <= m; j++) {
            ll suffix = 0;
            for (int k = i; k >= 1; k--) {
                suffix += d[k];
                dp[i][j] = max(dp[i][j], dp[k - 1][j - 1] + suffix);  //选从i开始的后缀子数组
            }
        }
    }
    cout << sum + dp[n][m];
    return 0;
}
相关推荐
不会代码的小猴2 小时前
7. JSON
开发语言·c++·笔记·qt·算法·json
怪奇云呼军2 小时前
知识库也会注入指令?闪电智能VoiceAgent 如何防住 Prompt Injection
人工智能·python·算法·云计算·音视频
阿里云大数据AI技术3 小时前
基于 EMR Serverless Ray 实现 Qwen 模型批量推理实践
人工智能·算法·agent
Rambo.xia3 小时前
为什么去马赛克算法,决定了ISP的画质上限
算法·接口隔离原则
Benny_Tang4 小时前
题解:P10230 [COCI 2023/2024 #4] Lepeze
c++·算法
Geek-Chow4 小时前
06 训练管线:数据如何变成权重
人工智能·算法
我变成萤火虫4 小时前
反悔贪心(经典题目讲解)
c++·算法·贪心算法·stl·排序算法·反悔贪心
nike0good4 小时前
April Fools Day Contest 2026 题解
算法·题解·愚人节比赛