传送门:B. Bills of Paradise

题意

给你一个函数,由它产生$n$个数$a_1...a_n$,现在有$q$次操作,操作有以下四种:
1. D x。标记大于等于$x$的第一个未标记的$a_i$;若没有,则不操作。
2. F x。查询大于等于$x$的第一个未标记的$a_i$;若没有,则输出$10^{12}$。
3. R x。清除小于等于$x$的所有标记;若没有,则不操作。本操作次数不超过10次。
4. C x。查询小于等于$x$的所有未标记数之和;若没有,则输出$0$。

思路

1e6个数,5e5次询问,一开口就知道是老数据结构题了(然而细节超多)。

首先sort一下数组,然后写两种数据结构来维护。

(1)用线段树来维护标记数之和,查询的时候就用总和减去标记数之和,就得到了未标记数之和。

实现功能:单点修改(每次只标记一个数)、区间修改(清除区间所有标记)、区间查询(区间未标记数之和)。

(2)用并查集 标记和查询 大于等于$x$的第一个未标记的$a_i$。

用$fa[i]$表示$i$位置的下一个未标记数的位置,初始化fa[i]=i(初始每个$i$都没被标记)。

标记$i$位置的下一个未标记数:
①若fa[i]==i,说明$a[i]$没被标记。要标记$a[i]$,即修改fa[i]=find_fa(i+1)
②否则,说明$a[i]$被标记。先找下一个未标记数的位置pos=find_fa(i+1),要标记$a[pos]$,即修改fa[pos]=find_fa(pos+1)

查询$i$位置的下一个未标记数即find_fa(i)

最后说下细节:
- 若二分查找到的位置是0或n+1,则不能用线段树,要特判;
- 可将n+1位置放并查集里一起维护,只不过fa[n+1]不会被修改,永远指向自己。

AC代码(线段树)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const ll N=1e6+10,M=1e12;
int n,q,fa[N];
ull k1,k2;
ll a[N],sum[N],tr[N<<2];
ull xorShift128Plus()
{
    ull k3=k1,k4=k2;
    k1=k4;
    k3^=k3<<23;
    k2=k3^k4^(k3>>17)^(k4>>26);
    return k2+k4;
}
void gen()
{
    cin>>n>>k1>>k2;
    for(int i=1;i<=n;i++)
        a[i]=xorShift128Plus()%999999999999+1;
}
/*void gen() // 方便调试,不能AC
{
    cin>>n;
    for(int i=1;i<=n;i++)
        cin>>a[i];
}*/
void init()
{
    sort(a+1,a+n+1);
    for(int i=1;i<=n;i++)
    {
        fa[i]=i;
        sum[i]=sum[i-1]+a[i];
    }
    fa[n+1]=n+1;
}
int find_fa(int x)
{
    return fa[x]=(x==fa[x]?x:find_fa(fa[x]));
}
void pushup(int i)
{
    tr[i]=tr[2*i]+tr[2*i+1];
}
void build(int i,int l,int r)
{
    if(l==r)
    {
        tr[i]=0; // 初始化为0而不是a[l]!
        return;
    }
    int mid=(l+r)/2;
    build(2*i,l,mid);
    build(2*i+1,mid+1,r);
    pushup(i);
}
void update(int i,int l,int r,int x,ll k) // a[x]+=k
{
    if(l==r&&l==x)
    {
        tr[i]+=k;
        return;
    }
    int mid=(l+r)/2;
    if(mid>=x)update(2*i,l,mid,x,k);
    else update(2*i+1,mid+1,r,x,k);
    pushup(i);
}
ll query(int i,int l,int r,int x,int y)
{
    if(l>=x&&r<=y)return tr[i];
    int mid=(l+r)/2;
    ll ans=0;
    if(x<=mid)ans+=query(2*i,l,mid,x,y);
    if(y>mid)ans+=query(2*i+1,mid+1,r,x,y);
    return ans;
}
void toZero(int i,int l,int r,int x,int y) // [l,r]区间值修改成0
{
    if(l==r) // 一定要递归到最底层的叶子!
    {
        tr[i]=0;
        return;
    }
    int mid=(l+r)/2;
    if(x<=mid)toZero(2*i,l,mid,x,y);
    if(y>mid)toZero(2*i+1,mid+1,r,x,y);
    pushup(i);
}
int main()
{
    ios::sync_with_stdio(false);
    gen();
    init();
    build(1,1,n);
    cin>>q;
    while(q--)
    {
        char opt;
        ll x;
        cin>>opt>>x;
        int pos,k;
        if(opt=='D') // 标记>=x的第一个未被标记的a[i]
        {
            k=lower_bound(a+1,a+n+1,x)-a;
            if(k==n+1)continue;
            if(k==find_fa(k)) // a[k]未被标记
            {
                update(1,1,n,k,a[k]); // 标记它,更新标记和
                fa[k]=find_fa(k+1); // 修改它的下一个未标记位置
            }
            else // a[k]被标记
            {
                pos=find_fa(k+1); // 找到下一个未标记位置
                update(1,1,n,pos,a[pos]); // 标记它,更新标记和
                fa[pos]=find_fa(pos+1); // 修改它的下一个未标记位置
            }
        }
        else if(opt=='F') // 查询>=x的第一个未被标记的a[i]
        {
            k=lower_bound(a+1,a+n+1,x)-a;
            if(k==n+1){printf("%lld\n",M);continue;}
            pos=find_fa(k);
            if(pos==n+1)printf("%lld\n",M);
            else printf("%lld\n",a[pos]);
        }
        else if(opt=='R') // <=x的标记全部清零
        {
            k=upper_bound(a+1,a+n+1,x)-a-1; // <=x的位置
            if(k==0)continue;
            toZero(1,1,n,1,k);
            for(int i=1;i<=k;i++) // 去除标记
                fa[i]=i;
        }
        else if(opt=='C') // 查询<=x未标记数之和
        {
            k=upper_bound(a+1,a+n+1,x)-a-1; // <=x的位置
            if(k==0){printf("0\n");continue;}
            ll s1=query(1,1,n,1,k); // 标记数之和
            ll s2=sum[k]; // 总和
            ll ans=s2-s1;
            printf("%lld\n",ans);
        }
    }
    return 0;
}
/*
修改gen()函数后的测试数据
输入格式:输入n以及序列a[1]~a[n]。
6
1 2 3 4 5 6
11
D 1
D 2
D 3
D 4
D 5
C 6
D 6
C 6
R 5
C 6
C 100

ans:
6
0
15
15
*/

