区间DP入门(石子合并,能量项链,矩阵取数游戏,Palindrome subsequence)


什么是区间DP

区间dp就是在区间上进行动态规划,求解一段区间上的最优解。主要是通过合并小区间的最优解进而得出整个大区间上最优解的dp算法。
重点在于求出递推公式确定边界条件


一、石子合并

石子合并

1.分析

要点:
1.断环成链:题目要求的是一个环形,我们需要借助数组模拟出一个环形结构。
在环型结构中,我们的要求是从任意起点开始均可以遍历整个环,那么我们不妨将原来的数组直接加倍,那么从数组的前半部分中的任意元素开始均可以得出一个完整的原数组。
注意此时DP数组的大小同时也要扩大为原来的两倍

2.预处理:每次得出一个新堆,最终结果要加上此区间内原来所有堆的和,所以我们需要用一个前缀和便于求出区间和,同时为了满足环形结构,我们仍然需要使用一个两倍大小的数组来存前缀和。

3.递推公式:以求该区间的最大值为例
i为起点,j为终点,k为切割点,sum为前缀和数组
dp[i][j]=max(dp[i][j],dp[i][k] + dp[k+1][j] + sum[j] - sum[i-1])
若求区间的最小值,则要先对DP[i][j]做一个预处理,使其为一个较大的值
DP[i][j]=min(DP[i][j],DP[i][k] + DP[k+1][j] + sum[j] - sum[i-1])

4.边界确定


2.代码

#include<bits/stdc++.h> 
using namespace std; 
const int MAN=105;
int n;
int a[MAN*2];
int s[MAN*2];
int dp[2*MAN][2*MAN];//min
int DP[2*MAN][2*MAN];//max
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		a[i+n]=a[i];
	}
	for(int i=1;i<=2*n;i++){
		s[i]=s[i-1]+a[i];
	}
	int mi=1e9;
	int ma=0;
	for(int len=1;len<n;len++){
		for(int i=1;i<2*n-len;i++){
			int j=i+len;
			dp[i][j]=1e9;
			for(int k=i;k<j;k++){
				dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+s[j]-s[i-1]);
				DP[i][j]=max(DP[i][j],DP[i][k]+DP[k+1][j]+s[j]-s[i-1]);
				ma=max(ma,DP[i][j]);
			}
		}
	}
	for(int i=1;i<=n;i++){
		mi=min(mi,dp[i][i+n-1]);
	}	
	cout<<mi<<endl<<ma<<endl;
}


二、能量项链

能量项链

1.分析

同样是一个区间处理的问题,先确定小区间再推广至大区间,跟上一题很相似,不过是递推公式有一些变化。
要点:
1.断链成环

2.条件转化:这里的每个珠子都有两个值,设有两个相邻的珠子a、b,存在条件a.rear==b.front,将两个珠子合并时需要在结果加上a.front * a.rear * b.rear,同时得出一个新珠子(a.front,b.rear),我们需要做的是将这一步与递推联系在一起。
i为起点,j为终点,k为切割点,此时求dp[i][j](范围更小的区间已有最优解)
那我们依照题目求一个区间的值的公式就是:
dp[i][k]+dp[k+1][j]+a[i] * a[k+1] * a[j+1]
对于珠子dp[i][k]、dp[k+1][j]来说:
a.front = a[i]
a.rear = b.front = a[k+1]
b.rear = a[j+1]

3.递推公式
dp[i][j]=max(dp[i][j],dp[i][k] + dp[k+1][j] + a[i] * a[k+1] * a[j+1])


2.代码

#include<bits/stdc++.h> 
using namespace std; 
const int MAN=105;
int n;
int a[MAN*2];
int DP[2*MAN][2*MAN];//max
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		a[i+n]=a[i];
	}
	int ma=0;
	for(int len=1;len<n;len++){
		for(int i=1;i<2*n-len;i++){
			int j=i+len;
			for(int k=i;k<j;k++){
				DP[i][j]=max(DP[i][j],DP[i][k]+DP[k+1][j]+a[i]*a[k+1]*a[j+1]);
				ma=max(ma,DP[i][j]);
			}
		}
	}
	cout<<ma<<endl; 
}

三、矩阵取数游戏

矩阵取数游戏

1.分析

数据有n行m列,但每一行之间却不存在联系,可以分开讨论,最后的结果其实是每行最优解的总和。
要点:
1.区间dp:对于最初的一行,我们需要从首或尾选一个数出来,并且将其与对应的权值相乘加到最后的结果里,不断重复,直至行为空。我们从长度为0的小区间(即自身)开始讨论,再不断推广到大区间(由1到m);
2.递推公式:我们以dp[i][j]表示从i到j的最优解;
对于dp[i][j]来说,其可能源于两个状态dp[i+1][j] (取首点)、dp[i][j-1] (取尾点);
我们选择顺序其实是倒着选的,意味着我们所使用的权值要也要倒着使用,从大到小;
关于这个,我们具体的实现思路有两个
第一个用数组分别存下权值:

