[SMOJ2212]郁闷的小J

这题很显然是数据结构题,做法比较多。但前提都是要先将编码离散化,否则数值太大,无法维护。

做法一:分块。
这是 cdc 跟我们介绍的一种很神奇的方法。把长度为 n 的区间分为 n 块,每块的长度为 n 。(可能会有多出来的部分,新开一块)
每一块就是一个小区间,独立维护自己的值,用一个数组保存各个编号出现的数量。总的就是一个二维数组, a[i][j] 表示第 i 段区间内编号 j 出现的次数。
对于将下标为 pos 处修改为 val 值的操作,可以用一个数组 O(1) 标记。先将 pos 所在的区间内,原先值的数量 -1,再将该区间 val 的值 +1。
对于求区间内指定编号个数的询问,将待查询区间分块。区间内整块整块的值直接加到答案中,左右两端如果有零散的部分,直接暴力统计。
例如,对于样例, n =5,则每块长度=2,共分为 3 块:[1, 2][3, 4][5]
初始时整个数组为 {1, 2, 3, 4, 5},意味着第一块内 1 和 2 都出现了一次,其他同理,即 a[1][1]=a[1][2]=a[2][3]=a[2][4]=a[3][5]=1,其他均为 0
询问 [1, 3] 内 2 的个数,发现由 [1, 2] 这个块和 3 这个点组成,即答案 = a[1][2] + 0(第 3 位暴力统计,没有出现 2) = 1
询问 [1, 3] 内 1 的个数,发现由 [1, 2] 这个块和 3 这个点组成,即答案 = a[1][1] + 0(第 3 位暴力统计,没有出现 1) = 1
将下标 2 的位置(属于第 1 块)修改为 1,原先 2 的位置是 2,现在被覆盖了, a[1][2] –(变成 0),新的数是 1, a[1][1] ++(变成 2)
询问 [1, 3] 内 2 的个数,发现由 [1, 2] 这个块和 3 这个点组成,即答案 = a[1][2] + 0(第 3 位暴力统计,没有出现 2) = 0
询问 [1, 3] 内 1 的个数,发现由 [1, 2] 这个块和 3 这个点组成,即答案 = a[1][1] + 0(第 3 位暴力统计,没有出现 1) = 2
时间复杂度:修改 O(1) ,询问 O(n) 。空间复杂度 O(nn) ,硕大无比。
分块的运行效率尚可,但缺陷就是所需的空间太大,每块都需要一个独立的数组计数,使用的内存几乎是 Treap 做法的十倍。(不 MLE 就谢天谢地了)

参考代码:

//2212.cpp
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>

using namespace std;

typedef pair <int, int> pii;

const int MAXN = 1e5 + 500;

int N, M;
int a[MAXN], pos[MAXN << 1];
char opt[MAXN << 1];
pii books[MAXN << 1];
vector <int> updates;
vector <pii> queries;

int block_len, block_cnt;
int b[317][MAXN];

int block_id(int pos) { //查询所在块 id
    return (pos - 1) / block_len + 1;
}

void block_update(int pos, int val) { //更新块
    int id = block_id(pos);
    --b[id][a[pos]]; //删除旧的
    ++b[id][val];
    a[pos] = val; //维护新的
}

int block_query(int l, int r, int v) { //区间查询
    int l_id = block_id(l), r_id = block_id(r); //端点所在块
    int ans = 0;
    if (l_id == r_id) {
        for (int i = l; i <= r; i++) ans += a[i] == v; //在同一块内,直接暴力统计
        return ans;
    }
    for (int i = l_id + 1; i < r_id; i++) ans += b[i][v]; //在它们之间的可以整块直接加

    //两边的区间都分类讨论一下,如果恰好包含了整块也直接加,否则暴力统计
    int ll = (l_id - 1) * block_len + 1, lr = l_id * block_len;
    if (l == ll) ans += b[l_id][v];
    else
        for (int i = l; i <= lr; i++) ans += a[i] == v;

    int rl = (r_id - 1) * block_len + 1, rr = r_id * block_len;
    if (r == rr) ans += b[r_id][v];
    else
        for (int i = rl; i <= r; i++) ans += a[i] == v;
    return ans;
}

