แผนการเรียน
/

Advanced Data Structures
Segment Tree & Fenwick Tree

Day 09 Morning: การจัดการข้อมูลในช่วง (Range Queries) อย่างมีประสิทธิภาพ

หัวข้อที่จะเรียนรู้

1
โครงสร้างและหลักการของ Fenwick Tree (Binary Indexed Tree)
2
การออกแบบและใช้งาน Segment Tree สำหรับปัญหาที่หลากหลาย
3
การเปรียบเทียบประสิทธิภาพในการทำ Range Query และ Point Update

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

Slide 2: ทำไมต้องใช้ Advanced Data Structures?

ข้อจำกัดของวิธี Naive

  • การหาผลรวมในช่วง ( [a, b] ) ด้วย loop ปกติใช้เวลา ( O(n) ) ต่อ Query
  • การใช้ Prefix Sum Array ช่วยให้ Query เร็ว ( O(1) ) แต่ถ้ามีการเปลี่ยนค่า (Update) จะใช้เวลา ( O(n) )
  • ในโจทย์โปรแกรมเชิงแข่งขันที่มี ( q ) Query และ ( n ) ข้อมูลขนาดใหญ่ การใช้ ( O(nq) ) จะทำให้เกิด TLE
  • เป้าหมาย: ทำให้ทั้ง Query และ Update ทำงานได้ใน ( O(\log n) )

แหล่งอ้างอิง: https://www.geeksforgeeks.org/array-range-queries/

Slide 3: รู้จัก Fenwick Tree (Binary Indexed Tree)

Fenwick Tree คืออะไร?

ถูกเสนอโดย P. M. Fenwick ในปี 1994 เป็นโครงสร้างข้อมูลที่:

  • จำลองการทำงานของ Prefix Sum Array ในรูปแบบที่สามารถ Update ได้รวดเร็ว
  • ใช้หน่วยความจำน้อย (เท่ากับ Array เดิม) โดยใช้ Array ขนาด ( n+1 )
  • มีความเร็วคงที่ (Constant factor) ที่ดีมากเมื่อเทียบกับ Segment Tree ในโจทย์บางประเภท
  • เหมาะสำหรับงานประเภท Range Sum Query และ Point Update

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

Slide 4: หลักการ Bit Magic ใน Fenwick Tree

กลไกการทำงานด้วย Bitwise

แต่ละตำแหน่ง ( k ) ใน Fenwick Tree จะเก็บผลรวมของช่วงที่มีความยาว ( p(k) )

p(k) = k & -k
  • ( p(k) ) คือกำลังของ 2 ที่มากที่สุดที่หาร ( k ) ลงตัว
  • ตัวอย่าง: ( p(6) = p(110_2) = 2 ) (ตำแหน่งที่ 6 เก็บผลรวม 2 ตัวล่าสุด)
  • ตัวอย่าง: ( p(8) = p(1000_2) = 8 ) (ตำแหน่งที่ 8 เก็บผลรวม 8 ตัวล่าสุด)

แหล่งอ้างอิง: https://dev.to/wengao_ye/fenwick-tree-vs-segment-tree-410k

Slide 5: การ Query และ Update ใน BIT

Query & Update ( O(\log n) )

Prefix Sum จนถึง ( k )

ใช้การเดินย้อนกลับ:

while (k > 0) {
  s += tree[k];
  k -= k & -k;
}

Update ตำแหน่ง ( k )

ใช้การเดินไปข้างหน้า:

while (k <= n) {
  tree[k] += val;
  k += k & -k;
}

แหล่งอ้างอิง: https://www.geeksforgeeks.org/binary-indexed-tree-or-fenwick-tree-2/

Slide 6: เข้าสู่ Segment Tree

ทำไมต้อง Segment Tree?

ในขณะที่ BIT เด่นเรื่องความง่ายและเร็วในงานผลรวม Segment Tree ให้ความยืดหยุ่นกว่า:

  • รองรับการ Query ได้หลายรูปแบบ: Min, Max, GCD, Sum
  • รองรับการทำ Range Update ได้ด้วยเทคนิค Lazy Propagation
  • โครงสร้างเป็น Binary Tree ที่สมบูรณ์ ทำให้เข้าใจขั้นตอนการแบ่งช่วงข้อมูลได้ง่าย
  • ใช้เวลา ( O(\log n) ) ต่อหนึ่ง Operation เช่นกัน

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

Slide 7: โครงสร้างของ Segment Tree

