C++动态规划经典案例解析之合并石子

1. 前言

区间类型问题,指求一个数列中某一段区间的值,包括求和、最值等简单或复杂问题。此类问题也适用于动态规划思想。

前缀和就是极简单的区间问题。如有如下数组:

int nums[]={3,1,7,9,12,78,32,5,10,11,21,32,45,22}

现给定区间信息[3,6],求区间内所有数字相加结果。即求如下图位置数字之和。

Tips: 区间至少包括 2 个属性,起始端和结束端,求和范围包含左端和右端数字。

1.png

直接的解法:

  • 累加数组中 0~6区间的值s1
  • 累加数组中0~2区间的值s2
  • s1中的值减去s2中的值。得到最终结果。

如果对任意区间的求解要求较频繁,会存在大量的重复计算。如分别求区间[2,5][1,5]之和时,分析可知区间[1,5]结果等于区间[2,5]的结果加上nums[1]的值,或者说区间[2,5]的值等于[1,5]的值减nums[1]。简而言之,只需要求出一个如上两个区间中一个区间的值,另一个区间的值就可得到。

2.png

为了减少重复计算,可使用区间缓存理念记录0~至任意位置的和。

3.png

如上的问题便是简单的区间类型问题,解决此类问题的方案称为简单区间类型动态规划。dp数组也可称为前缀和数组。

编码实现:

