news 2026/4/3 12:19:50

【例9.18】合并石子(信息学奥赛一本通- P1274)从暴搜到区间 DP:石子合并的四种写法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【例9.18】合并石子(信息学奥赛一本通- P1274)从暴搜到区间 DP:石子合并的四种写法

【题目描述】

在一个操场上一排地摆放着N堆石子。现要将石子有次序地合并成一堆。规定每次只能选相邻的2堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的得分。

计算出将N堆石子合并成一堆的最小得分。

【输入】

第一行为一个正整数N (2≤N≤100);

以下N行,每行一个正整数,小于10000,分别表示第i堆石子的个数(1≤i≤N)。

【输出】

一个正整数,即最小得分。

【输入样例】

7 13 7 8 16 21 4 18

【输出样例】

239

在信息学奥赛中,区间动态规划是一座必须翻越的大山。很多同学理解“状态转移方程”不难,但一到写代码,经常i, j, k三层循环绕晕。

今天我们以最经典的“石子合并”为例,通过四种写法的演变,彻底搞懂暴搜到标准 DP 模板的思维转变。


0. 题目回顾

题目描述:有N堆石子排成一排,每次只能合并相邻的两堆,代价为两堆石子总数。求将所有石子合并成一堆的最小代价。

数据范围(这暗示我们需要一个O(N^3)的算法)。


1. 朴素递归

拿到这个问题,我们的第一直觉通常是倒推

“要合成一大堆,最后一步一定是把‘左边一堆’和‘右边一堆’合并起来。”

既然不知道在哪里切分,那就枚举所有可能的切分点k。这就有了我们第一版的代码。

代码版本 1.0:纯递归(TLE)

//石子合并未记忆化 #include <iostream> using namespace std; int a[110];//记录原来第i堆石头有多少颗 int s[110];//前缀和数组 int rangecom(int l,int r){ if(l==r) return 0;//如果只有一堆石子了,合并不需要代价 int ans=0x3f3f3f3f;//最小总代价 for(int i=l;i<r;i++){//枚举分界线i //找出合并产生左半堆(l-i)和合并产生右半堆(i+1-r)的最小总代价 ans=min(ans,rangecom(l,i)+rangecom(i+1,r)); } //最后合并左右两堆,总代价还要加上合并左半堆和右半堆的代价(即l-r的石子总数 前缀和算出) return ans+s[r]-s[l-1]; } int main() { int n; cin>>n; //记录原来第i堆石头有多少颗 for(int i=1;i<=n;i++) cin>>a[i]; //对石子做前缀和 for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i]; cout<<rangecom(1,n);//1-n堆石子合并总代价 return 0; }

总结

  • 逻辑:完全正确,体现了区间DP的“最优子结构”性质。

  • 问题重复计算太严重了,比如rangecom(1, 2)这个小区间,会在计算rangecom(1, 3)rangecom(1, 4)等大区间时被反复调用成千上万次。

  • 结果:指数级复杂度O(2^N),提交即超时。


2. 加上记忆化搜索

为了解决超时,我们只需要准备一个数组f[][]。每次算出一个区间的答案,先记下来;下次遇到同样的区间,直接查表,不用再算。

代码版本 2.0:记忆化搜索(推荐初学者)

这是自顶向下的经典写法,逻辑最符合人类思维。

//石子合并记忆化 #include <iostream> #include <cstring>//对应memset using namespace std; int a[110];//记录原来第i堆石头有多少颗 int s[110];//前缀和数组 int f[110][110];//f[i][j]代表合并i-j堆石子的最小总代价 int rangecom(int l,int r){ if(f[l][r]!=-1) return f[l][r]; if(l==r) return f[l][r]=0;//如果只有一堆石子了,合并不需要代价 int ans=0x3f3f3f3f;//最小总代价 for(int i=l;i<r;i++){//枚举分界线i //找出合并产生左半堆(l-i)和合并产生右半堆(i+1-r)的最小总代价 ans=min(ans,rangecom(l,i)+rangecom(i+1,r)); } //最后合并左右两堆,总代价还要加上合并左半堆和右半堆的代价(即l-r的石子总数 前缀和算出) return f[l][r]=ans+s[r]-s[l-1]; } int main() { int n; cin>>n; //初始化f数组为-1,不能为0 因为记忆化搜索的初始化值,必须是一个绝对不可能在计算过程中出现的数值,这样才能用来标记“未访问” memset(f,-1,sizeof(f)); //记录原来第i堆石头有多少颗 for(int i=1;i<=n;i++) cin>>a[i]; //对石子做前缀和 for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i]; cout<<rangecom(1,n);//1-n堆石子合并总代价 return 0; }

总结

  • 初始化:一定要用-1初始化,避免与代价0混淆。

  • 适用场景:适合逻辑复杂的 DP 题,或者状态比较稀疏的情况。


3. 进阶:动态规划

记忆化搜索是“倒着求”,而动态规划是“正着推”。我们从小区间开始,慢慢填满一张表。

在写循环版本时,有两种主流的循环风格。

代码版本 3.0:枚举“区间跨度”(Gap写法)

有些同学喜欢用第一层循环变量i表示区间长度减1(即左右端点的距离 gap)。