dp[i][j]=max(dp[i+1][j]+a[i]*对应权值,dp[i][j-1]+a[i] * 对应权值 )

第二个参考进制思想:

dp[i][j]=max(dp[i+1][j]*2+a[i]*2,dp[i][j-1]*2+a[j]*2)
3.高精度操作
题目要求的数据大小太恐怖了,一般的数据类型是存不下的,所以我们需要使用高精度,在这里见识到了一种神奇的高精度写法———__int128;
详情见于一个大佬的文章:关于__int128
操作与int相同,但需要手写输入输出,可能要背下来了

void scan(__int128 &x)//输入
{
    x = 0;
    int f = 1;
    char ch;
    if((ch = getchar()) == '-') f = -f;
    else x = x*10 + ch-'0';
    while((ch = getchar()) >= '0' && ch <= '9')
        x = x*10 + ch-'0';
    x *= f;
}
void print(__int128 x)
{
    if(x < 0)
    {
        x = -x;
        putchar('-');
    }
     if(x > 9) print(x/10);
    putchar(x%10 + '0');
}

但这个并不是所有的GCC编译器都支持,所以最好还是要掌握高精度写法


2.代码

#include<bits/stdc++.h> 
using namespace std; 
const int MAN=100;
int a[MAN];
__int128 dp[MAN][MAN],ans;
int n,m;
//void scan(__int128 &x)//输入
//{
//    x = 0;
//    int f = 1;
//    char ch;
//    if((ch = getchar()) == '-') f = -f;
//    else x = x*10 + ch-'0';
//    while((ch = getchar()) >= '0' && ch <= '9')
//        x = x*10 + ch-'0';
//    x *= f;
//}
void print(__int128 x)
{
    if(x < 0)
    {
        x = -x;
        putchar('-');
    }
     if(x > 9) print(x/10);
    putchar(x%10 + '0');
}
int main(){
	cin>>n>>m;
	while(n--){
		for(int i=1;i<=m;i++){
			cin>>a[i];
		}
		memset(dp,0,sizeof(dp));
		for(int len=0;len<m;len++){
			for(int i=1;i<=m-len;i++){
				int j=i+len;
				dp[i][j]=max(dp[i+1][j]*2+a[i]*2,dp[i][j-1]*2+a[j]*2);
			}
		}
		ans+=dp[1][m];
	}
	print(ans);
}

四、Palindrome subsequence

Palindrome subsequence

1.分析

要求确定在字符串中有多少个回文子序列。
要点:
1.递推公式:仍然是以dp[i][j]表示这一区间的最优解;
对于dp[i][j]的确认:

在这里插入图片描述

首先考虑此时s[i]!=s[j],
dp[i][j]=dp[i+1][j]+dp[i][j-1]-dp[i+1][j-1],此时使用了一个容斥原理,先将满足条件的区间加在一起,再减去重复的区间,此时可以直接减去而不会对结果产生任何影响。

再考虑若s[i]==s[j],
dp[i][j]=dp[i][j]+dp[i+1][j-1]+1
dp[i][j]实际上可以解释为由两个部分所构成:
不同时包含s[i]跟s[j]的子序列(即前面那种情况)
同时包含s[i]跟s[j],不妨先剥离s[i]跟s[j],即dp[i+1][j-1],但此时需要考虑(i-1,j+1)区域为空,但仍然成立的情况。
2.取模:我一开始取模的时候只对结果取模,提交了好几次都是Wrong Answer,后来改成了
dp[i][j]=(dp[i+1][j]+dp[i][j-1]-dp[i+1][j-1]+mod)%mod;
dp[i][j]=dp[i][j]+dp[i+1][j-1]+1+mod;
dp[i][j]%=mod;

下次注意吧!


2.代码

#include<bits/stdc++.h> 
using namespace std; 
const int mod=10007;
int n;
string a;
int dp[1005][1005];
int main(){
	cin>>n;
	for(int k=1;k<=n;k++){
		cin>>a;
		memset(dp,0,sizeof(dp));
		int size=a.length();
		for(int i=0;i<size;i++){
			dp[i][i]=1;
		}
		for(int len=1;len<size;len++){
			for(int i=0;i+len<size;i++){
				int j=i+len;
				dp[i][j]=(dp[i+1][j]+dp[i][j-1]-dp[i+1][j-1]+mod)%mod;
				if(a[i]==a[j]){
					dp[i][j]=dp[i][j]+dp[i+1][j-1]+1+mod;
					dp[i][j]%=mod;
				}
			}
		}
		cout<<"Case "<<k<<": "<<dp[0][size-1]<<endl;
	}
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值