区间DP

第一类
变成回文串最小插入次数
cclass Solution {
public:
int f1(string s,int l,int r){
if(l==r)return 0;
else if(l+1==r) return s[l]!=s[r];
else if(s[l]==s[r])return f1(s,l+1,r-1);
else {
return min(f1(s,l,r-1),f1(s,l+1,r))+1;
}
}
int minInsertions(string s) {
int len=s.size();
return f1(s,0,len-1);
}
};
class Solution {
public:
int dp[501][501]={0};
int minInsertions(string s) {
int len=s.size();
for(int l=0;l<len-1;l++) dp[l][l+1]=s[l]!=s[l+1];
for(int l=len-3;l>=0;l--){
for(int r=l+2;r<len;r++){
if(s[l]==s[r]){
dp[l][r]=dp[l+1][r-1];
}
else {
dp[l][r]=min(dp[l+1][r],dp[l][r-1])+1;
}
}
}
return dp[0][len-1];
}
};
class Solution {
public:
int minInsertions(string s) {
int len=s.size();
if(len==1)return 0;
if(len==2)return s[0]!=s[1];
int dp[501]={0};
dp[len-1]=s[len-2]!=s[len-1];
for(int l=len-3,leftdown,tmp;l>=0;l--){
leftdown=dp[l+1];
dp[l+1]=s[l]!=s[l+1];
for(int r=l+2;r<len;r++){
int tmp=dp[r];
if(s[l]==s[r]) dp[r]=leftdown;
else dp[r]=min(dp[r-1],dp[r])+1;
leftdown=tmp;
}
}
return dp[len-1];
}
};
预测赢家
class Solution {
public:
int f1(vector<int>&nums,int l,int r){
if(l==r)return nums[l];
else if(l+1==r)return max(nums[l],nums[r]);
else {
int p1=nums[l]+min(f1(nums,l+2,r),f1(nums,l+1,r-1));
int p2=nums[r]+min(f1(nums,l,r-2),f1(nums,l+1,r-1));
return max(p1,p2);
}
}
bool predictTheWinner(vector<int>& nums) {
int len=nums.size(),sum=0;
for(int i=0;i<len;i++) sum+=nums[i];
int first,second;
first=f1(nums,0,len-1);
return first>=sum-first;
}
};
class Solution {
public:
bool predictTheWinner(vector<int>& nums) {
int dp[25][25]={0};
int len=nums.size(),sum=0;
if(len==1||len==2)return true;
for(int i=0;i<len;i++){
sum+=nums[i];
dp[i][i]=nums[i];
if(i!=len-1)dp[i][i+1]=max(nums[i],nums[i+1]);
}
for(int l=len-3;l>=0;l--){
for(int r=l+2;r<len;r++){
dp[l][r]=max(nums[l]+min(dp[l+2][r],dp[l+1][r-1]),nums[r]+min(dp[l][r-2],dp[l+1][r-1]));
}
}
return dp[0][len-1]>=sum-dp[0][len-1];
}
};
滚动数组版本:适用于更大范围,比如滚动4个5个也可以这么写
这个1 2 3是从下往上看1 2 3
class Solution {
public:
int fir=0,sec=1,thir=2;
void swap(){ //swap的顺序,tmp记录第一个,然后从第一个到倒数第二个,都是自己的后面那个福海,最后一个则是用第一个覆盖
int tmp=fir;
fir=sec;
sec=thir;
thir=tmp;
}
bool predictTheWinner(vector<int>& nums) {
int len=nums.size(),sum=0;
if(len==1||len==2)return true;
for(int i=0;i<len;i++)sum+=nums[i];
int dp[3][25]={0};
for(int i=0;i<2;i++) dp[i][len-1-i]=nums[len-1-i];
dp[1][len-1]=max(nums[len-2],nums[len-1]);
for(int l=len-3;l>=0;l--){
dp[thir][l]=nums[l];
dp[thir][l+1]=max(nums[l],nums[l+1]);
for(int r=l+2;r<len;r++){
dp[thir][r]=max(nums[l]+min(dp[fir][r],dp[sec][r-1]),nums[r]+min(dp[thir][r-2],dp[sec][r-1]));
}
swap();
}
return dp[sec][len-1]>=sum-dp[sec][len-1];
}
};
需要k个滚动数组时
class Solution {
public:
int s[3];
void init(int k){
for(int i=0;i<k;i++)s[i]=i;
}
void swap(int k){
int tmp = s[0];
for(int i=0;i<k-1;i++) s[i]=s[i+1];
s[k-1]=tmp;
}
bool predictTheWinner(vector<int>& nums) {
int len = nums.size(), sum = 0;
if (len == 1 || len == 2) return true;
init(3);
for (int i = 0; i < len; i++) sum += nums[i];
int dp[3][25] = {0};
for (int i = 0; i < 2; i++) {
dp[i][len - 1 - i] = nums[len - 1 - i];
}
dp[1][len - 1] = max(nums[len - 2], nums[len - 1]);
for (int l = len - 3; l >= 0; l--) {
dp[s[2]][l] = nums[l];
dp[s[2]][l + 1] = max(nums[l], nums[l + 1]);
for (int r = l + 2; r < len; r++) {
dp[s[2]][r] = max(
nums[l] + min(dp[s[0]][r], dp[s[1]][r - 1]),
nums[r] + min(dp[s[2]][r - 2], dp[s[1]][r - 1])
);
}
swap(3);
}
return dp[s[1]][len - 1] >= sum - dp[s[1]][len - 1];
}
};
第二类
枚举分界点,是 O ( N 3 ) O(N^3) O(N3)
三角剖分
class Solution {
public:
int dp[51][51]={0};
int f1(vector<int>&values,int l,int r){
if(dp[l][r]!=-1) return dp[l][r];
if(l==r||l+1==r) return 0;
int ans=INT_MAX;
for(int i=l+1;i<r;i++){
ans=min(ans,f1(values,l,i)+f1(values,i,r)+values[l]*values[r]*values[i]);
}
dp[l][r]=ans;
return ans;
}
int minScoreTriangulation(vector<int>& values) {
int len=values.size();
if(len<=2)return 0;
for(int i=0;i<len;i++){
for(int j=0;j<len;j++){
dp[i][j]=-1;
}
}
return f1(values,0,len-1);
}
};
依然依赖自己左边和右边,依然是从下往上,从左往右
class Solution {
public:
int minScoreTriangulation(vector<int>& values) {
int len=values.size();
if(len<=2)return 0;
int dp[51][51]={0};
for(int l=len-3;l>=0;l--){
for(int r=l+2;r<len;r++){
dp[l][r]=INT_MAX;
for(int i=l+1;i<r;i++){
dp[l][r]=min(dp[l][r],dp[l][i]+dp[i][r]+values[l]*values[r]*values[i]);
}
}
}
return dp[0][len-1];
}
};
切棍子的最小成本
先把边缘的两个数字放入,然后sort,基本上就变成上一个题目了
切得代价就r+1
和l-1
这两个的差值
#include<algorithm>
class Solution {
public:
int dp[105][105]={0};
int f1(vector<int>&cuts,int l,int r){
if(l>r)return 0;
if(l==r)return cuts[r+1]-cuts[l-1];
if(dp[l][r]!=-1)return dp[l][r];
int ans=INT_MAX;
for(int k=l;k<=r;k++){
ans=min(ans,f1(cuts,l,k-1)+f1(cuts,k+1,r));//每一个索引k位置都可以切开
}
dp[l][r]=ans+cuts[r+1]-cuts[l-1];
return dp[l][r];
}
int minCost(int n, vector<int>& cuts) {
cuts.push_back(0);
cuts.push_back(n);
int len=cuts.size();
sort(cuts.begin(),cuts.end());//排序vector的时候是begin(),end()
if(len==2)return 0;
for(int i=1;i<=len;i++){
for(int j=1;j<=len;j++){
dp[i][j]=-1;
}
}
return f1(cuts,1,len-2);
}
};
dp和递归一样,只考虑1~len-2范围内的
class Solution {
public:
int dp[105][105]={0};
int minCost(int n, vector<int>& cuts) {
cuts.push_back(0);
cuts.push_back(n);
int len=cuts.size();
sort(cuts.begin(),cuts.end());//排序vector的时候是begin(),end()
if(len==2)return 0;
for(int i=1;i<len-1;i++) dp[i][i]=cuts[i+1]-cuts[i-1];
for(int l=len-1;l>=1;l--){
for(int r=l;r<len-1;r++){
dp[l][r]=INT_MAX;
for(int k=l;k<=r;k++){
if(k-1>=0&&k+1<len)
dp[l][r]=min(dp[l][r],dp[l][k-1]+dp[k+1][r]);
}
dp[l][r]+=cuts[r+1]-cuts[l-1];
}
}
return dp[1][len-2];
}
};
戳气球
和上一题不同,不可以直接确认范围后就直接尝试,因为递归的过程之中不知道另一边的情况,比如区间0~6,选择了打爆4,然后递归0-3,5-6,可是递归到0-3的时候,不知道右边的气球是谁,之间有顺序的依赖
采取的策略是枚举最后一个爆的可能性,为什么这么选择,因为题目描述的是每个位置的值是自己左边没爆的和右边没爆的乘自己,所以尝试的时候要先让左右没爆才行,这是一个递归的基础
先将开头和结尾都加上一个一,这样就不要考虑边界问题了,然后调用递归。
这个递归的一个可以调用要求就是,只要递归那么这个,范围的左右两个一定还没爆
class Solution {
public:
int num[305]={0};
int f1(int l,int r){
if(l==r)return num[l-1]*num[l]*num[l+1];
int ans=max(num[l-1]*num[l]*num[r+1]+f1(l+1,r),num[l-1]*num[r]*num[r+1]+f1(l,r-1));
for(int k=l+1;k<=r-1;k++){
ans=max(ans,num[l-1]*num[k]*num[r+1]+f1(l,k-1)+f1(k+1,r));
}
return ans;
}
int maxCoins(vector<int>& nums) {
int len=nums.size();
num[0]=1;
num[len+1]=1;
for(int i=1;i<=len;i++){
num[i]=nums[i-1];
}
len+=2;
return f1(1,len-1);
}
};
class Solution {
public:
int num[305]={0};
int dp[305][305];
int maxCoins(vector<int>& nums) {
int len=nums.size();
num[0]=1;
num[len+1]=1;
for(int i=1;i<=len;i++){
num[i]=nums[i-1];
}
len+=2;
for(int i=1;i<=len-1;i++) dp[i][i]=num[i-1]*num[i]*num[i+1];
for(int l=len-1;l>=1;l--){
for(int r=l+1;r<len;r++){
dp[l][r]=max(num[l-1]*num[l]*num[r+1]+dp[l+1][r],num[l-1]*num[r]*num[r+1]+dp[l][r-1]);
for(int k=l+1;k<=r-1;k++){
dp[l][r]=max(dp[l][r],num[l-1]*num[k]*num[r+1]+dp[l][k-1]+dp[k+1][r]);
}
}
}
return dp[1][len-1];
}
};
布尔运算
因为连续两个数字或者符号没有办法运算,所以符号和数字必然是交错的,而且好开头和结尾一定是数字
虽然题目问的是加括号的顺序,但是不要去直接考虑括号加在哪里,这并没有意义
,而是那部分先算,那部分后算,这个已经体现了括号的功能,而且不是把“()”真的插到数组里面。
这题与上一题相同,枚举最后算的符号
运算符不超过19个,字符串不可以超过38
class Solution {
public:
int dp[40][40][2]={0};
void f1(string s,int l,int r,int res[2]){ //本质传的就是指针,因次不用引用
if(dp[l][r][0]!=0||dp[l][r][1]!=0){
res[0] = dp[l][r][0];
res[1] = dp[l][r][1];
return;
}
if(l==r){
res[0] = (s[l]=='0');
res[1] = (s[l]=='1');
dp[l][r][0] = res[0];
dp[l][r][1] = res[1];
return;
}
int cur[2]={0};
int tmp[2]={0};
for(int k=l+1,a,b,c,d;k<r;k+=2){
f1(s,l,k-1,tmp); // 用tmp接收左区间结果
a=tmp[0],b=tmp[1];
f1(s,k+1,r,tmp); // 用tmp接收右区间结果
c=tmp[0],d=tmp[1];
if(s[k]=='&'){
cur[0]+=a*c+a*d+b*c;
cur[1]+=b*d;
}else if(s[k]=='^'){
cur[0]+=a*c+b*d;
cur[1]+=b*c+a*d;
}else {
cur[0] += a*c;
cur[1] += a*d+b*c+b*d;
}
}
// 将结果存入dp和输出参数res
dp[l][r][0] = cur[0];
dp[l][r][1] = cur[1];
res[0] = cur[0];
res[1] = cur[1];
}
int countEval(string s, int result) {
int n = s.size();
int cur[2]; // 用于接收结果的数组
f1(s,0,n-1,cur); // 调用时传入数组
return cur[result];
}
};
括号区间匹配
两种可能,一个是并列,另一个是嵌套,嵌套可以像第一类一样,但是并列就是第二类了,必须枚举中间每个点
#include<iostream>
#include<string>
#include<climits>
using namespace std;
const int N=102;
string s;
int dp[N][N];
int f1(int l,int r){
if(l==r)return 1;
if(l+1==r){
if(s[l]=='('&&s[r]==')'||s[l]=='['&&s[r]==']'){
return 0;
}else return 2;
}
if(dp[l][r]!=-1) return dp[l][r];
int p1=INT_MAX,p2=INT_MAX;
if(s[l]=='('&&s[r]==')'||s[l]=='['&&s[r]==']') p1=f1(l+1,r-1);//记忆化搜索过程中都是递归,没有dp,刚开始写成dp[l+1][r-1]了
for(int i=l;i<r;i++){
p2=min(p2,f1(l,i)+f1(i+1,r));
}
int ans=min(p1,p2);
dp[l][r]=ans;
return ans;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>s;
int len=s.size();
for(int i=0;i<=len;i++){
for(int j=0;j<=len;j++){
dp[i][j]=-1;
}
}
cout<<f1(0,len-1);
}
涂色
对于两个边缘相同,并不是1+dp[l+1][r-1]
,而是dp[l][r-1]或者dp[l+1][r]
,因为在涂抹完成dp[l][r-1]
的时候,顺便已经把r位置涂抹完成了
#include<iostream>
#include<string>
#include<climits>
using namespace std;
const int N=102;
string s;
int dp[N][N];
int f1(){
int len=s.size();
if(len==1){
cout<<1<<endl;
return 0;
}
for(int i=0;i<len;i++){
dp[i][i]=1;
dp[i][i+1]=s[i]==s[i+1]?1:2;
}
for(int l=len-3;l>=0;l--){
for(int r=l+2;r<len;r++){
if(s[l]==s[r]){
dp[l][r]=dp[l][r-1];
}else{
dp[l][r]=INT_MAX;
for(int m=l;m<r;m++){
dp[l][r]=min(dp[l][r],dp[l][m]+dp[m+1][r]);
}
}
}
}
return dp[0][len-1];
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>s;
cout<< f1();
return 0;
}
合唱队
#include<iostream>
#include<string>
#include<climits>
using namespace std;
const int N=1001,MOD=19650827;
string s;
int dp[N][2];
int a[N];
void solve(){
int n;
cin>>n;
for(int i=0;i<n;i++) cin>>a[i];
for(int i=0;i<=n-1;i++) dp[i][0]=dp[i][1]=1;
for(int l=n-3;l>=0;l--){
dp[l+1][0]=dp[l+1][1]=a[l]<a[l+1]; //每一行的l+1都要根据情况算,不是在初始化的范围
for(int r=l+2;r<n;r++){
int p1=0,p2=0;
if(a[l]<a[l+1]) p1+=dp[r][0];
if(a[l]<a[r]) p1+=dp[r][1];
if(a[r]>a[r-1]) p2+=dp[r-1][1];
if(a[r]>a[l]) p2+=dp[r-1][0];
dp[r][0]=p1%MOD;
dp[r][1]=p2%MOD;
}
}
cout<<(dp[n-1][0]+dp[n-1][1])%MOD<<endl;
return ;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
solve();
return 0;
}
移除盒子
两种情况,和前缀一起,以及枚举中间和前缀数字一样的最后合并
class Solution {
public:
int dp[101][101][101]={0};
int f1(vector<int>a,int l,int r,int k){
if(l>r)return 0;
if(dp[l][r][k]>0)return dp[l][r][k];
int s=l;
while(s+1<=r&&a[l]==a[s+1]) s++;
int ans=0,cnt=k+s-l+1;
ans=cnt*cnt+f1(a,s+1,r,0);
for(int m=s+2;m<=r;m++){
if(a[m]==a[l]&&a[m-1]!=a[m])
ans=max(ans,f1(a,s+1,m-1,0)+f1(a,m,r,cnt));
}
dp[l][r][k]=ans;
return ans;
}
int removeBoxes(vector<int>& boxes) {
int len=boxes.size();
return f1(boxes,0,len-1,0);
}
};
合并石子最小成本
合并成了几块并不需要记录,因为没办法合并的时候也是返回0的代价,不会影响答案,而且能不能合并在枚举的时候就已经计算过了,只要枚举了就一定可以合并
#include <iostream>
#include <vector>
#include <climits>
#include <cstring>
using namespace std;
class Solution {
public:
int dp[35][35]; // 记忆化数组
int prefix[35]; // 前缀和数组
int k; // 每次合并的石子数
// 递归函数,计算区间[l, r]合并成符合要求的最小成本
int dfs(int l, int r) {
// 如果区间长度为1,不需要合并
if (l == r) {
return 0;
}
// 如果已经计算过,直接返回结果
if (dp[l][r] != -1) {
return dp[l][r];
}
int minCost = INT_MAX;
// 枚举分割点m,每次跳k-1步
for (int m = l; m < r; m += k - 1) {
minCost = min(minCost, dfs(l, m) + dfs(m + 1, r));
}
// 如果区间可以合并成一堆,加上合并成本
if ((r - l) % (k - 1) == 0) {
minCost += prefix[r + 1] - prefix[l];
}
// 记忆化存储结果
return dp[l][r] = minCost;
}
int mergeStones(vector<int>& stones, int k) {
int len = stones.size();
// 检查是否有解
if ((len - 1) % (k - 1) != 0) {
return -1;
}
this->k = k;
// 初始化前缀和数组
for (int i = 1; i <= len; i++) {
prefix[i] = prefix[i - 1] + stones[i - 1];
}
// 初始化记忆化数组
memset(dp, -1, sizeof(dp));
// 计算从0到len-1区间的最小合并成本
return dfs(0, len - 1);
}
};
class Solution {
public:
int dp[35][35]={0};
int mergeStones(vector<int>& stones, int k) {
int len=stones.size();
stones.push_back(0);//因为不是数组,所以要再开一个空间,否则会变异错误,这里值写谁都可以
if((len-1)%(k-1)!=0)return -1;//观察的规律,从k开始往外试就可以了
for(int i=len;i>=1;i--) stones[i]=stones[i-1];
stones[0]=0;//平移方便求前缀和
int prefix[35]={0};
for(int i=1;i<=len;i++){
prefix[i]=prefix[i-1]+stones[i];
}
for(int l=len-2;l>=0;l--){
for(int r=l+1;r<len;r++){
dp[l][r]=INT_MAX;
for(int m=l;m<r;m+=k-1){
dp[l][r]=min(dp[l][r],dp[l][m]+dp[m+1][r]);
}
if((r-l)%(k-1)==0){
dp[l][r]+=prefix[r+1]-prefix[l];//因为平移了一个,所以前缀和也平移了
}
}
}
return dp[0][len-1];
}
};
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<cstdio>
using namespace std;
const int N=1e3+1;
const int MOD=1e9+7;//998244353
string s;
int righ[N],lef[N],last[256];//如果只是26个字母就26,256就不需要减某个字符了,所有字符都包含
long long dp[N][N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>s;
int len=s.size();
memset(last,-1,sizeof(last));
//memset(last,-1,n*sizeof(int)); 一维数组初始化n个
//memset(arr, 0, n * n * sizeof(int)); 二维数组初始化前n*n个
//只能初始化为0,-1,其他的会有问题
for(int i=0;i<len;i++){
lef[i]=last[s[i]];
last[s[i]]=i;
}
for(int i=0;i<256;i++)last[i]=len;
for(int i=len-1;i>=0;i--){
righ[i]=last[s[i]];
last[s[i]]=i;
}
for(int i=0;i<len;i++)dp[i][i]=1; //所有只有一个字符的都是1
for(int l=len-2;l>=0;l--){
for(int r=l+1;r<len;r++){
//第一种情况:s[l],s[r]不同
if(s[l]!=s[r]){
dp[l][r]=dp[l+1][r]+dp[l][r-1]-dp[l+1][r-1];//容斥
}else {
//s[l],s[r]相同,都当成a
//第一种情况,内部没出现过这个字母
if(lef[r]<righ[l]){ //左边搜出来是l,右边搜出来是r
dp[l][r]=dp[l+1][r-1]*2+2;
//乘以2是因为:内部自己算上*1,加上lr后,里面所有的回文串又都*1,一共*2,在加上外面的a以及aa,这两个
}else if(lef[r]==righ[l]){//内部有一个
dp[l][r]=dp[l+1][r-1]*2+1;//少了一个1,是因为内部有a了
}else {
dp[l][r]=dp[l+1][r-1]*2-dp[righ[l]+1][lef[r]-1];
//这里减取得是+1,-1之后的,也就是内部两个a的内部,而不是内部两个a,重是因为这些已经被内部的两个a组成过了,外面再组成就重了
}
}
dp[l][r]%=MOD;
}
}
cout<<dp[0][len-1];
return 0;
}
总结
- 常见的递归终止条件,以及dp初始化的位置
l==r
l+1==r
l>r
- 框架
for(int l=len-3;l>=0;l--){ //有时候是len-2
for(int r=l+2;r<len;r++){ //有时候是l+1
//取决于初始化
}
}
- 第二类枚举节点的时候是
l,m m,r
,l,m m+1,r
,m可能从l开始,也有可能从l+1开始
,取决于m这个位置的功能 - 思路特殊的
1)戳气球,枚举最后爆的
2) 移除盒子,和前缀合并在一起
这两个都是枚举最后时刻的作为分界点