WIKIOI
wiki(I:)
比赛相关
工具软件
语言基础
算法基础
搜索
动态规划
字符串
数学
数据结构
图论
计算几何
杂项
专题
杂项简介
复杂度
离散化
离线算法简介
CDQ 分治
整体二分
莫队算法简介
普通莫队算法
带修改莫队
树上莫队
回滚莫队
莫队配合bitset
分数规划
随机函数
随机化技巧
爬山算法
模拟退火
悬线法
计算理论基础
字节顺序
约瑟夫问题
Stern-Brocot 树与 Farey 序列
格雷码
表达式求值
在一台机器上规划任务
主元素问题
26 objects
本站非官方,所收集资源均来源于网络。
CDQ 分治 - 杂项
## 引子 什么是 cdq 分治呢?,其实他是一种思想而不是具体的算法(就和 dp 是一样的),因此 cdq 分治涵盖的范围相当的广泛,由于这样的思路最早是被陈丹琦引入国内的,所以就叫 cdq 分治了 现在 oi 界对于 cdq 分治这个思想的拓展十分广泛,但是这些都叫 cdq 的东西其实原理和写法上并不相同不过我们可以大概的将它们分为三类 **1.cdq 分治解决和点对有关的问题** **2.cdq 分治优化 1D/1D 动态规划的转移** **3. 通过 cdq 分治,将一些动态问题转化为静态问题** * * * ## CDQ 分治解决和点对有关的问题 这类问题一般是给你一个长度为 n 的序列,然后让你统计有一些特性的点对 $(i,j)$ 有多少个,又或者说是找到一对点 $(i,j)$ 使得一些函数的值最大之类的问题 那么 cdq 分治基于这样一个算法流程解决这类问题 **1. 找到这个序列的中点 $mid$ ** **2. 将所有点对 $(i,j)$ 划分为 3 类** **第一种是 $1 \leq i \leq mid,1 \leq j \leq mid$ 的点对** **第二种是 $1 \leq i \leq mid ,mid+1 \leq j \leq n$ 的点对** **第三种是 $mid+1 \leq i \leq n,mid+1 \leq j \leq n$ 的点对** **3. 将 $(1,n)$ 这个序列拆成两个序列 $(1,mid)$ 和 $(mid+1,n)$ ** **会发现第一类点对和第三类点对都在这两个序列之中,递归的去解决这两类点对** **4. 想方设法处理一下第二类点对的信息** *实际应用的时候我们通常都是写一个函数 $solve(l,r)$ 表示我们正在处理 $l \leq i \leq r,l \leq j \leq r$ 的点对* *所以刚才的算法流程中的递归部分我们就是通过 $solve(l,mid),solve(mid,r)$ 来实现的* 所以说 cdq 分治只是一种十分模糊的思想,可以看到这种思想就是不断的把点对通过递归的方式分给左右两个区间 至于我们设计出来的算法真正干活的部分就是第 4 部分需要我们想方设法解决的部分了 所以说让我们上几道例题看一下第四部分一般该怎么写 比如说我们来一个 cdq 分治的经典问题——三维偏序 ### 三维偏序 给定一个序列,每个点有两个属性 $(a,b)$ ,试求:这个序列里有多少对点对 $(i,j)$ 满足 $i
#include
using namespace std; typedef long long ll; int n; int m; struct treearray { int ta[200010]; inline void ub(int& x) { x += x & (-x); } inline void db(int& x) { x -= x & (-x); } inline void c(int x, int t) { for (; x <= n + 1; ub(x)) ta[x] += t; } inline int sum(int x) { int r = 0; for (; x > 0; db(x)) r += ta[x]; return r; } } ta; struct data { int val; int del; int ans; } a[100010]; int rv[100010]; ll res; bool cmp1(const data& a, const data& b) { return a.val < b.val; } bool cmp2(const data& a, const data& b) { return a.del < b.del; } void solve(int l, int r) { if (r - l == 1) { return; } int mid = (l + r) / 2; solve(l, mid); solve(mid, r); int i = l + 1; int j = mid + 1; while (i <= mid) { while (a[i].val > a[j].val && j <= r) { ta.c(a[j].del, 1); j++; } a[i].ans += ta.sum(m + 1) - ta.sum(a[i].del); i++; } i = l + 1; j = mid + 1; while (i <= mid) { while (a[i].val > a[j].val && j <= r) { ta.c(a[j].del, -1); j++; } i++; } i = mid; j = r; while (j > mid) { while (a[j].val < a[i].val && i > l) { ta.c(a[i].del, 1); i--; } a[j].ans += ta.sum(m + 1) - ta.sum(a[j].del); j--; } i = mid; j = r; while (j > mid) { while (a[j].val < a[i].val && i > l) { ta.c(a[i].del, -1); i--; } j--; } sort(a + l + 1, a + r + 1, cmp1); return; } int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { scanf("%d", &a[i].val); rv[a[i].val] = i; } for (int i = 1; i <= m; i++) { int p; scanf("%d", &p); a[rv[p]].del = i; } for (int i = 1; i <= n; i++) { if (a[i].del == 0) a[i].del = m + 1; } for (int i = 1; i <= n; i++) { res += ta.sum(n + 1) - ta.sum(a[i].val); ta.c(a[i].val, 1); } for (int i = 1; i <= n; i++) { ta.c(a[i].val, -1); } solve(0, n); sort(a + 1, a + n + 1, cmp2); for (int i = 1; i <= m; i++) { printf("%lld\n", res); res -= a[i].ans; } return 0; } ``` * * * ## CDQ 分治优化 1D/1D 动态规划的转移 所谓 1D/1D 动态规划就是说我们的 dp 数组是 1 维的,转移是 $O(n)$ 的一类 dp 问题,如果条件良好的话我们有些时候可以通过 cdq 分治来把这类问题的时间复杂度由 $O(n^2)$ 降至 $O(n\log^2n)$ 那么比如说我们要优化这样的一个 $dp$ 式子给你一个序列每个元素有两个属性 $a,b$ 我们希望计算一个 dp 式子的值,它的转移方程如下: $dp_{i}=1+ \max_{j=1}^{i-1}dp_{j}[a_{j}
#include
using namespace std; typedef double db; const int N = 1e6 + 10; struct data { int h; int v; int p; int ma; db ca; } a[2][N]; int n; bool tr; inline bool cmp1(const data& a, const data& b) { if (tr) return a.h > b.h; else return a.h < b.h; } inline bool cmp2(const data& a, const data& b) { if (tr) return a.v > b.v; else return a.v < b.v; } inline bool cmp3(const data& a, const data& b) { if (tr) return a.p < b.p; else return a.p > b.p; } inline bool cmp4(const data& a, const data& b) { return a.v == b.v; } struct treearray { int ma[2 * N]; db ca[2 * N]; inline void c(int x, int t, db c) { for (; x <= n; x += x & (-x)) { if (ma[x] == t) { ca[x] += c; } else if (ma[x] < t) { ca[x] = c; ma[x] = t; } } } inline void d(int x) { for (; x <= n; x += x & (-x)) { ma[x] = 0; ca[x] = 0; } } inline void q(int x, int& m, db& c) { for (; x > 0; x -= x & (-x)) { if (ma[x] == m) { c += ca[x]; } else if (m < ma[x]) { c = ca[x]; m = ma[x]; } } } } ta; int rk[2][N]; inline void solve(int l, int r, int t) { if (r - l == 1) { return; } int mid = (l + r) / 2; solve(l, mid, t); sort(a[t] + mid + 1, a[t] + r + 1, cmp1); int p = l + 1; for (int i = mid + 1; i <= r; i++) { for (; (cmp1(a[t][p], a[t][i]) || a[t][p].h == a[t][i].h) && p <= mid; p++) { ta.c(a[t][p].v, a[t][p].ma, a[t][p].ca); } db c = 0; int m = 0; ta.q(a[t][i].v, m, c); if (a[t][i].ma < m + 1) { a[t][i].ma = m + 1; a[t][i].ca = c; } else if (a[t][i].ma == m + 1) { a[t][i].ca += c; } } for (int i = l + 1; i <= mid; i++) { ta.d(a[t][i].v); } sort(a[t] + mid, a[t] + r + 1, cmp3); solve(mid, r, t); sort(a[t] + l + 1, a[t] + r + 1, cmp1); } inline void ih(int t) { sort(a[t] + 1, a[t] + n + 1, cmp2); rk[t][1] = 1; for (int i = 2; i <= n; i++) { rk[t][i] = (cmp4(a[t][i], a[t][i - 1])) ? rk[t][i - 1] : i; } for (int i = 1; i <= n; i++) { a[t][i].v = rk[t][i]; } sort(a[t] + 1, a[t] + n + 1, cmp3); for (int i = 1; i <= n; i++) { a[t][i].ma = 1; a[t][i].ca = 1; } } int len; db ans; int main() { scanf("%d", &n); for (int i = 1; i <= n; i++) { scanf("%d%d", &a[0][i].h, &a[0][i].v); a[0][i].p = i; a[1][i].h = a[0][i].h; a[1][i].v = a[0][i].v; a[1][i].p = i; } ih(0); solve(0, n, 0); tr = 1; ih(1); solve(0, n, 1); tr = 1; sort(a[0] + 1, a[0] + n + 1, cmp3); sort(a[1] + 1, a[1] + n + 1, cmp3); for (int i = 1; i <= n; i++) { len = max(len, a[0][i].ma); } printf("%d\n", len); for (int i = 1; i <= n; i++) { if (a[0][i].ma == len) { ans += a[0][i].ca; } } for (int i = 1; i <= n; i++) { if (a[0][i].ma + a[1][i].ma - 1 == len) { printf("%.5lf ", (a[0][i].ca * a[1][i].ca) / ans); } else { printf("0.00000 "); } } return 0; } ``` * * * ## 需要 CDQ 将动态问题转化为静态问题的题 我们会发现 CDQ 分治一般是一种处理序列问题的套路,通过将序列折半之后递归处理点对间的关系来获得良好的复杂度 不过在这一部分当中我们分治的却不是一般的序列而是时间序列 什么意思呢? 众所周知的是有些数据结构题需要我们支持做 xxx 修改然后做 xxx 询问的情况 然后你会发现一个有趣的事实是如果我们把询问进行离线之后,所有操作按照时间自然的排成了一个序列,另一个比较显然的事实是每一个修改会对它之后的询问发生关系,而这样的修改 - 询问关系一共会有 $O(n^2)$ 对 因此我们可以使用 cdq 分治对于这个操作序列进行分治,按照 cdq 分治处理修改和询问之间的关系 还是和处理点对关系的 cdq 分治类似,我们假设我们正在分治的序列是 $(l,r)$ , 我们先递归的处理 $(l,mid)$ 和 $(mid,r)$ 之间的修改 - 询问关系 接下来我们处理所有 $l \leq i \leq mid,mid+1 \leq j \leq r$ 并且 $i$ 是一个修改并且 $j$ 是一个询问的修改 - 询问关系 注意如果我们的各个修改之间是 **独立** 的话我们不需要管处理 $l \leq i \leq mid,mid+1 \leq j \leq r$ 和 $solve(l,mid)$ 以及 $solve(mid+1,r)$ 之间时序关系(比如你的修改就是普通的加法和减法问题之类的) 但是如果你的各个修改之间并不独立,比如说我们的修改是一个赋值操作,这样的话我们做完这个赋值操作之后序列长什么样可能需要依赖于之前的序列长什么样 那这样的话我们处理所有跨越 mid 的修改 - 询问关系的时候就必须把它放在 $solve(l,mid)$ 和 $solve(mid+1,r)$ 之间了,理由和 cdq 分治优化 1D/1D 动态规划的原因是一样的,按照中序遍历序进行分治,然后我们就可以保证每一个修改都是严格按照时间顺序被执行的 这样光说是没办法解决我们的问题的,因此我们还是上道例题吧 ### 矩形加矩形求和 这里的矩形加矩形求和就是字面意思上的矩形加矩形求和,让你维护一个二维平面,然后支持在一个矩形区域内加一个数字,每次询问一个矩形区域的和 那么对于这个问题的静态版本,也就是二维平面里有一堆矩形,我们希望询问一个矩形区域的和这个问题,我们是有一个经典做法叫线段树 + 扫描线的 具体来讲就是我们将每个矩形拆成插入和删除两个操作,将每个询问拆成两个前缀和相减的形式然后离线跑一波就可以了 问题来了啊,我们现在的问题是动态的啊,怎么办呢? 不如强行套一个 cdq 分治试试? 我们将所有的询问和修改操作全部离线,这些操作形成了一个序列,并且有 $O(N^2)$ 对修改 - 询问的关系 那么我们依然使用 cdq 分治的一般套路,将所有的关系分成三类,在这一层分治过程当中仅仅处理跨越 $mid$ ,的修改 - 询问关系,而剩下的修改 - 询问关系通过递归的的方式来解决 那么这样的话我们会发现这样的一个事实就是所有的修改都在询问之前被做出了 这个问题就等价于平面上有静态的一堆矩形接下来不停的询问一个矩形区域的和了 那么我们可以套一个扫描线在 $O(n\log n)$ 的时间内处理好所有跨越 $mid$ 的修改 - 询问关系 剩下的事情就是递归的分治左右两侧修改 - 询问关系来解决这个问题了 这样实现的 cdq 分治的话你会发现同一个询问被处理了 $O(\log n)$ 次来回答,不过没有关系因为每次贡献这个询问的修改是互不相交的 时间复杂度为 $T(n)=T(\lfloor \frac{n}{2} \rfloor)+T(\lceil \frac{n}{2} \rceil)+ O(n\log n)=O(n\log^2n)$ 观察上述的算法流程,我们发现一开始我们只能解决静态的矩形加矩形求和问题,但是只是简单的套了一个 cdq 分治上去我们就可以离线的解决一个动态的矩形加矩形求和问题了。 那么我们可以将动态问题转化为静态问题的精髓就在于 cdq 分治每次仅仅处理跨越某一个点的修改和询问关系了,这样的话我们就只需要考虑所有询问都在修改之后这个简单的问题了。 也正是因为这一点 cdq 分治被称为 **动态问题转化为静态问题的工具** ### [Ynoi2016]镜中的昆虫 一句话题意区间赋值区间数颜色 我们维护一下每个位置左侧第一个同色点的位置,记为 $pre_{i}$ ,此时区间数颜色就被转化为了一个经典的二维数点问题 通过将连续的一段颜色看成一个点的方式我们可以证明 $pre$ 的变化量是 $O(n+m)$ 的,换句话说单次操作仅仅引起 $O(1)$ 的 $pre$ 值变化,那么我们可以用 cdq 分治来解决动态的单点加矩形求和问题 $pre$ 数组的具体变化可以使用 $std::set$ 来进行处理(这个用 set 维护连续的区间的技巧也被称之为*old driver tree*) ```cpp #include
#include
#include
#include
#define SNI set
::iterator #define SDI set
::iterator using namespace std; const int N = 1e5 + 10; int n; int m; int pre[N]; int npre[N]; int a[N]; int tp[N]; int lf[N]; int rt[N]; int co[N]; struct modi { int t; int pos; int pre; int va; friend bool operator<(modi a, modi b) { return a.pre < b.pre; } } md[10 * N]; int tp1; struct qry { int t; int l; int r; int ans; friend bool operator<(qry a, qry b) { return a.l < b.l; } } qr[N]; int tp2; int cnt; inline bool cmp(const qry& a, const qry& b) { return a.t < b.t; } inline void modify(int pos, int co) // 修改函数 { if (npre[pos] == co) return; md[++tp1] = (modi){++cnt, pos, npre[pos], -1}; md[++tp1] = (modi){++cnt, pos, npre[pos] = co, 1}; } namespace prew { int lst[2 * N]; map
mp; // 提前离散化 inline void prew() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) scanf("%d", &a[i]), mp[a[i]] = 1; for (int i = 1; i <= m; i++) { scanf("%d%d%d", &tp[i], &lf[i], &rt[i]); if (tp[i] == 1) scanf("%d", &co[i]), mp[co[i]] = 1; } map
::iterator it, it1; for (it = mp.begin(), it1 = it, ++it1; it1 != mp.end(); ++it, ++it1) it1->second += it->second; for (int i = 1; i <= n; i++) a[i] = mp[a[i]]; for (int i = 1; i <= n; i++) if (tp[i] == 1) co[i] = mp[co[i]]; for (int i = 1; i <= n; i++) pre[i] = lst[a[i]], lst[a[i]] = i; for (int i = 1; i <= n; i++) npre[i] = pre[i]; } } // namespace prew namespace colist { struct data { int l; int r; int x; friend bool operator<(data a, data b) { return a.r < b.r; } }; set
s; struct nod { int l; int r; friend bool operator<(nod a, nod b) { return a.r < b.r; } }; set
c[2 * N]; set
bd; inline void split(int mid) { // 将一个节点拆成两个节点 SDI it = s.lower_bound((data){0, mid, 0}); data p = *it; if (mid == p.r) return; s.erase(p); s.insert((data){p.l, mid, p.x}); s.insert((data){mid + 1, p.r, p.x}); c[p.x].erase((nod){p.l, p.r}); c[p.x].insert((nod){p.l, mid}); c[p.x].insert((nod){mid + 1, p.r}); } inline void del(set
::iterator it) { // 删除一个迭代器 bd.insert(it->l); SNI it1, it2; it1 = it2 = c[it->x].find((nod){it->l, it->r}); ++it2; if (it2 != c[it->x].end()) bd.insert(it2->l); c[it->x].erase(it1); s.erase(it); } inline void ins(data p) { // 插入一个节点 s.insert(p); SNI it = c[p.x].insert((nod){p.l, p.r}).first; ++it; if (it != c[p.x].end()) { bd.insert(it->l); } } inline void stv(int l, int r, int x) { // 区间赋值 if (l != 1) split(l - 1); split(r); int p = l; // split两下之后删掉所有区间 while (p != r + 1) { SDI it = s.lower_bound((data){0, p, 0}); p = it->r + 1; del(it); } ins((data){l, r, x}); // 扫一遍set处理所有变化的pre值 for (set
::iterator it = bd.begin(); it != bd.end(); ++it) { SDI it1 = s.lower_bound((data){0, *it, 0}); if (*it != it1->l) modify(*it, *it - 1); else { SNI it2 = c[it1->x].lower_bound((nod){0, *it}); if (it2 != c[it1->x].begin()) --it2, modify(*it, it2->r); else modify(*it, 0); } } bd.clear(); } inline void ih() { int nc = a[1]; int ccnt = 1; // 将连续的一段插入到set中 for (int i = 2; i <= n; i++) if (nc != a[i]) { s.insert((data){i - ccnt, i - 1, nc}), c[nc].insert((nod){i - ccnt, i - 1}); nc = a[i]; ccnt = 1; } else { ccnt++; } s.insert((data){n - ccnt + 1, n, a[n]}), c[a[n]].insert((nod){n - ccnt + 1, n}); } } // namespace colist namespace cdq { struct treearray // 树状数组 { int ta[N]; inline void c(int x, int t) { for (; x <= n; x += x & (-x)) ta[x] += t; } inline void d(int x) { for (; x <= n; x += x & (-x)) ta[x] = 0; } inline int q(int x) { int r = 0; for (; x; x -= x & (-x)) r += ta[x]; return r; } inline void clear() { for (int i = 1; i <= n; i++) ta[i] = 0; } } ta; int srt[N]; inline bool cmp1(const int& a, const int& b) { return pre[a] < pre[b]; } inline void solve(int l1, int r1, int l2, int r2, int L, int R) { // cdq if (l1 == r1 || l2 == r2) return; int mid = (L + R) / 2; int mid1 = l1; while (mid1 != r1 && md[mid1 + 1].t <= mid) mid1++; int mid2 = l2; while (mid2 != r2 && qr[mid2 + 1].t <= mid) mid2++; solve(l1, mid1, l2, mid2, L, mid); solve(mid1, r1, mid2, r2, mid, R); if (l1 != mid1 && mid2 != r2) { sort(md + l1 + 1, md + mid1 + 1); sort(qr + mid2 + 1, qr + r2 + 1); for (int i = mid2 + 1, j = l1 + 1; i <= r2; i++) { // 考虑左侧对右侧贡献 while (j <= mid1 && md[j].pre < qr[i].l) ta.c(md[j].pos, md[j].va), j++; qr[i].ans += ta.q(qr[i].r) - ta.q(qr[i].l - 1); } for (int i = l1 + 1; i <= mid1; i++) ta.d(md[i].pos); } } inline void mainsolve() { colist::ih(); for (int i = 1; i <= m; i++) if (tp[i] == 1) colist::stv(lf[i], rt[i], co[i]); else qr[++tp2] = (qry){++cnt, lf[i], rt[i], 0}; sort(qr + 1, qr + tp2 + 1); for (int i = 1; i <= n; i++) srt[i] = i; sort(srt + 1, srt + n + 1, cmp1); for (int i = 1, j = 1; i <= tp2; i++) { // 初始化一下每个询问的值 while (j <= n && pre[srt[j]] < qr[i].l) ta.c(srt[j], 1), j++; qr[i].ans += ta.q(qr[i].r) - ta.q(qr[i].l - 1); } ta.clear(); sort(qr + 1, qr + tp2 + 1, cmp); solve(0, tp1, 0, tp2, 0, cnt); sort(qr + 1, qr + tp2 + 1, cmp); for (int i = 1; i <= tp2; i++) printf("%d\n", qr[i].ans); } } // namespace cdq int main() { prew::prew(); cdq::mainsolve(); return 0; } // 拜拜程序~ ``` ### [HNOI2010]城市建设 一句话题意:给定一张图支持动态的修改边权,要求在每次修改边权之后输出这张图的最小生成树的最小代价和 事实上有一个线段树分治套 lct 的做法可以解决这个问题,但是这个实现方式常数过大可能需要精妙的卡常技巧才可以通过本题,因此我们不妨考虑 cdq 分治来解决这个问题 和一般的 cdq 分治解决的问题不同,我们此时 cdq 分治的时候并没有修改和询问的关系来让我们进行分治,因为我们是没有办法单独的考虑修改一个边对整张图的最小生成树有什么贡献,因此似乎传统的 cdq 分治思路似乎不是很好使 那么我们通过刚才的例题可以发现一般的 cdq 分治和线段树有着特殊的联系,我们在 cdq 分治的过程中其实隐式的建了一颗线段树出来(因为 cdq 分治的递归树就是一颗线段树) 通常的 cdq 是考虑线段树左右儿子之间的联系 而对于这道题来讲我们需要考虑的是父亲和孩子之间的关系 换句话来讲,我们在 $solve(l,r)$ 这段区间的时候如果我们可以想办法使图的规模变成和区间长度相关的一个变量的话我们就可以解决这个问题了 那么具体来讲如何设计算法呢? 假设我们正在构造 $(l,r)$ 这段区间的最小生成树边集,并且我们已知它父亲最小生成树的边集 我们将在 $(l,r)$ 这段区间中发生变化的边分别将边权赋成 $+ \infty$ 和 $-\infty$ 分别各跑一边 kruskal 求出那些边在最小生成树当中 对于一条边来讲,如果他没有出现在了所有被修改的边权都被赋成了 $+\infty$ 的最小生成树当中证明它不可能出现在 $(l,r)$ 这些询问的最小生成树当中,所以我们仅仅在 $(l,r)$ 的边集中加入最小生成树的树边 对于一条边来讲,如果它出现在了所有被修改的边权都被赋成了 $- \infty$ 的最小生成树当中,就证明它一定会出现 $(l,r)$ 这段的区间的最小生成树当中,这样的话我们就可以使用并查集将这些边对应的点缩起来,并且将答案加上这些边的边权 如此这般我们就将 $(l,r)$ 这段区间的边集构造出来了,用这些边求出来的最小生成树和直接求原图的最小生成树等价 那么为什么我们的复杂度是对的呢? 首先被修改的边一定会加入到我们的边集当中去,这些边的数目是 $O(len)$ 级别的 接下来我们需要证明的是边集当中不会有过多的未被修改的边 注意到我们只会加入所有边权取 $+\infty$ 最小生成树的树边,因此我们加入的边数目是不会超过当前图的点数的 接下来我们只需证明每递归一层图的点数是 $O(len)$ 级别的就可以说明图的边数是 $O(len)$ 级别的了 证明点数是 $O(len)$ 几倍就变的十分简单了,我们每次向下递归的时侯缩掉的边是在 $-\infty$ 生成树中出现的未被修改边,那么反过来想就是我们割掉了出现在 $-\infty$ 生成树当中的所有的被修改边,显然我们最多割掉 $len$ 条边,整张图最多分裂成 $O(len)$ 个连通块,这样的话新图点数就是 $O(len)$ 级别的了 所以我们就证明了每次我们用来跑 kruskal 的图都是 $O(len)$ 级别的了 从而每一层的时间复杂度都是 $O(n\log n)$ 了 因此我们的时间复杂度就是 $T(n)=T(\lfloor \frac{n}{2} \rfloor)+T(\lceil \frac{n}{2} \rceil)+ O(n\log n)=O(n\log^2n)$ 了 代码实现上可能会有一些难度,需要注意的是并查集不能使用路径压缩,否则就不支持回退操作了,执行缩点操作的时候也没有必要真的执行,而是每一层的 kruskal 都在上一层的并查集里直接做就可以了 ```cpp #include
#include
#include
#include
using namespace std; typedef long long ll; int n; int m; int ask; struct bcj { int fa[20010]; int size[20010]; struct opt { int u; int v; }; stack
st; inline void ih() { for (int i = 1; i <= n; i++) fa[i] = i, size[i] = 1; } inline int f(int x) { return (fa[x] == x) ? x : f(fa[x]); } inline void u(int x, int y) { // 带撤回 int u = f(x); int v = f(y); if (u == v) return; if (size[u] < size[v]) swap(u, v); size[u] += size[v]; fa[v] = u; opt o; o.u = u; o.v = v; st.push(o); } inline void undo() { opt o = st.top(); st.pop(); fa[o.v] = o.v; size[o.u] -= size[o.v]; } inline void clear(int tim) { while (st.size() > tim) { undo(); } } } s, s1; struct edge // 静态边 { int u; int v; ll val; int mrk; friend bool operator<(edge a, edge b) { return a.val < b.val; } } e[50010]; struct moved { int u; int v; }; // 动态边 struct query { int num; ll val; ll ans; } q[50010]; bool book[50010]; // 询问 vector
ve[30]; vector
vq; vector
tr; ll res[30]; int tim[30]; inline void pushdown(int dep) // 缩边 { tr.clear(); // 这里要复制一份,以免无法回撤操作 for (int i = 0; i < ve[dep].size(); i++) { tr.push_back(ve[dep][i]); } sort(tr.begin(), tr.end()); for (int i = 0; i < tr.size(); i++) { // 无用边 if (s1.f(tr[i].u) == s1.f(tr[i].v)) { tr[i].mrk = -1; continue; } s1.u(tr[i].u, tr[i].v); } s1.clear(0); res[dep + 1] = res[dep]; for (int i = 0; i < vq.size(); i++) { s1.u(vq[i].u, vq[i].v); } vq.clear(); for (int i = 0; i < tr.size(); i++) { // 必须边 if (tr[i].mrk == -1 || s1.f(tr[i].u) == s1.f(tr[i].v)) continue; tr[i].mrk = 1; s1.u(tr[i].u, tr[i].v); s.u(tr[i].u, tr[i].v); res[dep + 1] += tr[i].val; } s1.clear(0); ve[dep + 1].clear(); for (int i = 0; i < tr.size(); i++) { // 缩边 if (tr[i].mrk != 0) continue; edge p; p.u = s.f(tr[i].u); p.v = s.f(tr[i].v); if (p.u == p.v) continue; p.val = tr[i].val; p.mrk = 0; ve[dep + 1].push_back(p); } return; } inline void solve(int l, int r, int dep) { tim[dep] = s.st.size(); int mid = (l + r) / 2; if (r - l == 1) { // 终止条件 edge p; p.u = s.f(e[q[r].num].u); p.v = s.f(e[q[r].num].v); p.val = q[r].val; e[q[r].num].val = q[r].val; p.mrk = 0; ve[dep].push_back(p); pushdown(dep); q[r].ans = res[dep + 1]; s.clear(tim[dep - 1]); return; } for (int i = l + 1; i <= mid; i++) { book[q[i].num] = true; } for (int i = mid + 1; i <= r; i++) { // 动转静 if (book[q[i].num]) continue; edge p; p.u = s.f(e[q[i].num].u); p.v = s.f(e[q[i].num].v); p.val = e[q[i].num].val; p.mrk = 0; ve[dep].push_back(p); } for (int i = l + 1; i <= mid; i++) { // 询问转动态 moved p; p.u = s.f(e[q[i].num].u); p.v = s.f(e[q[i].num].v); vq.push_back(p); } pushdown(dep); // 下面的是回撤 for (int i = mid + 1; i <= r; i++) { if (book[q[i].num]) continue; ve[dep].pop_back(); } for (int i = l + 1; i <= mid; i++) { book[q[i].num] = false; } solve(l, mid, dep + 1); for (int i = 0; i < ve[dep].size(); i++) { ve[dep][i].mrk = 0; } for (int i = mid + 1; i <= r; i++) { book[q[i].num] = true; } for (int i = l + 1; i <= mid; i++) { // 动转静 if (book[q[i].num]) continue; edge p; p.u = s.f(e[q[i].num].u); p.v = s.f(e[q[i].num].v); p.val = e[q[i].num].val; p.mrk = 0; ve[dep].push_back(p); } for (int i = mid + 1; i <= r; i++) { // 询问转动 book[q[i].num] = false; moved p; p.u = s.f(e[q[i].num].u); p.v = s.f(e[q[i].num].v); vq.push_back(p); } pushdown(dep); solve(mid, r, dep + 1); s.clear(tim[dep - 1]); return; // 时间倒流至上一层 } int main() { scanf("%d%d%d", &n, &m, &ask); s.ih(); s1.ih(); for (int i = 1; i <= m; i++) { scanf("%d%d%lld", &e[i].u, &e[i].v, &e[i].val); } for (int i = 1; i <= ask; i++) { scanf("%d%lld", &q[i].num, &q[i].val); } for (int i = 1; i <= ask; i++) { // 初始动态边 book[q[i].num] = true; moved p; p.u = e[q[i].num].u; p.v = e[q[i].num].v; vq.push_back(p); } for (int i = 1; i <= m; i++) { if (book[i]) continue; ve[1].push_back(e[i]); } // 初始静态 for (int i = 1; i <= ask; i++) { book[q[i].num] = false; } solve(0, ask, 1); for (int i = 1; i <= ask; i++) { printf("%lld\n", q[i].ans); } return 0; // 拜拜程序~ } ```