[NOI1995] 石子合并
题目描述
在一个圆形操场的四周摆放 N
堆石子,现要将石子有次序地合并成一堆,规定每次只能选相邻的 2
堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
试设计出一个算法,计算出将 N
堆石子合并成 1
堆的最小得分和最大得分。
输入格式
数据的第 1
行是正整数 N
,表示有 N
堆石子。
第 2
行有 N
个整数,第 i
个整数 a_i
表示第 i
堆石子的个数。
输出格式
输出共 2
行,第 1
行为最小得分,第 2
行为最大得分。
样例输入 #1
4
4 5 9 4
样例输出 #1
43
54
提示
1<N< 100
,0<a_i<20
。题目分析主要是一个DFS深搜,要从n个数中找出任意k个数相加,计算其值是否为素数。
- 和排列组合为问题相似,n选3,n选n,有几种排列的方式
- 但是这个题要的是 k 个数,所以要使用 num 来计数
- 并且还需要用一个 for 循环来控制是否需要进入 dfs
题目分析:
这是一道区间dp十分经典的模板题,让我们揣测一下,前辈们是如何得到这个状态转移方程的。
首先,要计算合并的最大值、最小值,既然是动态规划,我们需要洞悉其中一些关联且确定的状态。
以下以最大值为例。
既然是最大值,那么求得的结果是否满足每一区间都是该区间所能达得到的的最大值?
显然是这样的。反证法:倘若有一个区间不是,那么换做该区间取得最大值的方案,最终结果将比原得分大。显然必定满足任意区间得分一定是该区间内的最大值。
这样我们可以定义状态f[i][j],表示i到j合并后的最大得分。其中1<=i<=j<=N。
既然这样,我们就需要将这一圈石子分割。很显然,我们需要枚举一个k,来作为这一圈石子的分割线。
这样我们就能得到状态转移方程:
f[i][j] = max(f[i][k] + f[k+1][j] + d(i,j));其中,1<=i<=<=k<j<=N。
d(i,j)表示从i到j石子个数的和。
那么如何编写更快的递推来解决这个问题?
在考虑如何递推时,通常考虑如下几个方面:
是否能覆盖全部状态?
求解后面状态时是否保证前面状态已经确定?
是否修改了已经确定的状态?
也就是说,在考虑递推顺序时,务必参考动态规划的适应对象多具有的性质,具体参考《算法导论》相关或百度百科或wiki。
既然之前说过我们需要枚举k来划分i和j,那么如果通过枚举i和j进行状态转移,很显然某些k值时并不能保证已经确定过所需状态。
如,i=1 to 10,j=1 to 10,k=1 to 9.当i=1,j=5,k=3时,显然状态f[k+1][j]没有结果。
那么,我们是不是应该考虑枚举k?
但这样i和j就难以确定了。
我们不难得到一个两全的方法:枚举j-i,并在j-i中枚举k。这样,就能保证地推的正确。
程序如下:
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int n,minl,maxl,f1[300][300],f2[300][300],num[300];
int s[300];
inline int d(int i,int j){return s[j]-s[i-1];}
//转移方程:f[i][j] = max(f[i][k]+f[k+1][j]+d[i][j];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n+n;i++) //好吧,终于有时间看看评论区,看来大家对这里异议蛮多的,这里统一解释一下,因为是一个环,所以需要开到两倍再枚举分界线,最后肯定是最大的
{
scanf("%d",&num[i]);
num[i+n]=num[i];
s[i]=s[i-1]+num[i];
}
for(int p=1;p<n;p++)
{
for(int i=1,j=i+p;(j<n+n) && (i<n+n);i++,j=i+p)
{
f2[i][j]=999999999;
for(int k=i;k<j;k++)
{
f1[i][j] = max(f1[i][j], f1[i][k]+f1[k+1][j]+d(i,j));
f2[i][j] = min(f2[i][j], f2[i][k]+f2[k+1][j]+d(i,j));
}
}
}
minl=999999999;
for(int i=1;i<=n;i++)
{
maxl=max(maxl,f1[i][i+n-1]);
minl=min(minl,f2[i][i+n-1]);
}
printf("%d\n%d",minl,maxl);
return 0;
}