【课程·研】算法 | 石子合并问题(动态规划)

本文专栏:研究生课程  点击查看系列文章

1. 问题描述

有N堆石子排成一排,每堆石子有一定的数量。现要将N堆石子并成为一堆。合并的过程只能每次将相邻的两堆石子堆成一堆,每次合并花费的代价为这两堆石子的和,经过N-1次合并后成为一堆。求出总的代价最小值。

2. 输入输出示例

输入

​ 有多组测试数据,输入到文件结束。

​ 每组测试数据第一行有一个整数n,表示有n堆石子。

​ 接下来的一行有n(0< n <200)个数,分别表示这n堆石子的数目,用空格隔开

输出

​ 输出总代价的最小值,占单独的一行

样例输入

3
1 2 3
7
13 7 8 16 21 4 18

样例输出

9
239

3. 算法设计思想与算法描述

首先分析本题。题目输入的数据是石子堆的个数n,以及每堆石子堆的数量a i 。合并的过程中,只能选择相邻的两堆石子堆进行合并。所以最终花费的代价,与石子堆的排列顺序也有关。

我们可以将大问题划分成小的问题进行分析。首先肯定是两两相邻的石子堆进行合并。最初我们不知道要从哪里开始合并,则遍历所有的石子堆,进行两两合并。比如序列 {1,2,3,4} ,1号和2号合并:sum[1][2]=3,2号和3号合并:sum[2][3]=5 ,3号和4号合并:sum[3][4]=7 ,这里我们用 sum[i][j] 表示当前合并步骤从 i 合并到 j 花费的代价,用 dp[i][j] 表示从 i 和并到 j 的累计代价,第1步的累计代价等于当前步骤的代价。

接下来再进行3个长度的合并,则只有2种合并方案:1号、2号、3号合并或者是2号、3号、4号合并。在1号、2号、3号合并的过程中,则又有两种方案:{{1,2},3} 还是 {1,{2,3}}。对于前者,当前步骤花费代价 sum[1][3] = sum[1][2] + sum[3][3] = 3+3 = 6 ,加上第一步已经花费的代价 sum[1][2]=3 ,总代价为 dp[1][3] = sum[1][2] + sum[1][3] = 9 ;对于后者,花费代价 sum[1][3] = sum[1][1] + sum[2][3] = 1+5 = 6 ,加上第一步已经花费的代价 sum[2][3]=5 ,总代价为 dp[1][3] = sum[1][3] + sum[2][3] = 9 。所以第二轮的最少代价应该在第一种方案中。

通过上面这个简单的示例,我们可以归纳总结出,实际的从 i 到 j 的最小合并代价,应该是选择:从已有的 i 到 j 的最小合并代价,与从 i 到 k 的最小代价、从 k+1 到 j 的最小代价、当前步骤从 i 到 j 的合并代价的和的最小值。

即状态转移方程为:

image-20210216154157313

通过上面的状态转移方程的建立过程,我们可以归纳出算法设计的主要思想。即本题所求解的问题,划分成若干的互相联系的子段,对每个子段求解,然后进行子段的合并,产生的多个阶段,进行决策,即使用状态转移方程进行取舍。最终从所有可选的序列中选取一个能解决本问题的最优解。

所以,本动态规划算法的算法描述如下所示。

算法描述:

最小代价数组dp初始化
for 合并的堆数lenth从1到n-1依次增加 do
    for 合并的起点i从1到n- lenth do
    合并的终点j=i+ lenth
    for 中间点k从i到j移动 do
      求出当前这一步从i到j的代价sum[i][j]
      执行状态转移方程,求出dp[i][j]的最小值
    end for
  end for
end for
输出dp[1][n]

image-20210216154247860

4. 算法实现

根据前文的算法描述,采用C++高级语言实现了该算法。

#include "pch.h"

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
using namespace std;

const int maxn = 250;		//石子个数上限
const int INF = 0x7fffff;	//∞

int n;					//石子个数
int S[maxn];			//输入的石子序列
int sum[maxn][maxn];	//表示第 i 堆到第 j 堆的石子总和
int dp[maxn][maxn];		//表示从第 i 堆合并到第 j 堆的最小代价


