P2617 Dynamic Rankings
前言
这是一道已经咕了很久的题。
第一次想写是因为 luan 讲了这道题。
直到她又出现在了我的智能推荐里。
我知道我不能再等了!
我要去和她表白!!
类题总结
关于各种第 k 大问题肯定已经见过很多了。
反正各种奇奇怪怪的解法都是有的。
比较主流的有:
静态整体第 k 大(小)
这玩意有两种解法。
首先是显而易见的 std::sort.
sort(a+1,a+1+n);
//reverse(a+1,a+1+n);
cout<<a[k]<<endl;
生怕你不会写这玩意
时间复杂度 $\text{O(nlogn)}$。
之后这玩意是存在 $\text{O(n)}$ 做法的。
考虑魔改快排。
每次选择基准值的时候记录一些大于基准值的个数 $cnt$ ,之后根据 $cnt$ 选择一半进入,就可以在平均线性之间内统计出第 k 大(小)。
int getk(int *v, int l, int r, int k){
if(l<r){
int i=l,j=l;
for(;j<r;j++){
if(v[j]<v[r]){
swap(v[i++],v[j]);
}
}
swap(v[i],v[r]);
if(k==i) return v[i];
else if(k<i) return getk(v,l,i-1,k);
else return getk(v,i+1,r,k);
}
else return v[l];
}
动态整体第 k 大(小)
权值线段树,插入直接在树上插入,然后查询也直接查就行,像个桶一样。
值域小一点的话直接建树就行了,大一点就动态开点。
namespace SegmentTree{
struct SegmentTree{
int v;
#define ls rt<<1
#define rs rt<<1|1
}st[maxn<<2];
inline void push_up(int rt){
st[rt].v=st[ls].v+st[rs].v;
}
void modify(int rt,int l,int r,int pos,int val){
if(l==r){
st[rt].v+=val;
return;
}
int mid=(l+r)>>1;
if(pos<=mid)modify(ls,l,mid,pos,val);
else modify(rs,mid+1,r,pos,val);
push_up(rt);
}
int ask(int rt,int l,int r,int val){//查询第 k 大
if(l==r)return l;
int mid=(l+r)>>1;
if(val<=st[rs].v)ask(rs,mid+1,r,val);
else ask(ls,l,mid,val-st[rs].v);
}
}
复杂度 $\text{O(nlogn)}$
静态区间第 k 大(小)
也就是主席树板子题了。
具体看这个就行:p3834 可持久化线段树模板(主席树)
动态区间第 k 大(小)
就是本题了。
考虑修改操作。
由于常规的主席树是依靠前缀和的思想来求解区间第 k 大的,于是可以想到求解动态区间第 k 大首要问题是如何维护前缀和的问题。
对于维护前缀和,我们通常有如下几种方式:
暴力,树状数组,线段树。
于是有一个思路:加一个数据结构来维护前缀和。
加上去的数据结构放在什么位置呢?
由于主席树需要前缀和来作差,于是可以发现树状数组是要套在主席树外面的。
于是明白树状数组每个节点都维护一个线段树的根节点。
分开考虑修改以及查询操作。
修改操作
树状数组每次单点修改复杂度 $\text{O(logn)}$ ,对应 $\text{O(logn)}$ 棵权值线段树会受到影响,又因为每次修改需要对应到线段树内,线段树内每次又会有 $\text{O(logn)}$ 个节点需要修改。于是单次复杂度 $\text{O}(log^2n)$
每次修改需要用树状数组定位到需要修改的线段树的根节点位置,之后进行修改即可:
void modify(int &rt,int l,int r,int pos,int val){
if(!rt)rt=++node;
if(l==r){
st[rt].v+=val;
return;
}
int mid=(l+r)>>1;
if(pos<=mid)modify(ls(rt),l,mid,pos,val);
else modify(rs(rt),mid+1,r,pos,val);
push_up(rt);
}
void update(int pos,int x,int val){
for(re int i=pos;i<=n;i+=lowbit(i)){//处理涉及到的 log(n) 个根节点,就是对应的主席树
modify(root[i],1,tmp,x,val);//线段树上修改log(n) 个节点
}
}
int main() {
...
for(re int i=1;i<=m;i++){
if(q[i].op){//modify
update(q[i].pos,a[q[i].pos],-1);//把原数删去
a[q[i].pos]=q[i].val;//换成现在要的数
update(q[i].pos,q[i].val,1);//在树上修改
}
}
}
查询操作
对于查询操作,依然是要用到前缀和。
由于使用了树状数组维护前缀和,于是我们直接在树状数组上取出对应的 $\text{O(logn)}$ 个根节点,也就是对应的主席树根节点,取的时候直接体现作差就行。
之后就和普通的主席树查询区间第 k 大一样,根据作差得到的值直接递归进入左子树或者右子树即可。
复杂度 $\text{O}(log^2n)$
int query(int l,int r,int k){
if(l==r)return l;
int mid=(l+r)>>1;
int xx=0;
for(re int i=1;i<=cnt1;i++)xx-=st[ls(ct1[i])].v;//作差得到cnt值
for(re int i=1;i<=cnt2;i++)xx+=st[ls(ct2[i])].v;
if(xx>=k){//左递归,连带着log(n)棵主席树一起进入左儿子
for(re int i=1;i<=cnt1;i++)ct1[i]=ls(ct1[i]);
for(re int i=1;i<=cnt2;i++)ct2[i]=ls(ct2[i]);
return query(l,mid,k);
}
else {////右递归,连带着log(n)棵主席树一起进入右儿子
for(re int i=1;i<=cnt1;i++)ct1[i]=rs(ct1[i]);
for(re int i=1;i<=cnt2;i++)ct2[i]=rs(ct2[i]);
return query(mid+1,r,k-xx);
}
}
inline void located(int l,int r){//取出对应的log(n)个线段树的根节点。
cnt1=cnt2=0;
for(re int i=l-1;i;i-=lowbit(i))ct1[++cnt1]=root[i];//取l-1和r,用于作差
for(re int i=r;i;i-=lowbit(i))ct2[++cnt2]=root[i];
}
int main() {
...
for(re int i=1;i<=m;i++){
else {
located(q[i].l,q[i].r);
ans=b[query(1,tmp,q[i].k)];
printf("%d\n",ans);
}
}
}
复杂度分析
查询操作 $\text{O}(log^2n)$,修改操作 $\text{O}(log^2n)$,总体复杂度 $\text{O}(mlog^2n)$
由于动态开点,所以空间复杂度就是对应的查询和修改所需要的空间。
于是 空间复杂度和时间复杂度可以认为是同阶的。
线段树大小开 $400$ 倍就够了。
具体实现的时候建议封一个 namespace ,看着也好看调着也爽。
似乎代码没有想象的那么长?
CODE:
//#define LawrenceSivan
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
#define INF 0x3f3f3f3f
#define re register
const int maxn=1e5+5;
int n,m,ans;
char op;
int a[maxn],b[maxn<<1],tot,tmp;
int ct1[maxn],ct2[maxn],cnt1,cnt2;
struct QUERY{
int l,r,k,op;
int pos,val;
}q[maxn];
namespace SegmentTree{
inline void Discretization(){
sort(b+1,b+1+tot);
tmp=unique(b+1,b+1+tot)-b-1;
for(re int i=1;i<=n;i++)a[i]=lower_bound(b+1,b+1+tmp,a[i])-b;
for(re int i=1;i<=m;i++){
if(q[i].op==1)q[i].val=lower_bound(b+1,b+1+tmp,q[i].val)-b;
}
}
struct SegmentTree{
int lc,rc,v;
#define ls(x) st[x].lc
#define rs(x) st[x].rc
}st[maxn*400];
int node,root[maxn];
inline void push_up(int rt){
st[rt].v=st[ls(rt)].v+st[rs(rt)].v;
}
void modify(int &rt,int l,int r,int pos,int val){
if(!rt)rt=++node;
if(l==r){
st[rt].v+=val;
return;
}
int mid=(l+r)>>1;
if(pos<=mid)modify(ls(rt),l,mid,pos,val);
else modify(rs(rt),mid+1,r,pos,val);
push_up(rt);
}
int query(int l,int r,int k){
if(l==r)return l;
int mid=(l+r)>>1;
int xx=0;
for(re int i=1;i<=cnt1;i++)xx-=st[ls(ct1[i])].v;
for(re int i=1;i<=cnt2;i++)xx+=st[ls(ct2[i])].v;
if(xx>=k){
for(re int i=1;i<=cnt1;i++)ct1[i]=ls(ct1[i]);
for(re int i=1;i<=cnt2;i++)ct2[i]=ls(ct2[i]);
return query(l,mid,k);
}
else {
for(re int i=1;i<=cnt1;i++)ct1[i]=rs(ct1[i]);
for(re int i=1;i<=cnt2;i++)ct2[i]=rs(ct2[i]);
return query(mid+1,r,k-xx);
}
}
}
using namespace SegmentTree;
namespace BIT{
#define lowbit(x) (x&-x)
void update(int pos,int x,int val){
for(re int i=pos;i<=n;i+=lowbit(i)){
modify(root[i],1,tmp,x,val);
}
}
inline void located(int l,int r){
cnt1=cnt2=0;
for(re int i=l-1;i;i-=lowbit(i))ct1[++cnt1]=root[i];
for(re int i=r;i;i-=lowbit(i))ct2[++cnt2]=root[i];
}
}
using namespace BIT;
template <typename T>
inline void read(T &x){
T f=1;char ch=getchar();
while (!isdigit(ch)) {if(ch=='-')f=-1;ch=getchar();}
while (isdigit(ch)){x=x*10+(ch^48);ch=getchar();}
x*=f;
}
int main() {
#ifdef LawrenceSivan
freopen("aa.in", "r", stdin);
freopen("aa.out", "w", stdout);
#endif
read(n);read(m);
for(re int i=1;i<=n;i++){
read(a[i]);
b[++tot]=a[i];
}
for(re int i=1;i<=m;i++){
//cin>>op;
scanf(" %c",&op);
if(op=='C'){
q[i].op=1,read(q[i].pos),read(q[i].val),b[++tot]=q[i].val;
}
else q[i].op=0,read(q[i].l),read(q[i].r),read(q[i].k);
}
Discretization();
for(re int i=1;i<=n;i++){
update(i,a[i],1);
}
for(re int i=1;i<=m;i++){
if(q[i].op){
update(q[i].pos,a[q[i].pos],-1);
a[q[i].pos]=q[i].val;
update(q[i].pos,q[i].val,1);
}
else {
located(q[i].l,q[i].r);
ans=b[query(1,tmp,q[i].k)];
printf("%d\n",ans);
}
}
return 0;
}