int main(void) {
    freopen("2212.in", "r", stdin);
    freopen("2212.out", "w", stdout);
    scanf("%d%d", &N, &M);    
    for (int i = 1; i <= N; i++) {
        int num; scanf("%d", &num);
        books[i] = make_pair(num, i);
    }
    for (int i = 1; i <= M; i++) {
        char tmp[2]; scanf("%s", tmp); opt[i] = tmp[0];
        if (opt[i] == 'C') {
            int A, P; scanf("%d%d", &A, &P);
            updates.push_back(A);
            books[N + i] = make_pair(P, N + i);
        }
        if (opt[i] == 'Q') {
            int A, B, K; scanf("%d%d%d", &A, &B, &K);
            queries.push_back(make_pair(A, B));
            books[N + i] = make_pair(K, N + i);
        }
    }
    sort(books + 1, books + N + M + 1); //这题的离散化比较麻烦,需要将所有数据统一先读入

    int ranking = 0;
    for (int i = 1; i <= N + M; i++)
        pos[books[i].second] = (ranking += books[i].first != books[i - 1].first);
    for (int i = 1; i <= N; i++) a[i] = pos[i]/*, printf("%d\n", a[i])*/;

    block_len = sqrt(N); block_cnt = block_len + (bool)(N % block_len); //最后多出来的新开一块
    for (int i = 1; i <= block_cnt; i++) //先 O(n) 算出初始时各块信息
        for (int j = (i - 1) * block_len + 1; j <= i * block_len; j++)
            ++b[i][a[j]];

    int p_upd = 0, p_qry = 0;
    for (int i = 1; i <= M; i++) {
        if (opt[i] == 'C') block_update(updates[p_upd++], pos[N + i]);
        if (opt[i] == 'Q') printf("%d\n", block_query(queries[p_qry].first, queries[p_qry].second, pos[N + i])), ++p_qry;
    }
    return 0;
}


做法二:Treap
考虑维护 m 棵 Treap,其中 m 为离散化后不同的数字个数。每棵 Treap 内的结点保存的是编码对应该棵 Treap 的下标。
例如,对于样例,需要建立 5 棵 Treap,分别维护值为 1~5 的下标。
初始时整个数组为 {1, 2, 3, 4, 5},则值为 1 的下标有 1,因此第 1 棵 Treap 里有且只有 1 个结点,值为 1;其他同理。
询问 [1, 3] 内 2 的个数,则在第 2 棵 Treap 的结点里找大于等于 1 而小于等于 3 的值。可以统一转化为小于等于 3 减去小于等于 0 求解,答案为 1。
第二次询问同理,在第 1 棵 Treap 里查询,小于等于 3 的有 1 个(1),小于等于 0 的有 0 个,因此答案为 1。
将下标 2 的位置修改为 1 时,发现原先其值为 2,则在第 2 棵 Treap 里删除 2,表示该下标的值不再为 2;同时在第 1 棵 Treap 里插入值为 2 的结点。
询问 [1, 3] 内 2 的个数时,该棵 Treap 为空,答案为 0。
询问 [1, 3] 内 1 的个数时,在第 1 棵 Treap 里查询,小于等于 3 的有 2 个(1 和 2),小于等于 0 的有 0 个,因此答案为 2。
…………
类似地,每次询问区间 [l,r] 时,就在对应编号的 Treap 里查找小于等于 r 的数的个数,减去小于等于 l1 的数的个数,就得到了答案。
修改时先在原编号的 Treap 里删去对应下标,再在新编号的 Treap 里插入对应下标,即可。
两个操作的期望时间复杂度都为 O(log2N) ,即总的时间复杂度为 O(Nlog2N)
而每个下标会且只会在某一棵 Treap 中出现。利用指针编写,所有 Treap 的总结点个数为 N ,因此空间复杂度仅为 O(n)

归纳一下,这种做法需要打破平时基于下标进行维护的惯性思维,转为基于值进行维护,思想比较巧妙。数据结构的应用自己还是要多练练,不能止步于裸题。

参考代码:

#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>

using namespace std;

typedef pair <int, int> pii;

const int INF = 0x3f3f3f3f;
const int MAXN = 1e5 + 100;
const int MAXM = 1e5 + 100;

struct Tnode {
    Tnode *child[2];
    int val, fix;
    int siz;
    Tnode (int v = 0) : val(v), fix(rand()), siz(1) { child[0] = child[1] = NULL; }
    void update() { siz = (child[0] ? child[0] -> siz : 0) + (child[1] ? child[1] -> siz : 0) + 1; }
} nodes[MAXN << 1], *current;

struct Treap {
    Tnode *root;
    Treap () : root(NULL) {}

    Tnode *get_node(int v) {
        (*current) = Tnode(v);
        return current ++;
    }

