【题解】 「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;
}
posted @ 2020-10-04 18:48  Imakf  阅读(377)  评论(0编辑  收藏  举报