BZOJ4408 - [FJOI2016]神秘数
Description
一个可重复数字集合\(S\)的神秘数定义为最小的不能被\(S\)的子集的和表示的正整数。现给出一个\(n(n\leq10^5)\)个数的数列\(\{a_n\}(\Sigma a_i\leq10^9)\)和\(m(m\leq10^5)\)次询问,每次给出两个数\(L,R\),求集合\(\{a_{L..R}\}\)的神秘数。
Solution
可持久化线段树。
首先考虑如何求出集合的神秘数。定义\(t\)和\(ans\),表示用不超过\(t\)的元素能够凑成\([1,ans-1]\)中的所有数。初始时有\(t=0,ans=1\)。接下来考虑加上一个数\(x\)后对\(ans\)有何影响。若\(x>ans\)则依然无法凑出\(ans\),不变;否则能够凑出\([1,ans+x-1]\)中的所有数,\(ans=ans+x\)。那么求出\([t+1,ans]\)中所有元素的和,加到\(ans\)上就得到了新的\(ans\),新的\(t\)等于原\(ans\)。简化一下过程:初始\(ans=1\),每次令\(ans\)等于\([1,ans]\)中所有元素的和\(+1\),直到\(ans\)不再增大为止。由于\(ans\)每次如果增大则至少增大\(t+1\),所以最多进行\(log\)次。
那么建立\(n\)棵权值线段树分别记录前若干个数中的权值分布情况,询问区间\([L,R]\)时用线段树\(R\)减线段树\(L-1\)即可。
时间复杂度\(O(mlog^2n)\)。
Code
//[FJOI2016]神秘数
#include <cstdio>
typedef long long lint;
inline char gc()
{
static char now[1<<16],*s,*t;
if(s==t) {t=(s=now)+fread(now,1,1<<16,stdin); if(s==t) return EOF;}
return *s++;
}
inline int read()
{
int x=0; char ch=gc();
while(ch<'0'||'9'<ch) ch=gc();
while('0'<=ch&&ch<='9') x=x*10+ch-'0',ch=gc();
return x;
}
const int N=1e5+10;
int n,m;
int cnt,rt[N];
struct node{int chL,chR; lint sum;} nd[N*32];
void update(int p) {nd[p].sum=nd[nd[p].chL].sum+nd[nd[p].chR].sum;}
void ins(int &p,int L0,int R0,int x)
{
nd[++cnt]=nd[p]; p=cnt;
if(L0==R0) {nd[p].sum+=x; return;}
int mid=L0+R0>>1;
if(x<=mid) ins(nd[p].chL,L0,mid,x);
else ins(nd[p].chR,mid+1,R0,x);
update(p);
}
lint optL,optR; lint qres;
void query(int p1,int p2,int L0,int R0)
{
if(p1==p2) return;
if(optL<=L0&&R0<=optR) {qres+=nd[p2].sum-nd[p1].sum; return;}
int mid=L0+R0>>1;
if(optL<=mid) query(nd[p1].chL,nd[p2].chL,L0,mid);
if(mid<optR) query(nd[p1].chR,nd[p2].chR,mid+1,R0);
}
int main()
{
n=read(); int maxA=1e9;
for(int i=1;i<=n;i++)
ins(rt[i]=rt[i-1],1,maxA,read());
m=read();
for(int i=1;i<=m;i++)
{
int L=read(),R=read();
lint ans=1;
while(true)
{
optL=1,optR=ans,qres=0,query(rt[L-1],rt[R],1,maxA);
if(qres<ans) break; else ans=qres+1;
}
printf("%lld\n",ans);
}
return 0;
}
P.S.
我又开始懒得写题解了...
本来我想权值\(10^9<2^{30}\)所以线段树只开了\(30N\)个节点,但事实上应该开\(31N\) 0(xp )~