AC代码(树状数组)

由于区间清零操作不超过10次,所以也能用树状数组来暴力区间修改。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const ll N=1e6+10,M=1e12;
int n,q,fa[N];
ull k1,k2;
ll a[N],sum[N],tr[N];
ull xorShift128Plus()
{
    ull k3=k1,k4=k2;
    k1=k4;
    k3^=k3<<23;
    k2=k3^k4^(k3>>17)^(k4>>26);
    return k2+k4;
}
void gen()
{
    cin>>n>>k1>>k2;
    for(int i=1;i<=n;i++)
        a[i]=xorShift128Plus()%999999999999+1;
}
void init()
{
    sort(a+1,a+n+1);
    for(int i=1;i<=n;i++)
    {
        fa[i]=i;
        sum[i]=sum[i-1]+a[i];
    }
    fa[n+1]=n+1;
}
int find_fa(int x)
{
    return fa[x]=(x==fa[x]?x:find_fa(fa[x]));
}
int lowbit(int x)
{
    return x&(-x);
}
void update(int i,ll k)
{
    while(i<=n)
    {
        tr[i]+=k;
        i+=lowbit(i);
    }
}
ll query(int i)
{
    ll s=0;
    while(i)
    {
        s+=tr[i];
        i-=lowbit(i);
    }
    return s;
}
int main()
{
    ios::sync_with_stdio(false);
    gen();
    init();
    cin>>q;
    while(q--)
    {
        char opt;
        ll x;
        cin>>opt>>x;
        int pos,k;
        if(opt=='D') // 标记>=x的第一个未被标记的a[i]
        {
            k=lower_bound(a+1,a+n+1,x)-a;
            if(k==n+1)continue;
            if(k==find_fa(k)) // a[k]未被标记
            {
                update(k,a[k]); // 标记它,更新标记和
                fa[k]=find_fa(k+1); // 修改它的下一个未标记位置
            }
            else // a[k]被标记
            {
                pos=find_fa(k+1); // 找到下一个未标记位置
                update(pos,a[pos]); // 标记它,更新标记和
                fa[pos]=find_fa(pos+1); // 修改它的下一个未标记位置
            }
        }
        else if(opt=='F') // 查询>=x的第一个未被标记的a[i]
        {
            k=lower_bound(a+1,a+n+1,x)-a;
            if(k==n+1){printf("%lld\n",M);continue;}
            pos=find_fa(k);
            if(pos==n+1)printf("%lld\n",M);
            else printf("%lld\n",a[pos]);
        }
        else if(opt=='R') // <=x的标记全部清零
        {
            k=upper_bound(a+1,a+n+1,x)-a-1; // <=x的位置
            if(k==0)continue;
            for(int i=1;i<=k;i++) // 去除标记
            {
                if(fa[i]!=i) // 被标记了
                {
                    fa[i]=i;
                    update(i,-a[i]);
                }
            }
        }
        else if(opt=='C') // 查询<=x未标记数之和
        {
            k=upper_bound(a+1,a+n+1,x)-a-1; // <=x的位置
            if(k==0){printf("0\n");continue;}
            ll s1=query(k); // 标记数之和
            ll s2=sum[k]; // 总和
            ll ans=s2-s1;
            printf("%lld\n",ans);
        }
    }
    return 0;
}

后话

这题细节是真的多,线段树日常写崩,比赛的时候也没想到并查集去维护下一个未标记数,补题的时候还找了半天bug,我太菜了QAQ。