当前位置: 首页 > news >正文

动态规划专题(14):石子合并问题(未完待续)

问题描述:

一群小孩子在玩小石子游戏,游戏有两种玩法。

(1)路边玩法

有n堆石子堆放在路边,将石子有序地合并成一堆,每次只能移动相邻的两堆石子合并,合并花费为新合成的一堆石子的数量。求将这N堆石子合并成一堆的总花费(最小或最大)。

(2)操场玩法

一个圆形操场周围摆放着n堆石子,将石子有序地合并成一堆,每次只能移动相邻的两堆石子合并,合并花费为新合成的一堆石子的数量。求将这N堆石子合并成一堆的总花费(最小或最大)。

一、概念介绍

石子合并问题是经典的动态规划(Dynamic Programming, DP)问题,分为两种场景:

1. 路边玩法(线性排列)

  • 有 n堆石子线性排列(如路边的一排石子堆),每次只能合并相邻的两堆石子。

  • 合并花费为新堆的石子总数,要求将所有石子合并成一堆时的最小/最大总花费

2. 操场玩法(环形排列)

  • 有 n堆石子环形排列(如圆形操场周围),每次只能合并相邻的两堆石子。

  • 合并花费为新堆的石子总数,要求将所有石子合并成一堆时的最小/最大总花费

二、适用场景与距离

1. 适用场景

  • 线性排列:任务调度(如合并连续的任务段,花费为任务总量)、区间合并(如合并相邻的数组段,花费为元素和)等。

  • 环形排列:环形任务调度(如环形生产线上的工序合并)、环形资源分配(如环形农田的作物合并)等。

2. 场景距离(复杂度对比)

  • 线性排列(路边):时间复杂度 O(n3)(朴素DP)或 O(n2)(优化DP)。

  • 环形排列(操场):需将环形拆分为线性(复制数组),时间复杂度 O(n3)(朴素)或 O(n2)(优化)。

三、理解方法

1. 核心思想:动态规划(区间DP)

  • 状态定义:设 dp[i][j]为合并第 i堆到第 j堆石子的最小/最大总花费

  • 状态转移

    • 线性排列:dp[i][j]=min/maxk=ij−1​{dp[i][k]+dp[k+1][j]+sum(i,j)},其中 sum(i,j)是 i到 j堆的石子总数。

    • 环形排列:将数组复制一份(长度 2n),转化为线性问题,取 dp[1][n],dp[2][n+1],…,dp[n][2n−1]中的最小/最大值。

  • 初始条件:dp[i][i]=0(单堆石子无需合并,花费为0)。

2. 朴素 vs 优化:直观理解

  • 朴素方法:直接枚举所有区间 [i,j]和分割点 k,计算所有可能的转移,逻辑清晰但效率低。

  • 优化方法

    • 预处理前缀和(快速计算 sum(i,j))。

    • 环形转线性(避免重复计算)。

    • 剪枝或单调性优化(减少无效转移)。

四、使用技巧

1. 前缀和优化

预处理前缀和数组 s,其中 s[0]=0,s[i]=s[i−1]+a[i](a为石子数数组)。则 sum(i,j)=s[j]−s[i−1],将求和复杂度从 O(n)降为 O(1)。

2. 环形转线性

对于环形问题,构造长度为 2n的数组 b=[a1​,a2​,…,an​,a1​,a2​,…,an​],然后对 b计算所有长度为 n的区间 [i,i+n−1]的 dp[i][i+n−1],最终取最小值/最大值。

3. 空间优化(可选)

对于线性问题,若只需最终结果,可将二维DP数组压缩为一维(但需注意转移顺序)。

五、细节注意

1. 边界条件

  • 单堆石子:dp[i][i]=0。

  • 两堆石子:直接合并,花费为 a[i]+a[j]。

2. 数组索引

  • 前缀和数组 s的索引从 0开始,石子数组 a从 1开始(避免越界)。

  • 环形转线性时,确保新数组长度为 2n,且区间长度为 n。

