这一题比较好的解法应该是这个博主写的
https://blog.csdn.net/qq_40663810/article/details/87375223
题目描述
在一个圆形操场的四周摆放N堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
试设计出1个算法,计算出将N堆石子合并成1堆的最小得分和最大得分.
输入格式
数据的第1行试正整数N,1≤N≤100,表示有N堆石子.第2行有N个数,分别表示每堆石子的个数.
输出格式
输出共2行,第1行为最小得分,第2行为最大得分.
输入输出样例
输入 #1复制
4
4 5 9 4
输出 #1复制
43
54
1.说明一下区间dp的常用思路与状态方程的定义
2.说明一下本题的遍历顺序与枚举顺序的原因
3.说明本题的dp终止条件与跳出循环的条件
本题和矩阵链相乘的那题很像,都是属于区间dp的题目。对于区间dp,一个明显的特点就是相邻元素之间有一些必要的计算。因此,对于这类问题,在计算dp的时候需要2个变量来确定开始端和结束端,因此这里定义dp[i][j]为:[i,j]区间合并得到的最大分。那么dp[i][j]就可以通过枚举不同的子组合方式得到,这里的枚举是按照长度枚举。具体可见第2点。
本题中的枚举是按照区间长度来枚举的。它的模板如下。
for len=2:n//定义len为区间长度
for left=1:n
for k=left:right-1
dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]+cost(i,j);
按照区间枚举是为了解决计算顺序的问题,因为在计算长区间的时候必定要用到短区间的结果(比如算长度为10的区间必定要用到长度为3,或者4等等的区间计算结果),因此我们先把短区间计算出来,是算是解决一个计算顺序的问题。
因此dp[i][j]矩阵也是按照长度先后顺序改写的,并且改写的元素i不等于j,l等于r的元素一直是0,含义是合并自己并没有得分。
本题对于计算代价使用了简单的前缀和,这里不赘述。由于是环状数组,因此开了2n的长度,但是实际上能用到的是1-2n-1的序列而已,因为长度为n的数组,左端是n的话,右端为2n-1,因此第2n个数是用不上的。
在计算的时候,遍历的时候要求r<2n(即r<=2n-1),而具体的r其实就是r=left+len-1(该公式类似于j-i+1=len),因此第二层我们的跳出遍历的条件就是left+len-1<2n,在该层循环中解决了环状数组的所有同一个长度的遍历问题,l一直算超出n是因为环状数组的计算需要。
在第三层循环中,k初始化为i,没有问题最左侧就dp[i][i]== 0,k的范围是到k
dp[l][r]=dp[l][k]+dp[k+1][r]+cost;因此如果k==r,那么dp[k+1][r]==dp[r+1][r]这是不合理的,左端比右边还大1.因此k的范围就是k
#include
#include
#include
using namespace std;
int combine(int i, int j, int arr[]) {
//计算合并两个部分的得分
return arr[j] - arr[i];
}
int main() {
int n;
int weight[205];
int presum[205] = {};
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> weight[i];
weight[i + n] = weight[i];//环状数组的延长,有效的是1-2n-1的部分
}
for (int i = 1; i < 2 * n; i++) {
presum[i] = presum[i - 1] + weight[i];
}
int maxdp[205][205] = {};//定义dp[i][j]为合并[i,j]区间能够得到的最大分数,主要是针对dp[i][i]的情况花费就是0
int mindp[205][205] = {};
for (int len = 2; len <= n; len++) {//枚举区间长度
for (int l = 1; l + len - 1 < 2 * n; l++) {//l+len-1实际上就r的范围
int r = l + len - 1;//注意,l永远不等于r
maxdp[l][r] = INT_MIN;//分别初始化
mindp[l][r] = INT_MAX;
for (int k = l; k < r; k++) {//这里kr,那么dp[r+1][r]就没有意义了
maxdp[l][r] = max(maxdp[l][r], maxdp[l][k] + maxdp[k + 1][r] + combine(l - 1, r, presum));
mindp[l][r] = min(mindp[l][r], mindp[l][k] + mindp[k + 1][r] + combine(l - 1, r, presum));
}
}
}
int maxres, minres;
maxres = INT_MIN;
minres = INT_MAX;
for (int i = 1; i <= n; i++) {
maxres = max(maxres, maxdp[i][i + n - 1]);
minres = min(minres, mindp[i][i + n - 1]);
}
cout << minres << endl;
cout << maxres << endl;
return 0;
}
这个思路是类似于贪心的方法,但是我觉得并不对,因为第i-1次的最大并不能递推得到第i次的最大值。
#include
#include
#include
#include
using namespace std;
int main() {
vector vec(105);
int n;
cin >> n;
for (int i = 1; i <= n; i++) {//从1-n
cin >> vec[i];
}
vector vec2(vec);
int dp[105];//dp[i]定义为合并i次后,总得分的最大值
int mindp[105];//mindp[i]定义为合并i次后,总得分的最小值
int sum[105];//sum[i]为第i堆与第i+1堆的求和的值,如果是末尾,就记为和第一堆求和的值
memset(dp, 0, sizeof(dp));
memset(sum, 0, sizeof(sum));
int pos;
int maxsum;
int cnt = n;
for (int i = 1; i < n; i++) {
maxsum = 0;
for (int j = 1; j <= cnt; j++) {
if (j != cnt) {
sum[j] = vec[j] + vec[j + 1];//最后一堆之前
}
else if (j ==cnt) {
sum[j] = vec[j] + vec[1];//最后一堆,和第一堆求和
}
if (sum[j] > maxsum) {
maxsum = sum[j];
pos = j;
}
}//跳出循环的时候已经找到了最大值和它的相对位置
dp[i] = dp[i - 1] + maxsum;//计算dp
//合并堆
if (pos == cnt) {//最后一堆的合并
vec[pos] = vec[pos] + vec[1];
vec.erase(vec.begin() + 1);
cnt--;
}
else if (pos != cnt) {
vec[pos] = vec[pos] + vec[pos + 1];
vec.erase(vec.begin() + pos + 1);
cnt--;
}
}
//计算最小的情况
for (int i = 0; i <= n; i++) {
mindp[i] = 0;
sum[i] = INT_MAX;
}
int pos2;
int minsum;
cnt = n;
for (int i = 1; i < n; i++) {
minsum = INT_MAX;
for (int j = 1; j <= cnt; j++) {
if (j != cnt) {
sum[j] = vec2[j] + vec2[j + 1];//最后一堆之前
}
else if (j == cnt) {
sum[j] = vec2[j] + vec2[1];//最后一堆,和第一堆求和
}
if (sum[j] < minsum) {
minsum = sum[j];
pos2 = j;
}
}//跳出循环的时候已经找到了最大值和它的相对位置
mindp[i] = mindp[i - 1] + minsum;//计算dp
//合并堆
if (pos2 == cnt) {//最后一堆的合并
vec2[pos2] = vec2[pos2] + vec2[1];
vec2.erase(vec2.begin() + 1);
cnt--;
}
else if (pos2 != cnt) {
vec2[pos2] = vec2[pos2] + vec2[pos2 + 1];
vec2.erase(vec2.begin() + pos2 + 1);
cnt--;
}
}
std::cout << mindp[n - 1] << endl;
std::cout << dp[n - 1];
return 0;
}