【题解】 「APIO2019」桥梁 操作分块+带权并查集 LOJ3145
Legend
Link \(\textrm{to LOJ}\)。
有一个带权无向图,\(n\) 个点 \(m\) 条边。请支持以下操作:
- 修改一条边的边权。
- 询问从点 \(x\) 开始只经过边权 \(\ge w\) 的边可以到达多少个点。
\(1 \le n \le 5\times 10^4\),\(0 \le m \le 10^5\),\(1 \le q \le 10^5\)。所有权值都 \(\le 10^9\)。
时空 \(\rm{2s/512MB}\)
Editorial
如果只有操作 \(2\) 并离线,那就是带权并查集的裸题了。
按照 \(w\) 从大到小询问并加入边就可以完成询问。
非常不幸的是有了修改边权的操作,而这个做法不能支持边权修改。
但你发现所有的数据范围都很小且离线,考虑一个分块的套路:操作分块。
具体来说就是把操作分成 \(S\) 块,每一块大小是 \(\frac{m}{S}\)。
对于每一个块,如果没有修改,我们就可以按照最开始说的“没有修改怎么做”的方法来做。
一次询问会导致若干次修改,如果有修改,我们就要稍微注意一点:如果遇到了被修改的边,就先不把它加入。
而是把所有被修改的边积攒起来,一起处理。
扫一遍时间小于当前询问的块内的修改,把边的权值改掉,然后把修改后依然合法的边加进去。
如果一条边没有被修改那就是因为修改在当前时间之后,直接以原本的权值加入就行了。
加入了所有的边之后就可以用并查集算连通块大小了。
以及询问完后要把修改后的边集体删除,需要支持修改的并查集。
这一部分的复杂度就是 \(O(\frac{m}{S}\log n)\)。
那么对于一条边最多被加进 \(O(S)\) 个块,复杂度是 \(O(Sm \log n)\)。
对于块内,被修改的边最多有 \(O(\frac{m}{S})\) 个,所以所有块的修改并撤回复杂度是 \(O(\frac{m^2}{S}\log n)\)。
总复杂度即 \(O(Sm \log n + \frac{S^2}{m}\log n)\)。
\(S= \sqrt{m}\) 时最优,复杂度为 \(O(m\sqrt{m} \log n)\)。
Code
撤回并查集的时候用了一点小技巧。
#include <bits/stdc++.h>
const int MX = 1e5 + 23;
int n ,m;
int read(){
char k = getchar(); int x = 0;
while(k < '0' || k > '9') k = getchar();
while(k >= '0' && k <= '9') x = x * 10 + k - '0' ,k = getchar();
return x;
}
struct EDGE{
int u ,v ,w ,id;
bool operator <(const EDGE& B)const{
return w > B.w;
}
}e[MX] ,UPD[MX];
struct OP{
int t ,a ,b ,id;
bool operator <(const OP& B)const{
return b > B.b;
}
}o[MX];
int fa[MX] ,sz[MX] ,change[MX];
void init(){
for(int i = 1 ; i < MX ; ++i) fa[i] = i ,sz[i] = 1;
}
int find(int x){return fa[x] == x ? x : find(fa[x]);}
std::vector<int*> withdraw;
std::vector<int> val;
void link(int x ,int y ,int ok = true){
x = find(x) ,y = find(y);
if(x == y) return;
if(sz[x] < sz[y]) std::swap(x ,y);
if(ok){
withdraw.push_back(&fa[y]);
val.push_back(fa[y]);
withdraw.push_back(&sz[x]);
val.push_back(sz[x]);
}
fa[y] = x ,sz[x] += sz[y];
}
int ques[MX];
int main(){
int n = read() ,m = read();
for(int i = 1 ,u ,v ,w ; i <= m ; ++i){
u = read() ,v = read() ,w = read();
e[i] = (EDGE){u ,v ,w ,i};
}
int q = read();
for(int i = 1 ,t ,a ,b ; i <= q ; ++i){
t = read() ,a = read() ,b = read();
o[i] = (OP){t ,a ,b ,i};
}
const int SIZE = 500;
for(int i = 1 ; i <= q ; i += SIZE){
int upper = std::min(q ,i + SIZE - 1);
int qcnt = 0;
for(int j = 1 ; j <= m ; ++j){
UPD[j] = e[j];
}
std::sort(UPD + 1 ,UPD + 1 + m);
int ucnt = 0;
OP upd[SIZE + 2];
OP ask[SIZE + 2];
for(int j = i ; j <= upper ; ++j){
if(o[j].t == 2){
ask[++qcnt] = o[j];
}
else{
change[o[j].a] = true;
upd[++ucnt] = o[j];
}
}
std::sort(ask + 1 ,ask + 1 + qcnt);
init();
int bri = 1;
for(int j = 1 ; j <= qcnt ; ++j){
// 枚举每一个询问
while(bri <= m && UPD[bri].w >= ask[j].b){
// 放入当前可以放的桥梁
if(change[UPD[bri].id]){
// 如果这个桥梁在当前块内有修改
// 我们最后再来单独讨论有修改的桥
;
}else{
// 将加入这条边
link(UPD[bri].u ,UPD[bri].v ,0);
}
++bri;
}
for(int k = ucnt ; k >= 1 ; --k){
if(change[upd[k].a] && upd[k].id < ask[j].id){
int w = upd[k].b ,eid = upd[k].a;
if(w >= ask[j].b) link(e[eid].u ,e[eid].v);
change[upd[k].a] = false;
}
}
for(int k = ucnt ; k >= 1 ; --k){
if(change[upd[k].a]){
int eid = upd[k].a ,w = e[eid].w;
if(w >= ask[j].b) link(e[eid].u ,e[eid].v);
change[upd[k].a] = false;
}
}
ques[ask[j].id] = sz[find(ask[j].a)];
for(int k = 1 ; k <= ucnt ; ++k) change[upd[k].a] = true;
for(int k = val.size() - 1 ; ~k ; --k){
*withdraw[k] = val[k];
}
withdraw.clear();
val.clear();
}
for(int j = i ; j <= upper ; ++j){
if(o[j].t == 1){
e[o[j].a].w = o[j].b;
change[o[j].a] = false;
}
}
}
for(int i = 1 ; i <= q ; ++i)
if(o[i].t == 2) printf("%d\n" ,ques[i]);
return 0;
}