LoginSignup
59
50

【競技プログラミング】online dynamic connectivity(削除可能union-find)の作り方を詳しく解説!!!

Last updated at Posted at 2020-03-06

コンテスト中にこの記事を見る方へ

もし、辺追加を行わないならばクエリを逆順に見る事でunion findで事足りるかもしれません。
また、クエリを先読みできる場合、offline dynamic connectivityで事足りるかもしれません。
offline dynamic connectivity には定数倍の軽いアルゴリズムが存在し、また、辺の削除される時間の最大全域木をパスクエリを行える動的木で管理する事で $\mathcal{O}(\log N)$ にする事も出来ます。

出来ること

  • グラフに辺を追加 $O(\log^2N)$
  • グラフから辺を削除 $O(\log^2N)$
  • グラフの2頂点$s,t$を結ぶpathが存在するか判定 $O(\log N)$

(N:グラフの頂点数)

加えて次のようなクエリにも答えられます

  • $s$と連結している頂点数を取得
  • 連結成分の個数

平衡二分木のようなことも出来ます

  • $s$と連結している頂点に作用素を作用させる
  • $s$と連結している頂点をfold(総和等)した結果を返す

前提知識

平衡二分木(splay木がおすすめ)
基本的に全ての機能を使います

1.Euler Tour Tree

Euler Tour Treeとは

  • 木に根を変更 $O(\log N)$
  • 森に辺を追加 $O(\log N)$
  • 森から辺を削除 $O(\log N)$
  • 森の2頂点$s,t$を結ぶpathが存在するか判定 $O(\log N)$(平衡二分木におけるrootのポインタ比較)
  • 森の指定した条件を満たす一点を取得 $O(\log N)$(平衡二分木における部分木に条件を満たす点があるかを持って探す)
  • 連結成分のサイズ取得$O(\log N )$(平衡二分木における部分木のサイズを持ってrootを見ればOK)

が出来るデータ構造です

どうやってやるのでしょうか
実はオイラーツアーを動的に構築するアプローチでできます
ただし、Euler Tour Treeで管理するEuler Tourは頂点情報を含みます
どのように含むかというと頂点sと頂点sを繋ぐ自己ループを最初から結んでおきます
ではlinkとcutについて実装法を掘り下げてみましょう
#オイラーツアーの考察とEuler Tour Treeの実装

reroot

0root2.png

上図のグラフにおいて$0$がrootの場合のオイラーツアーは
$(0,0),(0,1),(1,1),(1,2),(2,2),(2,1),(1,3),(3,3),(3,4),(4,4),(4,3),(3,5),(5,5),(5,3),(3,6),(6,6),(6,3),(3,1),(1,0)$
になります
3root.png

一方で$3$がrootの場合
$(3,3),(3,4),(4,4),(4,3),(3,5),(5,5),(5,3),(3,6),(6,6),(6,3),(3,1),(1,0),(0,0),(0,1),(1,1),(1,2),(2,2),(2,1),(1,3)$

ぐっと眺めると
$(0,0),(0,1),(1,1),(1,2),(2,2),(2,1),(1,3),(3,3),(3,4),(4,4),(4,3),(3,5),(5,5),(5,3),(3,6),(6,6),(6,3),(3,1),(1,0)$を
$(0,0),(0,1),(1,1),(1,2),(2,2),(2,1),(1,3)$と$(3,3),(3,4),(4,4),(4,3),(3,5),(5,5),(5,3),(3,6),(6,6),(6,3),(3,1),(1,0)$
に分けて左右反転させてくっつけた物になってます
これより$(s,s)$でsplitしてmerge(second,fist)すれば頂点sを根に出来ることがわかります

link

link.png
根同士をlinkすることを考えます
$(0,0),(0,1),(1,1),(1,2),(2,2),(2,1),(1,3),(3,3),(3,1),(1,0)$

$(4,4),(4,5),(5,5),(5,4),(4,6),(6,6),(6,4)$
をくっつけると
$(0,0),(0,1),(1,1),(1,2),(2,2),(2,1),(1,3),(3,3),(3,1),(1,0),(0,4),(4,4),(4,5),(5,5),(5,4),(4,6),(6,6),(6,4),(4,0)$
になります
これはわかりやすいですね
左の木を$s$、右の木を$t$とした時
$s,(0,4),t,(4,0)$になってます