Hierarchy of Ranges

  • Leaf Nodes: เก็บค่าดั้งเดิมของ Array ในแต่ละตำแหน่ง
  • Internal Nodes: เก็บผลลัพธ์ของฟังก์ชัน (เช่น ผลรวม) ของลูกซ้ายและลูกขวา
  • หากข้อมูลมีขนาด ( n ) ต้นไม้จะมีโหนดทั้งหมดประมาณ ( 2n ) (ในทางปฏิบัติมักจอง ( 4n ))
  • แต่ละชั้นของต้นไม้จะลดขนาดช่วงลงครึ่งหนึ่ง จนถึงโหนดเดี่ยว (Leaf)

แหล่งอ้างอิง: https://www.geeksforgeeks.org/segment-tree-set-1-sum-of-given-range/

Slide 8: การหาคำตอบด้วย Segment Tree Query

Efficient Range Querying

การ Query ช่วง ( [a, b] ) จะแบ่งโหนดออกเป็น 3 กรณี:

  1. ช่วงของโหนดอยู่นอกช่วงที่ Query: Return ค่าว่าง/Neutral
  2. ช่วงของโหนดอยู่ภายในช่วงที่ Query ทั้งหมด: ใช้ค่าจากโหนดนั้นทันที
  3. ช่วงของโหนดทับซ้อนบางส่วน: แบ่งไป Query ลูกซ้ายและลูกขวา แล้วนำมารวมกัน

ใช้เวลาเพียง ( O(\log n) ) เนื่องจากแต่ละระดับจะเลือกโหนดไม่เกิน 2 โหนด

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

Slide 9: การ Update ข้อมูลใน Segment Tree

Point Update Strategy

เมื่อต้องการเปลี่ยนค่าใน Array ที่ตำแหน่ง ( i ):

  • เริ่มจาก Update ที่ Leaf Node ที่เก็บค่าตำแหน่ง ( i )
  • จากนั้น Update Parent ย้อนกลับขึ้นไปจนถึง Root
  • เนื่องจากความสูงของต้นไม้คือ ( \lceil \log n \rceil ) การแก้ไขจึงใช้เวลาเพียง ( O(\log n) )
  • ค่าในโหนดพ่อแม่จะถูกคำนวณใหม่จากลูกที่เพิ่งเปลี่ยนค่าไป

แหล่งอ้างอิง: https://www.geeksforgeeks.org/segment-tree-set-1-range-minimum-query/

Slide 10: สรุปและข้อเปรียบเทียบ

BIT vs Segment Tree

หัวข้อ Fenwick Tree (BIT) Segment Tree
ความซับซ้อน ( O(\log n) ) ( O(\log n) )
หน่วยความจำ ( n ) ( 2n ) ถึง ( 4n )
ความง่ายในการเขียน ง่ายมาก (กะทัดรัด) ซับซ้อนกว่าเล็กน้อย
การใช้งาน Range Sum เป็นหลัก ยืดหยุ่นสูง (Min/Max/RMQ)

แหล่งอ้างอิง: https://dev.to/wengao_ye/fenwick-tree-vs-segment-tree-410k

การ Update ข้อมูลแบบช่วงใน BIT

เราสามารถใช้ Fenwick Tree จัดการการ Update แบบช่วง (Range Update) และ Query รายตัว (Point Query) ได้ด้วย Difference Array:

  • สร้าง BIT เพื่อเก็บค่าความต่างระหว่างสมาชิกที่ติดกัน ( (d[i] = a[i] - a[i-1]) )
  • Range Update: การเพิ่มค่าในช่วง ( [a, b] ) ด้วย ( x ) ทำได้โดยการ Update BIT ที่ตำแหน่ง ( a ) ด้วย ( +x ) และตำแหน่ง ( b+1 ) ด้วย ( -x )
  • Point Query: ค่าจริงที่ตำแหน่ง ( k ) คือผลรวม Prefix Sum ( sumq(1, k) ) ใน BIT

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

Slide 12: Sparse Table สำหรับ Static RMQ

Sparse Table: Query ในเวลา ( O(1) )

หากข้อมูลไม่มีการ Update (Static) Sparse Table มีประสิทธิภาพสูงมากสำหรับการหาค่าน้อยสุดในช่วง (RMQ):

  • Preprocessing: ใช้เวลา ( O(n \log n) ) เพื่อคำนวณคำตอบของทุกช่วงที่มีความยาวเป็นกำลังของ 2
  • Query: ใช้เวลา ( O(1) ) โดยอาศัยสมบัติ Idempotent ของฟังก์ชัน ( \min(a, a) = a )
  • สูตรการ Query: ( \min(rmq(a, a+k-1), rmq(b-k+1, b)) ) โดยที่ ( k ) คือกำลังของ 2 ที่ใหญ่ที่สุดที่ไม่เกินความยาวช่วง

