[学习笔记]莫队->主席树

前身在这里,建议先看看这个

题1

主席树可以用来在线求区间不同的数的个数
记录last数组,last[i]表示上一个与第i位置上的数相同的数的位置
把主席树建出来,每次把last的位置加上-1,自己加上1
保证每个不同的数只出现1次

然后在线询问,每次只要询问l~r值之和就行了,因为每个数只出现过一次

这种方法,对比之前的树状数组做法,时间一样,空间多个log
不过主席树有个非常优的优点:在线
树状数组只能离线,主席树可以实现在线

原题
Code:

//当然,空间问题,此题过不了,最后两个点MLE,仅仅是一种思想
#include <bits/stdc++.h>
#define maxn 500010
using namespace std;
struct Seg{
    int l, r, sum;
}seg[maxn << 5];
int sz, rt[maxn], last[maxn << 1], a[maxn], n, m;

inline int read(){
    int s = 0, w = 1;
    char c = getchar();
    for (; !isdigit(c); c = getchar()) if (c == '-') w = -1;
    for (; isdigit(c); c = getchar()) s = (s << 1) + (s << 3) + (c ^ 48);
    return s * w;
}

void build(int &rt, int l, int r){
    rt = ++sz;
    seg[rt].sum = 0;
    if (l == r) return;
    int mid = (l + r) >> 1;
    build(seg[rt].l, l, mid); build(seg[rt].r, mid + 1, r);
}

int update(int o, int l, int r, int u, int v){
    int oo = ++sz;
    seg[oo] = seg[o]; seg[oo].sum += v;
    if (l == r) return oo;
    int mid = (l + r) >> 1;
    if (mid >= u) seg[oo].l = update(seg[oo].l, l, mid, u, v); else
    seg[oo].r = update(seg[oo].r, mid + 1, r, u, v);
    return oo;
}

int query(int rt, int x, int l, int r){
    if (l == r) return seg[rt].sum;
    int mid = (l + r) >> 1;
    if (mid >= x) return query(seg[rt].l, x, l, mid) + seg[seg[rt].r].sum; else
    return query(seg[rt].r, x, mid + 1, r);
}

int main(){
    n = read();
    for (int i = 1; i <= n; ++i) a[i] = read();
    build(rt[0], 1, n);
    for (int i = 1; i <= n; ++i){
        rt[i] = rt[i - 1];
        if (last[a[i]]){
            rt[i] = update(rt[i], 1, n, last[a[i]], -1);
            rt[i] = update(rt[i], 1, n, i, 1);
        } else rt[i] = update(rt[i], 1, n, i, 1);
        last[a[i]] = i;
    }
    m = read();
    while (m--){
        int l = read(), r = read();
        printf("%d\n", query(rt[r], l, 1, n));
    }
    return 0;
}

题2

难度升级:求区间出现过至少两次的数的个数
无法用上面的方法
可以保证到r为止,这个数只出现过2次(像上面那样,之前的删除掉)
但是无法保证,这2个数都在l~r中

所以想想,是不是可以把最后出现的相同的两个数的前一个在l前统计一下?
也不行,实现不了,然后想到一种十分暴力的方法

用权值线段树,主席树优化空间,没有删除操作,每次加入的数落实到叶子结点,统计每个值域出现的数的个数,用权值线段树可以完成,然后用主席树的前缀和思想,还可以稍稍优化一下空间

查询的话,前缀和思想,如果当前值域的数的个数<2,直接退出就行了
否则往下跑,知道跑到叶子节点,中途没有退出说明,这个数是符合要求的,答案+1

原题
Code:

//这种暴力的方法十分不优,空间时间双废
//空间是肯定爆炸,时间的话,如果数据随机,时间可以快一点
//优点是可以强制在线
#include <bits/stdc++.h>
#define maxn 2000010
using namespace std;
struct Seg{
    int l, r, sum;
}seg[maxn << 5];
int rt[maxn], n, m, c, sz;

inline int read(){
    int s = 0, w = 1;
    char c = getchar();
    for (; !isdigit(c); c = getchar()) if (c == '-') w = -1;
    for (; isdigit(c); c = getchar()) s = (s << 1) + (s << 3) + (c ^ 48);
    return s * w;
}

void build(int &rt, int l, int r){
    rt = ++sz;
    seg[rt].sum = 0;
    if (l == r) return;
    int mid = (l + r) >> 1;
    build(seg[rt].l, l,  mid); build(seg[rt].r, mid + 1, r);
}

