คู่บ่อน้ำ

o61_may02_froghome

ในข้อนี้ สามารถสรุปโจทย์ได้ดังนี้ กำหนดให้กราฟต้นไม้ที่มี NN โหนด และ N1N - 1 เส้นเชื่อม อยากทราบว่ามีคู่โหนด u,vu, v กี่คู่ที่จำนวนเส้นเชื่อมจาก uu ไป vv (หรือระยะห่างระหว่าง uu กับ vv) มีค่าเท่ากับ aia_i สำหรับ 1ik1 \leq i \leq k

เนื่องจากกราฟเป็นต้นไม้ ดังนั้นเราสามารถคำนวณจำนวนคู่ u,vu, v สำหรับแต่ละ aia_i ด้วยการใช้ Dynamic Programming โดยเราจะเลือกโหนดใด ๆ มาหนึ่งโหนดเพื่อให้เป็นรากของต้นไม้ และกำหนดให้ dp(u,i)dp(u, i) เป็นจำนวนโหนด vv ใน Subtree ของ uu ที่ระยะห่างระหว่าง vv กับ uu เป็น ii สำหรับ Recurrence Formula จะเป็นดังนี้

dp(u,i)=vdp(v,i1)dp(u, i) = \sum \limits_{v} dp(v, i - 1) สำหรับ 1iN11 \leq i \leq N - 1 เมื่อ vv เป็นลูกของ uu ซึ่งมีที่มาจากการที่ระยะทางจาก vv มา uu เป็น 11 ทำให้โหนดที่มีห่างจาก vv เท่ากับ i1i - 1 จะมีห่างจาก uu เท่ากับ ii

นอกจากนี้ เราจะมีกรณีพิเศษคือ dp(u,0)=1dp(u, 0) = 1 เพราะเราจะนับว่า uu ห่างจากตัวเองเป็นระยะ 00 ด้วย

สำหรับการนับจำนวนคู่โหนดสำหรับค่า aia_i ใด ๆ เราสามารถทำได้โดย ขณะที่เรากำลังพิจารณาโหนด uu เราจะนับจำนวนคู่อันดับ (v,w)(v, w) ที่เส้นทางจาก vv มา ww ต้องผ่านโหนด uu ทำให้เราสามารถแบ่งเส้นทางจาก vv มา ww ออกเป็นเส้นทางจาก vv มา uu และ uu มา ww ตามลำดับ และเพื่อให้เส้นทางจาก vv มา ww ไม่ผ่านโหนดซ้ำ vv และ ww จะต้องเป็นโหนดในคนละ Subtree ของลูกของ uu

ดังนั้น สำหรับค่า aia_i ใด ๆ จำนวนคู่โหนดที่ตรงตามเงื่อนไข และเส้นทางระหว่างโหนดต้องผ่าน uu จะเท่ากับ j=0ai2dp(v,j)dp(w,aij2)\sum \limits_{j = 0}^{a_i - 2} dp(v, j) \cdot dp(w, a_i - j - 2) เมื่อ v,wv, w เป็นลูกของ uu ที่แตกต่างกัน สำหรับกรณีนี่เส้นทางผ่านโหนด uu ซึ่งมีที่มาจากการที่ (ระยะทางจากโหนดแรกของคู่อันดับมายัง vv ++ ระยะทางจาก vv ไป ww ที่เท่ากับ 22 เสมอ ++ ระยะทางจาก ww ไปโหนดที่สองของคู่อันดับ) =ai= a_i และ 2dp(u,ai)2 \cdot dp(u, a_i) สำหรับกรณีที่คู่อันดับมีตัวใดตัวหนึ่งเป็น uu โดยคูณ 22 เพราะสามารถสลับตำแหน่งของคู่อันดับได้

หากทำตามวิธีด้านบนนี้ จะใช้เวลา O(N2K)\mathcal{O}(N^2 K) แต่เราสามารถลดเวลาการทำงานลง ด้วยการเลือก uu ในการนับจำนวนคู่โหนดในลำดับที่เหมาะสมได้ โดย สมมติเราเลือก uu มา แล้วนับจำนวนคู่โหนดที่เส้นทางต้องผ่าน uu เป็นที่เรียบร้อยแล้ว เราจะลบ uu ออกจากต้นไม้ ทำให้กราฟกลายเป็นต้นไม้หลาย ๆ ต้น หลังจากนั้น เราจะทำการ Recursive ไปในต้นไม้แต่ละต้น เพื่อเลือก uu ถัดไปที่จะคำนวณ หากต้นไม้มีอยู่ NN โหนด และเราสามารถหาโหนด uu ที่เมื่อลบออกแล้ว จะทำให้ต้นไม้แต่ละต้นที่เหลือ มีจำนวนโหนดไม่เกิน N2\lfloor \frac{N}{2} \rfloor เราจะสามารถลดเวลารวมของการ Recursive ลงเหลือเพียง O(NKlogN)\mathcal{O}(N K \log N) ซึ่งโหนด uu ที่มีคุณสมบัตินี้ จะเรียกว่า Centroid ซึ่งสามารถพิสูจน์ได้ว่า สำหรับต้นไม้ใด ๆ จะมี Centroid อย่างน้อยหนึ่งโหนดเสมอ เทคนิคนี้มีชื่อเรียกว่า Centroid Decomposition

