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;
}