แหล่งอ้างอิง: https://www.geeksforgeeks.org/sparse-table/

Slide 13: Segment Tree: การ Implement แบบ Top-Down

Top-Down Recursive Implementation

การเขียนแบบ Recursive ช่วยให้จัดการช่วงข้อมูลที่ซับซ้อนได้ง่ายขึ้น:

int sum(int a, int b, int k, int x, int y) {
  if (b < x || a > y) return 0;
  if (a <= x && y <= b) return tree[k];
  int d = (x+y)/2;
  return sum(a, b, 2*k, x, d) + sum(a, b, 2*k+1, d+1, y);
}

ฟังก์ชันนี้จะแบ่งช่วง ( [x, y] ) ออกเป็นครึ่งซ้ายและขวา จนกว่าจะพบโหนดที่อยู่ในช่วง ( [a, b] ) พอดี

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

Slide 14: ปัญหา Range Update ใน Segment Tree

ความท้าทายของการ Update แบบช่วง

  • หากต้อง Update ทุกโหนดใบ (Leaf) ในช่วง ( [a, b] ) จะใช้เวลา ( O(n \log n) )
  • ซึ่งไม่มีประสิทธิภาพเพียงพอเมื่อมีการ Update บ่อยๆ
  • ทางออก: เทคนิค Lazy Propagation
  • หลักการคือ "เลื่อนการ Update ออกไปจนกว่าจะจำเป็นต้องใช้ข้อมูลในโหนดนั้นจริงๆ"

แหล่งอ้างอิง: https://www.geeksforgeeks.org/lazy-propagation-in-segment-tree/

Slide 15: Lazy Propagation: กลไกการทำงาน

Lazy Propagation ( O(\log n) )

  • เมื่อ Update ช่วงที่ตรงกับโหนดพอดี ให้เก็บค่า Update ไว้ใน Lazy Array และ Update ค่าสรุปของโหนดนั้นทันที
  • ไม่ต้องเดินลงไป Update ลูกจนกว่าจะมีการ Query หรือ Update ครั้งใหม่ผ่านโหนดนั้น
  • เมื่อต้องเข้าถึงโหนดที่มีค่าค้างอยู่ใน Lazy Array ให้ทำการ Push ค่าลงไปยังลูกซ้ายและขวาก่อนประมวลผล

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

Slide 16: Segment Tree สำหรับ Query หลากหลายรูปแบบ

ความยืดหยุ่นของ Segment Tree

เราสามารถประยุกต์ใช้ Segment Tree กับ Operation ใดๆ ที่มีสมบัติการเปลี่ยนกลุ่ม (Associative):

Range GCD: หา ห.ร.ม. ของเลขในช่วง
Range XOR: หาค่า XOR รวมในช่วง
Range Max Subarray Sum: หาผลรวมสูงสุดของ Subarray
Range Bitwise AND/OR: การจัดการระดับบิต

แหล่งอ้างอิง: https://dev.to/wengao_ye/fenwick-tree-vs-segment-tree-410k

Slide 17: Dynamic Segment Tree: ประหยัดหน่วยความจำ

Dynamic Segment Tree

ใช้ในกรณีที่ช่วงข้อมูลมีขนาดใหญ่มาก (เช่น ( 0 ) ถึง ( 10^9 )) แต่มีการ Update เพียงไม่กี่ตำแหน่ง

  • สร้างโหนดเฉพาะเมื่อมีการเข้าถึง (On-demand allocation)
  • ใช้ Pointers (หรือ Dynamic Array) แทนการใช้ Array ขนาดคงที่ ( 4n )
  • ประหยัดพื้นที่จาก ( O(n) ) เหลือเพียง ( O(k \log n) ) เมื่อ ( k ) คือจำนวนการ Update

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

Slide 18: Persistent Segment Tree

การเก็บประวัติด้วย Persistence

เทคนิคการสร้างโครงสร้างข้อมูลที่สามารถ Query ข้อมูลในอดีตได้ (Version Control):

  • เมื่อมีการ Update ตำแหน่งหนึ่ง เราจะไม่ทับข้อมูลเดิม แต่จะสร้าง เส้นทางใหม่ (New Path) จากโหนดใบขึ้นไปถึง Root ใหม่
  • โหนดที่ไม่เกี่ยวข้องกับการ Update จะใช้ลูกร่วมกับเวอร์ชันเดิม (Path Copying)
  • แต่ละการ Update เพิ่มโหนดใหม่เพียง ( O(\log n) )

