[NOI1995]石子合并

P1880 [NOI1995] 石子合并

本 人 的 萌 新 Q A Q 是 刚 学 O I 但 是 是 这 样 即 使 我 也 声 音 大 声 喊 出 要 用 蒟 蒻 的 I O I   A K   I \textcolor{green}{\small\text{\color{#ff0000}{\texttt{本}}}\huge\text{\color{#ff0000}{人}}_{\small\text{\color{#aa5500}{\texttt{的}}}^{\large\text{\color{#996600}{萌}\color{#996600}{新}}\small\texttt{\color{#887700}{Q}\color{#778800}{A}\color{#669900}{Q}}}}^{\large\text{\color{#ee1100}{是}}{\small\texttt{\color{#dd2200}{刚}\color{#cc3300}{学}}\large\texttt{\color{#cc3300}{O}\color{#bb4400}{I}}}}\huge\text{\color{#55aa00}{但}\color{#55aa00}{是}}^{\large\text{\color{#44bb00}{即}}{\small\text{\color{#33cc00}{使}}}}_{\normalsize\text{\color{#22dd00}{是}\color{#22dd00}{这}\color{#11ee00}{样}}}\text{\color{#00ff00}{我}\color{#00ee11}{也}}^{\small\text{\color{#00dd22}{要}}\normalsize\texttt{\color{#00dd22}{用}}\texttt{\color{#00cc33}{蒟}}_{\texttt{\color{#00bb44}{蒻}}\large\texttt{\color{#00aa55}{的}}}}_{\scriptsize\texttt{\color{#00aa55}{声}\color{#009966}{音}\color{#008877}{大}\color{#007788}{声}\color{#006699}{喊}\color{#006699}{出}}}\texttt{\color{#0055aa}{I}\color{#0044bb}{O}\color{#0033cc}{I} \color{#0033cc}{A}\color{#0022dd}{K} \color{#0011ee}{I}}} QAQOI使IOI AK I

题目

题目描述

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

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

输入格式

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

2 2 2 行有 N N N 个整数,第 i i i 个整数 a i a_i ai 表示第 i i i 堆石子的个数。

输出格式

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

样例 #1

样例输入 #1

4
4 5 9 4

样例输出 #1

43
54

提示

1 ≤ N ≤ 100 1\leq N\leq 100 1N100 0 ≤ a i ≤ 20 0\leq a_i\leq 20 0ai20

解题思路

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

关于 n n 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 n n的石子堆合并后能够产生的最大值,而每一段石子都是由两堆合并而来的(相当于两段石子)。很明显,这样的子段具有重叠性(子问题的重叠性),就不能够用分治的思想来做了(子问题重叠后用搜索会重复经过某一个状态,导致时间复杂度很大)。(当然记忆化搜索也可以)

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

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

为什么不是贪心

20231105181820.png

区间dp的思路

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

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

由转移式,我们很容易发现我们需要先求出区间长度较小的 d p 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 k k,即这个区间可能组成的方式(枚举划分点)。

对于这一道题而言,因为我们事先将环展开成为了链,而这个链的长度应该为 2 × n − 1 2\times n-1 2×n1,然而实际上我们取 2 × n 2\times n 2×n也没有什么问题,只是会和第一种情况重复。又因为我们最后要求的区间长度只是为 n 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]);
		}
	}
}

以上是针对于求石子合并所产生的最大值。其实求解最小值的思路也差不多,只是我们需要先将长度为二及以上的区间赋值为一个比较大的数(比如说 i n f inf 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;
}
);
			}
		}
	}
	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;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值