P1880 [NOI1995] 石子合并 题解复盘
模块:区间动态规划(Interval DP)
类型:区间DP + 前缀和 + 环形DP
目标:求石子合并的最小总代价和最大总代价。
基本信息
| 项目 | 内容 |
|---|---|
| 题目编号、来源 | P1880 NOI1995 |
| 训练层级 | B |
| 知识版块 | 区间DP、前缀和、环形DP |
解题前・关键信号识别
| 维度 | 分析 |
|---|---|
| 目标、约束、底层结构 | 每次只能合并相邻两堆石子,最终合并成一堆,求总代价最小值和最大值。 |
| 数据规模 | n≤100,O(n³) 可以通过。 |
| 候选算法和依据 | 每次最后一定是把左右两个区间合并,因此使用区间DP。由于是环形,需要断环成链。 |
| 复杂度预判 | 时间复杂度:O(n³) 空间复杂度:O(n²) |
解题后・外化复盘
| 维度 | 内容 |
|---|---|
| 状态定义 | mn[l][r]:区间[l,r]合并成一堆的最小总代价。mx[l][r]:区间[l,r]合并成一堆的最大总代价。 |
| 状态转移 | 枚举最后一次切分点k:mn[l][r]=min(mn[l][k]+mn[k+1][r]+sum(l,r))mx[l][r]=max(mx[l][k]+mx[k+1][r]+sum(l,r)) |
| 遍历顺序 | 按区间长度递增: ① 枚举区间长度 len ② 枚举左端点 l ③ 推出右端点 r ④ 枚举切分点 k |
| 实现结构 / 核心思路 | ① 将数组复制一遍,把环变成链。 ② 求前缀和。 ③ 区间DP计算所有长度≤n的区间。 ④ 枚举所有长度为 n 的区间,更新答案。 |
| 错因回溯 | ① 转移时写成了dp[k][r],导致第 k 堆重复计算。正确应为dp[k+1][r]。② 忘记复制数组,无法处理环。 ③ 忘记初始化 mn[][]=INF。 |
| 边界和易错点 | 1.dp[i][i]=0,一堆石子不用合并。2. 求最小值初始化 INF。 3. 求最大值初始化 0。 4. 环必须复制数组。 5. 最后答案只统计长度为 n 的区间。 |
| 下次看到什么信号,我应该想到这个方法 | 看到: ① 求一段区间最优值。 ② 最后一步可以拆成左右两个区间。 ③ 每次操作只能发生在区间内部。 想到:区间DP。 |
为什么要复制数组?
原数组:
4 5 9 4复制:
4 5 9 4 4 5 9 4例如:
从第二堆开始断开:
5 9 4 4对应:
区间 [2,5]因此:
所有长度为 n 的区间:
[1,n] [2,n+1] ... [n,2n-1]刚好对应所有断环方式。
为什么要加 sum(l,r)?
例如:
区间:
4 5 9最后一次一定会变成:
(4 5) + (9)或者:
(4) + (5 9)左右两边已经分别合并完成。
最后:
左右两堆还要再合并一次。
最后一次合并后的石子数:
4+5+9=18因此:
最后一次代价就是:
sum(l,r)所以状态转移必须写成:
dp[l][k]+dp[k+1][r]+sum(l,r)为什么枚举区间长度?
因为:
dp[1][4]依赖:
dp[1][2] dp[3][4] dp[1][3] dp[2][4]这些都是更短的区间。
因此必须:
长度2 ↓ 长度3 ↓ 长度4 ↓ …… ↓ 长度n模板:
for(intlen=2;len<=n;len++){for(intl=1;l+len-1<=2*n;l++){intr=l+len-1;for(intk=l;k<r;k++){}}}AC完整代码
#include<iostream>#include<algorithm>usingnamespacestd;constintN=205;constintINF=1e9;inta[N];ints[N];intmn[N][N];intmx[N][N];intmain(){intn;cin>>n;for(inti=1;i<=n;i++){cin>>a[i];a[i+n]=a[i];}for(inti=1;i<=2*n;i++){s[i]=s[i-1]+a[i];}for(inti=1;i<=2*n;i++){for(intj=1;j<=2*n;j++){mn[i][j]=INF;mx[i][j]=0;}mn[i][i]=0;mx[i][i]=0;}for(intlen=2;len<=n;len++){for(intl=1;l+len-1<=2*n;l++){intr=l+len-1;for(intk=l;k<r;k++){intsum=s[r]-s[l-1];mn[l][r]=min(mn[l][r],mn[l][k]+mn[k+1][r]+sum);mx[l][r]=max(mx[l][r],mx[l][k]+mx[k+1][r]+sum);}}}intansMin=INF;intansMax=0;for(intl=1;l<=n;l++){intr=l+n-1;ansMin=min(ansMin,mn[l][r]);ansMax=max(ansMax,mx[l][r]);}cout<<ansMin<<endl;cout<<ansMax<<endl;return0;}模型总结
| 模型 | 状态 | 转移 |
|---|---|---|
| 路径DP | dp[i][j] | 从相邻位置转移 |
| 背包DP | dp[j] | 从容量转移 |
| LIS | dp[i] | 从前面位置转移 |
| 区间DP | dp[l][r] | 从左右区间转移 |
区间DP统一模板
for(len=2;len<=n;len++){for(l=1;l+len-1<=n;l++){r=l+len-1;dp[l][r]=初始化;for(k=l;k<r;k++){dp[l][r]=最优(dp[l][k],dp[k+1][r]);}}}记忆
看到: ① 求一个区间最优值 ② 最后一步可以拆成左右两部分 ③ 枚举切分位置 ↓ 想到: 区间DP 状态: dp[l][r] ↓ 枚举长度 ↓ 枚举左端点 ↓ 枚举切分点 ↓ dp[l][k]+dp[k+1][r] ↓ 如果最后还需要一次操作 + 区间贡献(如sum(l,r))P3146 248 题解复盘
模块:动态规划(Dynamic Programming)
类型:区间DP
目标:通过不断合并相邻且相等的数字,使最终数字最大。
基本信息
| 项目 | 内容 |
|---|---|
| 题目编号、来源 | P3146 USACO 2016 Open Gold |
| 训练层级 | 普及+/提高− |
| 知识版块 | 区间DP |
解题前・关键信号识别
| 维度 | 分析 |
|---|---|
| 目标、约束、底层结构 | 每次只能合并相邻且相等的数字,最后求能够得到的最大数字。 |
| 数据规模 | n≤248,典型区间DP规模。 |
| 候选算法和依据 | 一个区间能否合并只与更短的两个子区间有关,因此采用区间DP。 |
| 复杂度预判 | 时间复杂度:O(n³) 空间复杂度:O(n²) |
解题后・外化复盘
| 维度 | 内容 |
|---|---|
| 状态定义 | dp[l][r]:表示区间[l,r]如果能够全部合并成一个数字,那么这个数字是多少;不能合并则为 0。 |
| 状态转移 | 枚举最后一次合并的位置k。若: dp[l][k]==dp[k+1][r]且都不为0。则: dp[l][r]=max(dp[l][r],dp[l][k]+1) |
| 遍历顺序 | 按区间长度从小到大枚举。因为长区间依赖短区间。 |
| 实现结构 / 核心思路 | ① 初始化所有长度为1的区间。 ② 枚举区间长度。 ③ 枚举左端点。 ④ 枚举分割点。 ⑤ 判断左右是否能合并且数值相同。 |
| 错因回溯 | ① 容易误认为和石子合并一样需要前缀和。 ② 容易忘记判断左右区间是否能够合并(即 dp!=0)。 |
| 边界和易错点 | 1. 初始化:dp[i][i]=a[i]。2. 左右区间必须都能合并。 3. 左右区间最终数字必须相同才能继续合并。 |
| 下次看到什么信号,我应该想到这个方法 | 看到: ① 连续区间。 ② 每次只能合并相邻区间。 ③ 最后一定由左右两个子区间组成。 想到:区间DP。 |
为什么这样定义状态?
例如:
1 1 2区间:
[1,2]可以:
1 1 ↓ 2因此:
dp[1][2]=2;如果:
1 2不能合并。
则:
dp[1][2]=0;表示:
这个区间无法最终变成一个数字。为什么这样转移?
设:
[l......k][k+1......r]最后一次操作一定是:
左区间 + 右区间因此:
必须:
左区间已经合并完成 右区间已经合并完成并且:
最终数字相同例如:
2 2才能:
2 2 ↓ 3因此:
if(dp[l][k]!=0&&dp[l][k]==dp[k+1][r]){dp[l][r]=max(dp[l][r],dp[l][k]+1);}为什么按区间长度枚举?
因为:
dp[l][r]依赖:
dp[l][k] dp[k+1][r]而:
这两个区间一定更短。所以:
必须:
长度1 ↓ 长度2 ↓ 长度3 ↓ …… ↓ 长度nAC完整代码
#include<iostream>#include<algorithm>usingnamespacestd;inta[255];intdp[255][255];intmain(){intn;cin>>n;intans=0;for(inti=1;i<=n;i++){cin>>a[i];dp[i][i]=a[i];ans=max(ans,a[i]);}for(intlen=2;len<=n;len++){for(intl=1;l+len-1<=n;l++){intr=l+len-1;for(intk=l;k<r;k++){if(dp[l][k]!=0&&dp[l][k]==dp[k+1][r]){dp[l][r]=max(dp[l][r],dp[l][k]+1);}}ans=max(ans,dp[l][r]);}}cout<<ans;return0;}模型总结
| 模型 | 状态 | 转移 |
|---|---|---|
| 石子合并 | dp[l][r]最小/最大代价 | min/max + sum |
| 248 | dp[l][r]最终数字 | 左右相等时+1 |
与石子合并区别
| 石子合并 | 248 |
|---|---|
| 求总代价 | 求最终数字 |
| 需要前缀和 | 不需要前缀和 |
| 左右区间一定能合并 | 左右必须最终数字相同 |
转移:+sum | 转移:+1 |
记忆
看到: ① 相邻区间合并 ② 一个区间最终变成一个数字 ③ 左右区间共同决定答案 ↓ 想到: 区间DP ↓ dp[l][r] 表示: 区间最终能合成的数字 ↓ 枚举分割点 k ↓ 左右相等 ↓ +1