peiwenjun's blog 没有知识的荒原

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;
}

posted on 2025-09-18 16:12  peiwenjun  阅读(12)  评论(0)    收藏  举报

导航