区间DP の 题(内含 最长回文串,石子合并,删除字符串,乘积最大,释放囚犯)
作者:互联网
乘积最大
由于题目给定的是m,需要分解成m+1部分的乘积,不难想到乘号刚好是m个,那么该题就转化成了m个乘号的插入方式。
最优子结构分析:
设数字字符串为a1a2…an
m=1 时,一个乘号可以插在a1a2…an中的n-1个位置,这样就得到n-1种乘积:
a1*a2…an, a1a2*a3…an, …, a1a2…a n-1*an
此时的最大值= max { a1*a2…an, a1a2*a3…an, …, a1a2…a n-1*an }
m=2时,两个乘号可以插在a1a2…an中n-1个位置的任两个地方,这样总共会产生
个乘积。把这些乘积分个类,便于观察规律。
Case1: a1a2 …*a n-1*an , a1a2 …*a n-2 a n-1*an , a1*a2 …a n-3 a n-2 a n-1 * an ,
因后一个乘号位置不变,要使这些乘积最大,就要找出在前n-1个数中插入一个乘号的最大值。引入符号F(n-1,1)表示在前n-1个数中插入一个乘号的最大值,则 Case1的最大值为F(n-1, 1) * an
Case2: a1a2 …*a n-2*a n-1 an , a1a2 …*a n-3 a n-2* a n-1 an , a1*a2 …a n-3 a n-2 * a n-1 an ,
因后一个乘号位置不变,要使这些乘积最大,就要找出在前n-2个数中插入一个乘号的最大值。设符号F(n-2)(1)为在前n-2个数中插入一个乘号的最大值,则 Case2的最大值为F(n-2,1) * a n-1 an
同理,Case3的最大值为F(n-3,1) * a n-2 a n-1 an …… Case n-2的最大值为F(2,1) * a3…an
把一段数字串转化为一个整数:
int get(int i,int j) { //将i~j这一段数字转化为一个整数
int ret=0;
for(int k=i;k<=j;k++) {
ret=ret*10+s[k]-'0';
}
return ret;
}
状态转移方程推导:
下面以n=9, m=4, s=‘321044105’为例,说明计算过程。
我们引入符号f(i,j)表示从a1~ai中插入j个乘号取得的最大值,g[i][j]表示从ai~aj的子串的数值。则上式可表示为:
F(9,4) = max{f(8,3)*g[9][9], f(7,3)*g[8][9], f(6,3)*g[7][9], f(5,3)*g[6][9], f(4,3)*g[5][9]}
F(8,3) = max{f(7,2)*g[8][8], f(6,2)*g[7][8], f(5,2)*g[6][8], f(4,2)*g[5][8],f(3,2)*g[4][8]}
F(7,3) = max{f(6,2)*g[7][7], f(5,2)*g[6][7], f(4,2)*g[5][7], f(3,2)*g[4][7]}
…………
上面的分析已经看出问题的最优子结构、重复子问题性质。
定义状态:dp[i][j]表示从a1~aj中插入i个乘号取得的最大值
dp[i][j] = max{ dp(i-1, k)*get(k+1, j) } (1<=i<=m, i+1<=j<=n, i<=k<j )
i的取值范围为:乘号个数
j的取值范围为:乘号数i加1 ~ 数字串总个数
k的取值范围为:乘号个数~右边界j减去1
上式的边界条件是什么?
dp[0][i] = get(1, i) (1<=i<=n)
我们要求的问题的最优解是dp[m][n]
Code
#include <bits/stdc++.h>
using namespace std;
long long n,k,dp[45][45],m;
char a[45];
long long num(long long x,long long y)
{
long long sum=0;
for(long long i=x;i<=y;i++)
{
sum=sum*10+a[i]-'0';
}
return sum;
}
int main()
{
scanf("%lld%lld",&m,&n);
scanf("%s",a+1);
for(int i=1;i<=m;i++)
{
dp[0][i]=num(1,i);
}
for(long long i=1;i<=n;i++)
{
for(long long j=i+1;j<=m;j++)
{
for(long long k=i;k<j;k++)
{
dp[i][j]=max(dp[i-1][k]*num(k+1,j),dp[i][j]);
}
}
}
printf("%lld\n",dp[n][m]);
}
删除字符串
定义dp[i][j]为删除区间[i,j]的最少次数
(1)如果s[i]==s[j],dp[i][j] = dp[i+1][j-1] + 1,即先删除区间[i+1,j-1]再把相同的s[i]s[j]一次删除;
(2)如果s[i] != s[j],dp[i][j] = min(dp[i+1][j],dp[i][j-1]) + 1,只能先删除区间[i+1,j]或者[i,j+1],最后删除区间端点的单个字符;但是如果有aabb这种串的话,上面第二种做法就要删3次,显然不对!
(3)然后枚举区间[i,j]的分割点k, dp[i][j] = min(dp[i][j],dp[i][k]+dp[k][j]-1),这样的话k这个点删了两次,所以要减1。
#include<bits/stdc++.h>
using namespace std;
char c[505];
int main()
{
int n, idx, a[505], dp[505][505];
scanf("%d",&n);
scanf("%s",c);
a[0]=c[0];
for (int i = 0; i < n; i++)
if (c[i]!= c[i-1])
a[++idx] = c[i];
for(int i=1;i<=idx;i++)
dp[i][i]=1;
for (int len=2;len<=idx; len++)
{
for (int i=1;i<=idx-len+1;i++)
{
int j=i+len-1;
if(a[i]==a[j])
dp[i][j]=dp[i+1][j-1]+1;
else
dp[i][j]=min(dp[i+1][j],dp[i][j-1])+1;
for(int k=i+1;k<=j;k++)
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]-1);
}
}
printf("%d", dp[1][idx]);
return 0;
}
石子合并
假设只有2堆石子,显然只有1种合并方案 如果有3堆石子,则有2种合并方案,((1,2),3)和(1,(2,3)) 如果有k堆石子呢?
不管怎么合并,总之最后总会归结为2堆,如果我们把最后两堆分开,左边和右边无论怎么合并,都必须满足最优合并方案,整个问题才能得到最优解。
令m[i,j]表示归并第i个数到第j数的最小代价,w[i,j]表示第i个数到第j个数的和,这个可以事先计算出来。有如下的状态转移方程:
预处理w[i][j]:
先算出前缀和s[i],然后w[i][j] = s[j] – s[i-1]
因此,w[i][j]可以不事先存放,在DP时直接O
(1)计算 s[0] = 0; for(int i = 1; i<=n; i++) s[i] = s[i-1]+a[i];
拓展成环:
对于环状序列,通用的处理方法是把这条链延长2倍,扩展成2n-1堆,其中第1堆与n+1堆完全相同,第i堆与n+i堆完全相同,这样我们只要对这2n堆动态规划后,枚举f(1,n),f(2,n+1),…,f(n,2n-1)取最优值即可即可。
Code
#include<bits/stdc++.h>
using namespace std;
int dp[305][305],len,a[305],n,sum[305];
int main()
{
cin>>n;
memset(dp,0x3f,sizeof(dp));
for(int i=1;i<=n;i++)
{
cin>>a[i];
sum[i]=sum[i-1]+a[i];
dp[i][i]=0;
}
for(int len=2;len<=n;len++)
{
for(int i=1;i<=n-len+1;i++)
{
int j=i+len-1;
for(int k=i;k<j;k++)
{
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);
}
}
}
cout<<dp[1][n];
}
最长回文串
令dp[i][j]表示在字符串区间s[i]..s[j]之间的最长回文串的长度。
分几种情况讨论:
(1)如果s[i] = s[j],这时如果s[i+1]..s[j-1]是回文串,则加上两端的回文,可构成一个更长的回文串。怎以判断s[i+1]..s[j-1]是不是回文串呢? 一个非常简单的办法是:dp[i+1][j-1]是否等于j-i-1
如果s[i+1]..s[j-1]不构成回文串,则dp[i][j]在dp[i+1][j]与dp[i][j-1]中取较大值。
(2)如果s[i]!=s[j],则dp[i][j]在dp[i+1][j]与dp[i][j-1]中取较大值者这仍然是一个区间DP的模型。
Code
#include<bits/stdc++.h>
using namespace std;
int dp[3005][3005];
int main()
{
char s[3005];
scanf("%s",s+1);
int n=strlen(s+1);
for(int i=1;i<=n;i++)
{
dp[i][i]=1;
}
for(int len=2;len<=n;len++)
{
for(int i=1;i<=n-len+1;i++)
{
int j=i+len-1;
if(dp[i+1][j-1]==j-i-1&&s[i]==s[j])
{
dp[i][j]=dp[i+1][j-1]+2;
}
else
{
dp[i][j]=max(dp[i+1][j],dp[i][j-1]);
}
}
}
printf("%d",dp[1][n]);
return 0;
}
释放囚犯
区间dp的套路:设f[i][j]为区间释放i~j号囚犯所需最少的肉(注意,i,j不是牢房编号,是释放的囚犯编号,也就是下面的a[i]数组)
枚举区间的分界点k,转移方程为:
f[i][j]=min{f[i][j],f[i][k-1]+f[k+1][j]+a[j+1]-a[i-1]-1-1}
把后面这一坨拿出来拆开看看,
f[i][k-1]+f[k+1][j]
,这个不必解释
a[j+1]-a[i-1]-1
就是第j+1个要放出的囚犯到第i-1个要放出的囚犯之间的人数,也就是要发的肉的数量;
最后一个-1 是什么呢,就是第k个放出去的囚犯,不用给他吃肉了
注意一件事:输入的囚犯的编号。当你细细的观察它们时,你会发现第 Qi 个囚犯的编号Qi 等于他及其前面的所有人的人数,那么这就相当于是一个前缀和,又因为我们放第 Qq 个囚犯的时候需要给最后一段人肉,所以我们可以假设在这段监狱的最后(p+1)还有一个需要释放的囚犯。
再假设我们此时释放囚犯 k,那么我们此时需要的肉的数量即为释放第 Qi 个囚犯到第 Qk−1 个囚犯与释放第 Qk+1 个囚犯到第 Qj 个囚犯所需的总肉数加上施放这个囚犯所需的肉的数量。由于我们先选择释放第Qk 个囚犯,所以我们需要用 a[j+1]-a[i-1]-2
的肉(我们的假设是除了第 Qi 个囚犯到第 Qj 个囚犯未释放外其他囚犯均已释放,只不过没用肉。),由于先放哪一个囚犯最优不清楚,于是取最小值。
#include<bits/stdc++.h>
using namespace std;
int a[105];
int dp[105][105];
int main()
{
int p,q;
scanf("%d%d",&p,&q);
for(int i=1;i<=q;i++)
scanf("%d",&a[i]);
a[0]=0;
a[q+1]=p+1;
sort(a+1,a+q+1);
for(int len=1;len<=q;len++)
{
for(int i=1;i+len-1<=q;i++)
{
int j=i+len-1;
dp[i][j]=0x3f3f3f3f;
for(int k=i;k<=j;k++)
dp[i][j]=min(dp[i][j],dp[i][k-1]+dp[k+1][j]+a[j+1]-a[i-1]-2);
}
}
printf("%d",dp[1][q]);
return 0;
}
标签:囚犯,乘积,int,最大值,long,a1a2,DP,dp,回文 来源: https://www.cnblogs.com/pangtuan666/p/16598200.html