แหล่งอ้างอิง: https://usaco.guide/adv/persistent-seg-tree

Slide 19: Two-Dimensional Segment Tree

2D Segment Tree: ตาราง 2 มิติ

ใช้จัดการปัญหา Query บนพื้นที่สี่เหลี่ยม (Rectangular Subarray Sum/Min):

  • เป็นโครงสร้างแบบ Segment Tree ซ้อน Segment Tree
  • ต้นไม้หลักจัดการช่วงของแถว (Rows) และในแต่ละโหนดของแถวจะเก็บ Segment Tree ของคอลัมน์ (Columns)
  • ความซับซ้อนในการ Query และ Update คือ ( O(\log n \cdot \log m) )
  • การ Implement ค่อนข้างซับซ้อนและใช้หน่วยความจำสูง ( (O(nm)) )

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

Slide 20: การประยุกต์ใช้กับโจทย์บนต้นไม้ (Tree Queries)

Subtree Queries & Path Queries

การเปลี่ยนโครงสร้างต้นไม้ให้เป็นช่วงของ Array:

  • Euler Tour / Tree Traversal Array: ใช้ DFS เพื่อบันทึกลำดับการเข้าถึงโหนด
  • แต่ละ Subtree จะกลายเป็น ช่วง (Range) ที่ต่อเนื่องกันใน Array ใหม่
  • หลังจากแปลงแล้ว เราสามารถใช้ BIT หรือ Segment Tree ปกติจัดการโจทย์บนต้นไม้ได้ในเวลา ( O(\log n) )
"เกือบทุกปัญหาบนต้นไม้ที่เกี่ยวกับช่วง สามารถแก้ได้ด้วยการ Flatten ต้นไม้ลงมาเป็น Array"

แหล่งอ้างอิง: https://usaco.guide/graphs/subtree-queries

Bottom-Up Implementation

การเขียนแบบ Iterative มักจะเร็วกว่าและใช้หน่วยความจำน้อยกว่าแบบ Recursive:

int sum(int a, int b) {
  a += n; b += n;
  int s = 0;
  while (a <= b) {
    if (a % 2 == 1) s += tree[a++];
    if (b % 2 == 0) s += tree[b--];
    a /= 2; b /= 2;
  }
  return s;
}

เทคนิคนี้ใช้การเลื่อนตำแหน่ง (a) และ (b) ขึ้นไปยังโหนดพ่อแม่ใน Array ขนาด ( 2n ) จนกว่าช่วงจะบรรจบกัน

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

Square Root Decomposition

หากไม่อยากใช้ Tree เราสามารถแบ่ง Array ออกเป็นบล็อก (Blocks):

  • แบ่ง Array ขนาด ( n ) ออกเป็นบล็อก บล็อกละ ( \sqrt{n} ) ตัว
  • แต่ละบล็อกจะเก็บค่าสรุป (เช่น ผลรวม) ของสมาชิกในบล็อกนั้น
  • การ Update: แก้ไขสมาชิกตัวเดียวและ Update ค่าบล็อก ใช้เวลา ( O(1) )
  • การ Query: คำนวณจากบล็อกที่อยู่ระหว่างช่วง และบวกเพิ่มสมาชิกที่เหลือที่ขอบ ใช้เวลา ( O(\sqrt{n}) )

แหล่งอ้างอิง: https://www.geeksforgeeks.org/sqrt-decomposition-set-1-introduction/

Mo's Algorithm

ใช้สำหรับปัญหา Range Query ที่ข้อมูล ไม่มีการ Update และสามารถเรียงลำดับการประมวลผล Query ได้ (Offline):

  • จัดกลุ่ม Query ตามบล็อกของจุดเริ่มต้น (Left endpoint) และเรียงตามจุดจบ (Right endpoint)
  • เคลื่อนย้ายช่วง (Active Range) ทีละขั้นเพื่อหาคำตอบของ Query ถัดไป
  • ความซับซ้อนรวมคือ ( O((n+q)\sqrt{n}) )
  • มีประโยชน์มากในปัญหาที่ Segment Tree แก้ได้ยาก เช่น การนับจำนวนตัวเลขที่แตกต่างกันในช่วง

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

Index Compression

แก้ปัญหาเมื่อพิกัดข้อมูลมีค่ามากเกินกว่าที่จะจอง Array (เช่น ( 10^9 )):

  • รวบรวมค่าพิกัด (Coordinates) ทั้งหมดที่ปรากฏในโจทย์
  • นำมาเรียงลำดับและกำจัดตัวซ้ำ
  • แทนที่ค่าจริงด้วย "ลำดับที่ (Rank)" จากการเรียงลำดับ (0, 1, 2, ...)
  • ทำให้เราสามารถใช้พิกัดเหล่านี้ใน BIT หรือ Segment Tree ขนาด ( O(n) ) ได้

