ไบนารี

o59_mar_c2_binary

ข้อนี้ถามว่าหากแปลงต้นไม้ TT ให้เป็นต้นไม้ไบนารี SS จะทำให้ SS มีความลึกน้อยสุดได้เท่าไหร่ โดยในการแปลงจะจะต้องใช้วิธีการสร้างจุดยอดใหม่เป็น parent ของสองจุดยอดใดๆ ที่เคยเป็น sibling กัน

สังเกตว่าสำหรับ Subtree ใดๆ ส่ามารถหาได้ว่าจะแปลง Subtree นี้ให้เป็นต้นไม้ไบนารีที่มีความลึกต่ำสุดได้เท่าใดโดยไม่ต้องพิจารณาจุดยอดนอก Subtree นั้นๆ

เราจึงสามารถกำหนด H[x]H[x] เป็นความลึกที่ต่ำที่สุดที่เป็นไปได้ของต้นไม้ไบนารีที่มีรากเป็น xx

สมมิตว่าเราคำนวณ H[c]H[c] สำหรับทุกจุดยอดลูก cc ของ xx ไปแล้ว หาก xx ไม่มีลูกหรือมีลูกไม่เกิน 2 จุดยอด ความลึกเป็นเพียง ค่า HH ของลูกที่สูงสุดบวก 1 แต่หากมีมากกว่า 2 จุดยอดจะต้องพิจารณาวิธีสร้างต้นไบนารีจากจุดยอดลูกเหล่านี้ที่จะทำให้ความลึกต่ำสุดที่จะเป็นไปได้

Greedy Algorithm

เราจะพิสูจน์ว่าการในการสร้างต้นไม้ไบนารีที่ตื้นที่สุดที่มีรากเป็น xx หาก xx มีลูกเกิน 2 ลูก จะสามารถเริ่มโดยการสร้างจุดยอดพิเศษเป็น parent ของสองจุดยอดลูกที่มีค่า HH ต่ำสุดเสมอ

นั่นคือหากจุดยอดลูก aa และ bb เป็นจุดยอดที่ H[a]H[a] กับ H[b]H[b] มีค่าที่น้อยที่สุดที่เป็นไปได้สองค่า เราจะพิสูจน์ว่ามีต้นไบนารีที่มีรากเป็น xx ที่มีความลึก H[x]H[x] (กล่าวคือตื้นสุดที่เป็นไปได้) โดยมี aa และ bb เป็น sibling (มี parent เป็นจุดยอดพิเศษเดียวกัน)

พิจารณาต้นไม้ไบนารี OO ใดๆ ที่มีความลึกน้อยที่สุด เราจะพิสูจน์ว่ามีต้นไม้ OO' ที่มีความลึกไม่เกิน OO โดยมี aa และ bb เป็น sibling

ให้ dO(z)d_O(z) เป็นความลึกของ zz ใน OO

