CDQ 分治
引子¶
什么是 cdq 分治呢?,其实他是一种思想而不是具体的算法(就和 dp 是一样的),因此 cdq 分治涵盖的范围相当的广泛,由于这样的思路最早是被陈丹琦引入国内的,所以就叫 cdq 分治了
现在 oi 界对于 cdq 分治这个思想的拓展十分广泛,但是这些都叫 cdq 的东西其实原理和写法上并不相同不过我们可以大概的将它们分为三类
1.cdq 分治解决和点对有关的问题
2.cdq 分治优化 1D/1D 动态规划的转移
3. 通过 cdq 分治,将一些动态问题转化为静态问题
CDQ 分治解决和点对有关的问题¶
这类问题一般是给你一个长度为 n 的序列,然后让你统计有一些特性的点对
那么 cdq 分治基于这样一个算法流程解决这类问题
1. 找到这个序列的中点
2. 将所有点对
第一种是
第二种是
第三种是
3. 将
会发现第一类点对和第三类点对都在这两个序列之中,递归的去解决这两类点对
4. 想方设法处理一下第二类点对的信息
实际应用的时候我们通常都是写一个函数
所以刚才的算法流程中的递归部分我们就是通过
所以说 cdq 分治只是一种十分模糊的思想,可以看到这种思想就是不断的把点对通过递归的方式分给左右两个区间
至于我们设计出来的算法真正干活的部分就是第 4 部分需要我们想方设法解决的部分了
所以说让我们上几道例题看一下第四部分一般该怎么写
比如说我们来一个 cdq 分治的经典问题——三维偏序
三维偏序¶
给定一个序列,每个点有两个属性
统计序列里点对的个数?我们给他套个 cdq 试试。
好了假设我们现在正在
那么我们现在就是统计满足
然后你会发现那个
你又发现现在还剩下两个限制条件
为了方便枚举,我们把
之后我们依次枚举每一个
此时只要对这个神奇数据结构询问一发:这个数据结构里有多少个点的
问题来了那个神奇数据结构叫什么呢?
树状数组啊
当我们插入一个
问题又来了,对于每一个
还记得你把所有的
所以通过这样一个算法流程我们就用
这样的话我们的算法复杂度就是
例题[CQOI2011]动态逆序对¶
仔细推一下就是和三维偏序差不多的式子了,基本就是一个三维偏序的板子
#include <algorithm>
#include <cstdio>
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 维的,转移是
那么比如说我们要优化这样的一个
如果你足够熟练的话可以看出这就是一个二维最长上升子序列的
解释一下上面的式子就是说只有
直接转移显然是
这个转移过程相对来讲比较套路,我们先介绍算法流程然后再慢慢证明为什么这样是对的
我们发现
具体来讲我们这样写 cdq, 假设我们现在正在处理的区间是
0. 如果
1. 先递归的
2. 处理所有
3. 然后递归的
那么第二步怎么做呢?
其实和 cdq 分治求三维偏序差不多,我们会发现处理
你会发现此时的 cdq 写法和上一种处理点对间关系的 cdq 写法最大的不同就是处理
因为 dp 的转移是 有序的,我们的 dp 的转移必须满足两个条件否则就是不对的
1. 用来计算
2. 用来计算
上述两个条件可能在
那就让我们看一看 cdq 分治的递归树好了
然后你会发现我们执行刚才的算法流程的话
你会发现比如说
又比如说
仔细观察就会发现一个 i 点的 dp 值被更新了 log 次,而且,更新它的区间刚好是
因此我们的第 2 个条件就满足了,我们的确保证了所有合法的
我们接着分析我们算法的执行流程
第一个结束的函数是
第一个执行转移过程的函数是
第二个结束的函数
接下来
下一个执行转移流程的函数是
接下来结束的函数是
接下来执行的转移是
因此
接下来
接下来
接下来
通过我们刚才手玩了半个函数流程我们会发现一个令人惊讶的事实就是每次
在刚才的过程我们发现,如果将 cdq 分治的递归树看成一颗线段树,那么 cdq 分治就是这个线段树的 中序遍历函数,因此我们相当于按顺序处理了所有的 dp 值,只是转移顺序被拆开了而已,所以我们的算法是正确的
例题[SDOI2011]拦截导弹¶
一道二维最长上升子序列的题,为了确定某一个元素是否在最长上升子序列中可以正反跑两遍 CDQ
#include <algorithm>
#include <cstdio>
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 询问的情况
然后你会发现一个有趣的事实是如果我们把询问进行离线之后,所有操作按照时间自然的排成了一个序列,另一个比较显然的事实是每一个修改会对它之后的询问发生关系,而这样的修改 - 询问关系一共会有
因此我们可以使用 cdq 分治对于这个操作序列进行分治,按照 cdq 分治处理修改和询问之间的关系
还是和处理点对关系的 cdq 分治类似,我们假设我们正在分治的序列是
接下来我们处理所有
注意如果我们的各个修改之间是 独立 的话我们不需要管处理
但是如果你的各个修改之间并不独立,比如说我们的修改是一个赋值操作,这样的话我们做完这个赋值操作之后序列长什么样可能需要依赖于之前的序列长什么样
那这样的话我们处理所有跨越 mid 的修改 - 询问关系的时候就必须把它放在
这样光说是没办法解决我们的问题的,因此我们还是上道例题吧
矩形加矩形求和¶
这里的矩形加矩形求和就是字面意思上的矩形加矩形求和,让你维护一个二维平面,然后支持在一个矩形区域内加一个数字,每次询问一个矩形区域的和
那么对于这个问题的静态版本,也就是二维平面里有一堆矩形,我们希望询问一个矩形区域的和这个问题,我们是有一个经典做法叫线段树 + 扫描线的
具体来讲就是我们将每个矩形拆成插入和删除两个操作,将每个询问拆成两个前缀和相减的形式然后离线跑一波就可以了
问题来了啊,我们现在的问题是动态的啊,怎么办呢?
不如强行套一个 cdq 分治试试?
我们将所有的询问和修改操作全部离线,这些操作形成了一个序列,并且有
那么我们依然使用 cdq 分治的一般套路,将所有的关系分成三类,在这一层分治过程当中仅仅处理跨越
那么这样的话我们会发现这样的一个事实就是所有的修改都在询问之前被做出了
这个问题就等价于平面上有静态的一堆矩形接下来不停的询问一个矩形区域的和了
那么我们可以套一个扫描线在
剩下的事情就是递归的分治左右两侧修改 - 询问关系来解决这个问题了
这样实现的 cdq 分治的话你会发现同一个询问被处理了
时间复杂度为
观察上述的算法流程,我们发现一开始我们只能解决静态的矩形加矩形求和问题,但是只是简单的套了一个 cdq 分治上去我们就可以离线的解决一个动态的矩形加矩形求和问题了。
那么我们可以将动态问题转化为静态问题的精髓就在于 cdq 分治每次仅仅处理跨越某一个点的修改和询问关系了,这样的话我们就只需要考虑所有询问都在修改之后这个简单的问题了。
也正是因为这一点 cdq 分治被称为 动态问题转化为静态问题的工具
[Ynoi2016]镜中的昆虫¶
一句话题意区间赋值区间数颜色
我们维护一下每个位置左侧第一个同色点的位置,记为
通过将连续的一段颜色看成一个点的方式我们可以证明
#include <algorithm>
#include <cstdio>
#include <map>
#include <set>
#define SNI set<nod>::iterator
#define SDI set<data>::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<int, int> 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<int, int>::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<data> s;
struct nod {
int l;
int r;
friend bool operator<(nod a, nod b) { return a.r < b.r; }
};
set<nod> c[2 * N];
set<int> 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<data>::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<int>::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 是考虑线段树左右儿子之间的联系
而对于这道题来讲我们需要考虑的是父亲和孩子之间的关系
换句话来讲,我们在
那么具体来讲如何设计算法呢?
假设我们正在构造
我们将在
对于一条边来讲,如果他没有出现在了所有被修改的边权都被赋成了
对于一条边来讲,如果它出现在了所有被修改的边权都被赋成了
如此这般我们就将
那么为什么我们的复杂度是对的呢?
首先被修改的边一定会加入到我们的边集当中去,这些边的数目是
接下来我们需要证明的是边集当中不会有过多的未被修改的边
注意到我们只会加入所有边权取
接下来我们只需证明每递归一层图的点数是
证明点数是
所以我们就证明了每次我们用来跑 kruskal 的图都是
从而每一层的时间复杂度都是
因此我们的时间复杂度就是
代码实现上可能会有一些难度,需要注意的是并查集不能使用路径压缩,否则就不支持回退操作了,执行缩点操作的时候也没有必要真的执行,而是每一层的 kruskal 都在上一层的并查集里直接做就可以了
#include <algorithm>
#include <cstdio>
#include <stack>
#include <vector>
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<opt> 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<edge> ve[30];
vector<moved> vq;
vector<edge> 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; // 拜拜程序~
}
buildLast update and/or translate time of this article,Check the history
editFound smelly bugs? Translation outdated? Wanna contribute with us? Edit this Page on Github
peopleContributor of this article OI-wiki
translateTranslator of this article Visit the original article!
copyrightThe article is available under CC BY-SA 4.0 & SATA ; additional terms may apply.