//石子合并动态规划写法 #include <iostream> #include <cstring>//对应memset using namespace std; int a[110];//每一堆石子有多少颗 int s[110];//前缀和数组 int dp[110][110];//dp[i][j]为合并第i--j堆石子所需的最小总代价 int main(){ int n; cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; //求前缀和 for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i]; memset(dp,0x3f,sizeof(dp));//初始化dp数组为无穷大 for(int i=1;i<=n;i++) dp[i][i]=0;//自己合并自己代价为0 for(int i=1;i<=n;i++){//遍历区间大小(右端点减去左端点的值)从1-n for(int j=1;j<=n-i;j++){//j为左端点 for(int k=j;k<i+j;k++){//k为分界线 分界线大于等于左端点 小于右端点 dp[j][j+i]=min(dp[j][j+i],dp[j][k]+dp[k+1][j+i]+s[j+i]-s[j-1]); } } } cout<<dp[1][n]; return 0; }

总结

这种写法完全正确,但i代表gap在理解上稍微有点“绕”,容易在考场紧张时搞错边界。


4. 终极形态:标准区间DP模板

为了让逻辑更加清晰,也为了方便后续学习(如四边形不等式优化),我们推荐使用“枚举长度”作为第一层循环的标准写法。

核心口诀:

  1. 先枚举长度len(从小到大,地基打好才能盖楼)

  2. 再枚举起点i(推算终点j

  3. 最后枚举分割点k(决策最优解)

代码版本4.0:标准模板

//石子合并动态规划写法优化 竞赛通用标准模板 #include <iostream> #include <cstring>//对应memset using namespace std; int a[110];//每一堆石子有多少颗 int s[110];//前缀和数组 int dp[110][110];//dp[i][j]为合并第i--j堆石子所需的最小总代价 int main(){ int n; cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; //求前缀和 for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i]; memset(dp,0x3f,sizeof(dp));//初始化dp数组为无穷大 for(int i=1;i<=n;i++) dp[i][i]=0;//自己合并自己代价为0 //第一层循环枚举:区间长度len (从2到n) for(int len=2;len<=n;len++) { //第二层循环枚举:左端点i (确保 i+len-1不越界) for(int i=1;i+len-1<=n;i++) { int j=i+len-1;//算出右端点j //第三层循环枚举:分割点k(从i到j-1) 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]); } } } cout<<dp[1][n]; return 0; }

总结

  • 物理意义明确len就是长度,i就是起点,代码可读性高。

  • 稳健性:显式初始化dp[i][i]=0是最稳妥的做法,防止基础状态为无穷大导致计算错误。

  • 扩展性:这是大多数高级区间DP题目(如环形石子合并、能量项链)的标准起手式。


总结

  • 理解逻辑:看版本 2(记忆化搜索)

  • 背诵模板:练版本 4(标准 DP)

希望同学们能通过这道题,彻底掌握区间DP的思想。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/23 16:23:51

Flutter for OpenHarmony 实战:网络监控登录系统完整开发指南

Flutter for OpenHarmony 实战&#xff1a;网络监控登录系统完整开发指南 文章目录Flutter for OpenHarmony 实战&#xff1a;网络监控登录系统完整开发指南摘要一、项目背景与功能概述1.1 网络监控应用场景1.2 应用功能规划1.3 界面设计要求二、数据模型设计2.1 客户端状态类2…

作者头像 李华
网站建设 2026/3/24 17:48:01

LLM评估系统完全指南:从传统评估到Agent裁判,一篇就够了!

文章详细介绍了AI评估系统的演进历程&#xff0c;从传统算法性能评估到基于LLM的评估系统架构&#xff0c;包括单LLM、多LLM、人机协作以及Agent评估系统的实现方法&#xff0c;并提供了Python和Java代码示例&#xff0c;展示了如何利用大模型进行自动化评估&#xff0c;解决了…

作者头像 李华
网站建设 2026/3/12 20:32:13

山东道恩高分子材料在越南买下的,不只是一个工厂

过去几年&#xff0c;越南制造业的变化更多是通过订单细节被感知的。一些原本在国内完成交付的项目&#xff0c;开始要求在越南本地供货。一些新项目在立项阶段&#xff0c;就提前询问供应商是否具备当地生产条件。这样的变化没有集中爆发&#xff0c;但却在持续出现&#xff0…

作者头像 李华
网站建设 2026/4/3 4:12:54

【软件推荐】壁纸引擎(Wallpaper Engine)免安装中文版

类型&#xff1a; 工具 链接&#xff1a;https://pan.quark.cn/s/26312df32633 游戏简介 Wallpaper Engine 使您可在 Windows 桌面上使用动态壁纸。它支持各种类型的动画壁纸&#xff0c;包括 3D 和 2D 动画、网站、视频、乃至某些应用程序。选择现有壁纸&#xff0c;或创建自…

作者头像 李华
网站建设 2026/3/29 3:46:51

手写系列:面试官问我 new 的原理,我直接甩出三个版本

今天我们来聊聊 JavaScript 中那个既熟悉又神秘的 new 操作符。相信很多小伙伴在面试时都经历过这样的“名场面”&#xff1a;面试官微微一笑&#xff0c;推过来那个熟悉的键盘&#xff1a;“来&#xff0c;能不能手写一个 new 的实现&#xff1f;” 这时候&#xff0c;如果你…

作者头像 李华