ให้ aa' เป็นหนึ่งในสองจุดยอด aa หรือ bb ดังกล่าวที่มี dO(a)d_O(a') มากสุดและ bb' เป็นอีกจุดยอด นั่นคือ dO(a)dO(b)d_O(a') \geq d_O(b')

สังเกตได้ว่าทุกจุกยอดลูกจะมี sibling ในต้นไบนารี OO เพราะในขั้นตอนการเพิ่มจุดยอดพิเศษ เพราะหากจุดยอดพิเศษนั้นไม่มี sibling แสดงว่าก่อนการเพิ่ม parent ของทั้งสองลูกมีเพียงสองลูกอยู่แล้ว ดังนั้นการเพิ่มจุดยอดพิเศษนี้จึงไม่จำเป็นและเพิ่มความลึกของ OO ซึ่งขัดกับการที่ OO มีความลึกน้อยสุดที่เป็นไปได้

ดังนั้นเราจะสามารถจำแนกเป็น 2 กรณี

  1. aa' และ bb' เป็น sibling กัน
  2. aa' มี sibling ที่ไม่ใช่ bb'

หากเข้ากรณีแรกสามารถเลือก O=OO'=O ตามที่ต้องการพิสูจน์

หากเข้ากรณีที่สอง ให้ sibling ของ aa' และ bb' เป็น cc และ dd ตามลำดับ เราสามารถเลือก OO' เป็นต้น OO ที่สลับ bb' มาเป็น sibling aa' และ cc ไปเป็น sibling dd ทั้งนี้จะทำให้ความลึกจุดยอด descendent ลึกสุดของ bb' ในต้นไบนารีใหม่เป็น dO(a)+H[b]d_O(a') + H[b'] และของ dd เป็น dO(b)+H[d]d_O(b') + H[d] จากเดิม dO(b)+H[b]d_O(b') + H[b'] และ dO(a)+H[d]d_O(a') + H[d] ตามลำดับ เนื่องจาก H[b]H[d]H[b'] \leq H[d] และ dO(b)dO(a)d_O(b') \leq d_O(a') (จากนิยามของ a,ba', b' และ HH) จะได้ว่า max(dO(a)+H[b],dO(b)+H[d])max(dO(b)+H[b],dO(a)+H[d])=dO(a)+H[d]\max(d_O(a') + H[b'], d_O(b') + H[d]) \leq \max(d_O(b') + H[b'], d_O(a') + H[d]) = d_O(a') + H[d] กล่าวคือการสลับนี้จะไม่เพิ่มความลึกของ OO' เมื่อเทียบกับ OO เพราะความลึกที่มากสุดในบรรดาจุดยอดที่ถูกกระทบไม่เพิ่ม

เพราะฉะนั้นจึงสรุปได้ว่าไม่ว่ากรณีใดๆ จะมีต้นไม้ไบนารี OO' ที่เลือก aa และ bb มาเป็น sibling กันและมีความลึกต่ำสุดที่เป็นไปได้

เมื่อเราเลือกสร้างจุดยอดพิเศษ cc มาเป็นลูกใหม่ของ xx ที่มีลูกเป็น aa กับ bb เราสามารถใช้เหตุผลแบบเดิมเลือกสองลูกที่มี HH ต่ำสุดมาเป็นลูกของจุดยอดพิเศษใหม่อีกรอบเรื่อยๆ จน xx เหลือลูกเพียงสองลูกและจะได้ว่า H[x]H[x] คือ max(H[y],H[z])\max(H[y],H[z]) ของลูกที่เหลืออยู่ y,zy,z

Algorithm เต็ม

  1. อ่าน input โดยเก็บ Adjacency List ของแต่ละจุดยอดว่ามีจุดยอดไหนเป็นลูกบ้าง

  2. คำนวณ H[x]H[x] สำหรับทุก xx โดยใช้วิธี recursive แบบด้านบน

    • หาก xx ไม่มีลูกจะได้ H[x]=0H[x]=0 และรีเทิร์น
    • มิเช่นนั้น xx มีลูกอย่างน้อย 1 ลูก ให้คำนวณ H[c]H[c] สำหรับทุกลูก cc ก่อน
    • เอา H[c]H[c] ทุกค่ามาไว้ใน minimum priority queue
    • เลือกสองค่าต่ำสุดใน priority queue เพื่อเอาออกและนำมาเป็นลูกของจุดพิเศษใหม่ (หากค่าเดิมคือ AA และ BB จุดยอดพิเศษใหม่จะมีความลึก max(A,B)+1max(A,B)+1 เพราะเพิ่มความลึกที่มากสุดในนั้นไป 1) และใส่จุดยอดใหม่เข้าไปใน queue โดยทำซ้ำจนใน queue เหลือไม่เกิน 2 ค่า
    • เลือกค่าที่มาก MM ที่สุดที่เหลืออยู่ใน queue และตั้ง H[x]=M+1H[x]=M+1
  3. คำตอบคือ H[1]H[1]

การอ่านข้อนำเข้าและเก็บ Adjacency List ใช้เวลา O(N)\mathcal{O}(N)

การคำนวณ HH ใช้เวลา O(CxlogCx)\mathcal{O}(C_x \log C_x) สำหรับทุก xx เมื่อ CxC_x คือจำนวนลูกของ xx เพราะเกิดการ push และ pop จาก priority queue O(Cx)\mathcal{O}(C_x) รอบ

ทั้งหมดจึงใช้เวลา O(N)+Σx=1NO(CxlogCx)=O(NlogN)\mathcal{O}(N) + \Sigma_{x=1}^{N} \mathcal{O}(C_x \log C_x) = \mathcal{O}(N \log N)

ตัวอย่างโค้ดสำหรับการคำนวณ HH ในขั้นตอนที่ 2

int H(int x) {
  if (child[x].size() == 0) {
    return 0;
  }

  std::priority_queue<int, std::vector<int>, std::greater<int>> q;

  for (int i = 0; i < child[x].size(); i++)
    q.push(H(child[x][i]));

  while (q.size() > 2) {
    int A = q.top();
    q.pop();
    int B = q.top();
    q.pop();
    q.push(max(A, B) + 1);
  }

  int H_x = 0;
  while (q.size() > 0) {
    H_x = max(H_x, q.top() + 1);
    q.pop();
  }

  return H_x;
}