int update(int o, int l, int r, int x){
    int oo = ++sz;
    seg[oo] = seg[o], ++seg[oo].sum;
    if (l == r) return oo;
    int mid = (l + r) >> 1;
    if (mid >= x) seg[oo].l = update(seg[oo].l, l, mid, x); else
    seg[oo].r = update(seg[oo].r, mid + 1, r, x);
    return oo;
}

int query(int u, int v, int l, int r){
    if (seg[v].sum - seg[u].sum < 2) return 0;
    if (l == r) return 1;
    int mid = (l + r) >> 1;
    return query(seg[u].l, seg[v].l, l, mid) + query(seg[u].r, seg[v].r, mid + 1, r);
}

int main(){
    n = read(), c = read(), m = read();
    build(rt[0], 1, c);
    for (int i = 1; i <= n; ++i){
        int x = read();
        rt[i] = update(rt[i - 1], 1, c, x);
    }
    while (m--){
        int l = read(), r = read();
        printf("%d\n", query(rt[l - 1], rt[r], 1, c));
    }
    return 0;
}

题3

继续强化,求区间出现过至少k次的数的个数
跟上面那一题只差一个数字,只要把2改成k就行了

那么,求区间出现过刚好k次的数的个数?
一样,只需要在叶子结点时,判断一下个数是不是等于k个就行了

把两个操作合在一起,形成一道题目

原题

//同样的,空间时间都不优,但是强制在线的优点是莫队、树状数组比不上的
#include <bits/stdc++.h>
#define maxn 600010
using namespace std;
struct chairman{
    int l, r, sum;
}seg[maxn << 5];
int a[maxn], b[maxn], p, q, n, m, sz, k, rt[maxn];

inline int read(){
    int s = 0, w = 1;
    char c = getchar();
    for (; !isdigit(c); c = getchar()) if (c == '-') w = -1;
    for (; isdigit(c); c = getchar()) s = (s << 1) + (s << 3) + (c ^ 48);
    return s * w;
}

void build(int &rt, int l, int r){
    rt = ++sz;
    seg[rt].sum = 0;
    if (l == r) return;
    int mid = (l + r) >> 1;
    build(seg[rt].l, l, mid); build(seg[rt].r, mid + 1, r);
}

int update(int o, int l, int r){
    int oo = ++sz;
    seg[oo] = seg[o], ++seg[oo].sum;
    if (l == r) return oo;
    int mid = (l + r) >> 1;
    if (mid >= p) seg[oo].l = update(seg[oo].l, l, mid); else
    seg[oo].r = update(seg[oo].r, mid + 1, r);
    return oo;	
}

int query(int u, int v, int l, int r, int opt){
    int x = seg[v].sum - seg[u].sum, mid = (l + r) >> 1;
    if (x < k) return 0;
    if (l == r){
        if (opt == 1 && x == k) return 1;
        if (opt == 2 && x >= k) return 1;
        return 0;
    }
    return query(seg[u].l, seg[v].l, l, mid, opt) + query(seg[u].r, seg[v].r, mid + 1, r, opt);
}

int main(){
    n = read(), m = read(), k = read();
    for (int i = 1; i <= n; ++i) a[i] = read(), b[i] = a[i];
    sort(b + 1, b + 1 + n);
    q = unique(b + 1, b + 1 + n) - b - 1;
    build(rt[0], 1, n);
    for (int i = 1; i <= n; ++i){
        p = lower_bound(b + 1, b + 1 + q, a[i]) - b;
        rt[i] = update(rt[i - 1], 1, n);
    }
    while (m--){
        int opt = read(), l = read(), r = read();
        printf("%d\n", query(rt[l - 1], rt[r], 1, n, opt));
    }
    return 0;
}

题4

继续增强,这是我莫队->树状数组未完成的设想
就是把上一题的k放到询问里去,就是每次询问的k都不一样

如果可以离线的话, 树状数组的做法我暂时想不出来
但是,如果空间时间的约束宽一点,数据友好一点,主席树就可以解决

因为,主席树建出来的树把当前所有的数都包含了进来
不存在删除操作避免了树状数组做法的限制

只要在query里面小小修改一下,就可以实现
不但可以把k放到询问里,还可以支持强制在线,真是美好

总结

知识不够,导致我只能在有限的算法中寻求创新,这一类题目,我相信可以用更加好的算法实现,效率可以更加高,但是我目前的水平只能做到这里

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值