【Coel.学习笔记】【半途跑路】CDQ 分治

最近在刷状压 DP,结果发现太难不会做,跑来学点别的。
反正 CSP-S2 之前刷完就行了,吧?
放在数据结构里面是因为 CDQ 分治和树套树能解决的问题差不多,所以放了进去(绝不是因为懒得开一个“离线算法”的 Tag!)

引入

CDQ 分治是一种通过把动态询问/点对问题等离线处理,并分治求解的算法。这种思想最早于 IOI2008 国家集训队选手陈丹琦在其论文 从《Cash》谈一类分治算法的应用 中总结,故得名。

例题讲解

虽然原论文给出的例题是[NOI2007] 货币兑换,但我们先从一些更简单的题目说起。其实是我不会斜率优化 DP

[BJOI2016]回转寿司 - 二维偏序的 CDQ 分治

洛谷传送门
给定一个序列 \(a_i\) 和两个数字 \(L,R\),求出满足 \(L\leq\sum a_i\leq R\) 的连续子序列个数。
解析:这道题严格来说其实不算 CDQ 分治,但思想类似,所以也放过来。
对原序列求一个前缀和,那么问题就转化成 \(i < j\)\(L\leq s_i-s_j\leq R\) 的配对 \(i,j\) 个数。这和逆序对的求法一样,套用归并排序就好了。

如果不想写归并排序也可以用 sort,不过常数略大。代码很精简,只有 \(35\) 行(请珍惜现在的短代码……)

#include <iostream>
#include <algorithm>

#define int long long

using namespace std;

const int maxn = 1e5 + 10;

int n, L, R, ans, s[maxn];

void merge_solve(int l, int r) {
	if (l == r) return;
	int mid = (l + r) >> 1;
	merge_solve(l, mid), merge_solve(mid + 1, r);
	int i = l, j = l - 1;
	for (int k = mid + 1; k <= r; k++) {
		while (j + 1 <= mid && s[k] >= s[j + 1] + L) j++;
		while (i <= mid && s[k] > s[i] + R) i++;
		ans += j - i + 1;
	}
	std::sort(s + l, s + r + 1);
}

signed main(void) {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin >> n >> L >> R;
	for (int i = 1; i <= n; i++) {
		cin >> s[i];
		s[i] += s[i - 1];
	}
	merge_solve(0, n);
	cout << ans;
}

【模板】三维偏序 - 标准 CDQ 分治

洛谷传送门
有 $ n $ 个元素,第 $ i $ 个元素有 $ a_i,b_i,c_i $ 三个属性,设 $ f(i) $ 表示满足 $ a_j \leq a_i $ 且 $ b_j \leq b_i $ 且 $ c_j \leq c_i $ 且 $ j \ne i $ 的 \(j\) 的数量。

对于 $ d \in [0, n-1] $,求 $ f(i) = d $ 的数量。(这里描述用 \(n-1\) 代替右开)

解析:这题有很多做法,如树套树、K-D Tree。这里当然只介绍 CDQ 分治的做法。
假设只有一个属性,即对每个 \(i\) 找到 \(a_j\leq a_i\) 的数量。显然我们只要按照 \(a_i\) 大小排序,那么对于 \(a_i\) 就有 \(i-1\) 个数字比它小。
当属性有两个的时候,同样按照 \(a_i\) 排序,然后从前往后扫描。那么对于 \(i\) 前面的元素 \(j\) 一定有 \(a_j\leq a_i\),任务就变成找 \(b_j\leq b_i\) 的数量。一种办法是先把 \(b\) 离散化,然后用类似树状数组求逆序对的方式求解即可。


当然也可以像归并排序求逆序对一样,先做排序,然后根据 \(i,j\) 在两个分治区间内的情况分类讨论:

  1. \(i,j\) 均在左区间,则对左区间分治求解;
  2. \(i,j\) 均在右区间,则对右区间分治求解;
  3. \(i,j\) 分别在左、右区间(显然 \(i\) 一定在 \(j\) 右边),那么对于左区间的任何一个元素,寻找 \(b_j\leq b_i\) 的数量即可。由于分治同时会把 \(b\) 排序,所以对 \(i,j\) 各开一个指针,双指针扫描一遍就可以求出答案。

