动态规划: 四道例题入门区间DP (c++ )
石子合并
在一个圆形操场的四周摆放 �N 堆石子,现要将石子有次序地合并成一堆,规定每次只能选相邻的 22 堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。试设计出一个算法,计算出将 �N 堆石子合并成 11 堆的最小得分和最大得分。
设dp[i] [j]表示 将i堆合并到j堆的最小得分(最大得分同理)
sum[i] [j]表示 i堆到 j堆的石子的和
我们可以将石子堆从小堆逐渐推导:
- 合并一堆:dp[i] [j]=sum[i] [j],其中 i==j
- 合并两堆:dp[i] [j]=dp[i] [i]+dp[j] [j]
- 合并三堆:
- 前两堆合并后与第三堆合并:dp[i] [j]=dp[i] [j-1] +dp[j] [j]
- 后两堆合并后与第一堆合并:dp[i] [j]=dp[i+1] [j]+dp[i] [i]
- 两者取一个最小值
- 合并四堆:
- 前三堆合并后与第四堆合并:dp[i] [j]=dp[i] [j-1] +dp[j] [j]
- 后三堆合并后与第一堆合并:dp[i] [j]=dp[i+1] [j]+dp[i] [i]
- 两者取一个最小值
综上: 状态转移方程:
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
]
[
k
]
+
d
p
[
k
+
1
]
[
j
]
+
s
u
m
[
i
]
[
j
]
,
d
p
[
i
]
[
j
]
)
dp[i][j]=min(dp[i][k]+dp[k+1][j]+sum[i][j],dp[i][j])
dp[i][j]=min(dp[i][k]+dp[k+1][j]+sum[i][j],dp[i][j])
其中 k表示两堆之间的分割,再最后合并成两堆后,别忘了合并成最后一堆,加上sum[i] [j]
本题需要注意点:
-
这是一个环形,而我们上述所讨论的情况是一条链,因此我们把这个环形拉长变成一条链
-
环形拉长后形成的链长应为初始的长度的二倍
-
最后:在这条长链中的任意起点和终点位置寻找一个最小值和最大值,得到的即是N堆石子合并后的最小值和最大值
AC code:
//TODO: Write code here int n,m; const int N=2e3+10; int nums[N],dpmax[N][N],dpmin[N][N],sum[N][N]; signed main() { cin>>n; for (int i=1;i<=n;i++) { cin>>nums[i]; nums[i+n]=nums[i]; sum[i][i]=nums[i],sum[i+n][i+n]=nums[i+n]; } for (int len=2;len<=n;len++) { for (int i=1;i+len<=(n<<1)+1;i++) //寻找起点 { int j=i+len-1;//对应的终点 dpmax[i][j]=0,dpmin[i][j]=INF; for (int k=i;k<j;k++) //分割成两堆, { sum[i][j]=sum[i][k]+sum[k+1][j];//首先计算[i,j]的和 dpmin[i][j]=min(dpmin[i][k]+dpmin[k+1][j]+sum[i][j],dpmin[i][j]); dpmax[i][j]=max(dpmax[i][k]+dpmax[k+1][j]+sum[i][j],dpmax[i][j]); } } } int ansMax=0,ansMin=INF; for (int i=1;i<=n;i++) { //寻找一条链中能够形成的最大和最小的值 ansMin=min(ansMin,dpmin[i][i+n-1]); ansMax=max(ansMax,dpmax[i][i+n-1]); } cout<<ansMin<<endl<<ansMax; #define one 0 return 0; }
Cheapest Palindrome G
字串S长M,由N个小写字母构成。欲通过增删字母将其变为回文串,增删特定字母花费不同,求最小花费。
我们可以得到一个性质: 当我们把一个字符串通过添加a字符或者删除a字符变为回文串的时候,我们可以发现我们只需要进行a字符的最小花费的操作即可。
例如 : abcb 删除a:1200 添加a:1000
- 删除头a,则bcb是回文串,总花费为1200
- 添加尾a,则abcba是回文串,总花费为1000
- 所以我们只需要统计一下每个字符花费的最小的操作即可
定义dp[i] [j]表示将字符串的[i,j] 修改为回文串的最小花费
- 当a[i] =a[j]的时候,意味着首尾字符相等,则[i,j]的最小花费就等于 [i+1,j-1]的最小花费,即此时无需操作。
- 否则,首尾字符不相等,我们需要对字符串进行操作。
- 把[i,j-1]改为回文串,然后再单独操作一个a[j]
- 把[i+1,j]改为回文串,然后再单独操作一个a[i]
- 求两种操作的最小值
状态转移方程如下:
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
+
1
]
[
j
]
+
c
o
s
t
[
a
[
i
]
]
,
d
p
[
i
]
[
j
−
1
]
+
c
o
s
t
[
s
[
j
]
]
)
dp[i][j]=min(dp[i+1][j]+cost[a[i]],dp[i][j-1]+cost[s[j]])
dp[i][j]=min(dp[i+1][j]+cost[a[i]],dp[i][j−1]+cost[s[j]])
AC code
//TODO: Write code here int n,m; const int N=5e3+10; char s[N]; int dp[N][N]; umap<char,int> me; signed main() { cin>>n>>m; scanf("%s",s+1); //memset(dp,INF,sizeof(dp)); for (int i=1;i<=n;i++) { char ch; int a,b; cin>>ch>>a>>b; me[ch]=min(a,b); //只需要记录最小代价即可 //dp[i][i]=0; } //dbgumap(me); /* dp[i][j]: 表示以i,j为字符串左右构成的回文串的最小代价 */ for (int len=2;len<=m;len++) //区间长度 { for (int i=1;i<=m-len+1;i++) //区间起始点 { int j=i+len-1; //区间终点 if (s[i]==s[j]) dp[i][j]=dp[i+1][j-1]; else dp[i][j]=min(dp[i+1][j]+me[s[i]],dp[i][j-1]+me[s[j]]); } } #define one 0 //dbg(dp,m,m); cout<<dp[1][m]; return 0; }
最少操作数(粉刷字符串)
给定两个长度相等的字符串 �A、�B,由小写字母组成。
现有一种操作,可以把 �A 中的一个连续子串(区间)都转换为某个字符(就像用刷子刷成一样的字符)。
问要把 �A 转换为 �B,最少的操作数是多少?
定义dp[i] [j]表示字符串A从[i,j] 范围的转换的最少操作数
首先用一个空串模拟转换为B串的过程,然后用A串转换为B串
首先模拟用空串转换为B:
- b[i]=b[j],如果B两端字符相同,则会产生这种情况:空串: a***a, B串:abbba
- 粉刷[i,j-1],则变为 aaaaa,然后再对 [i+1,j-1]粉刷:则变为 abbba,粉刷次数为2
- 粉刷[i+1,j],则变为 aaaaa,然后再对 [i+1,j-1]粉刷:则变为 abbba,粉刷次数也为2
- 因此,当两端字符相同时,对dp[i] [j] 的操作次数就为dp[i] [j-1] 或者dp[i+1] [j],两者的操作次数是一致的
- 当两端字符不一致时,使用” 石子合并“ 的方法,分割区间,逐步取得区间的最小值
然后我们再根据空串转换为B串的DP方程所得到的结果来处理A串转换为B串的形式,注意此时我们只对终点进行操作,因为我们要固定a的起点,得到从a[1]转换为a[j]的最少操作数
状态转移方程如下:
d p [ i ] [ j ] = d p [ i ] [ j − 1 ] ∣ ∣ ( d p [ i + 1 ] [ j ] ) , b [ i ] = = b [ j ] d p [ i ] [ j ] = m i n ( d p [ i ] [ k ] + d p [ k + 1 ] [ j ] , d p [ i ] [ j ] ) dp[i][j]=dp[i][j-1] || (dp[i+1][j]) , b[i]==b[j] \\ dp[i][j]=min(dp[i][k]+dp[k+1][j],dp[i][j]) dp[i][j]=dp[i][j−1]∣∣(dp[i+1][j]),b[i]==b[j]dp[i][j]=min(dp[i][k]+dp[k+1][j],dp[i][j])
AC code
#include <bits/stdc++.h> using namespace std; const int N = 1005,INF=0x3F3F3F3F; char a[N], b[N]; int dp[N][N]; int main() { scanf("%s%s", a + 1, b + 1); int n = strlen(a + 1); for (int i = 1; i <= n; i++) dp[i][i] = 1; //单个字符转换次数为1 /* dp[i][j]: 表示[i,j]内转换的最少操作数 */ //将一个空串转换为b串:预处理 for (int len = 2; len <= n; len++) { for (int i = 1; i <= n - len + 1; i++)//起点 { int j = i + len - 1; //终点 dp[i][j] = INF; if (b[i] == b[j]) { dp[i][j] = dp[i][j - 1]; //dp[i][j]=dp[i+1][j] } else { /* "合并石子": abbbc */ for (int k = i; k < j; k++) //区间分割 { dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j]); } } } } //将a串转换为b串: dp[1][j]: 从1转换到j的最少操作数 for (int j = 1; j <= n; j++) { if (a[j] == b[j]) { dp[1][j] = dp[1][j - 1];//当前字符相同不用转,[1,j]的操作数就等于上一次的操作数 } else { for (int k = 1; k < j; k++) //分割[1,j]的区间 { dp[1][j] = min(dp[1][j], dp[1][k] + dp[k + 1][j]); } } } cout << dp[1][n]; return 0; }
相亲的男孩们
n 个男孩去相亲,排成一队上场。大家都不想等,排队越靠后越愤怒。每人的耐心不同,用 �D 表示火气,设男孩 �i 的火气是 ��D**i,他排在第 �k 个时,愤怒值是 (�−1)∗��(k−1)∗D**i。
主持人不想看到会场气氛紧张。他安排了一个黑屋,可以调整这排男孩上场的顺序,屋子很狭长,先进去的男孩最后出来(黑屋就是一个堆栈)。
例如,当男孩 �A 排到时,如果他后面的男孩 �B 火气更大,就把 �A 送进黑屋,让 �B 先上场。一般情况下,那些火气小的男孩要多等等,让火气大的占便宜。不过,零脾气的你也不一定吃亏,如果你原本排在倒数第二个,而最后一个男孩脾气最坏,主持人为了让这个坏家伙第一个上场,把其他人全赶进了黑屋,结果你就排在了黑屋的第 11 名,第二个上场相亲了(注意,每个男孩都要进出黑屋)。
对所有男孩的愤怒值求和,求所有可能情况的最小和。
定义dp[i] [j]表示[i,j]个人的最小愤怒值之和
我们假设第 i 个人第 k个出场,男孩们的火气值分别为 1,2,3,4,5
如果 i =1 , k = 3,则我们应该让 第一个人第三个出场,根据栈的含义,我们应该这样安排:
1入栈,2入栈,3入栈 ,3出栈,2出栈,1出栈 ,后面的4,5不变
则男孩们的出场顺序为: 3 2 1 4 5
我们可以分三段对他进行分析
- k-1 之前的人全部往左移动,即往前移动,表示为dp[i+1] [k]
- 第 i 个人往后移动 k-i个位置,因此它的火气值应该为: D[i]*(k-i)
- 剩余的k后面的人,全部不动,离他们上场最少还有 k-i+1 个位置,并且他们也有火气值:dp[k+1] [j]+ (k-i+1)* (sum[j]-sum[k])
状态转移方程为:
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
]
[
j
]
,
d
p
[
i
+
1
]
[
k
]
+
D
[
i
]
∗
(
k
−
i
)
+
d
p
[
k
+
1
]
[
j
]
+
(
k
−
i
+
1
)
∗
(
s
u
m
[
j
]
−
s
u
m
[
k
]
)
)
dp[i][j]=min(dp[i][j],\\ dp[i+1][k]+D[i]*(k-i)+dp[k+1][j]+(k-i+1)*(sum[j]-sum[k]))
dp[i][j]=min(dp[i][j],dp[i+1][k]+D[i]∗(k−i)+dp[k+1][j]+(k−i+1)∗(sum[j]−sum[k]))
其中,k表示分割的区间的位置,即第i个男孩移动到的位置,sum表示区间和
AC code
#include <bits/stdc++.h> using namespace std; const int N = 1005,INF=0x3F3F3F3F; char a[N], b[N]; int dp[N][N]; int t, n; int D[N]; int sum[N]; void solve2() { cin >> t; while (t--) { memset(dp, 0, sizeof(dp)); cin >> n; sum[0] = 0; for (int i = 1; i <= n; i++) { cin >> D[i]; sum[i] = sum[i - 1] + D[i]; } /* dp[i][j]:表示 [i,j]的最小愤怒值之和 */ for (int len = 2; len <= n; len++) { for (int i = 1; i <= n - len + 1; i++) { int j = i + len - 1; dp[i][j] = INF; for (int k = i; k <= j; k++) { dp[i][j] = min(dp[i][j], dp[i + 1][k] + //1. i+1到k的人往前挪 D[i] * (k - i) + //2. 第i个人往后移动k-i个位置 dp[k + 1][j] + (k - i + 1) * (sum[j] - sum[k]));//3. k后面的人往前移动k-i+1个位置 } } } cout << dp[1][n]<<endl; } } int main() { solve2(); return 0; }
本文来自博客园,作者:hugeYlh,转载请注明原文链接:https://www.cnblogs.com/helloylh/p/17209610.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)