目录
简介
区间DP在leetcode上也有体现(主要是合并石子类型),本文就leetcode上出现的区间DP做一个总结。
题目原型:
5301 石子合并 0x50「动态规划」例题
描述
设有N堆沙子排成一排,其编号为1,2,3,…,N(N<=300)。每堆沙子有一定的数量,可以用一个整数来描述,现在要将这N堆沙子合并成为一堆,每次只能合并相邻的两堆,合并的代价为这两堆沙子的数量之和,合并后与这两堆沙子相邻的沙子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同,如有4堆沙子分别为 1 3 5 2 我们可以先合并1、2堆,代价为4,得到4 5 2 又合并 1,2堆,代价为9,得到9 2 ,再合并得到11,总代价为4+9+11=24,如果第二步是先合并2,3堆,则代价为7,得到4 7,最后一次合并代价为11,总代价为4+7+11=22;问题是:找出一种合理的方法,使总的代价最小。输出最小代价。
输入格式
第一行一个数N表示沙子的堆数N。
第二行N个数,表示每堆沙子的质量(<=1000)。
输出格式
合并的最小代价
样例输入
4
1 3 5 2
样例输出
22
分析
若最初的第 l 堆石子和第 r 堆石子被合并成一堆,则说明l~r之间的石子也已经被合并,只有这样,l和r才能相邻。因此可以在任意时刻,任意一堆石子均可以用一个闭区间[l, r]来描述,表示这堆石子是由最初的第l~r个石子合并而成的,其数量为SUM(A[I],A[R])。另外,一定存在一个整数K,在这对石子形成之前,先有第l~k堆石子被合并成一堆,第k+1~r堆石子被合并成一堆,然后这辆堆石子合并成一堆。
对应到动态规划中,就是一长度较小的区间上的信息向一个更长的区间发生了转移;因此我们可以根据长度len作为dp阶段,划分点K作为决策。
dp[l][r] = min (dp[l,k] + dp[k+1][r]) + sum(A[l],A[r]) ( l <= k < r )
dp[l][r] : 把最初的第l堆到第r堆石子合并成一堆,需要消费的最少体力。
using namespace std;
#define rep(i,a,n) for (int i=a;i<n;i++)
#define per(i,a,n) for (int i=n-1;i>=a;i--)
#define oo cout<<"!!!"<<endl;
typedef long long ll;
typedef unsigned long long ull;
#define ms(s) memset(s, 0, sizeof(s))
const int inf = 0x3f3f3f3f;
//head
const int maxn = 1e6+11;
int f[333][333],sum[333],a[333];
int main()
{
int n;
memset(f,0x3f,sizeof f);
cin>>n;
rep(i,1,n+1)scanf("%d",a+i),sum[i] += sum[i-1] + a[i],f[i][i] = 0;//dp数组初始化,求前缀和
for(int len = 2;len<=n;len++)
for(int l = 1;l<=n-len+1;l++)
{
int r = l+len-1;
for(int k = 1;k<r;k++)
f[l][r] = min(f[l][r],f[l][k] +f[k+1][r]);
f[l][r] += sum[r] - sum[l-1];
}
cout << f[1][n] << endl;
return 0;
}
例题:312. 戳气球
有 n 个气球,编号为0 到 n-1,每个气球上都标有一个数字,这些数字存在数组 nums 中。
现在要求你戳破所有的气球。每当你戳破一个气球 i 时,你可以获得 nums[left] * nums[i] * nums[right] 个硬币。 这里的 left 和 right 代表和 i 相邻的两个气球的序号。注意当你戳破了气球 i 后,气球 left 和气球 right 就变成了相邻的气球。
求所能获得硬币的最大数量。
说明:
你可以假设 nums[-1] = nums[n] = 1,但注意它们不是真实存在的所以并不能被戳破。
0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100
分析:和合并石子问题很像,只是这个的决策是 区间中最后剩余的那个气球。
dp[l][r] = max(dp[l][r],dp[l][k-1] + dp[k+1][r]+ nums[l-1] * nums[r+1] * nums[k]); ( l<=k<=r)
class Solution {
public:
int maxCoins(vector<int>& a) {
int n = a.size();
if(!n)return 0;
vector<int> nums(n+2,0);
nums[0] = 1;
nums[n+1] = 1;
for(int i = 0;i<n;++i)nums[i+1] = a[i];
//n+=2;
vector< vector<int> > dp(n+11,vector<int>(n+11,0));
for(int len = 1;len<=n;++len)
{
for(int l = 1;l <= n - len + 1;++l)
{
int r = l+len-1;
for(int k = l;k<=r;++k)//最终剩余的气球
dp[l][r] = max(dp[l][r],dp[l][k-1] + dp[k+1][r]+ //k+1>r k-1<l都没事,dp数组为0
nums[l-1] * nums[r+1] * nums[k]);
}
}
return dp[1][n];
}
};
例题:1000. 合并石头的最低成本
有 N 堆石头排成一排,第 i 堆中有 stones[i] 块石头。
每次移动(move)需要将连续的 K 堆石头合并为一堆,而这个移动的成本为这 K 堆石头的总数。
找出把所有石头合并成一堆的最低成本。如果不可能,返回 -1 。
这题加上了限制条件:每次合并是连续合并k堆,并不是合并两堆了;
当k=2时,我们其实是将一部分合并成1堆,另一部分合并成1堆。
现在K!=2了,我们可以考虑先将一部分合并成K-1堆,另外一部分合并成1堆,分界点为k,然后再合并。至于这里为什么要分出来1堆,而不是2,3,4? 因为1是更小的子集,并且初始化dp[i][i][0] = 0;
初始化 dp[i][i][1]=0
dp[i][j][k]=min(dp[i][j][k],dp[i][k][k-1]+dp[k+1][j][1])
dp[i][j][1]=min(dp[i][j][K]+sum[j]-sum[k-1]) // K堆合并为1堆
class Solution {
public:
int dp[33][33][33],sum[33];
int mergeStones(vector<int>& stones, int K) {
int n = stones.size();
if( (n-1) % (K-1) ) return -1;
memset(dp,0x3f,sizeof (dp));
sum[0] = 0;
for(int i = 1;i<=n;++i)
{
dp[i][i][1] = 0;
sum[i] = sum[i-1] + stones[i-1];
}
for(int len = 2;len<=n;++len)
for(int l = 1;l<=n-len+1;++l)
{
int r = l + len - 1;
for(int k = l;k<r;++k)//分界点
{
for(int m = 2;m<=len;++m)//m堆
dp[l][r][m] = min(dp[l][r][m],dp[l][k][m-1] + dp[k+1][r][1]);
}
dp[l][r][1] = min(dp[l][r][1],dp[l][r][K] + sum[r] - sum[l-1]);//K堆 -> 1堆
}
return dp[1][n][1];
}
};