cut

cut.png
$(0,0),(0,1),(1,1),(1,2),(2,2),(2,1),(1,3),(3,3),(3,1),(1,0),(0,4),(4,4),(4,5),(5,5),(5,4),(4,6),(6,6),(6,4),(4,0),(0,7),(7,7),(7,0)$

$(0,0),(0,1),(1,1),(1,2),(2,2),(2,1),(1,3),(3,3),(3,1),(1,0),(0,7),(7,7),(7,0)$

$(4,4),(4,5),(5,5),(5,4),(4,6),(6,6),(6,4)$
に分けます
基本的にlinkの逆やればいいのですが$(4,0)$が最後に来る保証が有りません
そこで$(0,4)$と$(4,0)$を仕切りとしたとき
$s,(0,4),t,(4,0),u$としたら
tとmerge(s,u)を新たな木とすればOKです
$(0,4)$と$(4,0)$はなくなったのでdeleteしてください

Dynamic Connectivity

以上のEuler Tour Treeの出来る事を踏まえてDynamic Connectivityの構築を考えます
基本方針としては全域森をEuler Tour Tree(以下ETT)で管理して全域森が辺削除によって崩れたら復元を試みる、という方針です
ですがそのまま辺を全探索すると流石に遅いです
ですので、levelという概念を付け加えます

$level :その辺を見た回数$

この、levelの最大値を$O(\log N)$に抑える事で辺追加/削除のオーダー$O(log^2N)$にするのが目的です
また、説明のため次の様なグラフを定義します

$G:=与えられたグラフ$
$G_i:=Gのlevelがi以上である辺の集合からなるグラフ$
$F:=Gの全域森(最小全域森とは限らない)$
$F_i:=Fのlevelがi以上である辺の集合からなる森$
$E_i:=levelがi丁度であるFに使われてない辺$
定義より
$G=G_0 \supseteq G_1 \supseteq G_2 ...$
$F=F_0 \supseteq F_1 \supseteq F_2 ...$

実装としてはETTの配列として$F_i$を管理、隣接リストの配列として$E_i$を管理するといいでしょう
(隣接リストは削除/挿入を沢山行うので、二次元配列ではなくhash_setの配列、またはpairのhash_setをおすすめします)

それでは各操作のやり方を見ていきましょう

実装

辺追加(辺(s,t)を追加) link(s,t)

  • もし $F_0$において$s$と$t$が同じ木に属しているなら$E_0$に辺$(s,t)$を追加
  • そうで無いなら$F_0$に辺$(s,t)$を追加

グラフの2頂点s,tを結ぶpathが存在するか判定 same(s,t)

  • $F_0$において$s$と$t$が同じ木に属しているかを返す

辺削除(辺(s,t)を削除) cut(s,t)

  • もし $F_0$に辺$(s,t)$が含まれないなら辺$(s,t)$が存在する$E_i$から辺$(s,t)$を削除する
  • そうで無いなら辺$(s,t)$が存在する$F_i$から、辺$(s,t)$を削除して辺$(s,t)$が存在した最もlevelが高い森のlevelをkとした時、次に説明する辺置き換えreplace(s,t,k)を呼ぶ

辺置き換え(辺(s,t)の代替となる辺をE_kの内頂点sとF_kにおいて同じ木に属する頂点を端点に持つ辺から探す) replace(s,t,k)

  • 頂点$s$を$F_k$における木のサイズが大きくない方の頂点とする(条件を満たさないなら$s$と$t$をswap)(サイズの取得は$O(\log N)$で出来る)

  • $T_k:=頂点sを含むF_k上の木$とする

  • $U_k:=頂点tを含むF_k上の木$とする

  1. $T_k$上の木上のレベルがkである辺のlevelを全て上げる(レベルが$k$である辺の検索と追加に一個あたり$O(\log N)$かかる(ETTの機能、森の指定した条件を満たす一点を取得を使います))
  2. $T_k$上の、$E_k$に含まれている辺が接続された頂点を検索し、その頂点と接続された$E_k$上の辺$(x,y)$($x$を検索されたほうの頂点とする(つまり$x \in T_k$))を以下の様に処理する
  • もし$y \in T_k $なら、辺$(x,y)$を$E_{k+1}$に追する
  • もしそうでないなら$y \in U_k$なので、辺$(s,t)$の代わりになる辺$(x,y)$を見つけた事になり$F_0,F_1,...,F_k$に辺$(x,y)$を追加して$replace(s,t,k)$を終了する

