HDU 5919 主席树
一开始拿到这个题目的时候没啥思路,纠结再三,看了一下网上大佬们的题解,**nb。颠覆了我对主席树题的认知,因此记一下笔记。
题目大意
初始时有一个数列,每次给一个区间,询问该区间内,假设只取区间内的每种数字第一次出现的位置,形成一个新的序列,问这个序列中间数是多少?题目在此。
思路
这道题目巧妙地利用了主席树的保存历史版本的特性:
要点
- 主席树上的叶子节点表示的是当前这个位置有几个数字(当然,无论怎么运行都只有0或1);
- 在原来主席树建立的基础上添加一个操作:新加入的数如果已经出现过了,将上次出现的位置权值-1,然后再在当前的位置+1,保持树上不存在任何两个位置表示的是同一个数字的出现;
- 最最最重要的一点,题目要求的是保留每个数字的第一次出现的位置,那么我们就需要从后往前更新主席树,那么能够保证树上每个1表示的都是[L-n]之间的某个数字的第一次出现。有人可能会疑惑为什么需要从后往前,大家可以自己脑补一下,就能轻易得知从前往后是不可行的;
我们拿题目的样例二来举例子:
一:加入 25
2 | 5 | 2 | 1 | 2 |
---|---|---|---|---|
0 | 0 | 0 | 0 | 1 |
二:加入 14
2 | 5 | 2 | 1 | 2 |
---|---|---|---|---|
0 | 0 | 0 | 1 | 1 |
三:去除 25,添加23
2 | 5 | 2 | 1 | 2 |
---|---|---|---|---|
0 | 0 | 1 | 1 | 0 |
四:添加52
2 | 5 | 2 | 1 | 2 |
---|---|---|---|---|
0 | 1 | 1 | 1 | 0 |
五:去除23,添加21
2 | 5 | 2 | 1 | 2 |
---|---|---|---|---|
1 | 1 | 0 | 1 | 0 |
比如第一个询问:
[2, 3]会被转化成[3, 4],那么我们只需要看第 3 棵树(因为在此询问中,[1, 2]之间所有的数字的出现是没有意义的)。首先我们要对区间计数(不会区间计数的同学,可以做一下SPOJ D-Query一题),然后获得中位数是k。接下来,只需要在这棵树里直接查询第k小即可(因为能查到第k小的话,一定在[L, R]中,[R-n]之间的值已经超过第k个了),主席树经典用法。
AC代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 201000;
const int M = MAXN * 50;
int n, q, tot;
int a[MAXN];
int tree[MAXN], lson[M], rson[M], c[M];
int __build(int l, int r) {
int root = tot++;
c[root] = 0;
if (l != r) {
int mid = (l + r) >> 1;
lson[root] = __build(l, mid);
rson[root] = __build(mid + 1, r);
}
return root;
}
/**
* 更新root树表示的历史版本的pos位置加上了val值
* @param root 树根节点编号
* @param pos 指定位置
* @param val 添加的值
* @return 新树的根节点
*/
int update(int root, int pos, int val) {
int newroot = tot++, tmp = newroot;
c[newroot] = c[root] + val;
int l = 1, r = n;
while (l < r) {
int mid = (l + r) >> 1;
if (pos <= mid) {
lson[newroot] = tot++;
rson[newroot] = rson[root];
newroot = lson[newroot];
root = lson[root];
r = mid;
} else {
rson[newroot] = tot++;
lson[newroot] = lson[root];
newroot = rson[newroot];
root = rson[root];
l = mid + 1;
}
c[newroot] = c[root] + val;
}
return tmp;
}
int query(int root, int pos) {
int ret = 0;
int l = 1, r = n;
while (pos < r) {
int mid = (l + r) >> 1;
if (pos <= mid) {
r = mid;
root = lson[root];
} else {
ret += c[lson[root]];
root = rson[root];
l = mid + 1;
}
}
return ret + c[root];
}
/**
* 求区间第k大
* @param root
* @param k
* @return
*/
int query1(int root, int k) {
int l, r;
l = 1, r = n;
int res = 0;
while (l <= r) {
int mid = (l + r) / 2;
if (c[lson[root]] >= k) {
r = mid;
root = lson[root];
} else if (c[lson[root]] < k) {
res = l;
l = mid + 1;
k -= c[lson[root]];
root = rson[root];
}
}
return res;
}
void build() {
tree[n + 1] = __build(1, n);
map<int, int> mp;
for (int i = n; i >= 1; i--) {
if (mp.find(a[i]) == mp.end()) {
//没找到重复的点,直接更新
tree[i] = update(tree[i + 1], i, 1);
} else {
//先在新树上去掉先前重复的点
int tmp = update(tree[i + 1], mp[a[i]], -1);
//在当前位置添加这个点,保持每棵树上不存在重复节点
tree[i] = update(tmp, i, 1);
}
mp[a[i]] = i;
}
}
int getAns(int l, int r) {
// 获取不同数的个数
int k = query(tree[l], r);
double tmp = k / 2.0;
k = ceil(tmp);
return query1(tree[l], k);
}
void reset() {
n = q = tot = 0;
memset(a, 0, sizeof(a));
memset(tree, 0, sizeof(tree));
memset(lson, 0, sizeof(lson));
memset(rson, 0, sizeof(rson));
memset(c, 0, sizeof(c));
}
int main() {
#ifdef ACM_LOCAL
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
#endif
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t;
cin >> t;
for (int tt = 1; tt <= t; tt++) {
cout << "Case #" << tt << ":";
cin >> n >> q;
for (int i = 1; i <= n; i++)
cin >> a[i];
build();
int ans = 0;
while (q--) {
int l, r;
cin >> l >> r;
int tmpl = l, tmpr = r;
l = min((tmpl + ans) % n + 1, (tmpr + ans) % n + 1);
r = max((tmpl + ans) % n + 1, (tmpr + ans) % n + 1);
ans = getAns(l, r);
cout << " " << ans;
}
cout << endl;
reset();
}
return 0;
}
总结
一开始感觉是非常复杂的一个题,不过看了网上大佬们的操作并理清楚思路之后,感觉真的就是一个模板题,只不过要套两个主席树的用法(哎,自己竟然想了半天没想到,捶桌!)。
细节也不多,主要是二分的地方容易出错。题目数据可能比较水,稍微改了一下板子调整了一下数组大小就A了(中间经历了一发RE一发TLE和一发MLE,枯了)。不过还是要承认,这题是一个为数不多的能颠覆新手对主席树认知的题目。tql!!!