这样每次分治都要做一次 \(O(n)\) 的双指针,总复杂度为 \(O(n\log n)\)


现在来到三维。类比上面提到的归并排序方法,先做一遍排序。那么对于每个 \(i\),满足条件的 \(j\) 一定在左边。同样,我们对 \(i,j\) 的情况做分类讨论:同在左区间,同在右区间,分别位于两个区间。前两个分治即可,重点看第三个。

由于做了排序,所以 \(a_j\leq a_i\) 一定满足。 接下来对于 \(b_j\leq b_i\),用双指针寻找第一个 \(b_j> b_i\) 的位置,那么每个 \(i\) 都可以 \(O(n)\) 找到对应的 \(j\),此时答案在左区间边界到 \(j-1\) 的范围内。最后对于 \(c_j\leq c_i\),用树状数组就可求出。

这样每次分治都要做 \(O(n)\) 双指针,且每次移动指针都要用 \(O(\log n)\) 的树状数组维护答案,故总时间复杂度为 \(O(n\log ^2 n)\)

#include<cctype>
#include<cstdio>
#include<algorithm>
#include<cstring>

struct __Coel_FastIO {
#ifndef LOCAL
#define _getchar_nolock getchar_unlocked
#define _putchar_nolock putchar_unlocked
#endif

    inline __Coel_FastIO& operator>>(int& x) {
        x = 0;
        bool f = false;
        char ch = _getchar_nolock();
        while (!isdigit(ch)) {
            if (ch == '-') f = true;
            ch = _getchar_nolock();
        }
        while (isdigit(ch)) {
            x = x * 10 + ch - '0';
            ch = _getchar_nolock();
        }
        if (f) x = -x;
        return *this;
    }

    inline __Coel_FastIO& operator<<(int x) {
        if (x < 0) {
            x = -x;
            _putchar_nolock('-');
        }
        static int buf[35];
        int top = 0;
        do {
            buf[top++] = x % 10;
            x /= 10;
        } while (x);
        while (top) _putchar_nolock(buf[--top] + '0');
        return *this;
    }

    inline __Coel_FastIO& operator<<(char x) {
        return _putchar_nolock(x), *this;
    }

} qwq;

const int maxn = 2e5 + 10;

int n, m, top = 1;
int ans[maxn];

struct node {
    int a, b, c;
    int res, cnt; //记录出现次数,处理属性相等的状态
    bool operator<(const node &x) const {
        if (a != x.a) return a < x.a;
        if (b != x.b) return b < x.b;
        return c < x.c;
    }
    bool operator==(const node &x) const {
        return a == x.a && b == x.b && c == x.c;
    }
} q[maxn], tem[maxn];

class Fenwick_Tree {
    private:
#define lowbit(x) (x & (-x))
        int c[maxn];
    public:
        void add(int x, int v) {
            for (int i = x; i < maxn; i += lowbit(i)) c[i] += v;
        }
        int query(int x) {
            int res = 0;
            for (int i = x; i; i -= lowbit(i)) res += c[i];
            return res;
        }
} T;

void CDQ_Divide(int l, int r) {
    if (l >= r) return;
    int mid = (l + r) >> 1;
    CDQ_Divide(l, mid), CDQ_Divide(mid + 1, r);
    int i = l, j = mid + 1, k = 0; //把当前区间分成 [l, mid] 和 [mid + 1, r] 两个小区间
    while (i <= mid && j <= r) //两个区间都没遍历完时
        if (q[i].b <= q[j].b) //不满足条件,把信息存入树状数组
            T.add(q[i].c, q[i].cnt), tem[k++] = q[i++];
        else // 满足条件,在树状数组上得到答案
            q[j].res += T.query(q[j].c), tem[k++] = q[j++];
    while (i <= mid) T.add(q[i].c, q[i].cnt), tem[k++] = q[i++];
    while (j <= r) q[j].res += T.query(q[j].c), tem[k++] = q[j++]; //将没有处理完的部分继续处理
    for (i = l; i <= mid; i++) T.add(q[i].c, -q[i].cnt); //还原树状数组
    for (i = l, j = 0; j < k; i++, j++) q[i] = tem[j]; //将归并排序后结果复制到原数据中
}

