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

这篇具有很好参考价值的文章主要介绍了C++动态规划经典案例解析之合并石子。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1. 前言

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

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

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

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

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

C++动态规划经典案例解析之合并石子,C++编程之美,c++,动态规划,开发语言

直接的解法:

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

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

C++动态规划经典案例解析之合并石子,C++编程之美,c++,动态规划,开发语言

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

C++动态规划经典案例解析之合并石子,C++编程之美,c++,动态规划,开发语言

如上的问题便是简单的区间类型问题,解决此类问题的方案称为简单区间类型动态规划。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 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";
	}
	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]的和。

C++动态规划经典案例解析之合并石子,C++编程之美,c++,动态规划,开发语言

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

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

C++动态规划经典案例解析之合并石子,C++编程之美,c++,动态规划,开发语言

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

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

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

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

C++动态规划经典案例解析之合并石子,C++编程之美,c++,动态规划,开发语言

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

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

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

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

C++动态规划经典案例解析之合并石子,C++编程之美,c++,动态规划,开发语言

编码实现:

  • 初如化变量。
#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) {
	//只有一堆石子,返回 0
	if(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;
}

是否存在重叠子问题?

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

C++动态规划经典案例解析之合并石子,C++编程之美,c++,动态规划,开发语言

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

//记忆数组
int dp[100][100]={0}; 
int  getSz(int l,int r) {
	//只有一堆石子,返回 0
	if(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;
		}
		//一堆石子的值为 0
		dp[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

C++动态规划经典案例解析之合并石子,C++编程之美,c++,动态规划,开发语言

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

C++动态规划经典案例解析之合并石子,C++编程之美,c++,动态规划,开发语言

动态规划实现:

#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. 总结

沉淀过程是一种修行。文章来源地址https://www.toymoban.com/news/detail-670739.html

到了这里,关于C++动态规划经典案例解析之合并石子的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【动态规划】之石子合并问题

    在一个操场上一排地摆放着N堆石子。现要将石子有次序地合并成一堆。规定每次只能选相邻的2堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的得分。请设计一个程序,计算出将N堆石子合并成一堆的最小得分和最大得分。 我们对n的取值逐步分析 当n=1时,没有进

    2024年04月28日
    浏览(37)
  • 动态规划——区间dp [石子合并]

    动态规划(dp)是一种通过将问题分解为子问题,并利用已解决的子问题的解来求解原问题的方法。适用于具有重叠子问题和最优子结构性质的优化问题。通过定义状态和状态转移方程,动态规划可以在避免重复计算的同时找到问题的最优解,是一种高效的求解方法,常用于

    2024年02月12日
    浏览(46)
  • 石子合并(动态规划 区间DP)+详细注释

    原题链接   活动 - AcWing 题目 设有 N 堆石子排成一排,其编号为 1,2,3,…,N。 每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。 每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相

    2024年02月16日
    浏览(39)
  • 租用游艇问题 石子合并问题 动态规划实验

    实验名称:                动态规划                          一、实验预习 1、实验目的 1. 理解并掌握动态规划方法的设计思想; 2. 提高应用动态规划方法解决问题和设计算法的能力; 3. 通过编程实现租用游艇问题和石子合并问题,进一步理解动态规划方

    2024年02月07日
    浏览(47)
  • C++动态规划经典试题解析之打家劫舍系列

    力扣上有几道与打家劫舍相关的题目,算是学习动态规划时常被提及的经典试题,很有代表性,常在因内大大小小的社区内看到众人对此类问题的讨论。 学习最好的方式便是归纳总结、借鉴消化,基于这个目的,本文对此类问题也做了讲解,在一些优秀思想的基础上添加了个

    2024年02月13日
    浏览(43)
  • 【动态规划】【C++算法】1563 石子游戏 V

    【数位dp】【动态规划】【状态压缩】【推荐】1012. 至少有 1 位重复的数字 动态规划汇总 几块石子 排成一行 ,每块石子都有一个关联值,关联值为整数,由数组 stoneValue 给出。 游戏中的每一轮:Alice 会将这行石子分成两个 非空行(即,左侧行和右侧行);Bob 负责计算每一

    2024年02月21日
    浏览(34)
  • 多线程四大经典案例

    本节内容很重要, 希 望 大 家 可 以 好 好 看 看 , 一 起 加 油~ 1.单线模式 1.1饿汉模式 1.2懒汉模式 2.阻塞式队列 2.1阻塞队列是什么 2.2生产者消费者模型 2.3标准库中的阻塞队列 2.4阻塞队列的实现 3.定时器 3.1定时器是什么 3.2标准库中的定时器 3.3实现定时器 4.线程池 4.1什么

    2023年04月27日
    浏览(49)
  • solidity经典案例-----智能投票

    角色分析:包括主持人、选民 功能分析: 仅主持人能授权给每个选民1票,即每个参与投票的选民拥有1票投票权。 选民可以选择将票数委托给其它选民,当然,收委托的选民仍然可以将票数继续委托给其它选民,即存在a—b–c–d,但是,一旦将票数委托给其它选民后,自己

    2024年01月16日
    浏览(51)
  • 国内外大数据经典案例研究

    大数据时代的来临使得产生的数据量呈爆炸式增长,各行各业均面临着海量数据的分析、处理问题。如何运用大数据技术从海量数据中挖掘出有价值的信息,将是今后企业发展的一个巨大挑战。点评收集研究了国内外大数据应用的经典案例,希望可以对读者有所启示。 1 、塔

    2024年02月05日
    浏览(77)
  • 路由器故障排错三大经典案例

    对于网络管理员来说,熟悉与掌握路由排错的思路和技巧是非常必要的。小编将通过三例典型的路由故障排错案例进行分析。 案例1 不堪重负,路由器外网口关闭 1、网络环境 某单位使用的是Cisco路由器,租用电信30MB做本地接入和l0MB教育网双线路上网,两年来网络运行稳定,

    2024年02月05日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包