洛谷线性动态规划训练(3):P1880 [NOI1995]石子合并——区间DP

P1880 [NOI1995]石子合并

这一题比较好的解法应该是这个博主写的
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终止条件与跳出循环的条件

1 dp的常用思路与状态方程的定义

本题和矩阵链相乘的那题很像,都是属于区间dp的题目。对于区间dp,一个明显的特点就是相邻元素之间有一些必要的计算。因此,对于这类问题,在计算dp的时候需要2个变量来确定开始端和结束端,因此这里定义dp[i][j]为:[i,j]区间合并得到的最大分。那么dp[i][j]就可以通过枚举不同的子组合方式得到,这里的枚举是按照长度枚举。具体可见第2点。

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,含义是合并自己并没有得分。

3 dp终止条件与跳出循环的条件

本题对于计算代价使用了简单的前缀和,这里不赘述。由于是环状数组,因此开了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<r, 为什么不是k<=r呢? 因为方程是写为

dp[l][r]=dp[l][k]+dp[k+1][r]+cost;因此如果k==r,那么dp[k+1][r]==dp[r+1][r]这是不合理的,左端比右边还大1.因此k的范围就是k<r;

#include<iostream> 
#include<climits>
#include<algorithm>
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++) {//这里k<r而不是<=的原因我觉得就是因为如果k==r,k+1==r+1>r,那么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<iostream>
#include<vector>
#include<cstring>
#include<climits>
using namespace std;

int main() {
	vector<int> vec(105);
	int n;
	cin >> n;
	for (int i = 1; i <= n; i++) {//从1-n
		cin >> vec[i];
	}
	vector<int> 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;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值