HDU 3333 Turing Tree [线段树(树状数组)+离线操作+数字去重求区间和]
题目传送门
一、题目大意
统计一段序列【\(L,R\)】的和,重复元素只算一次。
二、解题思路
不能直接用线段树来维护,原因是我们不知道是不是重复,光会做区间和就没用了。
题目意在求一个给定序列的区间不同数之和,所以我们要对区间的数字进行去重。
朴素的做法是\(O(n^3)\)的复杂度,显然是不能接受的。
在这里用线段树和排序处理的巧妙做法,就可以把复杂度降到\(O(nlogn)\)。
(1)建一棵节点\(sum\)都为\(0\)的线段树。
(2)先将所求区间根据右端点从小到大排序
(3)每次以右端点为终点,从上次停下的位置扫一下,在它上次出现的地方,减掉它,在某一个数的节点处加上它,然后求区间和即可。
正确性证明:
这么做可以把重复的数尽可能往后推,每一次都是扫到要求的区间的右端点,所以能保证不会多减,
且不论左端点如何,都可以保证区间里必定计上了所有要计的数,所以只需要对右端点进行排序即可。
三、实现代码
// #include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
typedef long long LL;
const int N = 30010, M = 100010;
//线段树结构体
struct Node {
int l, r;
LL sum; //区间和
} tr[N << 2];
//离线处理的问题结构体
struct Question {
int l, r, id; //左右区间,问题编号
bool operator<(const Question &q) const {
return r < q.r; //按右端点由小到大排序
}
};
int n, m; // n个数字的原始数列,m次询问
int a[N]; //原始数组
Question question[M]; //问题
LL res[M]; //问题答案
unordered_map pos; //采用STL+unordered_map 进行保存某个数字首次出现的位置
#pragma region 线段树
void pushup(int u) {
tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}
void build(int u, int l, int r) {
tr[u] = {l, r};
int mid = (l + r) >> 1;
if (l == r) return;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
}
void modify(int u, int x, int v) {
if (tr[u].l == tr[u].r) {
tr[u].sum += (LL)v;
return;
}
int mid = (tr[u].l + tr[u].r) >> 1;
if (x <= mid) modify(u << 1, x, v);
if (x > mid) modify(u << 1 | 1, x, v);
pushup(u);
}
LL query(int u, int l, int r) {
if (l <= tr[u].l && tr[u].r <= r) return tr[u].sum;
LL ans = 0;
int mid = (tr[u].l + tr[u].r) >> 1;
if (l <= mid) ans += query(u << 1, l, r);
if (r >= mid + 1) ans += query(u << 1 | 1, l, r);
return ans;
}
#pragma endregion
int main() {
//核心思想:离线处理+线段树
int T;
scanf("%d", &T);
while (T--) { //多组测试数据
//清空
pos.clear();
memset(res, 0, sizeof(res));
scanf("%d", &n); //原数列
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
scanf("%d", &m); // q次询问
//将q次询问先记录到结构体中,统一处理后,再按询问要求输出答案
for (int i = 1; i <= m; i++) {
scanf("%d %d", &question[i].l, &question[i].r);
question[i].id = i;
}
//按右端点排序,以保证从左到右的遍历
sort(question + 1, question + 1 + m);
//构建线段树
build(1, 1, n);
int j = 1;
for (int i = 1; i <= m; i++) { //遍历每个排好序的问题,从左到右按右端点进行的排序,也就是由小到大
while (j <= question[i].r) {
//如果数字a[j]出现过,在出现的位置减去a[j]
//假如a[j]=3,我们需要检查3是否在以前出现过,如果出现过,就把旧位置的清除掉
if (pos.count(a[j])) modify(1, pos[a[j]], -a[j]);
//在当前位置上加上a[j]
modify(1, j, a[j]);
//记录a[j]出现的位置j
pos[a[j]] = j;
j++;
}
//如此处理,可以保证区间统计中没有重复数字
//记录答案 之所以需要记录下答案,而不是直接输出答案,是因为现在顺序是按右端点排序完成后顺序
//题目要求输出的是顺序是按输入的顺序,所以直接输出就不对了,需要记录下来,然后按输入号输出
res[question[i].id] = query(1, question[i].l, question[i].r);
// cout << question[i].id << "--->" << res[question[i].id] << endl;
}
//输出答案
for (int i = 1; i <= m; i++) printf("%lld\n", res[i]);
}
return 0;
}