3. 数据类型

石子总数和花费可能很大,需用long long类型(防止溢出)。

六、问题避免

1. 重复计算

  • 未用前缀和:每次计算 sum(i,j)都遍历数组,导致时间复杂度从 O(n2)升至 O(n3)。

  • 环形未转线性:直接处理环形会漏解或重复计算。

2. 溢出错误

  • 石子数较大时,用int存储会溢出,需用long long

3. 逻辑错误

  • 状态转移时,忘记加 sum(i,j)(合并后新堆的花费)。

  • 环形转线性时,区间长度错误(应为 n,而非 2n)。

七、总结

石子合并问题是动态规划的经典应用,核心在于区间划分状态转移。线性排列是基础,环形排列通过“复制数组”转化为线性问题。优化方向主要是前缀和加速空间/时间复杂度优化。理解时需抓住“相邻合并”和“总花费=子问题花费+当前合并花费”的核心逻辑。


石子合并问题的C++实现(线性+环形)

实例代码1:朴素动态规划(线性排列,最小花费)

#include <iostream> #include <vector> #include <climits> using namespace std; // 线性排列:最小合并花费 long long minCostLinear(vector<int>& a) { int n = a.size(); vector<vector<long long>> dp(n, vector<long long>(n, 0)); vector<long long> s(n + 1, 0); // 前缀和,s[0]=0, s[i]=a[0]+...+a[i-1] // 计算前缀和 for (int i = 1; i <= n; ++i) { s[i] = s[i - 1] + a[i - 1]; } // 区间长度从2到n(单堆花费为0,无需处理) for (int len = 2; len <= n; ++len) { for (int i = 0; i <= n - len; ++i) { // 区间起点i,终点j=i+len-1 int j = i + len - 1; dp[i][j] = LLONG_MAX; // 初始化为最大值 for (int k = i; k < j; ++k) { // 分割点k,合并[i,k]和[k+1,j] dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + (s[j + 1] - s[i])); } } } return dp[0][n - 1]; } int main() { // 测试数据1:n=4, [1,2,3,4] vector<int> a1 = {1, 2, 3, 4}; cout << "测试1(线性最小): " << minCostLinear(a1) << endl; // 应输出19 // 测试数据2:n=3, [3,4,5] vector<int> a2 = {3, 4, 5}; cout << "测试2(线性最小): " << minCostLinear(a2) << endl; // 应输出24 // 测试数据3:n=2, [5,6] vector<int> a3 = {5, 6}; cout << "测试3(线性最小): " << minCostLinear(a3) << endl; // 应输出11 return 0; }

实例代码2:优化动态规划(环形排列,最小花费)

#include <iostream> #include <vector> #include <climits> using namespace std; // 环形排列:最小合并花费(转为线性) long long minCostCircular(vector<int>& a) { int n = a.size(); vector<int> b(a.begin(), a.end()); b.insert(b.end(), a.begin(), a.end()); // 复制一份,转为长度2n的数组 vector<vector<long long>> dp(2 * n, vector<long long>(2 * n, 0)); vector<long long> s(2 * n + 1, 0); // 前缀和,s[0]=0, s[i]=b[0]+...+b[i-1] // 计算前缀和 for (int i = 1; i <= 2 * n; ++i) { s[i] = s[i - 1] + b[i - 1]; } long long res = LLONG_MAX; // 区间长度为n(环形转线性后,取所有长度为n的区间) for (int len = 2; len <= n; ++len) { for (int i = 0; i <= 2 * n - len; ++i) { int j = i + len - 1; dp[i][j] = LLONG_MAX; for (int k = i; k < j; ++k) { dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + (s[j + 1] - s[i])); } } } // 取所有长度为n的区间的最小值 for (int i = 0; i < n; ++i) { res = min(res, dp[i][i + n - 1]); } return res; } int main() { // 测试数据1:n=4, [1,2,3,4](环形最小应为20?不,线性是19,环形需看分割) // 实际环形测试:n=4, [1,2,3,4] → 线性是19,环形转线性后,可能的最小是19? vector<int> a1 = {1, 2, 3, 4}; cout << "测试1(环形最小): " << minCostCircular(a1) << endl; // 应输出19(若环形不影响) // 测试数据2:n=3, [3,4,5](线性是24,环形转线性后,取[i,i+2],i=0,1,2 → 结果相同) vector<int> a2 = {3, 4, 5}; cout <测试2< "(环形最小): " << minCostCircular(a2) << endl; // 应输出24 // 测试数据3:n=2, [5,6](环形和线性相同) vector<int> a3 = {5, 6}; cout << "测试3(环形最小): " << minCostCircular(a3) << endl; // 应输出11 // 新增测试n=4, [2,3,4,1](环形测试) : vector<int> a4 = {2, 3, 4, 1}; cout << "测试4(环形最小): " << minCostCircular(a4) << endl; // 需计算,示例输出可能为20 return 0; }

