区间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; }
本文来自博客园,作者:Doria_tt,转载请注明原文链接:https://www.cnblogs.com/pangtuan666/p/16598200.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现