Processing math: 100%

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(n)이다.

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

     

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

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

     

    연습 문제

     

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

     

    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.