P1880 [NOI1995]石子合并[环形DP]

时间:2023-03-09 13:43:45
P1880 [NOI1995]石子合并[环形DP]

题目来源:洛谷

题目描述

在一个圆形操场的四周摆放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

解析:

这是一道相当经典的四边形不等式和环形dp,以至于一道NOI级别的题目被研究成了绿题。。。


首先题目意思很清楚,很容易看出来这就是个首尾相接的环。

我们先想一下如果不是环,而是一条链怎么做。
dp的思路很容易想出来,就是区间dp的思想,我们将将要合并的两堆石子的距离当做阶段,合并哪两堆石子作为状态,决策就是选择哪两堆石子合并。
我们定义dp[l][r]表示合并l~r之间的石子所需耗费的最小/大费用,其中l表示左端点的石子,r表示右端点的石子。
我们可以把每个l~r的状态看做两个子问题,假设k为l~r之间的任意一堆石子,我们就要使得l~k的答案和k+1~r的答案最优。因此我们枚举l~r之间的每个位置k,并寻找使得结果最优的一个k。
对应到动态规划中,就意味着两个较小的区间向一个更长的区间转移,k点不仅是两个小区间的划分点,也是转移的决策。
比方说如果我们求最小值,得出这样的状态转移方程:

dp[l][r]=min(1<=l<=n-len+1,l<r<=n,l<=k<r){dp[l][k]+dp[k+1][r]+w(l,r)};

其中w(l,r)函数表示合并l~r之间的石子的花费,len表示区间长度

这个东西叫二维四边形不等式,常用于优化区间dp,详细情况我这里就不讲了(我懒),如果有需要可以百度或者戳这里看dalao博客。

PS:关于初始化,我们知道每个石子堆移到它自己上面花费是0,所以。。。

思考一下为什么每次决策增加的是区间和:移动石子堆的花费是可叠加的,而且费用是会重叠的,如果我们要合并l~r之间的所有石子,那么显然无论它之前是由哪两堆合并而来的,花费都是sum[r]-sum[l-1](sum为前缀和数组)。

关于w(l,r)函数,我们可以用前缀和优化搞定,于是我们需要一个三重循环来解决此问题,复杂度也就差不多达到了O(n^3),这是无法承受大规模数据的。

动态规划有一种基本的优化思路,就是如果一个状态的某个维度可以由其他维度推出,那么我们直接在每循环的时候把它算出来就得了。

在这里,如果我们知道了左端点l和区间长度len的话,那么r就是可求得的,即r=l+len-1;

至此,链式dp的思路就讲完了。


你如果拿这个思路做这道题,会发现过不了(WA4个点),这是因为题目说了是环。

那环跟链有啥区别呢?

很容易发现,如果是环形的话,某两个点之间的距离可以有两种可能。

举个例子,我们假设石子总堆数为n,对于第一堆石子到最后一堆石子之间的距离len,如果是链式的话,只有n-1这种可能;而如果是环形的话,我们就有n-1和1两种可能。也就是说,每个石子堆之间都有两个距离(当然可以相等),那我们怎么设计dp算法呢?

有一种普遍的解法,就是把环拆开成相连的两条相同的链,然后在1~2n上跑dp。具体来说其实就是把上面讲到的两个距离拆出来分开处理,某个点l到第一条链有一个最优解,到第二条链又有一个最优解,我们再取这两个最优解的最优解。

所以之前的步骤我们可以参考链式dp写出来,而寻找两个最优解中的最优解,我们需要再分析一下。

链式的解法最优解就在dp[1][n],即把1~n堆石子全部合并。注意,这种情况下有且仅有1~n之间的距离为n-1

然而如果这是一个环,就有n个n-1的距离,也就是有n个局部最优解,我们还要在这些最优解里面找全局最优解,这也是本题的又一个关键点。

所以实际上我们扫一遍dp[i][i+n-1](1<=i<=n),找到最优解就行了。

参考代码:

 #include<cstdio>
#include<iostream>
#include<cstring>
#define N 1010
#define INF 0x3f3f3f3f
using namespace std;
int a[N<<],dp1[N][N],dp2[N][N],s[N];
int main()
{
int n;
cin>>n;
for(int i=;i<=n;i++) scanf("%d",&a[i]),a[i+n]=a[i];
memset(dp1,0x3f,sizeof(dp1));
memset(dp2,0xff,sizeof(dp2));
for(int i=;i<=n+n;i++){
dp1[i][i]=;dp2[i][i]=;
s[i]=s[i-]+a[i];
}
for(int len=;len<=n*;len++)
for(int l=;l<=n*-len+;l++)
{
int r=l+len-;
for(int k=l;k<r;k++)
{
dp1[l][r]=min(dp1[l][r],dp1[l][k]+dp1[k+][r]+(s[r]-s[l-]));
dp2[l][r]=max(dp2[l][r],dp2[l][k]+dp2[k+][r]+(s[r]-s[l-]));
}
}
int maxx=-INF,minn=INF;
for(int i=;i<=n;i++){
maxx=max(maxx,dp2[i][i+n-]);
minn=min(minn,dp1[i][i+n-]);
}
cout<<minn<<endl;
cout<<maxx<<endl;
return ;
}