Problem Solving

BOJ 13896. Sky Tax

thisisuserr 2024. 8. 13. 17:43

13896번: Sky Tax (acmicpc.net)

solved.ac: Platinum III (2024.09.10)

문제를 요약하면, 루트 노드가 수시로 바뀌는 상황에서 서브트리 크기를 구하는 쿼리를 처리하라는 것이다.

 

일단 아직 뭐 rerooting technique라던가 배운 적도 없고, ETT는 쓸 생각도 못했다.

여기서는 쌩 LCA만 이용한 풀이를 작성한다.


처음 볼 때부터 그때그때 서브트리 크기를 직접 구하는 건 미친짓이라고 생각했다.

미리 한번 돌려서 크기를 구해놓고, 적절히 끼워맞추는 방식으로 접근했다.

 

이후 몇 개의 간단한 케이스워크를 통해 규칙을 찾았다.

이후의 모든 서술은 처음에 한번 잡고 돌린 트리를 기준으로 한다.

 

Case 1: 현재 루트노드가 쿼리 노드의 조상일 때

서브트리라는 것을 다시 생각해보면, p의 서브트리 내의 모든 정점들은 p의 조상으로 가기 위해서는 반드시 p를 거쳐야 한다.

따라서 r에서 쭉 내려가다가 p를 만나면, p에서 내려가는 모든 정점들은 항상 p를 거쳐가야 하게 된다. (트리의 부모는 항상 1개이므로)

이 경우 그냥 처음에 돌려놨던 서브트리 크기를 그대로 가져오면 된다.

 

Case 2: 쿼리 노드가 현재 루트노드의 조상일 때

r에서 p로 가는 경로가 유일하므로, 해당 경로를 거쳤다면 그 이후에 탐색하는 모든 정점들은 r에 도달하기 위해 반드시 p를 거쳐야 한다.

이는 서브트리 내의 노드의 성질과 일치한다.

 

조금 관점을 바꾸면 r에서 부모 노드를 타고 p로 갈 때, p의 직전 노드까지는 p를 거치지 않고도 r에 도달할 수 있음을 알 수 있다.

따라서 이 경우 p에서 r이 있는 방향으로 한 칸 내려가서, 이 정점을 s라 하면 n - sz[s]로 구할 수 있다.

 

한 칸 내려가는 경우가 조금 애매할 수 있다. 근데 돌이켜보면 희소 배열로 LCA 구할 때 마지막에 한 칸 올려주지 않았던가?

그 과정 생략하면 바로 얻을 수 있다. 아래 코드에선 따로 함수를 하나 더 짰다.


Case 3: LCA(cur, root) != cur, root

root에서 LCA로 가는 경로는 유일하고, cur과 LCA의 경우 Case 1과 겹친다. 따라서 그냥 서브트리 크기를 출력해주면 된다.

 

아래는 풀이 코드이다.

#include <iostream>
#include <vector>
#include <cstring>

using namespace std;

int n, root;
vector<int> node[100001];
int tree[100001];

int depth[100001];
int parent[100001][18];
int child[100001];

void init()
{
  for(int i = 0; i < 100001; i++) node[i].clear();
  memset(tree, 0, sizeof(tree));
  memset(parent, 0, sizeof(parent));
}

int dfs(int cur, int prv = 0)
{
  depth[cur] = depth[prv] + 1;
  parent[cur][0] = prv;
  
  tree[cur] = 1;
  for(auto nxt: node[cur])
    {
      if(nxt == prv) continue;
      tree[cur] += dfs(nxt, cur);
    }
  return tree[cur];
}

void structLCA()
{
  for(int j= 1; j < 18; j++) for(int i = 1; i <= n; i++) parent[i][j] = parent[parent[i][j-1]][j-1];
}

int LCA(int p, int q)
{
  if(depth[p] < depth[q]) swap(p, q);
  int diff = depth[p] - depth[q];
  for(int i = 0; i < 18; i++) if(diff & (1 << i)) p = parent[p][i];
  if(p == q) return p;

  for(int i = 17; i >= 0; i--)
    {
      if(parent[p][i] != parent[q][i])
      {
        p = parent[p][i];
        q = parent[q][i];
      }
    }
  return parent[p][0];
}

int LCAprv(int p, int q)
{
  if(depth[p] < depth[q]) swap(p, q);
  if(LCA(p, q) == q)
  {
    int diff = depth[p] - depth[q] - 1;
    for(int i = 0; i < 18; i++) if(diff & (1 << i)) p = parent[p][i];
    return p;
  }
  else
  {for(int i = 17; i >= 0; i--)
      {
        if(parent[p][i] != parent[q][i])
        {
          p = parent[p][i];
          q = parent[q][i];
        }
      }
    return p;
  }
}

void solve()
{
  init();

  int Q;
  cin >> n >> Q >> root;
  for(int i = 1; i < n; i++)
    {
      int p, q;
      cin >> p >> q;
      node[p].push_back(q);
      node[q].push_back(p);
    }
  dfs(1); structLCA();
  while(Q--)
    {
      int op, cur;
      cin >> op >> cur;

      if(op == 0) root = cur;
      else
      {
        int prv = LCAprv(cur, root);
        int lca = LCA(cur, root);
        
        if(lca != cur) cout << tree[cur] << '\n';
        else cout << n - tree[prv] << '\n';
      }
    }
}

int main()
{
  ios::sync_with_stdio(false);
  cin.tie(0);

  int T;
  cin >> T;
  for(int i = 1; i <= T; i++)
    {
      cout << "Case #" << i << ":\n";
      solve();
    }
}

'Problem Solving' 카테고리의 다른 글

BOJ 3665. 최종 순위  (0) 2024.08.23
BOJ 1450. 냅색문제  (0) 2024.08.22
BOJ 2957. 이진 탐색 트리  (0) 2024.08.12
Centroid Decomposition (BOJ 5820)  (0) 2024.07.21
2024 KOI 1차 풀이  (0) 2024.06.18