本文专栏:研究生课程 点击查看系列文章
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 的合并代价的和的最小值。
即状态转移方程为:
通过上面的状态转移方程的建立过程,我们可以归纳出算法设计的主要思想。即本题所求解的问题,划分成若干的互相联系的子段,对每个子段求解,然后进行子段的合并,产生的多个阶段,进行决策,即使用状态转移方程进行取舍。最终从所有可选的序列中选取一个能解决本问题的最优解。
所以,本动态规划算法的算法描述如下所示。
算法描述:
最小代价数组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]
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