这题很显然是数据结构题,做法比较多。但前提都是要先将编码离散化,否则数值太大,无法维护。
做法一:分块。
这是 cdc 跟我们介绍的一种很神奇的方法。把长度为
n
的区间分为
每一块就是一个小区间,独立维护自己的值,用一个数组保存各个编号出现的数量。总的就是一个二维数组,
a[i][j]
表示第
i
段区间内编号
对于将下标为
pos
处修改为
val
值的操作,可以用一个数组
O(1)
标记。先将
pos
所在的区间内,原先值的数量 -1,再将该区间
val
的值 +1。
对于求区间内指定编号个数的询问,将待查询区间分块。区间内整块整块的值直接加到答案中,左右两端如果有零散的部分,直接暴力统计。
例如,对于样例,
n
=5,则每块长度=2,共分为 3 块:[1, 2][3, 4][5]
初始时整个数组为 {1, 2, 3, 4, 5},意味着第一块内 1 和 2 都出现了一次,其他同理,即
询问 [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,其中
例如,对于样例,需要建立 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
的数的个数,减去小于等于
修改时先在原编号的 Treap 里删去对应下标,再在新编号的 Treap 里插入对应下标,即可。
两个操作的期望时间复杂度都为
O(log2N)
,即总的时间复杂度为
O(Nlog2N)
。
而每个下标会且只会在某一棵 Treap 中出现。利用指针编写,所有 Treap 的总结点个数为
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;
}