[NOI 1995] 石子合并 · 教师题解
[NOI 1995] 石子合并
知识点
区间动态规划、环形结构、前缀和。
解题思路
复制数组,将圆环展开成长度为2N的序列。任何一种圆环合并方案最后都可以选择一个断点,转化成长度为N的连续区间;枚举这个区间的左端点即可覆盖所有断点。
令mn[l][r]、mx[l][r]分别为将区间[l,r]合并成一堆的最小、最大得分。最后一次合并必定将[l,k]与[k+1,r]这两堆相加,额外得分为原区间石子总数。于是分别对所有k取最小值和最大值。用前缀和O(1)求区间总数,按区间长度从小到大计算。
正确性说明
单堆不需合并,两种状态值都为0。对于更长区间,任意合法方案的最后一步都对应唯一分界k,之前的操作仅发生于左右两个子区间。若某个子区间未采用相应最优方案,替换它可改进总得分,因此最优方案满足上述转移。反过来,每个分界与左右合法方案都能拼成一个合法合并方案,所以转移没有遗漏或引入非法方案。圆环的任意最终分界都包含在N个断点中,对它们取最优值得到圆环答案。
易错点
N=1时两行都应输出0。石子允许为0,不能把0当作“未计算”。最小状态初始值设为足够大的数,最大状态可由0开始,因为所有得分非负。只统计长度N的区间,并在最小值和最大值上分别取最优,不能用一般贪心合并替代相邻合并限制。
复杂度
复制后状态数O(N²),每个状态枚举O(N)个分界;总时间O(N³),总空间O(N²)。
参考代码(GNU C++17)
#include<bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false);cin.tie(nullptr);
int n;cin>>n;
vector<long long> a(2*n+1),sum(2*n+1);
for(int i=1;i<=n;++i){cin>>a[i];a[i+n]=a[i];}
for(int i=1;i<=2*n;++i) sum[i]=sum[i-1]+a[i];
vector<vector<long long>> mn(2*n+1,vector<long long>(2*n+1));
vector<vector<long long>> mx=mn;
for(int len=2;len<=n;++len){
for(int l=1;l+len-1<=2*n;++l){
int r=l+len-1;mn[l][r]=LLONG_MAX;
long long weight=sum[r]-sum[l-1];
for(int k=l;k<r;++k){
mn[l][r]=min(mn[l][r],mn[l][k]+mn[k+1][r]+weight);
mx[l][r]=max(mx[l][r],mx[l][k]+mx[k+1][r]+weight);
}
}
}
long long bestMin=LLONG_MAX,bestMax=0;
for(int l=1;l<=n;++l){bestMin=min(bestMin,mn[l][l+n-1]);bestMax=max(bestMax,mx[l][l+n-1]);}
cout<<bestMin<<'\n'<<bestMax<<'\n';
return 0;
}