int main()
{
	//输入石子个数n
	while (~scanf_s("%d", &n))
	{
		//输入n堆石子的数量(代价)
		for (int i = 1; i <= n; i++)
			scanf_s("%d", &S[i]), sum[i][i] = S[i];

		//dp 数组初始化
		for (int i = 1; i <= n; i++)
			for (int j = i; j <= n; j++)
				//从节点i到j:如果是从当前节点到当前节点,则最小代价为0,反之为无穷大
				dp[i][j] = (i == j ? 0 : INF);

		//三重循环求动态规划问题
		//最外层循环:len是多少堆进行合并,
		for (int len = 1; len < n; len++)
		{
			// i 表示合并的起点,i+len表示合并的终点,循环终止条件保证合并的终点不超n
			for (int i = 1; i + len <= n; i++)
			{
				//i+len表示合并的终点
				int j = i + len;
				//k 游离在i到j之间,进行动态合并
				for (int k = i; k < j; k++)
				{
					//求出当前这一步从i合并到j的代价
					sum[i][j] = sum[i][k] + sum[k + 1][j];
					//执行状态转移方程,取:已有的i到j合并代价,和i到k,k+1到j,以及这一步的代价和的最小值
					dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + sum[i][j]);
				}
			}
		}
		//输出从1合并到最后一堆石子的最小代价
		cout << dp[1][n] << endl;
		cout << endl;

		//测试输出
		/*
		for (int i = 1; i <= n; i++)
		{
			cout << S[i] << "\t";
		}
		cout << endl;
		for (int i = 1; i <= n; i++)
		{
			for (int j = 1; j <= n; j++) {
				cout << sum[i][j] << "\t";
			}
			cout << endl;
		}
		cout << endl;
		for (int i = 1; i <= n; i++)
		{
			for (int j = 1; j <= n; j++) {
				cout << dp[i][j] << "\t";
			}
			cout << endl;
		}*/
	}
	return 0;
}

5. 测试结果

控制台输入数据,然后执行算法。两组测试结果(输入与输出)如下文所示。

测试用例1

输入数据:7 {13 7 8 16 21 4 18}

输出数据:239

测试用例2

输入数据:15 {1 2 3 4 5 6 7 8 9 10 9 8 7 6 5}

输出数据:341

image-20210216154457920

  • 4
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
将n堆石子合并成一堆的规则是每次只能选相邻的石子堆进行合并。为了使合并次数最少,我们需要考虑如何选择相邻的石子堆。 假设有4堆石子,分别有a、b、c、d颗石子。如果我们选择合并a和b堆,然后将新堆和c堆合并,最后再将新堆和d堆合并合并次数是a+b+c+d。但是如果我们先合并b和c堆,然后是a和合并后的bc堆,最后再与d合并合并次数是b+c+(a+bc)+d。 可以观察到,合并次数会受到选择合并顺序的影响。因此,我们需要根据石子堆的数量和石子堆的石子数量来确定合并的顺序。 设dp[i][j]表示合并从第i堆到第j堆石子所需的最小合并次数。那么有以下递推公式: dp[i][j] = min(dp[i][k] + dp[k+1][j] + sum[i][j]), k∈[i, j-1],其中sum[i][j]表示第i堆到第j堆石子的总数。 当i=j时,dp[i][j]为0,因为此时只有一堆石子,无需合并。当i<j时,dp[i][j]可以通过遍历k来求得最小值。 具体操作是,首先初始化dp数组为0。然后从2堆石子开始,依次递增地计算合并次数。根据递推公式,遍历每个dp[i][j],通过计算dp[i][k] + dp[k+1][j] + sum[i][j]的值,更新dp[i][j]的最小值。 最终,dp[1][n]的值即为将n堆石子合并成一堆所需的最小合并次数。 例如,有4堆石子,分别有a、b、c、d颗。通过计算dp[1][4],可以得到将这4堆石子合并成一堆的最小合并次数。 这样,我们可以利用动态规划的方法解决将n堆石子合并成一堆的问题

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拾年之璐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值