算法笔记:带修主席树(树套树)

試著忘記壹切 提交于 2020-02-28 04:48:00
从我短暂的ACMer生涯当中学到一件事———越是玩弄数据结构,就越会发现树的能力是有极限的…
除非超越树。
那就再套一层树吧!Wryyyyy!!!

最近打算研究一波树套树,平衡树怎么套还没搞懂,目前学习了一点套主席树的方法,平衡树的内容可能也会补在这吧。


众所周知,主席树维护的是一种类似前缀和的结构,每个节点都是包含了之前所有节点值的权值线段树,通过继承上一个节点权值线段树的部分结构以减少大量的空间和时间。
因为维护的是前缀和的结构,因此主席树满足可减性,在解决如静态区间第k小等问题中只需要取区间右端的树减区间左端的树即可得到仅包含有区间内值的权值线段树,这其实就类似于求一个序列的某个区间和可以用前缀和数组,区间右端的值减区间左端的值得到。
当然,以上都是废话,会写主席树的话肯定也知道这些东西。不过我还是要写出来,是因为想展示主席树其实本身也是一种“数据结构套数据结构”的形式,把每个节点的权值线段树抽象成点,主席树的上层就是一个简单的前缀和数组,下层使用权值线段树代替了前缀和数组中的每一个位置。而带修主席树不外乎就是把这个上层结构更换了一下,换成树状数组或线段树之类的其它数据结构。
以下通过两个简单的板子题展示下如何使用树状数组套主席树(其实应该是树状数组套权值线段树


洛谷 P2617 Dynamic Rankings

题意:

给一个含有n个数的序列,需要支持两种操作:
1、查询下标在 [l, r] 内的第 k 小的数
2、把序列中第 x 个数更改为 y
共进行m次操作, n,m≤105

解题思路:

相比于静态第k小,多了一个单点修改的操作。

如果直接莽,用普通主席树写,每次修改操作从第i位到最后一位全部改一遍,那必将t的很惨。

要支持单点修改,普通主席树的上层结构:前缀和数组 显然是无法满足的。想到既要支持单点修改,又要支持区间查询的有啥数据结构?答案肯定就是树状数组啦!(为啥这题不用线段树?因为不好写没必要而且占空间太大)

把主席树的整个上层结构换成树状数组,单点修改用树状数组(或其它数据结构)的方法,查询也用树状数组(或其它数据结构)的方法,这就是带修主席树的主要思路了。

既然上层选择了树状数组,那整个树的构建方法肯定也不能和普通主席树相同了。考虑树状数组的写法,每次修改序列中一个位置的值,最多会修改树状数组中log(n)个位置的值。而对一颗权值线段树添加一个值,只需要多开一条新的最多长log(n)的链就行了(开链过程十分类似普通主席树从第 i 个权值线段树构建第 i+1 个权值线段树的过程)。那么整体而言,修改序列一个位置的值在树状数组套主席树的结构中复杂度就是O(log(n)*log(n))。

查询时类似,在上层结构树状数组中,单次查询最多要访问log(n)个点,在下层权值线段树中,最多访问log(n)个点,总体复杂度也是O(log(n)*log(n))的。如何找第k小,相信大家都做过主席树找静态数组第k小,参照那个写法写就行了。

ps:以上为了方便写的都是log(n),其实在树状数组部分log里面确实是n(即序列中数的个数),而在权值线段树部分log里面应该是数字的范围。

注意,此题还需要先离散化一下,还有树状数组套主席树的空间复杂度和时间复杂度都是O(n*log(n)*log(n))的,因此要十分注意空间是否开够。

细节可以参考代码。

#pragma GCC optimize(2)
#include <bits/stdc++.h>
#define inf 1000000000
#define maxn 101000
using namespace std;
typedef long long ll;

int cnt, root[maxn];
struct Chair
{
    int sum, ls, rs;
}tr[40100000];

int n, m, u, a[maxn];
map<int, int> uni, reuni;
struct item
{
    int l, r, k;
}opt[maxn];

int update(int pre, int l, int r, int x, int f)
{
    int rt=++cnt, mid=(l+r)/2;
    tr[rt]=tr[pre], tr[rt].sum+=f;
    if (l==r) return rt;
    if (x<=mid) tr[rt].ls=update(tr[rt].ls, l, mid, x, f);
    else tr[rt].rs=update(tr[rt].rs, mid+1, r, x, f);
    return rt;
}

void add(int p, int x, int f)
{
    for (int i=p; i<=n; i+=i&-i)
        root[i]=update(root[i], 1, u, x, f);
}

int query(vector<int> rt_l, vector<int> rt_r, int l, int r, int k)
{
    if (l==r) return l;
    int suml=0, sumr=0, mid=(l+r)/2;
    for (auto &i: rt_l)
        suml+=tr[tr[i].ls].sum;
    for (auto &i: rt_r)
        sumr+=tr[tr[i].ls].sum;
    if (sumr-suml>=k)
    {
        for (auto &i: rt_l) i=tr[i].ls;
        for (auto &i: rt_r) i=tr[i].ls;
        return query(rt_l, rt_r, l, mid, k);
    }
    for (auto &i: rt_l) i=tr[i].rs;
    for (auto &i: rt_r) i=tr[i].rs;
    return query(rt_l, rt_r, k-sumr+suml, mid+1, r);
}

int main()
{
    ios::sync_with_stdio(0); cin.tie(0);
    cin>>n>>m;
    for (int i=1; i<=n; i++)
    {
        cin>>a[i];
        uni[a[i]]=1;
    }
    for (int i=1; i<=m; i++)
    {
        char op;
        cin>>op;
        if (op=='Q')
            cin>>opt[i].l>>opt[i].r>>opt[i].k;
        else
        {
            cin>>opt[i].r>>opt[i].k;
            uni[opt[i].k]=1;
        }
    }
    for (auto &i: uni)
    {
        i.second=++u;
        reuni[u]=i.first;
    }
    for (int i=1; i<=n; i++)
        a[i]=uni[a[i]];
    for (int i=1; i<=m; i++)
        if (!opt[i].l) opt[i].k=uni[opt[i].k];
    for (int i=1; i<=n; i++)
        add(i, a[i], 1);
    for (int i=1; i<=m; i++)
    {
        if (opt[i].l)
        {
            vector<int> rt_l, rt_r;
            for (int j=opt[i].l-1; j; j-=j&-j)
                rt_l.push_back(root[j]);
            for (int j=opt[i].r; j; j-=j&-j)
                rt_r.push_back(root[j]);
            cout<<reuni[query(rt_l, rt_r, opt[i].k, 1, u)]<<"\n";
        }
        if (!opt[i].l)
        {
            add(opt[i].r, a[opt[i].r], -1);
            add(opt[i].r, opt[i].k, 1);
            a[opt[i].r]=opt[i].k;
        }
    }
    return 0;
}


洛谷P3380 【模板】二逼平衡树(树套树)

题意:

给一个含有n个数的序列,需要支持一下操作:
1、查询k在区间内的排名
2、查询区间内排名为k的值
3、修改某一位值上的数值
4、查询k在区间内的前驱
5、查询k在区间内的后继
共进行m次操作, n,m≤5*104

解题思路:

相当于是上面那题的升级版,多了三个操作。

找区间排名为k可以直接copy上面那题。

如何查询k在区间内的排名?其实就是找有几个值小于k,可以先考虑普通的主席树怎么做。比如说现在得到了包含这个区间所有点的权值线段树,从树根开始,当前点在权值线段树中代表的空间为 [l, r] ,讨论 k 是否大于区间的中点 mid ,若小于等于mid的话,往左儿子计算。若大于mid的话往往右儿子计算的同时要加上左儿子节点的个数,递归下去就行了。

第4,5个操作,要是专门各写一个函数就又要多好几十行,想想这两个操作其实就是1,2操作的结合,比如查k的前驱,就是找到k的排名,然后查找排第k-1的数是什么,后继类似。

加上这题主要是让大家稍微巩固一下(难题我也不会了

#pragma GCC optimize(2)
#include <bits/stdc++.h>
#define inf 1000000000
#define maxn 101000
using namespace std;
typedef long long ll;

ll cnt, root[maxn], n, u;
map<ll, ll> uni, reuni;

struct Chair
{
    int sum, ls, rs;
}tr[40100000];

int update(int pre, int l, int r, int x, int f)
{
    int rt=++cnt, mid=(l+r)/2;
    tr[rt]=tr[pre], tr[rt].sum+=f;
    if (l==r) return rt;
    if (x<=mid) tr[rt].ls=update(tr[rt].ls, l, mid, x, f);
    else tr[rt].rs=update(tr[rt].rs, mid+1, r, x, f);
    return rt;
}

void add(int p, int x, int f)
{
    for (int i=p; i<=n; i+=i&-i)
        root[i]=update(root[i], 1, u, x, f);
}

int query_rankk(vector<int> rt_l, vector<int> rt_r, int l, int r, int k)
{
    if (l==r) return l;
    int suml=0, sumr=0, mid=(l+r)/2;
    for (auto &i: rt_l) suml+=tr[tr[i].ls].sum;
    for (auto &i: rt_r) sumr+=tr[tr[i].ls].sum;
    if (sumr-suml>=k)
    {
        for (auto &i: rt_l) i=tr[i].ls;
        for (auto &i: rt_r) i=tr[i].ls;
        return query_rankk(rt_l, rt_r, l, mid, k);
    }
    for (auto &i: rt_l) i=tr[i].rs;
    for (auto &i: rt_r) i=tr[i].rs;
    return query_rankk(rt_l, rt_r, mid+1, r, k-sumr+suml);
}

int query_krank(vector<int> rt_l, vector<int> rt_r, int l, int r, int k)
{
    int suml=0, sumr=0, mid=(l+r)/2;
    if (l==r) return 0;
    vector<int> nxl, nxr;
    for (auto &i: rt_l) nxl.push_back(tr[i].ls);
    for (auto &i: rt_r) nxr.push_back(tr[i].ls);
    if (k<=mid) return query_krank(nxl, nxr, l, mid, k);
    for (auto &i: rt_l) suml+=tr[tr[i].ls].sum;
    for (auto &i: rt_r) sumr+=tr[tr[i].ls].sum;
    nxl.clear(), nxr.clear();
    for (auto &i: rt_l) nxl.push_back(tr[i].rs);
    for (auto &i: rt_r) nxr.push_back(tr[i].rs);
    return sumr-suml+query_krank(nxl, nxr, mid+1, r, k);
}

struct item
{
    ll f, l, r, k;
}opt[maxn];
ll m, a[maxn];

int main()
{
    ios::sync_with_stdio(0); cin.tie(0);
    cin>>n>>m;
    for (int i=1; i<=n; i++)
    {
        cin>>a[i];
        uni[a[i]]=1;
    }
    for (int i=1, op, l, r, k; i<=m; i++)
    {
        cin>>op>>l>>r;
        opt[i].f=op, opt[i].l=l, opt[i].r=r;
        if (op==3) {uni[opt[i].r]=1; continue;}
        cin>>k;
        opt[i].k=k;
        if (op==5) opt[i].k++;
        if (op==1 || op==4 || op==5) uni[opt[i].k]=1;
    }
    for (auto &i: uni)
    {
        i.second=++u;
        reuni[u]=i.first;
    }
    for (int i=1; i<=n; i++)
        a[i]=uni[a[i]];
    for (int i=1; i<=m; i++)
    {
        int op=opt[i].f;
        if (op==1 || op==4 || op==5) opt[i].k=uni[opt[i].k];
        else if (op==3) opt[i].r=uni[opt[i].r];
    }
    for (int i=1; i<=n; i++)
        add(i, a[i], 1);
    for (int i=1; i<=m; i++)
    {
        if (opt[i].f==3)
        {
            add(opt[i].l, a[opt[i].l], -1);
            add(opt[i].l, opt[i].r, 1);
            a[opt[i].l]=opt[i].r;
            continue;
        }
        vector<int> rt_l, rt_r;
        for (int j=opt[i].l-1; j; j-=j&-j)
            rt_l.push_back(root[j]);
        for (int j=opt[i].r; j; j-=j&-j)
            rt_r.push_back(root[j]);
        if (opt[i].f==1)
            cout<<query_krank(rt_l, rt_r, 1, u, opt[i].k)+1<<"\n";
        if (opt[i].f==2)
            cout<<reuni[query_rankk(rt_l, rt_r, 1, u, opt[i].k)]<<"\n";
        if (opt[i].f==4)
        {
            int rk=query_krank(rt_l, rt_r, 1, u, opt[i].k)+1;
            if (rk==1) cout<<"-2147483647\n";
            else cout<<reuni[query_rankk(rt_l, rt_r, 1, u, rk-1)]<<"\n";
        }
        if (opt[i].f==5)
        {
            int rk=query_krank(rt_l, rt_r, 1, u, opt[i].k)+1;
            if (rk==opt[i].r-opt[i].l+2) cout<<"2147483647\n";
            else cout<<reuni[query_rankk(rt_l, rt_r, 1, u, rk)]<<"\n";
        }
    }
    return 0;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!