สำหรับการทำ Centroid Decomposition นั้น เราจะลดเวลาการคำนวณ dp(u,i)dp(u, i) ลง โดยแทนที่จะคิดจาก Recurrence Formula (ต้องคิดสำหรับทุก uu ใน Subtree) เราใช้การ DFS จาก uu เพื่อนับว่ามีกี่โหนดที่มีระยะห่างจาก uu ไป 1,2,3,...1, 2, 3, ... เส้น (ไม่สนระยะห่างจากโหนดอื่นใน Subtree) โดยจัดการแยกแต่ละ Subtree ไม่ให้นับซ้ำ จึงกำหนดให้ dp(i)dp(i) เท่ากับจำนวนโหนดที่ห่างจาก uu เป็นระยะ ii และก่อนที่เราจะอัพเดทค่าของ dp(i)dp(i) สำหรับ Subtree ของ vv ซึ่งเป็นลูกของ uu เราจะทำการ DFS เพื่อนับจำนวนคู่อันดับที่มีตัวแรกเป็นโหนดใน Subtree ของ vv และอีกตัวมาจาก Subtree ก่อนหน้านี้ที่ได้อัพเดทค่าใน dp(i)dp(i) เป็นที่เรียบร้อยแล้ว หลังจากนั้นเราจะทำการ DFS อีกครั้งใน Subtree ของ vv เพื่ออัพเดทค่า dp(i)dp(i) แล้วคำนวณ Subtree ถัดไป หลังจากนั้น เมื่อพิจารณา Centroid uu เป็นที่เรียบร้อยแล้ว เราจะลบ uu ออกจากกราฟ แล้ว Recursive ไปยังลูกของ uu ที่ยังไม่โดนลบ เพื่อคำนวณคำตอบต่อไป วิธีนี้จะได้จำนวนคำตอบเพียงครึ่งเดียวจึงต้องคูณ 22 ก่อนแสดงคำตอบ

โค้ดตัวอย่างดังนี้

#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 5;

int n, k, A[N];
int sz[N], chk[N], book;
long long dp[N], t[N], ans[N];
vector<int> g[N];

int getsz(int u, int p) {
  sz[u] = 1;
  for (int v : g[u])
    if (v != p && !chk[v])
      sz[u] += getsz(v, u);
  return sz[u];
}

int find_cen(int u, int p, int all, pair<int, int> &ret) {
  int mx = all - sz[u];
  for (int v : g[u])
    if (v != p && !chk[v])
      mx = max(mx, find_cen(v, u, all, ret));
  ret = min(ret, pair<int, int>(mx, u));
  return sz[u];
}

void dfs(int u, int p, int len, bool fill) {
  if (!fill) {
    for (int i = 1; i <= k; i++)
      if (len <= A[i]) {
        if (len == A[i])
          ++ans[i];
        if (t[A[i] - len] == book)
          ans[i] += dp[A[i] - len];
      }
  } else {
    if (t[len] < book)
      dp[len] = 0;
    ++dp[len], t[len] = book;
  }
  for (int v : g[u])
    if (v != p && !chk[v])
      dfs(v, u, len + 1, fill);
}

void proc(int u) {
  if (getsz(u, u) <= 1)
    return;
  pair<int, int> ret(sz[u] + 1, -1);
  find_cen(u, u, sz[u], ret);
  u = ret.second;
  ++book;
  for (int v : g[u])
    if (!chk[v]) {
      dfs(v, u, 1, 0);
      dfs(v, u, 1, 1);
    }
  chk[u] = true;
  for (int v : g[u])
    if (!chk[v])
      proc(v);
}

int main() {
  scanf("%d %d", &n, &k);
  for (int i = 1; i <= k; i++)
    scanf("%d", A + i);
  for (int i = 1, a, b; i < n; i++) {
    scanf("%d %d", &a, &b);
    g[a].emplace_back(b), g[b].emplace_back(a);
  }
  proc(1);
  for (int i = 1; i <= k; i++)
    printf("%lld\n", ans[i] * 2ll);

  return 0;
}

สำหรับโค้ดด้านบนนี้ จะมีการใช้อาร์เรย์ t เนื่องจากเราจะต้องล้างค่าของอาร์เรย์ dp ทุก ๆ ครั้งที่คำนวณคำตอบของ Centroid อันใหม่

Time Complexity: O(NKlogN)\mathcal{O}(N K \log N)