P5065 [Ynoi Easy Round 2014] 不归之人与望眼欲穿的人们 题解
题目描述
给定长为 \(n\) 的序列 \(a\) ,接下来 \(m\) 次操作:
1 i x:将 \(a_i\) 修改为 \(x\) 。2 k:求最小的 \(len\) ,使得存在长为 \(len\) 的区间 \(or\) 和 \(\ge k\) 。
数据范围
- \(1\le n,m\le 5\cdot 10^4,0\le a_i,k\le 2^{30}\) 。
时间限制 \(\texttt{1s}\) ,空间限制 \(\texttt{125MB}\) 。
分析
解法一
固定区间的一个端点,显然有用的另一端点只有 \(\log a\) 个。
据此可以设计一个单次询问 \(\mathcal O(n\log a)\) 的暴力:从左往右扫描整个序列,称缩短一位后会导致 \(or\) 和突变的后缀为关键后缀,动态维护所有关键后缀(任意时刻不超过 \(\log a\) 个)的起始位置和 \(or\) 和,用扫到的所有关键后缀更新一遍答案即可。
pii h[70];/// h 数组存储所有 pair<后缀位置,or和> ,第一维递增,第二维递减
void clean()
{///去重,将 or 和相同的后缀仅保留最大的后缀位置
h[top+1]=mp(0,0);
int k=0;
for(int i=1;i<=top;i++) if(h[i].se!=h[i+1].se) h[++k]=h[i];
top=k;
}
int query(int k)
{
int res=inf;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=top;j++) h[j].se|=a[i];
h[++top]=mp(i,a[i]),clean();
for(int j=1;j<=top;j++) if(h[j].se>=k) chmin(res,i-h[j].fi+1);
}
return res;
}
接下来考虑分块,设块长为 \(B\) 。
-
对于区间完全包含在某一块内的情况,维护 \(f_{i,j}\) 表示第 \(i\) 个块内长为 \(j\) 的区间最大 \(or\) 值,查询直接二分 \(\mathcal O(\frac nB\log B)\) ,修改时用上面的暴力重构第 \(i\) 块的 \(f\) 数组,时间复杂度 \(\mathcal O(B\log a)\) 。
-
对于跨越两块的情况,从左往右扫描每个块,钦定右端点在第 \(i\) 个块中。
我们只需考虑由前 \(i-1\) 个块的关键后缀和第 \(i\) 个块的关键前缀拼起来的区间,对这 \(\log a\) 个关键后缀和 \(\log a\) 个关键前缀做双指针即可,时间复杂度 \(\mathcal O(\frac nB\log a)\) 。
因此我们还需要求出每个块的关键前缀和关键后缀,预处理时顺便维护一下即可。
取 \(B=\sqrt n\) ,时间复杂度 \(\mathcal O(m\sqrt n\log a)\) 。实测取 \(B=180\sim 200\) 均可无压力通过。
#include<bits/stdc++.h>
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int B=180,maxn=5e4+5,inf=1e9;
int m,n,top;
int a[maxn];
int st[maxn],ed[maxn],bel[maxn],len[maxn];
int all[maxn],f[maxn/B+5][B+5];/// all[i] 为第 i 个块的 or 和, f[i][j] 表示第 i 个块内长为 j 的区间最大 or 和
pii h[70];
vector<pii> l[maxn],r[maxn];/// l[i],r[i] 分别存储第 i 个块的关键前缀,后缀
void chmin(int &x,int y) {if(x>y) x=y;}
void chmax(int &x,int y) {if(x<y) x=y;}
void clean()
{
h[top+1]=mp(0,0);
int k=0;
for(int i=1;i<=top;i++) if(h[i].se!=h[i+1].se) h[++k]=h[i];
top=k;
}
void build(int i)
{
l[i].clear(),r[i].clear(),top=0;
memset(f[i]+1,0,4*len[i]);
int &x=all[i]=0;
for(int j=st[i];j<=ed[i];j++)
{
if((x|a[j])!=x) l[i].push_back(mp(j,x|=a[j]));
for(int k=1;k<=top;k++) h[k].se|=a[j];
h[++top]=mp(j,a[j]),clean();
for(int k=1;k<=top;k++) chmax(f[i][j-h[k].fi+1],h[k].se);
}
r[i]=vector<pii>(h+1,h+top+1);
for(int j=1;j<=B;j++) chmax(f[i][j],f[i][j-1]);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]),bel[i]=(i-1)/B+1;
for(int i=1;i<=bel[n];i++) st[i]=(i-1)*B+1,ed[i]=min(i*B,n),len[i]=ed[i]-st[i]+1,build(i);
for(int op,k,x,res;m--;)
{
scanf("%d%d",&op,&k);
if(op==1) scanf("%d",&x),a[k]=x,build(bel[k]);
else
{
res=inf,top=0;
for(int i=1;i<=bel[n];i++)
{
///第 i 个块内的贡献
if(f[i][len[i]]>=k) chmin(res,lower_bound(f[i]+1,f[i]+len[i]+1,k)-f[i]);
///双指针求右端点在第 i 个块内的贡献
for(int x=1,y=0;x<=top;x++)
{
while(y<l[i].size()&&(h[x].se|l[i][y].se)<k) y++;
if(y<l[i].size()) chmin(res,l[i][y].fi-h[x].fi+1);
}
///更新关键后缀
for(int j=1;j<=top;j++) h[j].se|=all[i];
for(auto p:r[i]) h[++top]=p;
clean();
}
printf("%d\n",res!=inf?res:-1);
}
}
return 0;
}
解法二(不推荐)
对于一个区间 \([l,r]\) ,如果它是相对 \(l\) 的关键后缀,也是相对 \(r\) 的关键前缀,则称它是关键区间。
显然如果一个区间不是关键区间那么一定不优,而且关键区间只有 \(\mathcal O(n\log a)\) 个。
考虑维护所有关键区间。每个关键区间有两个属性 \((len,val)\) ,我们需要求满足 \(val\ge k\) 的关键区间中, \(len\) 的最小值。
对每个 \(len\) 开一个 set 或可删堆,存储所有对应的 \(val\) 。
再对 \(len\) 开线段树,叶节点存放值为 \(len\) 时 \(val\) 的最大值,查询直接线段树二分。
至此增删单个关键区间可以在 \(\mathcal O(\log n)\) 的时间内完成。
接下来,我们需要对每个修改操作,找出所有受到影响的关键区间。
容易发现,关键区间的左端点一定是相对 \(i\) 的关键后缀,右端点一定是相对 \(i\) 的关键前缀,因此单次修改涉及到的关键区间不超过 \(\log^2a\) 个。
用一棵线段树维护区间的关键前缀、关键后缀、区间 \(or\) 和,每次合并时枚举左侧的关键后缀和右侧的关键前缀。
对于建树过程,总共有 \(\mathcal O(n\log n)\) 次合并,但总共只有 \(n\log a\) 个关键区间,时间复杂度 \(\mathcal O(n\log n\cdot\log^2a+n\log a\cdot\log a)=\mathcal O(n\log n\log^2a)\) 。
对于单次修改操作,总共有 \(\log n\) 次合并,至多只会修改 \(\mathcal O(\log^2a)\) 个关键区间,时间复杂度 \(\mathcal O(\log n\cdot\log^2a+\log^2a\cdot\log n)=\mathcal O(\log n\log^2a)\) 。
完整时间复杂度 \(\mathcal O((n+m)\log n\log^2a)\) 。
但是事情远远没有这么简单。。。
如果所有操作全部是修改操作,并且 \(a_i\) 和 \(x\) 都是 \(2\) 的方幂,可以把 set 或可删堆上的操作(添加和删除都算)次数卡到超过 \(7\cdot 10^7\) 次。 set 的常数无需多言,如果使用可删堆,由于其懒惰删除的特性,空间复杂度会达到惊人的 \(\mathcal O(n\log^2a)\) 。
最后看着本地 \(\gt\texttt{6s}\) 的代码博主还是选择了放弃,下面的代码可以获得 \(95\) 分的成绩,仅供参考。强烈建议追求 \(\texttt{AC}\) 的读者不要尝试这种做法。
#include<bits/stdc++.h>
#define ls p<<1
#define rs p<<1|1
using namespace std;
const int maxn=5e4+5;
int m,n;
int a[maxn];
priority_queue<int> tmp;
struct heap
{
priority_queue<int> q1,q2;
void push(int x)
{
q1.push(x);
if(q1.size()>5e5)
{///占用内存过大时手动删除,否则会 MLE
while(!tmp.empty()) tmp.pop();
while(!q1.empty())
{
if(!q2.empty()&&q1.top()==q2.top()) q1.pop(),q2.pop();
else tmp.push(q1.top()),q1.pop();
}
swap(q1,tmp);
}
}
void pop(int x)
{
q2.push(x);
while(!q2.empty()&&q1.top()==q2.top()) q1.pop(),q2.pop();
}
int top()
{
return !q1.empty()?q1.top():0;
}
}q[maxn];
namespace sgmt
{
int mx[1<<17];
void pushup(int p)
{
mx[p]=max(mx[ls],mx[rs]);
}
void modify(int p,int l,int r,int pos,int val)
{
if(l==r) return mx[p]=val,void();
int mid=(l+r)>>1;
pos<=mid?modify(ls,l,mid,pos,val):modify(rs,mid+1,r,pos,val);
pushup(p);
}
void add(int len,int val,int sgn)
{
int lst=q[len].top();
sgn==1?q[len].push(val):q[len].pop(val);
int now=q[len].top();
if(now!=lst) modify(1,1,n,len,now);
}
int query(int p,int l,int r,int k)
{
if(mx[p]<k) return -1;
if(l==r) return l;
int mid=(l+r)>>1,now=query(ls,l,mid,k);
return ~now?now:query(rs,mid+1,r,k);
}
}
struct seg
{
int p,x,y;
///前缀存储 l,or(l,r),or(l+1,r)
///后缀存储 r,or(l,r),or(l,r-1)
};
struct node
{
int l,r,all;
vector<seg> pre,suf;
}f[1<<17];
bool key(const seg &a,const seg &b)
{///判断前缀 a 和后缀 b 能否拼成关键区间
return (a.x|b.y)!=(a.x|b.x)&&(a.y|b.x)!=(a.x|b.x);
}
void pushup(int p)
{
int L=f[ls].all,R=f[rs].all;
f[p].all=L|R,f[p].pre=f[ls].pre,f[p].suf=f[rs].suf;
for(auto u:f[rs].pre)
{
u.x|=L,u.y|=L;
if(u.x!=u.y) f[p].pre.push_back(u);
}
for(auto u:f[ls].suf)
{
u.x|=R,u.y|=R;
if(u.x!=u.y) f[p].suf.push_back(u);
}
for(auto &u:f[ls].suf)
for(auto &v:f[rs].pre)
if(key(u,v)) sgmt::add(v.p-u.p+1,u.x|v.x,1);
}
void build(int p,int l,int r)
{
f[p].l=l,f[p].r=r;
if(l==r) return f[p].all=a[l],f[p].pre=f[p].suf={{l,a[l],0}},sgmt::add(1,a[l],1);
int mid=(l+r)>>1;
build(ls,l,mid),build(rs,mid+1,r);
pushup(p);
}
void modify(int p,int pos,int val)
{
if(f[p].l==f[p].r)
{
sgmt::add(1,a[pos],-1),sgmt::add(1,a[pos]=val,1);
f[p].all=val,f[p].pre=f[p].suf={{pos,val,0}};
return ;
}
for(auto u:f[ls].suf)
for(auto v:f[rs].pre)
if(key(u,v)) sgmt::add(v.p-u.p+1,u.x|v.x,-1);
int mid=(f[p].l+f[p].r)>>1;
pos<=mid?modify(ls,pos,val):modify(rs,pos,val);
pushup(p);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
build(1,1,n);
for(int op,k,x;m--;)
{
scanf("%d%d",&op,&k);
if(op==1) scanf("%d",&x),modify(1,k,x);
else printf("%d\n",sgmt::query(1,1,n,k));
}
return 0;
}
如果纯随机答案很难超过 \(2\) ,下面的数据生成器可以造出相对强力的数据,仅供参考:
///datamaker.cpp
#include<bits/stdc++.h>
using namespace std;
int n,m;
mt19937_64 rnd(timr(0));
int myrnd(int l,int r)
{
return l+rnd()%(r-l+1);
}
int main()
{
freopen("data.in","w",stdout);
n=5e4,m=5e4;
printf("%d %d\n",n,m);
for(int i=1;i<=n;i++) printf("%d ",1<<myrnd(0,15));
putchar('\n');
for(int i=1;i<=m;i++)
{
int op=myrnd(1,10);
if(op>1) printf("1 %d %d\n",myrnd(1,n),1<<myrnd(0,15));
else printf("2 %d\n",myrnd(1<<15,1<<16));
}
return 0;
}
本文来自博客园,作者:peiwenjun,转载请注明原文链接:https://www.cnblogs.com/peiwenjun/p/19099011
浙公网安备 33010602011771号