แหล่งอ้างอิง: https://usaco.guide/general/coords-comp

Example: Range XOR Queries

โจทย์: ต้องการหาค่า ( a_L \oplus a_{L+1} \oplus \dots \oplus a_R ) และมีการ Update ค่า

  • เนื่องจาก XOR มีสมบัติการเปลี่ยนกลุ่ม: ( (x \oplus y) \oplus z = x \oplus (y \oplus z) )
  • เราสามารถเก็บค่า XOR รวมไว้ในโหนดของ Segment Tree ได้
  • การรวมผลลัพธ์จากลูกซ้ายและขวา: `tree[k] = tree[2*k] ^ tree[2*k+1]`
  • ฟังก์ชัน Query และ Update ใช้โครงสร้างเดียวกับ Range Sum ทุกประการ

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

RMQ with Updates

  • Internal Node: เก็บค่าที่น้อยที่สุดในกลุ่มลูก `min(left_child, right_child)`
  • Update: เมื่อเปลี่ยนค่าที่ใบ ต้องไล่ Update ค่า ( \min ) ขึ้นไปใหม่
  • Neutral Value: ในการ Query ให้ใช้ค่า ( \infty ) (Infinity) เป็นค่าเริ่มต้นเพื่อให้ ( \min(x, \infty) = x )
  • ประสิทธิภาพ: ( O(\log n) ) เหมาะสำหรับโจทย์ที่ BIT ทำได้ลำบาก

แหล่งอ้างอิง: https://www.geeksforgeeks.org/segment-tree-set-1-range-minimum-query/

Binary Search on Segment Tree

เราสามารถใช้โครงสร้างของต้นไม้ช่วยทำ Binary Search ได้ในเวลา ( O(\log n) ):

  • หาก Segment Tree เก็บจำนวนสมาชิกในแต่ละช่วง (Frequency)
  • เมื่อต้องการหาตัวที่ ( k ): ดูที่ลูกซ้าย ถ้าลูกซ้ายมีสมาชิก ( \ge k ) ตัว ให้เดินลงไปทางซ้าย
  • ถ้าสมาชิกน้อยกว่า ให้หักจำนวนนั้นออกจาก ( k ) แล้วเดินลงไปทางขวา
  • เทคนิคนี้นิยมใช้หา Order Statistics ในแบบ Dynamic

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

Persistence Concepts

โจทย์บางประเภทต้องการ "ย้อนเวลา" หรือ Query ข้อมูลในอดีต:

  • Path Copying: แทนที่จะแก้ไขโหนดเดิม เราสร้างโหนดใหม่เฉพาะในเส้นทางจาก Root ถึงใบที่เปลี่ยนไป
  • โหนดใหม่จะเชื่อมต่อกับลูกของโหนดเดิมในส่วนที่ไม่มีการเปลี่ยนแปลง
  • ทำให้เราได้ Root ใหม่ที่เป็นตัวแทนของ Version ใหม่ โดยใช้พื้นที่เพิ่มเพียง ( O(\log n) ) ต่อการ Update

แหล่งอ้างอิง: https://usaco.guide/adv/persistent-seg-tree

Implementation Tips

  • Array Size: สำหรับ Segment Tree จอง ( 4n ) เพื่อป้องกัน Index Out of Bounds ในกรณีทั่วไป
  • Base Case: ตรวจสอบเงื่อนไขช่วงทับซ้อน (Overlap) ให้แม่นยำเพื่อเลี่ยง Infinite Loop
  • BIT Index: Fenwick Tree มักใช้ 1-based indexing ดังนั้นต้องระวังการใช้ `index 0`
  • Data Type: ผลรวมช่วงอาจเกินค่าของ `int` ควรพิจารณาใช้ `long long` ใน C++

แหล่งอ้างอิง: https://dev.to/wengao_ye/fenwick-tree-vs-segment-tree-410k

Morning Session Wrap-up

เราได้เรียนรู้เครื่องมือที่ทรงพลังที่สุดในการจัดการช่วงข้อมูล:

Fenwick Tree (BIT)
Segment Tree
Lazy Propagation
Offline Algorithms

"ช่วงบ่ายเราจะนำโครงสร้างเหล่านี้ไปใช้ร่วมกับ Greedy และ Divide & Conquer เพื่อหาคำตอบที่ดีที่สุด!"

พักกลางวัน: 12.00 - 13.00 น.