全て見終わったらレベルkでは見つからなかったと言う事なので$replace(s,t,k-1)$を実行。
但し、$k=0$なら、辺$(s,t)$の代わりになるような辺はなかった(つまり辺$(s,t)$は橋だった)という事になるので、$F$に辺を追加せず終了

計算量解析

  • 補題1:$F_k$に含まれる木の最大サイズは$[N/2^k]$である

これは $|T_k|\leq|U_k|$を満たす、即ち$F_k$に含まれる木のサイズの半分以下のサイズの木しか$F_{k+1}$に追加していない事からわかります

  • 補題2:levelの最大値は$O(\log N)$である

補題1よりlevelが$\log_2N+1$を超えると$F_k$のサイズの最大値が0になることからわかります

以下の補題を踏まえた上で計算量解析をしていきます

  • 辺追加/削除:levelの最大値が$O(\log N)$、ETTのlink/cutで$O(\log N)$で$O(\log^2N)$
  • replaceの1.における辺のlevel上げ:同上&頂点検索は一つあたり$O(\log N)$で出来る
  • replaceの2.における頂点検索:$E_k$上の辺が接続されている頂点しか見ないので、頂点を見るなら辺を見る。よって、辺を見た回数で抑えられるので$O(\log^2N)$

故に全体の計算量も$O(\log^2N)$!!

あとがき

ずっと悩んでいたのでわかってよかったです
日本語解説が少ないのでこの記事で日本の競技プログラミング界に貢献出来たらいいなと思います
わからないところがあれば筆者のhotman(twitter)まで相談しに来てください(リプでもDMでもいいです)
誤字脱字/間違いがあればQiitaの機能またはtwitterで伝えてくれると幸いです

参考文献

英wikipedia
スライド
論文

謝辞

理解の手助けをしてくれたnoshiさん(twitter)、ありがとうございます
#verify
dynamic_graph_vertex_add_component_sum(yosupo judge) 解答例
僕が昨日作成した問題です(盛大な自作自演...)
余裕を持ったTLなのでverifyしやすいかと思います
テストケースも一応見れます(面倒くさいですが...)
Graph Construction(AOJ) 解答例
yosupo judge程ではないですが制限がゆるくテストケースも簡単に見れます
Connect and Disconnect(Codeforces) 解答例 (権限が無い人向け(wandbox))
テストケースが見れないのでおすすめはしないです、上の2つのACを確認した後念の為とかでいいかもです

おまけ(実装例)