int main(void) {
    qwq >> n >> m;
    for (int i = 0; i < n; i++)
        qwq >> q[i].a >> q[i].b >> q[i].c, q[i].cnt = 1;
    std::sort(q, q + n);
    for (int i = 1; i < n; i++) { // 特判属性相等时的答案
        // 这里的 top 其实也起到了去重的作用
        if (q[i] == q[top - 1]) q[top - 1].cnt++;
        else q[top++] = q[i];
    }
    CDQ_Divide(0, top - 1);
    for (int i = 0; i < top; i++)
        ans[q[i].res + q[i].cnt - 1] += q[i].cnt;
    for (int i = 0; i < n; i++)
        qwq << ans[i] << '\n';
    return 0;
}

总结一下,CDQ 分治的本质其实就是消除偏序维度。对于一维,直接排序;对于二维。在一维的基础上做树状数组/分治;对于三维,再在二维基础上加。从某种程度上说,CDQ 分治也可以看作归并排序一类分治的扩展算法。

顺带一提,树套树求三维偏序的本质也是消除维度,不过是用权值线段树代替分治、支持强制在线罢了。

[CQOI2017]老C的任务 - 二维数点转偏序

洛谷传送门
给定平面直角坐标系上的若干个点,每个点都有一个权值。每次询问给出一个矩形一条对角线的端点所对应坐标,求出矩形内所有点的权值之和。

解析:对于矩形覆盖问题,我们可以想到使用二维前缀和把查询操作优化到 \(O(1)\),那么我们的目标就在于快速预处理出前缀和。

由于本题允许离线,所以可以把一开始给定的点标记为 \(1\),矩形上的点标记为 \(0\),那么这两类点都会有三个属性 \((x,y,z)\),并且限制为矩形点的属性小于等于给定点的属性。这就是一个标准的三维偏序问题。

由于 \(z\) 只有 \(0,1\) 两种取值,所以我们不需要使用树状数组维护 \(z\),直接用前缀和数组代替即可,时间复杂度也降低到了 \(O(n\log n)\)

struct node {
    int x, y, z, p;
    int id, sign, sum;
    // id 表示这个点的属性,sign 记录求矩形和时为加还是减,sum 为前缀和
    bool operator<(const node &t) const {
        if (x != t.x) return x < t.x;
        if (y != t.y) return y < t.y;
        return z < t.z;
    }
} q[maxn], tmp[maxn];

void CDQ_Divide(int l, int r) {
    if (l >= r) return;
    int mid = (l + r) / 2;
    CDQ_Divide(l, mid), CDQ_Divide(mid + 1, r);
    int i = l, j = mid + 1, k = 0, sum = 0;
    while (i <= mid && j <= r)
        if (q[i].y <= q[j].y) sum += (!q[i].z) * q[i].p, tmp[k++] = q[i++];
    // 因为 1 为给定点而 0 为查询点,所以 q[i].z == 0 时才求值
        else q[j].sum += sum, tmp[k++] = q[j++];
    while (i <= mid) sum += (!q[i].z) * q[i].p, tmp[k++] = q[i++];
    while (j <= r) q[j].sum += sum, tmp[k++] = q[j++];
    for (i = l, j = 0; j < k; i++, j++) q[i] = tmp[j];
}

