ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Fragmented Tree
    Problem Solving/Algorithm 2023. 9. 3. 21:43

    Codeforces를 돌아다니다가 흥미로운 글을 하나 읽었다. Sqrt Fragmented Tree라는 자료구조로, 글 자체는 7년 전의 것이지만 한국에서 잘 알려지지 않았기에 설명하는 글을 작성한다.

     

     

    Approach

     

    업데이트가 있는 트리 DP는 Centroid Decomposition Tree나, Euler Tour Trick과 HLD를 사용한 소위 Dynamic Tree DP등을 통해서 할 수 있음이 알려져 있다. 물론, 이러한 자료구조들은 구현이 어렵고, 최소한의 생각을 해야 문제가 풀리기 때문에 머리가 아프다.

     

    트리에서 Square Root Decomposition을 사용할 수 있을까라는 생각을 해본 적이 있을 것이다. 트리는 connected component로 나누면 component의 크기 또는 개수 중에 하나는 너무 커지게 된다. 트리를 잘 분할하려면 어떻게 해야할까? 'Connected' component를 포기하면 된다.

     

     

    Fragmented Tree

     

    Sqrt Fragmented Tree는 위의 고민을 해결하기 위한 자료구조이다. 정점들을 connected component 조건을 포기하고 sqrt개씩 버킷으로 나누려고 한다. 이 버킷을 Fragment라고 한다.

    그냥 막 나누면 관리가 너무 어려우니깐, Fragment 내의 각 connected component에 대해서 제일 깊이가 얕은 정점들의 부모가 전부 동일하도록 하면 관리가 좀 더 편할 것이다. 그 부모 정점을 'Fragment의 부모' 정점이라고 하자.

     

    DFS를 통해 Fragment를 형성한다. Fragment를 만드는 방법은 다음과 같다:

    1. 각 정점마다 일단 자식으로 향하는 DFS를 도는 것으로 서브트리의 크기를 계산한다.

    2. 현재 정점을 부모로 하는 Fragment 후보 집합을 만든다. 현재 정점을 포함하지 않음에 주의하자.

    3. 각 자식쪽 서브트리마다 Fragment에 추가되지 않은 정점 집합을 현재 정점의 집합에 추가해준다. 현재 정점의 집합 크기가 sqrt를 넘을 때마다 이 집합으로 이루어진 Fragment를 만들어주고, 집합을 비워준다.

    4. 트리의 루트는 아무 Fragment에도 속하지 않기 때문에, 트리의 루트를 포함해서 현재 아무 Fragment에도 속하지 않은 정점들을 가지고 마지막 Fragment를 만들어준다.

     

    각 Fragment의 크기는 sqrt 이상 2*sqrt 미만일 것이다. Fragment 후보 집합의 크기가 sqrt가 넘어갈때만 Fragment를 형성하고 Fragment를 비워주기 때문에, 해당 집합이 sqrt보다 살짝 작은 크기일 때 마찬가지로 sqrt보다 살짝 작은 크기의 집합을 합쳐주게 된다면 2*sqrt에 근접할 수 밖에 없다. 이는 상수 면에서 아쉬운 부분이다.

     

    Fragment를 만들때는 가상의 노드를 새로 하나 더 만들어주면 편리하다. 원래 트리의 각 노드들에 대해서, 이 노드가 어떤 Fragment에 속해 있는지 역추적하는 배열도 만들어주면 편리하다. 또한, Fragment 간의 인접 리스트도 만들어 두면 계산 쿼리를 처리할 때 편리하다.

     

    구현은 다음과 같다. 구현이 짧은 편은 아니지만, 템플릿으로 만들어 두면 쓰기 편할 것으로 예상한다.

     

    int makeNode(int u, const vector<int> &im) {
    	fragPos.push_back(-1);
    	fragPar.push_back(u);
    	dep.push_back(u<0?0:dep[u]+1);
    	int num=adj.size();
    	adj.push_back(im);
    	return num;
    }
    
    void makeFragment(int u, Fragment &fr) {
    	if (fragPos[u]!=-1) return ;
    	fragPos[u]=frags.size()-1;
    	fr.add(u);
    	for (int v:adj[u]) makeFragment(v, fr);
    }
    
    void build(int rt=1) {
    	dep=vector<int>(n+1);
    
    	{
    		queue<array<int, 2>> q;
    		q.push({rt, -1});
    		while (q.size()) {
    			auto [u, p]=q.front();
    			q.pop();
    			for (int v:org[u]) if (v!=p) q.push({v, u}), dep[v]=dep[u]+1;
    		}
    	}
    
    	fragPos=fragPar=vector<int>(n+1, -1);
    
    	adj=org;
    
    	vector<int> ord(n);
    	iota(ord.begin(), ord.end(), 1);
    	sort(ord.begin(), ord.end(), [&](int x, int y) {
    		return dep[x]>dep[y];
    	});
    
    	vector<int> sz(n+1);
    	for (int u:ord) {
    		sz[u]=1;
    		int sum=0;
    		vector<int> im, ch;
    		for (int v:adj[u]) if (dep[u]<dep[v]) sz[u]+=sz[v];
    		for (int v:adj[u]) if (dep[u]<dep[v]) {
    			sum+=sz[v];
    			im.push_back(v);
    			if (sum>SQ) {
    				int nd=makeNode(u, im);
    				im.clear();
    				ch.push_back(nd);
    				frags.emplace_back(nd, n);
    				makeFragment(nd, frags.back());
    				sz[u]-=sum;
    				sum=0;
    			}
    		}
    		adj[u]=im;
    		for (int i:ch) adj[u].push_back(i);
    	}
    
    	int nd=makeNode(-1, vector<int>({rt}));
    	frags.emplace_back(nd, n);
    	makeFragment(nd, frags.back());
    
    	subAdj=vector<vector<int>>(frags.size());
    	for (int i=0; auto &fr:frags) {
    		fr.init();
    		if (fragPar[fr.t]==-1) continue;
    		fr.p=fragPos[fragPar[fr.t]];
    		subAdj[fr.p].push_back(i);
    		i++;
    	}
    }

     

    업데이트 및 쿼리

     

    Fragment 내부에서의 업데이트를 Naive하게 해주면 $O(\sqrt{n})$이다.

    모든 Fragment들을 DFS로 순회하면 마찬가지로 $O(\sqrt{n})$이다. 서브트리 쿼리가 들어온 경우, 서브트리의 루트 정점에서부터 시작해서 내려가는 DFS를 원본 트리에서 돌리고, 다른 Fragment를 만나게 된다면 Fragment간의 DFS를 돌려준다. 이 또한 각 Fragment의 크기가 $O(\sqrt{n})$이기 때문에, $O(\sqrt{n})$이다.

     

    자세한 구현은 밑에 첨부하는 코드를 참고하자. 추가적인 응용은 독자에게 맡긴다.

    https://github.com/dsstar-codes/Problem-Solving/blob/master/Library/FragmentedTree.cpp

     

    연습 문제

     

    Naive가 Tree DP로 $O(N)$에 풀리는 문제이고, 제한이 $O(Q\sqrt{N})$이 동작하는 제한이라면 웬만하면 풀 수 있을 것으로 예상한다.

     

    2015 CodeCraft - Queries on the Tree (구현에 어려움이 있다면, 코드는 이 링크를 참고하자.)

    Singapore NOI 2022 - Grapevine

    BOJ 13515 - 트리와 쿼리 6

    SNUPC 2021 - 누텔라 트리 (Hard)

    BOJ 13516 - 트리와 쿼리 7

    JOI Open Contest 2018 - Cats or Dogs (굉장히 트리키한 구현을 요구한다. HLD를 이용한 풀이가 훨씬 쉽고 간편할 것이다.)

    댓글

Designed by Tistory.