传送门:B. Bills of Paradise
题意
给你一个函数,由它产生 n n n个数 a 1 . . . a n a_1...a_n a1...an,现在有 q q q次操作,操作有以下四种:
D x
。标记大于等于 x x x的第一个未标记的 a i a_i ai;若没有,则不操作。F x
。查询大于等于 x x x的第一个未标记的 a i a_i ai;若没有,则输出 1 0 12 10^{12} 1012。R x
。清除小于等于 x x x的所有标记;若没有,则不操作。本操作次数不超过10次。C x
。查询小于等于 x x x的所有未标记数之和;若没有,则输出 0 0 0。
思路
1e6个数,5e5次询问,一开口就知道是老数据结构题了(然而细节超多)。
首先sort
一下数组,然后写两种数据结构来维护。
(1)用线段树来维护标记数之和,查询的时候就用总和减去标记数之和,就得到了未标记数之和。
实现功能:单点修改(每次只标记一个数)、区间修改(清除区间所有标记)、区间查询(区间未标记数之和)。
(2)用并查集 标记和查询 大于等于 x x x的第一个未标记的 a i a_i ai。
用 f a [ i ] fa[i] fa[i]表示 i i i位置的下一个未标记数的位置,初始化fa[i]=i
(初始每个 i i i都没被标记)。
标记 i i i位置的下一个未标记数:
①若fa[i]==i
,说明 a [ i ] a[i] a[i]没被标记。要标记 a [ i ] a[i] a[i],即修改fa[i]=find_fa(i+1)
。
②否则,说明 a [ i ] a[i] a[i]被标记。先找下一个未标记数的位置pos=find_fa(i+1)
,要标记 a [ p o s ] a[pos] a[pos],即修改fa[pos]=find_fa(pos+1)
。
查询 i i 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。