《算法竞赛进阶指南》这本书中讲到: DP也属于线性DP中的一种,它以 区间长度 作为DP的阶段,使用两个坐标(区间的左右端点),描述每个维度。
区间DP中一个,状态有若干个比他更小,且包含于它的区间所代表的状态转移而来。因此区间DP的决策往往就是划分区间的方法。区间DP的状态,一般由长度为一的元区间构成,这种向下划分、再向上递推的模式与某些树形结构的线段数有很大相似之处。同时借助区间dp这种与树形相关的结构,我们也将提及记忆化搜索,本质是动态规划的递归实现方法。
leetcode312. 戳气球
有 n 个气球,编号为0 到 n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。
现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1 和 i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1或 i + 1 超出了数组的边界,那么就当它是一个数字为 1 的气球。
求所能获得硬币的最大数量。
示例 1:
输入: nums = [3,1,5,8]
输出: 167
解释:
nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167
分析:
不妨设f[l, r]为消去[l + 1, r - 1]这段区间的气球(两个端点不消去)的最大分值.然后我们看最近一次的状态转移。应当注意到,无论我们我们怎么消法,最后一定是在[l, r]之间留下一个气球(设为k),把k消去后恰好得到当前状态。因此可得状态转移 f[l][r] = max(f[l][k] + f[k][r] + score[k] * score[l] * score[r]) (l < k < r)
class Solution {
public:
int maxCoins(vector<int>& nums) {
// 区间dp
int n=nums.size();
vector<int> a(n+2,1);
for(int i=1;i<=n;i++) a[i]=nums[i-1]; // 首尾加1
vector<vector<int>> f(n+2,vector<int>(n+2));
for(int len=3;len<=n+2;len++) // 阶段:枚举区间长度len
{
for(int i=0;i+len-1<=n+1;i++) // 左端点
{
int j=i+len-1; // 右端点
for(int k=i+1;k<j;k++) // 决策
f[i][j]=max(f[i][j],f[i][k]+f[k][j]+a[i]*a[j]*a[k]);
}
}
return f[0][n+1];
}
};
375. 猜数字大小 II
我们正在玩一个猜数游戏,游戏规则如下:
- 我从
1****到n之间选择一个数字。 - 你来猜我选了哪个数字。
- 如果你猜到正确的数字,就会 赢得游戏 。
- 如果你猜错了,那么我会告诉你,我选的数字比你的 更大或者更小 ,并且你需要继续猜数。
- 每当你猜了数字
x并且猜错了的时候,你需要支付金额为x的现金。如果你花光了钱,就会 输掉游戏 。
给你一个特定的数字 n ,返回能够 确保你获胜 的最小现金数,不管我选择那个数字 。
dp分析:
设f[i][j]表示的集合为[i, j]区间内所有target以及其对应的所有选法,状态表示这些target和选法的组合所用钱数最坏(多)情况下的最小值。
可以根据选[i, j]内的哪个数进行猜测,进而对集合进行划分。假设选的数是k,则在选k的前提条件下, 最坏情况所用钱数为max(f[i, k - 1], f[k + 1, j]) + k。我们遍历区间[i,j]范围内的每个k,求出每个对应的最坏(多)情况下的钱数,对这些值取最小值,即为题目所求。
需要理解的是:最坏情况下的最小值
class Solution {
public:
// 区间dp
int getMoneyAmount(int n) {
vector<vector<int>> f(n+2,vector<int>(n+2));
for(int len=2;len<=n;len++) // 枚举区间长度
{
for(int i=1;i+len-1<=n;i++) // 左端点
{
int j=i+len-1; // 右端点
f[i][j]=INT_MAX;
for(int k=i;k<=j;k++) // 决策
{
f[i][j]=min(f[i][j],max(f[i][k-1],f[k+1][j])+k);
}
}
}
return f[1][n];
}
};
leetcode516. 最长回文子序列
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1:
输入: s = "bbbab"
输出: 4
解释: 一个可能的最长回文子序列为 "bbbb" 。
示例 2:
输入: s = "cbbd"
输出: 2
解释: 一个可能的最长回文子序列为 "bb" 。
分析:
class Solution {
public:
int longestPalindromeSubseq(string s) {
int n=s.size();
vector<vector<int>> f(n,vector<int>(n));
for(int len=1;len<=n;len++) // 枚举区间长度len
{
for(int i=0;i+len-1<n;i++) // 左端点
{
int j=i+len-1; //右端点
if(len==1) f[i][j]=1;
else
{
if(s[i]==s[j]) f[i][j]=f[i+1][j-1]+2;
f[i][j]=max(f[i][j],max(f[i+1][j],f[i][j-1]));
}
}
}
return f[0][n-1];
}
};
从上面的几道题可以看出,区间dp也属于线性dp的一种,它是以区间长度作为DP的阶段,使用两个坐标(区间的左端点和区间的右端点)来描述每一个维度。区间长度一般从一开始。