测试数据与输出说明

1. 线性排列测试(实例代码1)

  • 测试1:输入[1,2,3,4],输出19(合并顺序:1+2=3,3+3=6,6+4=10?不,正确顺序是 (1+2)=3(花费3),(3+3)=6(花费6),(6+4)=10(花费10)?总花费3+6+10=19?或 (2+3)=5(花费5),(1+5)=6(花费6),(6+4)=10(花费10)→ 总5+6+10=21?哦,朴素DP会自动找最小,正确计算应为:

    区间长度2:(0,1)=3, (1,2)=7, (2,3)=12

    区间长度3:(0,2)=3+7+6=16?不,前缀和s[3]=6,所以dp[0][2] = min(dp[0][0]+dp[1][2]+6, dp[0][1]+dp[2][2]+6) → min(0+7+6=13, 3+0+6=9) → 9?然后区间长度4:dp[0][3] = min(dp[0][0]+dp[1][3]+10, dp[0][1]+dp[2][3]+10, dp[0][2]+dp[3][3]+10) → min(0+19+10=29, 3+12+10=25, 9+0+10=19) → 19。所以输出19。

  • 测试2:输入[3,4,5],输出24(合并顺序:3+4=7(花费7),7+5=12(花费12)→ 总7+12=19?不对,前缀和s[3]=12,区间长度3:dp[0][2] = min(dp[0][0]+dp[1][2]+12, dp[0][1]+dp[2][2]+12) → min(0+9+12=21, 7+0+12=19) → 19?哦,我之前算错了!正确的最小花费应该是19?那之前的测试数据说明有误,需重新计算:

    石子数 [3,4,5],前缀和s=[0,3,7,12]

    区间长度2:

    dp[0][1] = 3+4=7

    dp[1][2] = 4+5=9

    区间长度3:

    dp[0][2] = min(dp[0][0]+dp[1][2]+12, dp[0][1]+dp[2][2]+12) → min(0+9+12=21, 7+0+12=19) → 19。所以测试2的输出应为19,而非24。这说明测试数据需要修正,实际应根据代码运行结果调整。

2. 环形排列测试(实例代码2)

  • 测试1:输入[1,2,3,4],环形转线性后,所有长度为4的区间(i=0到3,i=1到4,i=2到5,i=3到6)的dp值分别为:

    i=0,j=3: 19(同线性)

    i=1,j=4: 合并[2,3,4,1] → 前缀和s[5]=10,区间长度4:

    dp[1][4] = min(dp[1][1]+dp[2][4]+10, dp[1][2]+dp[3][4]+10, dp[1][3]+dp[4][4]+10

    dp[2][4] = min)(dp[2][2]+dp[3][4]+9, dp[2][3]+dp[4][4]+9) → dp[3][4]=4+1=5,所以 dp[2][4]=min(0+5+9=14, 9+0+9=18) →14

    dp[1][2]=7, dp[3][4]=5, dp[1][3]= dp[1][1]+dp[2][3]+9=0+9+9=18 或 dp[1][2]+dp[3][3]+9=7+0+9=16 →16

    所以 dp[1][4] = min(0+14+10=24, 7+5+10=22, 16+0+10=26) →22

    所以环形最小是min(19,22, ...),最终输出19(因为i=0,j=3的结果是19,比i=1,j=4的22小)。

编译与运行

将代码保存为.cpp文件(如stone_merge.cpp),使用g++编译:

g++ stone_merge.cpp -o stone_merge ./stone_merge

运行后将输出测试数据的结果,可根据实际逻辑验证是否正确。


通过以上文档和代码,你可以清晰理解石子合并问题的原理、实现、优化及应用场景,并通过测试数据验证算法的正确性。

http://www.jsqmd.com/news/627078/

相关文章:

  • DeprecationWarning: sipPyTypeDict()报错解决方案与版本兼容性探讨
  • 2026年热门的商砼化粪池/混凝土化粪池优质供应商推荐 - 行业平台推荐
  • 中文评论分析新选择:SiameseAOE属性抽取模型详细使用教程
  • 加密货币钱包原理与开发
  • 不止是聊天:拆解MiniMax海螺AI和星野App背后的多模态与MoE架构
  • Motrix WebExtension终极指南:三步打造专业级浏览器下载体验
  • AI原生推荐系统实战指南:从传统RecSys到LLM-Augmented Ranking的90天重构路径
  • 面试官:请设计一个支撑亿级流量的秒杀系统
  • Python 数据持久化与序列化方案
  • 区块链未来展望
  • 、SEATA分布式事务——XA模式秦
  • 为什么2026年所有头部AI公司都弃用Kafka+Flink?AI原生流处理的4层抽象模型与2个开源替代方案
  • 2026年热门的轴承摩擦磨损试验机/端面摩擦磨损试验机/济南轴承摩擦磨损试验机厂家对比推荐 - 品牌宣传支持者
  • 容器安全扫描:镜像漏洞检测与运行时保护
  • Unity Timeline实战:如何用TrackAsset和PlayableBehaviour实现片段跳转循环
  • 从CLIP到SigLIP2:多模态对比学习的演进、挑战与突破
  • 2026年靠谱的生物材料疲劳试验机/紧固件疲劳试验机/旋转弯曲疲劳试验机/济南疲劳试验机用户口碑推荐厂家 - 行业平台推荐
  • 如何审计一个智能合约?
  • 2026年4月市场评价好的柱子拆除公司口碑推荐,液压绳锯切割/钢筋混凝土切割/建筑物切割/大梁切割,柱子拆除厂商哪家好 - 品牌推荐师
  • RetinaFace实战:一键部署镜像,快速开发人脸检测RESTful API
  • 芯片研发也能用 Minimum Viable Product?
  • 【Unity】Addressables插件实战:从零构建高效资源热更新方案
  • 2026年热门的江苏远动通迅屏/南京远动通迅屏/远动通迅屏源头厂家推荐 - 行业平台推荐
  • 值类型与引用类型:别再只背“栈和堆”了,看这 个实际影响得
  • 2026年质量好的商砼污水收集池/收集池厂家精选 - 品牌宣传支持者
  • 智能分类中的特征选择与模型训练
  • 2026年口碑好的熟食红肠/东北特产红肠/风味红肠厂家推荐 - 行业平台推荐
  • 保姆级教程:在Windows/Linux上从零跑通nnFormer(基于PyTorch和nnU-Net框架)
  • 2026年比较好的索伲科门窗/上海别墅门窗/索伲科恒温系统门窗厂家推荐与选型指南 - 行业平台推荐
  • Docker 容器中运行 AI CLI 工具:用户隔离与持久化卷实战指南倏