树状数组上二分(logn求第k小)

 

主要是留个板子免得以后慢慢推。

模板:

//需要满足(1<<_log)>=n,且N>(1<<_log)
//多测时注意赋初值 
const int N=200005;

int t[N],_log;

inline int lowbit(int x)
{
    return x&(-x);
}

inline void add(int k,int x)
{
    for(int i=k;i<N;i+=lowbit(i))
        t[i]+=x;
}

inline int query(int k)
{
    int ans=0;
    for(int i=k;i;i-=lowbit(i))
        ans+=t[i];
    return ans;
}

inline int kth(int k)
{
    int pos=1<<_log;
    for(int i=_log-1;i>=0;i--)
        if(t[pos-(1<<i)]>=k)
            pos-=(1<<i);
        else
            k-=t[pos-(1<<i)];
    return pos;
}
View Code

 


 

类似在主席树中使用到的 用线段树求区间第$k$小,树状数组也是可以支持类似的操作的。不过由于数据结构的局限性,能够求的是全局第$k$小

 

举个例子,当$n=8$,$a=1\ 1\ 4\ 5\ 1\ 4\ 1\ 9$表示数字$1$到$8$分别出现的次数,我们可以建出形状如下的树状数组:

(上面图中,最下面一层的第二个0001应为0011)

其中,每个位置上标红的数值表示对应区间中数字的出现总数。比如,$t[0100]=a_1+a_2+a_3+a_4=1+1+4+5=11$。

同时可以看出,节点$i$所覆盖的区间为$[i-2^{lowbit(i)}+1,i]$、长度为$2^{lowbit(i)}$。比如,$t[0100]$覆盖了区间$[0001,0100]$,$t[0110]$覆盖了区间$[0101,0110]$。

现在考虑找到所有数中从小到大第$14$个数。我们从“根节点”$t[1000]$开始:

1. 类似线段树中二分,我们将其与左儿子$t[0100]$进行比较。能够发现$t[0100]=11<14$,故我们进入右儿子,找其中的第$14-11=3$大。此时右儿子为空,不过我们之后有办法进行处理,不妨先认为有一个节点存在。

2. 再与左儿子$t[0110]$进行比较。能够发现$t[0110]=5\geq 3$,故目标答案在$t[0110]$所包含的区间中,我们进入左儿子$t[0110]$。

3. 再与左儿子$t[0101]$进行比较。能够发现$t[0101]=1<3$,故进入右儿子。这个不存在的右儿子对应的其实就是$0110$。

 

现在考虑如何处理进入(不存在的)右儿子的情况。

其实我们完全可以不进行任何处理。为什么这样说呢?我们每一步所在的节点,其实都是所在区间的右边界。在上面的例子中,在一开始,$1000$是$0001$到$1000$的右边界;第一步过后,$1000$是$0101$到$1000$的右边界;第二步过后,$0110$是$0101$到$1000$的右边界;第三步后,$0110$是$0110$到$0110$的右边界。在每一步过后,目标答案所在的区间长度就减小了一半,而区间的右边界就是目前的节点编号。

于是我们在寻找全局第$k$小的过程中,只需要如下处理:

1. 在一开始,当前右边界$pos=2^{\lceil logn\rceil}$。必须为$2$的幂次,否则不能保证包含了$[1,n]$的区间。

2. 从高到低确定目标答案的每一位。具体来说,就是将$i$从$\lceil logn\rceil-1$一直循环到$0$。此时左儿子(一定存在)的节点编号为$pos-2^i$,我们将$t[pos-2^i]$与$k$进行比较。

3. 若$t[pos-2^i]\geq k$,则答案为左儿子对应的区间中的第$k$小,故$pos=pos-2^i$、$k$不变;否则答案出现在右儿子对应的区间中,不过是第$k-t[pos-2^i]$小,故$k=k-t[pos-2^i]$、$pos$不变。

inline int kth(int k)
{
    int pos=1<<_log;
    for(int i=_log-1;i>=0;i--)
        if(t[pos-(1<<i)]>=k)
            pos-=(1<<i);
        else
            k-=t[pos-(1<<i)];
    return pos;
}

 


 

Nowcoder 5671J  (Josephus Transform,2020牛客暑期多校第六场)

包含了一个奇妙的用法:以$O(nlogn)$求约瑟夫环的出队顺序。

首先考虑将树状数组的$[1,n]$位置全部$+1$,表示每个数都是存在的。设上一个被干掉的人编号为$cur$(初始为$0$),在树状数组上二分求出下一个可用的编号$nxt$,其中需要满足$query(nxt)-query(cur)=k$,换句话说也就是求全体区间第$query(cur)+k$小。

若$query(cur)+k>query(n)$,就表示要在环上绕多次,那就相当于求全体区间第$(query(cur)+k-1)\text{%} query(n)+1$小。调用kth函数即可得到编号。

剩余的部分就是对一个排列置换多次了,暴力找出循环节,在每个数都在循环上向后移动$x$次即可得到置换后的排列。

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int N=200005;

int t[N],_log;

inline int lowbit(int x)
{
    return x&(-x);
}

inline void add(int k,int x)
{
    for(int i=k;i<N;i+=lowbit(i))
        t[i]+=x;
}

inline int query(int k)
{
    int ans=0;
    for(int i=k;i;i-=lowbit(i))
        ans+=t[i];
    return ans;
}

inline int kth(int k)
{
    int pos=1<<_log;
    for(int i=_log-1;i>=0;i--)
        if(t[pos-(1<<i)]>=k)
            pos-=(1<<i);
        else
            k-=t[pos-(1<<i)];
    return pos;
}

int n,m;
int p[N],trans[N],tmp[N];

void gen(int k)
{
    for(int i=1;i<=(1<<_log);i++)
        t[i]=0;
    for(int i=1;i<=n;i++)
        add(i,1);
    
    int cur=0;
    for(int i=1;i<=n;i++)
    {
        int npos=query(cur)+k;
        if(npos>(n-i+1))
            npos=(npos-1)%(n-i+1)+1;
        
        trans[i]=cur=kth(npos);
        add(cur,-1);
    }
}

int cycle[N],vis[N];

void pw(int x)
{
    for(int i=1;i<=n;i++)
        vis[i]=-1;
    
    for(int i=1;i<=n;i++)
    {
        if(vis[i]!=-1)
            continue;
        
        int cur=i,top=0;
        while(vis[cur]==-1)
        {
            vis[cur]=top;
            cycle[top++]=cur;
            cur=trans[cur];
        }
        
        for(int j=0;j<top;j++)
            trans[cycle[j]]=cycle[(vis[cycle[j]]+x)%top];
    }
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        p[i]=i;
    
    _log=1;
    while((1<<_log)<n)
        _log++;
    
    while(m--)
    {
        int k,x;
        scanf("%d%d",&k,&x);
        
        gen(k);
        pw(x);
        
        for(int i=1;i<=n;i++)
            tmp[i]=p[trans[i]];
        for(int i=1;i<=n;i++)
            p[i]=tmp[i];
    }
    
    for(int i=1;i<=n;i++)
        printf("%d%c",p[i],i==n?'\n':' ');
    return 0;
}
View Code

 

(其余部分暂时咕咕咕)

posted @ 2020-07-28 00:38  LiuRunky  阅读(2456)  评论(0编辑  收藏  举报