#include <iostream>
using namespace std;
int main() {int nums[]= {3,1,7,9,12,78,32,5,10,11,21,32,45,22};int dp[100];int size=sizeof(nums)/sizeof(int);for(int i=0; i<size; i++) {if(i==0){//base case dp[i]=nums[i];}else{dp[i]=dp[i-1]+nums[i];}}//输出dp信息for(int i=0; i<size; i++) {cout<<dp[i]<<"\t";}return 0;
}

有了前缀和数组,计算任意区间数字和的公式为:

//[l,r]:l表示左端位置,r表示右端位置
dp[r]-dp[l-1];

如下代码实现,输入任意区间信息,输出区间和信息。

#include <iostream>
using namespace std;
int main() {int nums[]= {3,1,7,9,12,78,32,5,10,11,21,32,45,22};int dp[100];int size=sizeof(nums)/sizeof(int);for(int i=0; i<size; i++) {if(i==0) {//base casedp[i]=nums[i];} else {dp[i]=dp[i-1]+nums[i];}}//输出dp信息for(int i=0; i<size; i++) {cout<<dp[i]<<"\t";}cout<<endl;int l,r,sum;while(1) {cin>>l>>r;if(l==-1)break;sum=dp[r]-dp[l-1];cout<<sum<<endl;}return 0;
}

前缀和是区间动态规划的极简单应用,下文继续讲解几道典型的区间类型问题。

2. 典型案例

2.1 石子合并

问题描述:

设有N(N<=300)堆石子排成一排,其编号为1,2,3...N,每堆石子有一定的质量m[i] (m[i]<=1000)。现在要将这N堆石子合并成为一堆,每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻。合并时由于选择的顺序不同,合并的总代价也不相同。试找出一种合理的方法,使总的代价最小,并输出最小代价。

此问题为什么也是区间类型问题?

先看几个样例。如有编号为 1,2,3 的 3 堆石子,质量分别为 1,2,3。则合并方案有如下 2 种:

  • 合并编号为1、2的石子,合并代价为3,再合并新堆和第3堆石子,代价为6。总代价为9
  • 合并编号为2、3的石子,合并代价为5,再合并新堆和第1堆石子,代价为6。总代价为11

通过上述合并过程,可得到如下有用的结论:

  • 任意相邻两堆石子合并的结果是以这两堆石子的编号作为左、右边界的区间和。如合并编号1,2的石子,代价为区间[1,2]的和 。合并编号为2、3的石子,结果为区间[2,3]的和。

4.png

  • 无论采用何种合并方案,最后一次合并都是相当于求整个数列的和。

    如样例所示,两种合并方案的代价分别为3,5,取最小值3再加上所有石子的质量和6,即为最后答案9

5.png

  • 对于n堆的石子,可以随意在中间画出一条分割线,把n堆石子抽象成左、右2 堆石子(两个区间),根据上述分析,可知最后一次的合并值为这 2堆石子的质量总和。

    但是,左堆不是真正意义上只有一堆石子,是由许多石子堆组成的一个逻辑整体,有其内部的合并方案,且不止一种,站在宏观的角度,不用关心其内部如何变化,只需关心多种合并方案的最小值是多少。同理,也只需关心右堆最终返回的最佳值。

    所以,求解问题可以抽象成:

    最终合并最小值=所有石子堆的总质量值+左堆最小合并值+右堆最小合并值
    

6.png

如果原始问题是一个根问题,则求解左堆或右堆的最佳合并值就是一个子问题,所以,合并石子这道题本质是符合递归特点的。

既然符合递归特点,现在就要考虑如何划分子问题。

绘制如下图递归树,根问题为原始问题,区间划分可以从第一堆石子开始,然后再移动分割线,最后再在多个子问题返回值中取最小值。

Tips: 如果只有一堆石子,则代价为 0

7.png

编码实现:

  • 初如化变量。
#include <iostream>
#include <cmath>
using namespace std;
//石子质量 
int sz[100]={0};
//石子堆数量
int n; 
//前缀和
int s[100]={0}; 
  • 初始化石子信息和前缀和。
/*
*  初始化 
*/
void init(){cin>>n;for(int i=1;i<=n;i++){cin>>sz[i];}//动态规划计算前缀和for(int i=1;i<=n;i++){s[i]=s[i-1]+sz[i];} 
}
  • 递归实现,子问题是一个区间问题,由左、右分界线确定。
int  getSz(int l,int r) {//只有一堆石子,返回 0if(l==r)return 0;int res=1<<30;//得到区间的和,最后一次合并值int sum=s[r]-s[l-1];//计算可分方案,且返回所有分割方案中的最小值for(int k=l; k<r; k++) {res=min(res,  getSz(l,k)  + getSz(k+1,r) );}//返回最后一次合并的值加上左、右区间的合并值return sum+res;
}
  • 测试。
int main() {init();int res= getSz(1,n);cout<<res;return 0;
}

是否存在重叠子问题?

如下图所示,当石子堆更多时,重叠子问题更多。

8.png

使用记忆搜索解决重叠子问题。

//记忆数组
int dp[100][100]={0}; 
int  getSz(int l,int r) {//只有一堆石子,返回 0if(l==r)return 0;if(dp[l][r]!=0)return dp[l][r];int res=1<<30;//得到区间的和,最后一次合并值int sum=s[r]-s[l-1];//计算可分方案,且返回所有分割方案中的最小值for(int k=l; k<r; k++) {res=min(res,  getSz(l,k)  + getSz(k+1,r) );}return dp[l][r]=sum+res;
}

递归是由上向下逐步向子问题求助,类似问题也可以采用由下向上的动态规划方案实现。基本思路,每一次合并过程,先两两合并,再三三合并,…最后N堆合并。

/*
*动态规划
*/
int dpSz() {int ans=0;//初始化dp 数组for (int i = 1; i <= n; i++) {for(int j=1; j<=n; j++) {dp[i][j]=1<<30;}//一堆石子的值为 0dp[i][i] = 0;}//从长度为 1 的区间开始扫描,逐步增加区间的长度for (int len = 1; len < n; len++)//左边界for (int i = 1; i < n; i++) {//右边界int j = i + len;//左右之间的所有子区间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]);}ans=max(ans,dp[i][j]);}return ans;
}

测试:

int main() {init();int res=dpSz();cout<<"动态规划方案:"<<res<<endl;printf("%d\n", dp[1][n]);return 0;
}

2.2 石子合并 II

问题描述:

有 n 堆石子围成一个圈,第 i 堆石子有 a[i] 颗,每次我们可以选择相邻的两堆石子合并,代价是两堆石子数目的和,现在我们要一直合并这些石子,使得最后只剩下一堆石子,问总代价最少是多少?

因为首尾可合并,相比较上述问题,差异在于增加合并的方案。

那么,到底增加了那些合并?

假设石子有 3 堆,每堆的质量分别为 1 2 3

如果考虑环形问题,则任何数字都可以为头、为尾,则会出现如下几种数列。

  • 1 2 3

  • 2 3 1

  • 3 1 2

9.png

可以理解,数列变成如下 形式,即将环形变成线性。

10.png

动态规划实现:

#include <bits/stdc++.h>
using namespace std;int n, a[501], f[501][501], s[501];int main() {scanf("%d", &n);for (int i = 1; i <= n; i++) {scanf("%d", &a[i]);a[n + i] = a[i];}    for (int i = 1; i <= 2 * n; i++)s[i] = s[i - 1] + a[i];memset(f, 127, sizeof(f));for (int i = 1; i <= 2 * n; i++)f[i][i] = 0;for (int l = 1; l < 2 *n; l++)for (int i = 1; i <= 2 * n - l; i++) {int j = i + l;for (int k = i; k < j; k++)f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);}int ans = 1 << 30;for (int i = 1; i <= n; i++)ans = min(ans, f[i][i + n - 1]);printf("%d\n", ans);
}

3. 总结

沉淀过程是一种修行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/51291.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Java 高阶】一文精通 Spring MVC - 标签库 (八)

&#x1f449;博主介绍&#xff1a; 博主从事应用安全和大数据领域&#xff0c;有8年研发经验&#xff0c;5年面试官经验&#xff0c;Java技术专家&#xff0c;WEB架构师&#xff0c;阿里云专家博主&#xff0c;华为云云享专家&#xff0c;51CTO 专家博主 ⛪️ 个人社区&#x…

提高nodejs中promise的性能

提高nodejs中promise的性能 我们先来看一个常见问题&#xff0c;假设我们有 N 条记录需要处理&#xff0c;或者例如&#xff0c;为每条记录发出 API 请求以获取数据。 通常情况下我们都是使用promise.all方法来实现这一需求&#xff1a; // 记录 const data [{}, {}, {}];/…

枚举和反射

枚举 枚举 枚举是一种特殊的类&#xff0c;它可以有自己的属性、方法和构造方法。 两种枚举的方法 自定义枚举 a.将构造器私有化&#xff0c;防止外部直接new b.去掉set方法&#xff0c;防止属性被修改 c.在内部直接创建固定的对象 通过类名直接去访问 关键字枚举 用…

vue js 回调函数 异步处理 为什么要 let that = this

1 异步就是开个事务(只有主线程 等主线程空闲),用that 值 做处理,然后返回处理结果,而that的值是开启事务那一刻的this的值.而在主线程处理的时候,this的一直在变化, that的值保留在那一刻 ps 或是将本obj 传递给其他的obj使用处理 ps 开启新事务或开启新子线程都是 在新的ob…

Repo manifests默认default.xml清单文件中的各个标签详解

Repo简介 “Repo” 是一个用于管理多个Git存储库的工具&#xff0c;通常与Google的Android开发项目一起使用。它允许您在一个命令下轻松地进行多个Git存储库的同步、下载和管理。 repo下载安装 从清华镜像源下载 mkdir ~/bin PATH~/bin:$PATH curl https://mirrors.tun…

C++ 新特性 | C++ 11 | decltype 关键字

一、decltype 关键字 1、介绍 decltype 是 C11 新增的一个用来推导表达式类型的关键字。和 auto 的功能一样&#xff0c;用来在编译时期进行自动类型推导。引入 decltype 是因为 auto 并不适用于所有的自动类型推导场景&#xff0c;在某些特殊情况下 auto 用起来很不方便&…

2023.8各大浏览器11家对比:Edge/Chrome/Opera/Firefox/Tor/Vivaldi/Brave,安全性,速度,体积,内存占用

测试环境&#xff1a;全默认设置的情况下&#xff0c;均在全新的系统上进行测试&#xff0c;系统并未进行任何改动&#xff0c;没有杀毒软件&#xff0c;浏览器进程全部在后台&#xff0c;且为小窗模式&#xff0c;小窗分辨率均为浏览器厂商默认缩放大小(变量不唯一)&#xff0…

ARM linux ALSA 音频驱动开发方法

+他V hezkz17进数字音频系统研究开发交流答疑群(课题组) 一 linux ALSA介绍 ALSA (Advanced Linux Sound Architecture) 是一个用于提供音频功能的开源软件框架。它是Linux操作系统中音频驱动程序和用户空间应用程序之间的接口。ALSA 提供了访问声卡硬件的低级别API,并支持…

DevExpress WinForms数据编辑器组件,提供丰富的数据输入样式!(二)

DevExpress WinForms超过80个高影响力的WinForms编辑器和多用途控件&#xff0c;从屏蔽数据输入和内置数据验证到HTML格式化&#xff0c;DevExpress数据编辑库提供了无与伦比的数据编辑选项&#xff0c;包括用于独立数据编辑或用于容器控件(如Grid, TreeList和Ribbon)的单元格。…

nginx会话保持

ip_hash:通过IP保持会话 作用&#xff1a; nginx通过后端服务器地址将请求定向的转发到服务器上。 将客户端的IP地址通过哈希算法加密成一个数值 如果后端有多个服务器&#xff0c;第一次请求到服务器A&#xff0c; 并在务器登录成功&#xff0c;那么再登录B服务器就要重新…

AIGC ChatGPT 制作地图可视化分析

地图可视化分析是一种将数据通过地图的形式进行展示的方法&#xff0c;可以让人们更加直观、快速、准确的理解和分析数据。以下是地图可视化分析的一些主要好处&#xff1a; 加强数据理解&#xff1a;地图可视化可以将抽象的数字转化为直观的图形&#xff0c;帮助我们更好地理解…

黑马头条-kafka配置

生产者配置 NAMEDESCRIPTIONTYPEDEFAULTVALID VALUESIMPORTANCEbootstrap.servershost/port列表&#xff0c;用于初始化建立和Kafka集群的连接。列表格式为host1:port1,host2:port2,…&#xff0c;无需添加所有的集群地址&#xff0c;kafka会根据提供的地址发现其他的地址&…

科技赋能,教育革新——大步迈向体育强国梦

在 "全民健身"、"体育强国建设"战略的推进下&#xff0c;体育考试成绩被纳入重要升学考试且分值不断提高&#xff0c;体育科目的地位逐步上升到前所未有的高度&#xff0c;在此趋势下&#xff0c;体育教学正演变出更多元化、个性化的需求。然而现实中却面临…

『C语言入门』探索C语言函数

文章目录 导言一、函数概述定义与作用重要性 二、函数分类库函数自定义函数定义使用好处 三、函数参数实际参数&#xff08;实参&#xff09;形式参数&#xff08;形参&#xff09;内存分配 四、函数调用传值调用传址调用 五、函数嵌套调用与链式访问嵌套调用链式访问 六、函数…

8.8 【C语言】动态内存分配与指向它的指针变量

8.8.1 什么是内存的动态分配 栈&#xff1a;全局变量和局部变量&#xff0c;全局变量是分配在内存中的静态存储区的&#xff0c;非静态的局部变量是分配在内存中的动态存储区的。 堆&#xff1a;数据临时存放在一个特别的自由存储区。 8.8.2 怎样建立内存的动态分配 对内存…

Python标准库概览

Python标准库概览 知识点 标准库: turtle库(必选)标准库: random库(必选)、time库(可选&#xff09; 知识导图 1、turtle库概述 turtle&#xff08;海龟&#xff09;是Python重要的标准库之一&#xff0c;它能够进行基本的图形绘制。turtle库绘制图形有一个基本框架&#x…

RabbitMQ特性介绍和使用案例

❤ 作者主页&#xff1a;李奕赫揍小邰的博客 ❀ 个人介绍&#xff1a;大家好&#xff0c;我是李奕赫&#xff01;(&#xffe3;▽&#xffe3;)~* &#x1f34a; 记得点赞、收藏、评论⭐️⭐️⭐️ &#x1f4e3; 认真学习!!!&#x1f389;&#x1f389; 文章目录 RabbitMQ特性…

Web 开发 Django 管理工具

上次为大家介绍了 Django 的模型&#xff0c;通过模型就可以操作数据库&#xff0c;从而就可以改变页面的展示内容&#xff0c;那问题来了&#xff0c;我们只能通过手动编辑模型文件来配置模型吗&#xff1f;当然不是&#xff0c;Django 为我们提供了强大的工具&#xff0c;可以…

【架构】探索计算机处理器的世界:ARM和x86架构解析及指令集

目录 导语ARM架构x86架构AMD公司对比与应用不同架构处理器的指令集结语 导语 计算机处理器是数字化时代的核心引擎&#xff0c;而在众多处理器架构中&#xff0c;ARM和x86是备受关注的三个。本文将带您深入探索这三个架构&#xff0c;介绍它们的特点、公司背景以及应用领域。让…

ARM Linux 系统稳定性分析入门及渐进 13 -- gdb 反汇编 disassemble 命令详细介绍及举例】

文章目录 1.1 gdb 调试回顾1.1.1 gdb list 命令介绍 1.2 反汇编命令 dis 介绍1.2.1 如何设置 gdb 汇编代码的格式 1.1 gdb 调试回顾 在GNU调试器&#xff08;GDB&#xff09;中&#xff0c;有许多命令可以帮助我们调试应用程序。 gdb: 这是一个强大的Unix下的程序调试工具。以…