dynamic_connectivity.cpp
template<typename T>
class dynamic_connectivity{
	class euler_tour_tree{
		public:
		struct node;
		using np=node*;
		using lint=long long;
		struct node{
			np ch[2]={nullptr,nullptr};
			np p=nullptr;
			int l,r,sz;
			T val=et,sum=et;
			bool exact;
			bool child_exact;
			bool edge_connected=0;
			bool child_edge_connected=0;
			node(){}
			node(int l,int r):l(l),r(r),sz(l==r),exact(l<r),child_exact(l<r){}
			bool is_root() {
				return !p;
			}
		};
		vector<unordered_map<int,np>>ptr;
		np get_node(int l,int r){
			if(ptr[l].find(r)==ptr[l].end())ptr[l][r]=new node(l,r);
			return ptr[l][r];
		}
		np root(np t){
			if(!t)return t;
			while(t->p)t=t->p;
			return t;
		}
		bool same(np s,np t){
			if(s)splay(s);
			if(t)splay(t);
			return root(s)==root(t);
		}
		np reroot(np t){
			auto s=split(t);
			return merge(s.second,s.first);
		}
		pair<np,np> split(np s){
			splay(s);
			np t=s->ch[0];
			if(t)t->p=nullptr;
			s->ch[0]=nullptr;
			return {t,update(s)};
		}
		pair<np,np> split2(np s){
			splay(s);
			np t=s->ch[0];
			np u=s->ch[1];
			if(t)t->p=nullptr;
			s->ch[0]=nullptr;
			if(u)u->p=nullptr;
			s->ch[1]=nullptr;
			return {t,u};
		}
		tuple<np,np,np> split(np s,np t){
			auto u=split2(s);
			if(same(u.first,t)){
				auto r=split2(t);
				return make_tuple(r.first,r.second,u.second);
			}else{
				auto r=split2(t);
				return make_tuple(u.first,r.first,r.second);
			}
		}
		template<typename First, typename... Rest>
		np merge(First s,Rest... t){
			return merge(s,merge(t...));
		}
		np merge(np s,np t){
			if(!s)return t;
			if(!t)return s;
			while(s->ch[1])s=s->ch[1];
			splay(s);
			s->ch[1]=t;
			if(t)t->p=s;
			return update(s);
		}
		int size(np t){return t?t->sz:0;}
		np update(np t){
			t->sum=et;
			if(t->ch[0])t->sum=fn(t->sum,t->ch[0]->sum);
			if(t->l==t->r)t->sum=fn(t->sum,t->val);
			if(t->ch[1])t->sum=fn(t->sum,t->ch[1]->sum);
			t->sz=size(t->ch[0])+(t->l==t->r)+size(t->ch[1]);
			t->child_edge_connected=(t->ch[0]?t->ch[0]->child_edge_connected:0)|(t->edge_connected)|(t->ch[1]?t->ch[1]->child_edge_connected:0);
			t->child_exact=(t->ch[0]?t->ch[0]->child_exact:0)|(t->exact)|(t->ch[1]?t->ch[1]->child_exact:0);
			return t;
		}
		void push(np t){
			//遅延評価予定
		}
		void rot(np t,bool b){
			np x=t->p,y=x->p;
			if((x->ch[1-b]=t->ch[b]))t->ch[b]->p=x;
			t->ch[b]=x,x->p=t;
			update(x);update(t);
			if((t->p=y)){
				if(y->ch[0]==x)y->ch[0]=t;
				if(y->ch[1]==x)y->ch[1]=t;
				update(y);
			}
		}
		void splay(np t){
			push(t);
			while(!t->is_root()){
				np q=t->p;
				if(q->is_root()){
					push(q), push(t);
					rot(t,q->ch[0]==t);
				}else{
					np r=q->p;
					push(r), push(q), push(t);
					bool b=r->ch[0]==q;
					if(q->ch[1-b]==t)rot(q,b),rot(t,b);
					else rot(t,1-b),rot(t,b);
				}
			}
		}
		void debug(np t){
			if(!t)return;
			debug(t->ch[0]);
			cerr<<t->l<<"-"<<t->r<<" ";
			debug(t->ch[1]);
		}
		public:
		euler_tour_tree(){}
		euler_tour_tree(int sz){
			ptr.resize(sz);
			for(int i=0;i<sz;i++)ptr[i][i]=new node(i,i);
		}
		int size(int s){
			np t=get_node(s,s);
			splay(t);
			return t->sz;
		}
		bool same(int s,int t){
            return same(get_node(s,s),get_node(t,t));
        }
		void set_size(int sz){
			ptr.resize(sz);
			for(int i=0;i<sz;i++)ptr[i][i]=new node(i,i);
		}
		void update(int s,T x){
			np t=get_node(s,s);
			splay(t);
			t->val=fn(t->val,x);
			update(t);
		}
		void edge_update(int s,auto g){
			np t=get_node(s,s);
			splay(t);
			function<void(np)>dfs=[&](np t){
				assert(t);
                if(t->l<t->r&&t->exact){
					splay(t);
					t->exact=0;
					update(t);
					g(t->l,t->r);
					return;
				}
				if(t->ch[0]&&t->ch[0]->child_exact)dfs(t->ch[0]);
				else dfs(t->ch[1]);
			};
			while(t&&t->child_exact){
				dfs(t);
				splay(t);
			}
		}
		bool try_reconnect(int s,auto f){
			np t=get_node(s,s);
			splay(t);
			function<bool(np)>dfs=[&](np t)->bool{
				assert(t);
				if(t->edge_connected){
					splay(t);
					return f(t->l);
				}
				if(t->ch[0]&&t->ch[0]->child_edge_connected)return dfs(t->ch[0]);
				else return dfs(t->ch[1]);
			};
			while(t->child_edge_connected){
				if(dfs(t))return 1;
				splay(t);
			}
			return 0;
		}
		void edge_connected_update(int s,bool b){
			np t=get_node(s,s);
			splay(t);
			t->edge_connected=b;
			update(t);
		}
		bool link(int l,int r){
			if(same(l,r))return 0;
			merge(reroot(get_node(l,l)),get_node(l,r),reroot(get_node(r,r)),get_node(r,l));
			return 1;
		}
		bool cut(int l,int r){
			if(ptr[l].find(r)==ptr[l].end())return 0;
			np s,t,u;
			tie(s,t,u)=split(get_node(l,r),get_node(r,l));
			merge(s,u);
			np p=ptr[l][r];
			np q=ptr[r][l];
			ptr[l].erase(r);
			ptr[r].erase(l);
			delete p;delete q;
			return 1;
		}
		T get_sum(int p,int v){
			cut(p,v);
			np t=get_node(v,v);
			splay(t);
			T res=t->sum;
			link(p,v);
			return res;
		}
		T get_sum(int s){
			np t=get_node(s,s);
			splay(t);
			return t->sum;
		}
	};
	int dep=1;
	vector<euler_tour_tree> ett;
	vector<vector<unordered_set<int>>>edges;
	int sz;
	public:
	dynamic_connectivity(int sz):sz(sz){
		ett.emplace_back(sz);
		edges.emplace_back(sz);
	}
	bool link(int s,int t){
		if(s==t)return 0;
		if(ett[0].link(s,t))return 1;
		edges[0][s].insert(t);
		edges[0][t].insert(s);
		if(edges[0][s].size()==1)ett[0].edge_connected_update(s,1);
		if(edges[0][t].size()==1)ett[0].edge_connected_update(t,1);
		return 0;
	}
	bool same(int s,int t){
		return ett[0].same(s,t);
	}
	int size(int s){
		return ett[0].size(s);
	}
	vector<int>get_vertex(int s){
		return ett[0].vertex_list(s);
	}
	void update(int s,T x){
		ett[0].update(s,x);
	}
	T get_sum(int s){
		return ett[0].get_sum(s);
	}
	bool cut(int s,int t){
		if(s==t)return 0;
		for(int i=0;i<dep;i++){
			edges[i][s].erase(t);
			edges[i][t].erase(s);
			if(edges[i][s].size()==0)ett[i].edge_connected_update(s,0);
			if(edges[i][t].size()==0)ett[i].edge_connected_update(t,0);
		}
		for(int i=dep-1;i>=0;i--){
			if(ett[i].cut(s,t)){
				if(dep-1==i){
					dep++;
					ett.emplace_back(sz);
					edges.emplace_back(sz);
				}
				return !try_reconnect(s,t,i);
			}
		}
		return 0;
	}
	bool try_reconnect(int s,int t,int k){
		for(int i=0;i<k;i++){
			ett[i].cut(s,t);
		}
		for(int i=k;i>=0;i--){
			if(ett[i].size(s)>ett[i].size(t))swap(s,t);
			auto g=[&](int s,int t){ett[i+1].link(s,t);};
			ett[i].edge_update(s,g);
			auto f=[&](int x)->bool{
				for(auto itr=edges[i][x].begin();itr!=edges[i][x].end();){
					auto y=*itr;
					itr=edges[i][x].erase(itr);
					edges[i][y].erase(x);
					if(edges[i][x].size()==0)ett[i].edge_connected_update(x,0);
					if(edges[i][y].size()==0)ett[i].edge_connected_update(y,0);
					if(ett[i].same(x,y)){
						edges[i+1][x].insert(y);
						edges[i+1][y].insert(x);
						if(edges[i+1][x].size()==1)ett[i+1].edge_connected_update(x,1);
						if(edges[i+1][y].size()==1)ett[i+1].edge_connected_update(y,1);
					}else{
						for(int j=0;j<=i;j++){
							ett[j].link(x,y);
						}
						return 1;
					}
				}
				return 0;
			};
			if(ett[i].try_reconnect(s,f))return 1;
		}
		return 0;
	}
	constexpr static T et=T();
	constexpr static T fn(T s,T t){
		return s+t;
	}
};
59
50
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
59
50