signed main(void) {
    qwq >> n >> m;
    top = n;
    for (int i = 0; i < n; i++) {
        int x, y, p;
        qwq >> x >> y >> p;
        q[i] =  {x, y, 0, p};
    }
    for (int i = 1; i <= m; i++) {
        int x1, y1, x2, y2;
        qwq >> x1 >> y1 >> x2 >> y2;
        q[top++] = {x2, y2, 1, 0, i, 1};
        q[top++] = {x1 - 1, y2, 1, 0, i, -1};
        q[top++] = {x2, y1 - 1, 1, 0, i, -1};
        q[top++] = {x1 - 1, y1 - 1, 1, 0, i, 1};
    }
    std::sort(q, q + top);
    CDQ_Divide(0, top - 1);
    for (int i = 0; i < top; i++)
        if (q[i].z) ans[q[i].id] += q[i].sum * q[i].sign;
    for (int i = 1; i <= m; i++)
        qwq << ans[i] << '\n';
    return 0;
}

实际上,绝大多数的 CDQ 分治应用场景可以分成两种:斜率优化动态规划和二维数点计数。下面看一道动态修改的题目。

[Balkan OI 2007] Mokia - 支持修改的二维数点

洛谷传送门
给定一个边长为 \(w\) 的矩阵,一开始权值均为零,然后进行 \(m\) 次操作:
1 x y a\((x,y)\) 加上权值 \(a\)
2 x1 y1 x2 y2 查询 \((x_1,y_1)\)\((x_2,y_2)\) 对应矩形中的权值和。

解析:上一道题老C的任务实际上把二维数点问题转换成了三维偏序问题,从而利用 CDQ 分治求解,这道题也是如此。

但和上一题不同的是,这题并没有先给出点再询问,而是在询问的过程中给点。这时我们可以建立一个时间轴,把时间作为第三维跑三维偏序

由于修改操作是动态的,我们不能直接用二维前缀和查询,而是用树状数组,时间复杂度为 \(O(m\log m \times \log w)\)

int n, m, top, dfn,ans[maxn];
struct node {
    int x, y, z, pos, op, p;
    // 这里的 z 是时间轴
    //op == 0 -> 修改 op == 1 -> 查询且乘 1 op == -1 -> 查询且乘 -1
    bool operator<(const node &t) const {
        if (x != t.x) return x < t.x;
        if (y != t.y) return y < t.y;
        if (z != t.z) return z < t.z;
        return p > t.p; // 属性均相等时按照权值大小排序
    }
} q[maxn], tmp[maxn];

class Fenwick_Tree {
    private:
        int c[maxn];
#define lowbit(x) (x & (-x))
    public:
        void add(int x, int v) {
            for (int i = x; i <= top; i += lowbit(i)) c[i] += v;
        }
        int query(int x) {
            int res = 0;
            for (int i = x; i; i -= lowbit(i)) res += c[i];
            return res;
        }
} T;

void CDQ_Divide(int l, int r) {
    if (l >= r) return;
    int mid = (l + r) / 2;
    CDQ_Divide(l, mid), CDQ_Divide(mid + 1, r);
    int i = l, j = mid + 1, k = l;
    while (i <= mid && j <= r)
        if (q[i].y <= q[j].y) {
            if (!q[i].op) T.add(q[i].z, q[i].p);
            tmp[k++] = q[i++];
        } else {
            if (q[j].op) ans[q[j].pos] += T.query(q[j].z) * q[j].op;
            tmp[k++] = q[j++];
        }
    while (j <= r) {
        if (q[j].op) ans[q[j].pos] += T.query(q[j].z) * q[j].op;
        tmp[k++] = q[j++];
    }
    for (int t = l; t < i; t++) // 还原树状数组
        if (!q[t].op) T.add(q[t].z, -q[t].p);
    while (i <= mid) tmp[k++] = q[i++];
    for (i = l; i <= r; i++) q[i] = tmp[i];
}

int main(void) {
    qwq >> n >> n;
    n++; //坐标偏移
    while (1) {
        int op;
        qwq >> op;
        if (op == 1) {
            int x, y, p;
            qwq >> x >> y >> p;
            x++, y++, dfn++;
            q[++top] =  {x, y, dfn, 0, 0, p};
        } else if (op == 2) { // 注意查询操作不需要让时间轴增加
            int x1, y1, x2, y2;
            qwq >> x1 >> y1 >> x2 >> y2;
            x2++, y2++, m++;
            q[++top] = {x2, y2, dfn, m, 1, 0};
            q[++top] = {x1, y2, dfn, m, -1, 0};
            q[++top] = {x2, y1, dfn, m, -1, 0};
            q[++top] = {x1, y1, dfn, m, 1, 0};
        } else break;
    }
    std::sort(q + 1, q + top + 1);
    CDQ_Divide(1, top);
    for (int i = 1; i <= m; i++) qwq << ans[i] << '\n';
    return 0;
}

