【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\) 在两个分治区间内的情况分类讨论:
- 若 \(i,j\) 均在左区间,则对左区间分治求解;
- 若 \(i,j\) 均在右区间,则对右区间分治求解;
- 若 \(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;
}