P1880 [NOI1995] 石子合并

发布时间 2023-11-05 20:10:21作者: GXYZY

题目

题目描述

在一个圆形操场的四周摆放 \(N\) 堆石子,现要将石子有次序地合并成一堆,规定每次只能选相邻的 \(2\) 堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。

试设计出一个算法,计算出将 \(N\) 堆石子合并成 \(1\) 堆的最小得分和最大得分。

输入格式

数据的第 \(1\) 行是正整数 \(N\),表示有 \(N\) 堆石子。

\(2\) 行有 \(N\) 个整数,第 \(i\) 个整数 \(a_i\) 表示第 \(i\) 堆石子的个数。

输出格式

输出共 \(2\) 行,第 \(1\) 行为最小得分,第 \(2\) 行为最大得分。

样例 #1

样例输入 #1

4
4 5 9 4

样例输出 #1

43
54

提示

\(1\leq N\leq 100\)\(0\leq a_i\leq 20\)

解题思路

这是一道区间\(dp\)的模板题(好像所有人接触到区间dp都是通过这一道题的吧)

关于\(n\)堆石子围成一个环

关于环的基本解决方法无非就是两种,第一种就是将环展开后复制一遍并接到原来的后面,第二种则是用指针或数据结构直接模拟环。当然,最简单的办法还是第一种。

20231104221112.png

n=read();
for(int i=1;i<=n;i++){
	a[i]=read();
	a[n+i]=a[i];//将环展开并且复制一遍
}
for(int i=1;i<=2*n;i++){
	sum[i]=sum[i-1]+a[i];//求前缀和
}

求合并所产生的最大值

为什么是动态规划

合并石子的过程当我们把它展开成为一条链以后,就变成了求所有长度为\(n\)的石子堆合并后能够产生的最大值,而每一段石子都是由两堆合并而来的(相当于两段石子)。很明显,这样的子段具有重叠性(子问题的重叠性),就不能够用分治的思想来做了(子问题重叠后用搜索会重复经过某一个状态,导致时间复杂度很大)。(当然记忆化搜索也可以)

而同时这个问题也具备最优子结构的特性,就是当某一段和其他段合并时,无论得到的结果是否是最终的最优结果,就当前来说,这一段的最优值就是这一次合并后的最优值。

很显然,这个问题也没有后效性,求解这一段的最优解并不会影响其他结果。

为什么不是贪心

20231105181820.png

区间dp的思路

首先是区间\(dp\)最基本的思路:f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+val)

区间\(dp\),顾名思义,就是求解某一个区间的(最值?)问题,而这一个区间是由两段合并而来的,这两段可以任意划分。

由转移式,我们很容易发现我们需要先求出区间长度较小的\(dp\)值,然后一步一步将区间的长度变大,最后得到我们需要的区间答案。于是,我们就有了以下代码:

//先要处理环
for(int i=2;i<=n;i++){//枚举每一种长度,每次只会算这一种长度的串的最值
	for(int j=1;j<=n-i+1;j++){//枚举开始位置
		for(int k=j;k<=j+i-2;k++){//枚举断开的位置
			f[j][j+i-1]=max(f[j][j+i-1],f[j][k]+f[k+1][j+i-1]+sum[j+i-1]-sum[j-1]);
		}
	}
}

第一层循环从小到大枚举区间的长度,第二层循环枚举这个区间开始的位置,然后第三层循环枚举\(k\),即这个区间可能组成的方式(枚举划分点)。

对于这一道题而言,因为我们事先将环展开成为了链,而这个链的长度应该为\(2\times n-1\),然而实际上我们取\(2\times n\)也没有什么问题,只是会和第一种情况重复。又因为我们最后要求的区间长度只是为\(n\),所以代码实际上长这样:

for(int i=2;i<=n;i++){
	for(int j=1;j<=2*n-i+1;j++){//这里改了一下
		for(int k=j;k<=j+i-2;k++){
			f[j][j+i-1]=max(f[j][j+i-1],f[j][k]+f[k+1][j+i-1]+sum[j+i-1]-sum[j-1]);
		}
	}
}

以上是针对于求石子合并所产生的最大值。其实求解最小值的思路也差不多,只是我们需要先将长度为二及以上的区间赋值为一个比较大的数(比如说\(inf\))。然后就可以得到以下代码:

for(int i=1;i<=2*n;i++){
	for(int j=1;j<=2*n;j++){
		if(i!=j) l[i][j]=inf;
	}
}
for(int i=2;i<=n;i++){
	for(int j=1;j<=2*n-i+1;j++){
		for(int k=j;k<=j+i-2;k++){
			f[j][j+i-1]=max(f[j][j+i-1],f[j][k]+f[k+1][j+i-1]+sum[j+i-1]-sum[j-1]);
			l[j][j+i-1]=min(l[j][j+i-1],l[j][k]+l[k+1][j+i-1]+sum[j+i-1]-sum[j-1]);
		}
	}
}

以上就是这一道题的核心代码(没有优化)太弱啦

最后的代码

#include<bits/stdc++.h>
// #pragma GCC optimize(2)
const int inf=0x7fffffff;
using namespace std;
int read(){
	int x=0,f=1;
	char c=getchar();
	while(c>'9'||c<'0'){
		if(c=='-') f=-1;
		c=getchar();
	}
	while(c>='0'&&c<='9'){
		x=x*10+c-'0';
		c=getchar();
	}
	return x*f;
}
void write(int x){
	if(x<0) putchar('-'),x=-x;
	if(x>9) write(x/10);
	putchar(x%10+'0');
	return ;
}
int n,a[300],sum[300],f[300][300],l[300][300];
int main(){
	// freopen(".in","r",stdin);
	// freopen(".out","w",stdout);
	n=read();
	for(int i=1;i<=n;i++){
		a[i]=read();
		a[n+i]=a[i];
	}
	for(int i=1;i<=2*n;i++){
		sum[i]=sum[i-1]+a[i];
	}
	for(int i=1;i<=2*n;i++){
		for(int j=1;j<=2*n;j++){
			if(i!=j) l[i][j]=inf;
		}
	}
	//先要处理环
	for(int i=2;i<=n;i++){//枚举每一种长度,每次只会算这一种长度的串的最值
		for(int j=1;j<=2*n-i+1;j++){//枚举开始位置
			for(int k=j;k<=j+i-2;k++){//枚举断开的位置
				f[j][j+i-1]=max(f[j][j+i-1],f[j][k]+f[k+1][j+i-1]+sum[j+i-1]-sum[j-1]);
				l[j][j+i-1]=min(l[j][j+i-1],l[j][k]+l[k+1][j+i-1]+sum[j+i-1]-sum[j-1]);
			}
		}
	}
	int ansm=-inf;
	int ansl=inf;
	for(int i=1;i<=n;i++){
		ansm=max(ansm,f[i][i+n-1]);
		ansl=min(ansl,l[i][i+n-1]);
	}
	cout<<ansl<<endl<<ansm<<endl;
	return 0;
}