    void rotate(Tnode *&cur, int dir) {
        Tnode *ch = cur -> child[dir ^ 1];
        cur -> child[dir ^ 1] = ch -> child[dir];
        ch -> child[dir] = cur;

        cur -> update();
        ch -> update();
        cur = ch;
    }

    void insert_val(Tnode *&cur, int v) {
        if (!cur) { cur = get_node(v); return ; }
        int t = v > cur -> val;
        insert_val(cur -> child[t], v);
        if (cur -> child[t] -> fix < cur -> fix) rotate(cur, t ^ 1); else cur -> update();
    }

    void remove_val(Tnode *&cur, int v) {
        if (!cur) return ;
        if (cur -> val == v) {
            if (!cur -> child[0] && !cur -> child[1]) { cur = NULL; return ; }
            else if (cur -> child[0] && cur -> child[1]) {
                int t = cur -> child[0] -> fix < cur -> child[1] -> fix;
                rotate(cur, t);
                remove_val(cur -> child[t], v);
            } else if (cur -> child[0]) cur = cur -> child[0]; else cur = cur -> child[1];
        } else remove_val(cur -> child[v > cur -> val], v);
        cur -> update();
    }

    int query_section(Tnode *cur, int l, int r) {
        if (!cur || l > r) return 0;
        if (r < cur -> val) return query_section(cur -> child[0], l, r);
        else if (l > cur -> val) return query_section(cur -> child[1], l, r);
        else return query_section(cur -> child[0], l, cur -> val - 1) + 1 + query_section(cur -> child[1], cur -> val + 1, r);
    }

    int query_leq(Tnode *cur, int v) { //以 cur 为根的子树中,值小于等于 v 的结点数
        if (!cur) return 0;
        if (v <= cur -> val) return query_leq(cur -> child[0], v) + (v == cur -> val);
        //小于当前结点,则只有左子树才有可能有答案。注意小于等于可以跟小于合并处理,缩短代码量。
        else return (cur -> child[0] ? cur -> child[0] -> siz : 0) + 1 + query_leq(cur -> child[1], v); //大于当前结点,则左子树和当前结点肯定都小于 v
    }
} treaps[MAXN << 1];

int N, M, pos[MAXN << 1], A[MAXN << 1];
char opt[MAXM << 1];
pii books[MAXN << 1];
vector <int> updates;
vector <pii> queries;

int main(void) {
    freopen("2212.in", "r", stdin);
    freopen("2212.out", "w", stdout);
    scanf("%d%d", &N, &M);
    for (int i = 1; i <= N; i++) {
        int num; scanf("%d", &num);
        books[i] = make_pair(num, i);
    }
    for (int i = 1; i <= M; i++) {
        char tmp[2]; scanf("%s", tmp); opt[i] = tmp[0];
        if (opt[i] == 'C') {
            int A, P; scanf("%d%d", &A, &P);
            updates.push_back(A);
            books[N + i] = make_pair(P, N + i);
        }
        if (opt[i] == 'Q') {
            int A, B, K; scanf("%d%d%d", &A, &B, &K);
            queries.push_back(make_pair(A, B));
            books[N + i] = make_pair(K, N + i);
        }
    }
    sort(books + 1, books + N + M + 1);
//  for (int i = 1; i <= N; i++) printf("(%d, %d) ", books[i].first, books[i].second);
    int ranking = 0;
    for (int i = 1; i <= N + M; i++)
        pos[books[i].second] = (ranking += books[i].first != books[i - 1].first);
//  for (int i = 1; i <= N + M; i++) printf("%d ", pos[i]);
    current = nodes;
    for (int i = 1; i <= N; i++) A[i] = pos[i], treaps[pos[i]].insert_val(treaps[pos[i]].root, i);
    int p_upd = 0, p_qry = 0;
    for (int i = 1; i <= M; i++) {
        if (opt[i] == 'C') {
            treaps[A[updates[p_upd]]].remove_val(treaps[A[updates[p_upd]]].root, updates[p_upd]);
            treaps[pos[N + i]].insert_val(treaps[pos[N + i]].root, updates[p_upd]);
            A[updates[p_upd++]] = pos[N + i];
        }
        if (opt[i] == 'Q') printf("%d\n", treaps[pos[N + i]].query_leq(treaps[pos[N + i]].root, queries[p_qry].second) - treaps[pos[N + i]].query_leq(treaps[pos[N + i]].root, queries[p_qry].first - 1)/*query_section(treaps[pos[N + i]].root, queries[p_qry].first, queries[p_qry].second)*/), ++p_qry;
    }
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值