[Ynoi2016]镜中的昆虫 - 珂朵莉树预处理

洛谷传送门

注:本题背景对应的 Galgame 为 素晴らしき日々 ~不连続存在~,题目名为《爱丽丝镜中奇遇记》的第三章标题,在该 Galgame 中也有 Neta。这款游戏是十年前的老游戏了,想玩可以去试试看反正我不感兴趣

给定一个长为 \(n\) 的序列,进行 \(m\) 次操作:
1 l r x 将区间 \([l,r]\) 的值全部赋为 \(x\)
2 l r 求出区间 \([l,r]\) 中不同数字的个数。

解析:对于区间推倒的操作,我们很容易想到珂朵莉树,但显然只用珂朵莉树是没法过的,因为数据不随机(毒瘤题有随机可言?)

考虑维护一个 \(pre_i\) 数组表示 \(i\) 左侧第一个同数字的位置(不存在时 \(pre_i=0\)),那么查询操作可以转换成询问 \([l,r]\)\(pre_i<l\) 的数字个数。套用二维数点的思路,由于每次对 \(pre_i\) 的修改都会使得区间查询的贡献至多变化 \(1\),所以利用 CDQ 分治,每次在树状数组上修改查询区间和即可。

但是很容易发现一个问题:对 \(pre_i\) 量可能很大,会不会超时呢?答案是否定的。利用珂朵莉树,我们可以把相同颜色段看作一个点,那根据珂朵莉树的原理,区间推倒就相当于删除 \([l,r]\) 上所有点再加一个单独点。由于每个节点最多只会被删除一次,那么对 \(pre_i\) 的修改操作就会和序列长度 \(n\),操作次数 \(m\) 线性相关,即 \(O(n+m)\) 水平。

利用这一个重要的性质,我们就可以用 CDQ 分治处理掉这道题了。总时间复杂度为 \(O((n+m)\log^2n)\),实际跑起来还是很快的。码量很大(格式化完有 \(345\) 行,相当于一个猪国杀),所以细节很多。

#include <cctype>
#include <cstdio>
#include <algorithm>
#include <set>
#include <cstring>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/hash_policy.hpp>

using namespace __gnu_pbds;

struct __Coel_FastIO {
#ifndef LOCAL
#define _getchar_nolock getchar_unlocked
#define _putchar_nolock putchar_unlocked
#endif
    
    inline __Coel_FastIO &operator>>(int &x) {
        x = 0;
        bool f = false;
        char ch = _getchar_nolock();
        
        while (!isdigit(ch)) {
            if (ch == '-')
                f = true;
            
            ch = _getchar_nolock();
        }
        
        while (isdigit(ch)) {
            x = x * 10 + ch - '0';
            ch = _getchar_nolock();
        }
        
        if (f) x = -x;
        
        return *this;
    }
    
    inline __Coel_FastIO &operator<<(int x) {
        if (x < 0) {
            x = -x;
            _putchar_nolock('-');
        }
        
        static int buf[35];
        int top = 0;
        
        do {
            buf[top++] = x % 10;
            x /= 10;
        } while (x);
        
        while (top) _putchar_nolock(buf[--top] + '0');
        
        return *this;
    }
    
    inline __Coel_FastIO &operator<<(char x) {
        return _putchar_nolock(x), *this;
    }
    
} qwq;

const int maxn = 2e5 + 10, maxr = 8e5 + 10;

int n, m, a[maxn], pre[maxn], lst[maxn], rec_pre[maxn], bel[maxn];

gp_hash_table<int, int> Hash;

int cnt = 0;

struct query {
    int op, l, r, x;
} q[maxn];

