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}}} 本人的萌新QAQ是刚学OI但是是这样即使我也声音大声喊出要用蒟蒻的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 1≤N≤100, 0 ≤ a i ≤ 20 0\leq a_i\leq 20 0≤ai≤20。
解题思路
这是一道区间 d p dp dp的模板题~~(好像所有人接触到区间dp都是通过这一道题的吧)~~
关于 n n n堆石子围成一个环
关于环的基本解决方法无非就是两种,第一种就是将环展开后复制一遍并接到原来的后面,第二种则是用指针或数据结构直接模拟环。当然,最简单的办法还是第一种。
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的石子堆合并后能够产生的最大值,而每一段石子都是由两堆合并而来的(相当于两段石子)。很明显,这样的子段具有重叠性(子问题的重叠性),就不能够用分治的思想来做了(子问题重叠后用搜索会重复经过某一个状态,导致时间复杂度很大)。(当然记忆化搜索也可以)
而同时这个问题也具备最优子结构的特性,就是当某一段和其他段合并时,无论得到的结果是否是最终的最优结果,就当前来说,这一段的最优值就是这一次合并后的最优值。
很显然,这个问题也没有后效性,求解这一段的最优解并不会影响其他结果。
为什么不是贪心
区间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×n−1,然而实际上我们取 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;
}