[题解]P2300 合并神犇
题意:给定一个长度为\(n\)的序列,每次可以合并相邻的两个数,求最小合并次数使得序列从左到右单调不减。
令\(dp_i\)为将前\(i\)个数变为单调不减序列的最小合并次数,\(s_i\)为\(i\)的前缀和。
那么显然\(dp_i=min(dp_j+i-j-1)\),\(j\)满足\(s[i]-s[j] \ge s[j]-s[pre[j]]\),其中\(pre[j]\)为\(j\)的决策点。
枚举\(j\)可以拿到\(50pts\)。
代码:
#include <cstdio>
using namespace std;
#define il inline
#define re register
typedef long long ll;
const int N=1e6+10;
const ll inf=0x3f3f3f3f;
namespace FastIO
{
char buf[1<<21],buf2[1<<21],*p1=buf,*p2=buf;
int p,p3=-1;
il int getc(){return p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++;}
il void flush(){fwrite(buf2,1,p3+1,stdout),p3=-1;}
#define isdigit(ch) (ch>=48&&ch<=57)
template <typename T>
il void read(T &x)
{
re bool f=0;x=0;
re char ch=getc();
while(!isdigit(ch)) f|=(ch=='-'),ch=getc();
while(isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getc();
x=f?-x:x;
}
template <typename T>
il void print(T x)
{
if(p3>(1<<20)) flush();
if(x<0) buf2[++p3]=45,x=-x;
re int a[50]={};
do{a[++p]=x%10+48;}while(x/=10);
do{buf2[++p3]=a[p];}while(--p);
}
}
using namespace FastIO;
int n;
int a[N];
ll s[N];
int dp[N],pre[N];
int main()
{
read(n);
for(re int i(1);i<=n;++i) read(a[i]);
for(re int i(1);i<=n;++i) s[i]=s[i-1]+a[i];
for(re int i(2);i<=n;++i)
for(re int j(i-1);j;--j)
if(s[i]>=s[j]*2-s[pre[j]]){pre[i]=j;break;}
for(re int i=1;i<=n;++i) dp[i]=dp[pre[i]]+i-pre[i]-1;
printf("%d",dp[n]);
flush();return 0;
}
细心的人会发现这个代码中有一个优化:if(s[i]>=s[j]*2-s[pre[j]]){pre[i]=j;break;}
因为合并完之后的序列长度为原长度减去总共合并次数,所以要求合并次数最小可以转化为使得最终的序列尽量长。而且最终得到的序列中的一个数一定是由原序列中一段连续的区间得到的(因为每次只能合并相邻的两个数),我们可以把问题转化为将原序列划分成尽量多的区间,且保证区间的和单调不减。既然是要让区间数尽可能地多,那我们就应该让每个区间尽可能地短,然而我们的元素都为正数,所以区间长度和区间和成正关系。又因为我们最后一段的和是最大的,之前的区间的和一定小于等于最后一段,所以我们只需让最后一段最小即可保证前面都最小。
于是我们只需找到\(i\)之前第一个满足\(s[i]-s[j] \ge s[j]-s[pre[j]]\)的\(j\)就是\(i\)的决策点了。
现在考虑如何优化找\(j\)的过程。
我比较喜欢暴力数据结构,所以这是一篇不太正经的线段树题解,想学单调队列的同学可以跳过了
将条件移项可得:\(s[i] \ge s[j]*2-s[pre[j]]\),当处理完\(j\)后,我们可以将右边的值放到线段树中,然后在处理\(i\)时直接在线段中查找所有慢足\(s[i] \ge s[j]*2-s[pre[j]]\)最靠右的一个\(j\)。
具体查找方式:在线段树的节点中维护区间最小值(初始为\(inf\)),若左右儿子的值都大于\(s[i]\)则返回\(0\),若当前节点右儿子维护的最小值小于\(s[i]\)那么向右儿子查找,否则向左儿子,到叶子节点即找到了一个最靠右满足条件的\(j\)。总复杂度\(O(nlogn)\)。
代码(部分细节与描述有出入):
#include <cstdio>
using namespace std;
#define il inline
#define re register
typedef long long ll;
const int N=1e6+10;
const ll inf=0x3f3f3f3f3f3f3f3f;
namespace FastIO
{
char buf[1<<21],buf2[1<<21],*p1=buf,*p2=buf;
int p,p3=-1;
il int getc(){return p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++;}
il void flush(){fwrite(buf2,1,p3+1,stdout),p3=-1;}
#define isdigit(ch) (ch>=48&&ch<=57)
template <typename T>
il void read(T &x)
{
re bool f=0;x=0;
re char ch=getc();
while(!isdigit(ch)) f|=(ch=='-'),ch=getc();
while(isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getc();
x=f?-x:x;
}
template <typename T>
il void print(T x)
{
if(p3>(1<<20)) flush();
if(x<0) buf2[++p3]=45,x=-x;
re int a[50]={};
do{a[++p]=x%10+48;}while(x/=10);
do{buf2[++p3]=a[p];}while(--p);
}
}
using namespace FastIO;
il ll min(ll a,ll b){return a<b?a:b;}
int n;
int a[N];
ll s[N];
int dp[N],pre[N];
int L[N<<2],R[N<<2];
ll val[N<<2];
#define pushup(p) (val[p]=min(val[p<<1],val[p<<1|1]))
il void build(int p,int l,int r)
{
L[p]=l;R[p]=r;val[p]=inf;
//初始值为inf
if(l==r) return;
re int mid=(l+r)>>1;
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
return;
}
il void insert(int p,int x,ll v)
{
if(L[p]==R[p]){val[p]=v;return;}
re int mid=(L[p]+R[p])>>1;
if(x>mid) insert(p<<1|1,x,v);
else insert(p<<1,x,v);
pushup(p);
}//更新信息
il int query(int p,ll lim)
{
if(L[p]==R[p]) return L[p];
re int mid=(L[p]+R[p])>>1;
if(val[p<<1|1]<=lim) return query(p<<1|1,lim);
else return query(p<<1,lim);
}//查找j
int main()
{
read(n);build(1,0,n);
for(re int i=1;i<=n;++i) read(a[i]);
for(re int i=1;i<=n;++i) s[i]=s[i-1]+a[i];
for(re int i=1;i<=n;++i)
{
pre[i]=query(1,s[i]);
dp[i]=dp[pre[i]]+i-pre[i]-1;
insert(1,i,s[i]*2-s[pre[i]]);
}
printf("%d",dp[n]);
flush();return 0;
}