struct Node {
    int tim, l, r, ans;
    bool operator<(const Node &x) const {
        return l < x.l;
    }
} Q[maxn];
int qtop, dfn;

struct release {
    int tim, pos, pre, val;
    bool operator<(const release &x) const {
        return pre < x.pre;
    }
} rel[maxn * 4];
int rtop;

void modify(int pos, int x) { //修改 pre 数组,这里注意 pre 需要复制一个
    if (rec_pre[pos] == x)
        return;
    
    rel[++rtop] = {++dfn, pos, rec_pre[pos], -1};
    rec_pre[pos] = x;
    rel[++rtop] = {++dfn, pos, rec_pre[pos], 1};
}

class Ctholly_Tree {
public:
    struct ODT { // 珂朵莉树的结构体
        int l, r, x;
        bool operator<(const ODT &t) const {
            return r < t.r;
        }
    };
    
    struct node { // 二维数点的结构体
        int l, r;
        bool operator<(const node &t) const {
            return r < t.r;
        }
    };
    
    std::set<ODT> S;
    std::set<node> N[maxn];
    std::set<int> cont;
    
    void split(int x) {
        auto it = S.lower_bound({0, x, 0});
        auto _it = *it; // 额外开一个指针防止删除时出现 RE
        
        if (x == _it.r)
            return;
        
        S.erase(_it);
        S.insert({_it.l, x, _it.x});
        S.insert({x + 1, _it.r, _it.x});
        
        N[_it.x].erase({_it.l, _it.r});
        N[_it.x].insert({_it.l, x});
        N[_it.x].insert({x + 1, _it.r});
    }
    
    void erase(std::set<ODT>::iterator it) { // 删除迭代器
        cont.insert(it -> l);
        std::set<node>::iterator it1, it2;
        it1 = it2 = N[it -> x].find({it->l, it->r});
        it2++;
        
        if (it2 != N[it->x].end())
            cont.insert(it2->l);
        
        N[it->x].erase(it1);
        S.erase(it);
    }
    
    void insert(ODT t) { // 插入操作
        S.insert(t);
        auto it = N[t.x].insert({t.l, t.r}).first;
        it++;
        
        if (it != N[t.x].end())
            cont.insert(it->l);
    }
    
    void assign(int l, int r, int x) { // 区间推倒
        if (l != 1)
            split(l - 1);
        
        split(r);
        int t = l;
        
        while (t != r + 1) {
            auto it = S.lower_bound({0, t, 0});
            t = it->r + 1;
            erase(it);
        }
        
        insert({l, r, x});
        
        for (auto it = cont.begin(); it != cont.end(); it++) {
            auto it1 = S.lower_bound({0, *it, 0});
            
            if (*it != it1->l)
                modify(*it, *it - 1);
            else {
                auto it2 = N[it1->x].lower_bound({0, *it});
                
                if (it2 != N[it1->x].begin())
                    --it2, modify(*it, it2->r);
                else
                    modify(*it, 0);
            }
        }
        
        cont.clear();
    }
    
    void init_colour() {
        int _a = a[1], len = 1;
        
        for (int i = 2; i <= n; i++)
            if (_a != a[i]) { //颜色不同时重新初始化色块
            S.insert({i - len, i - 1, _a});
            N[_a].insert({i - len, i - 1});
            _a = a[i];
            len = 1;
        } else
            len++; //颜色相同时增加色块长
        
        S.insert({n - len + 1, n, a[n]}); // 注意最后一块要单独加上去
        N[a[n]].insert({n - len + 1, n});
    }
    
} odt;

class Fenwick_Tree {
private:
    int c[maxn];
#define lowbit(x) (x & (-x))
    
public:
    void add(int x, int v) {
        for (int i = x; i <= n; i += lowbit(i))
            c[i] += v;
    }
    
    int query(int x) {
        int res = 0;
        
        for (int i = x; i; i -= lowbit(i))
            res += c[i];
        
        return res;
    }
    
    void to_zero(int x) {
        for (int i = x; i <= n; i += lowbit(i))
            c[i] = 0;
    }
    
    void clear() {
        for (int i = 1; i <= n; i++)
            c[i] = 0;
    }
    
} T;

inline bool cmp_pre(int x, int y) {
    return pre[x] < pre[y];
}

inline bool cmp_dfn(Node x, Node y) {
    return x.tim < y.tim;
}

inline void init_pre() {
    for (int i = 1; i <= n; i++)
        a[i] = Hash[a[i]];
    
    for (int i = 1; i <= n; i++)
        if (q[i].op == 1)
            q[i].x = Hash[q[i].x];
    
    for (int i = 1; i <= n; i++)
        pre[i] = lst[a[i]], lst[a[i]] = i;
    
    for (int i = 1; i <= n; i++)
        rec_pre[i] = pre[i];
}

void CDQ_Divide(int l1, int r1, int l2, int r2, int x, int y) {
/*传入三个数组分别表示修改操作,查询操作和时间轴*/
    if (l1 == r1 || l2 == r2)
        return;
    
    int mid = (x + y) >> 1, mid1 = l1, mid2 = l2;
    
    while (mid1 != r1 && rel[mid1 + 1].tim <= mid)
        mid1++;
    
    while (mid2 != r2 && Q[mid2 + 1].tim <= mid)
        mid2++;
    
    CDQ_Divide(l1, mid1, l2, mid2, x, mid);
    CDQ_Divide(mid1, r1, mid2, r2, mid, y);
    
    if (l1 != mid1 && r2 != mid2) {
        std::sort(rel + l1 + 1, rel + mid1 + 1);
        std::sort(Q + mid2 + 1, Q + r2 + 1);
        
        for (int i = mid2 + 1, j = l1 + 1; i <= r2; i++) {
            while (j <= mid1 && rel[j].pre < Q[i].l)
                T.add(rel[j].pos, rel[j].val), j++;
            
            Q[i].ans += T.query(Q[i].r) - T.query(Q[i].l - 1);
        }
        
        for (int i = l1 + 1; i <= mid1; i++)
            T.to_zero(rel[i].pos);
    }
}

int main(void) {
    qwq >> n >> m;
    
    for (int i = 1; i <= n; i++) {
        qwq >> a[i];
        Hash[a[i]] = ++cnt; // 用哈希表进行离散化
        //因为色块不需要区分大小,所以可以不排序
    }
    
    for (int i = 1; i <= m; i++) {
        qwq >> q[i].op >> q[i].l >> q[i].r;
        
        if (q[i].op == 1) {
            qwq >> q[i].x;
            Hash[q[i].x] = ++cnt;
        }
    }
    
    init_pre(); // 预处理 pre 数组
    odt.init_colour(); // 用珂朵莉树得到序列颜色均摊
    
    for (int i = 1; i <= m; i++) //进行操作 + 建立时间轴
        if (q[i].op == 1)
            odt.assign(q[i].l, q[i].r, q[i].x);
    else
        Q[++qtop] = {++dfn, q[i].l, q[i].r, 0};
    
    std::sort(Q + 1, Q + qtop + 1);
    
    for (int i = 1; i <= n; i++) // 维护一个数组用来存颜色
        bel[i] = i;
    
    std::sort(bel + 1, bel + n + 1, cmp_pre);
    
    for (int i = 1, j = 1; i <= qtop; i++) { //处理所有颜色段
        while (j <= n && pre[bel[j]] < Q[i].l)
            T.add(bel[j], 1), j++;
        
        Q[i].ans += T.query(Q[i].r) - T.query(Q[i].l - 1);
    }
    
    T.clear();
    std::sort(Q + 1, Q + qtop + 1, cmp_dfn);
    CDQ_Divide(0, rtop, 0, qtop, 0, dfn);
    std::sort(Q + 1, Q + qtop + 1, cmp_dfn); 
    // CDQ 分治时会按照大小排序,而输出要按照时间轴重新排序
    
    for (int i = 1; i <= qtop; i++)
        qwq << Q[i].ans << '\n';
    
    return 0;
}
posted @ 2022-09-08 20:13  秋泉こあい  阅读(34)  评论(0编辑  收藏  举报