DSU กับการบีบอัดเส้นทาง
นำ find ที่มีการบีบอัดเส้นทางไปใช้งาน เพื่อให้โหนดทุกตัวบนเส้นทางชี้ตรงไปยังราก และทำให้ find มีเวลาเฉลี่ยตัดจำหน่ายใกล้เคียง O(1)
DSU กับการบีบอัดเส้นทาง เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
DSU คืออะไร
การรวมเซตที่ไม่ทับซ้อนกัน (DSU) หรือที่เรียกว่าการรวมและค้นหา เป็นโครงสร้างข้อมูลที่ดูแลกลุ่มของเซตที่ไม่ทับซ้อนกัน โดยรองรับการดำเนินการหลักสองอย่าง: find (องค์ประกอบ x อยู่ในเซตใด) และ union (รวมเซตที่มี x และ y อยู่เข้าด้วยกัน) DSU เหมาะอย่างยิ่งสำหรับปัญหาการเชื่อมต่อแบบพลวัต ซึ่งกลุ่มต่าง ๆ จะถูกรวมเข้าด้วยกันตามเวลาแต่ไม่เคยถูกแยกออก
องค์ประกอบทุกตัวจะเริ่มต้นเป็นเซตของตัวเอง เมื่อประมวลผลเส้นเชื่อมหรือความสัมพันธ์ เราจะรวมเซตต่าง ๆ เข้าด้วยกัน ความท้าทายคือการทำสิ่งนี้ให้มีประสิทธิภาพ — การนำไปใช้แบบตรงไปตรงมาใช้เวลา O(n) ต่อการดำเนินการ แต่เมื่อใช้การปรับปรุงประสิทธิภาพ เราจะเข้าใกล้ O(1) เมื่อคิดต้นทุนเฉลี่ย
# Naive DSU without optimisations
class DSU:
def __init__(self, n):
self.parent = list(range(n)) # each node is its own parent
def find(self, x):
while self.parent[x] != x:
x = self.parent[x]
return x
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px != py:
self.parent[px] = pyปัญหาของการดำเนินการ find แบบพื้นฐาน
ใน DSU แบบพื้นฐาน find(x) จะเดินขึ้นไปตามสายโซ่โหนดแม่จนถึงโหนดที่ชี้มายังตัวเอง (หรือ ราก) หากต้นไม้สมดุล การดำเนินการนี้จะใช้เวลา O(log n) แต่หากเรา union โดยให้รากที่สองชี้ไปอยู่ใต้รากแรกเสมอ เราอาจสร้าง สายโซ่ (ต้นไม้เสื่อมสภาพ) ที่มีความยาว n ได้ ทำให้การดำเนินการ find แต่ละครั้งใช้เวลา O(n)
ลองพิจารณาการ union 0→1→2→3→4 ตามลำดับ การเรียก find ของโหนด 0 ต้องเดินผ่านสายโซ่ทั้งหมด ด้วย การบีบอัดเส้นทาง เราจะแก้ปัญหานี้โดยทำให้ทุกโหนดที่ผ่านชี้ตรงไปยังรากระหว่างการดำเนินการ find เอง
# Worst case without compression: a chain
# parent = [1, 2, 3, 4, 4] => find(0) takes 4 steps
# After path compression: parent = [4, 4, 4, 4, 4] => find(0) takes 1 step
parent = [1, 2, 3, 4, 4]
print('Before:', parent)
# Simulate find(0) with naive approach
x = 0
steps = 0
while parent[x] != x:
x = parent[x]
steps += 1
print('Root:', x, 'Steps taken:', steps)การบีบอัดเส้นทาง: แบบเวียนเกิดเที่ยวเดียว
การบีบอัดเส้นทาง จะปรับการดำเนินการ find โดยหลังจากพบรากแล้ว จะปรับโหนดทุกโหนดบนเส้นทางให้ชี้ ตรง ไปยังราก การเรียก find ครั้งต่อไปสำหรับโหนดเหล่านั้นจึงใช้เวลา O(1) ส่วนแบบเวียนเกิดทำสิ่งนี้ได้อย่างกระชับภายในการเดินเพียงเที่ยวเดียว
แนวคิดสำคัญคือ หลังจากการเรียกแบบเวียนเกิดคืนค่ารากแล้ว เรากำหนด self.parent[x] = root ก่อนคืนค่า การทำเช่นนี้จะ ทำให้ต้นไม้แบนลง — โหนดทั้งหมดบนเส้นทางค้นหาจะชี้ตรงไปยังราก การดำเนินการนี้ไม่ได้เปลี่ยนว่าโหนดนั้นอยู่ในเซตใด แต่เพียงทำให้เส้นทางค้นหาในอนาคตสั้นลง
class DSU:
def __init__(self, n):
self.parent = list(range(n))
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # path compression
return self.parent[x]
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px != py:
self.parent[px] = py
dsu = DSU(5)
dsu.union(0, 1)
dsu.union(1, 2)
dsu.union(2, 3)
print('Root of 0:', dsu.find(0))
print('Parent array after compression:', dsu.parent)การบีบอัดเส้นทาง: แบบวนซ้ำสองเที่ยว
การบีบอัดเส้นทางแบบวนซ้ำใช้ การเดินสองเที่ยว: เที่ยวแรกเดินขึ้นไปเพื่อหาราก ส่วนเที่ยวที่สองย้อนกลับไปยังโหนดทุกโหนดในเส้นทางและปรับโหนดแม่ให้ชี้ตรงไปยังราก วิธีนี้หลีกเลี่ยงค่าใช้พื้นที่ของสแตกการเรียกแบบเวียนเกิด และปลอดภัยสำหรับต้นไม้ที่ลึกมากซึ่งมีความลึกใกล้ขีดจำกัดการเรียกแบบเวียนเกิดของ Python
ทั้งแบบเวียนเกิดและแบบวนซ้ำยังคงมี ความถูกต้อง เหมือนเดิม — find ยังคงคืนค่ารากเดิม ความแตกต่างเพียงอย่างเดียวคือมีการปรับตัวชี้โหนดแม่เป็นผลข้างเคียง ซึ่งทำให้การเรียก find ในอนาคตสำหรับโหนดเหล่านั้นใช้เวลา O(1)
class DSU:
def __init__(self, n):
self.parent = list(range(n))
def find(self, x):
root = x
while self.parent[root] != root:
root = self.parent[root] # first pass: find root
while self.parent[x] != root:
nxt = self.parent[x]
self.parent[x] = root # second pass: compress
x = nxt
return root
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px != py:
self.parent[px] = py
return True
return False # already connected
dsu = DSU(6)
for a, b in [(0,1),(1,2),(2,3),(3,4)]:
dsu.union(a, b)
print('Parent before find(0):', dsu.parent[:])
dsu.find(0)
print('Parent after find(0):', dsu.parent[:])ความซับซ้อนแบบเฉลี่ยตัดจำหน่ายของการบีบอัดเส้นทาง
การบีบอัดเส้นทางเพียงอย่างเดียวทำให้มีเวลาแบบเฉลี่ยตัดจำหน่าย O(log n) ต่อการดำเนินการหนึ่งครั้งตลอดลำดับการดำเนินการ m ครั้ง การดำเนินการ find แต่ละครั้งอาจใช้เวลามากในครั้งแรกที่เดินผ่านสายโซ่ แต่จะทำให้สายโซ่นั้นแบนลง ดังนั้นการเรียก find ครั้งถัดไปบนโหนดเหล่านั้นจะใช้เวลา O(1) งานทั้งหมดจึงถูกเฉลี่ยกระจายไปตามการดำเนินการจำนวนมาก
การวิเคราะห์อย่างเป็นทางการใช้ วิธีฟังก์ชันศักย์: ศักย์ของ DSU จะลดลงทุกครั้งที่โหนดมีโหนดแม่อยู่ใกล้ขึ้น และการลดลงนี้จะชดเชยต้นทุนการเดินผ่าน หากไม่มี union ตามอันดับ การบีบอัดเส้นทางเพียงอย่างเดียวจะให้เวลาแบบเฉลี่ยตัดจำหน่าย O(log n) ซึ่งดีขึ้นอย่างมากจาก O(n) แบบพื้นฐาน
# Demonstrating amortised benefit
import time
def build_chain(n):
parent = list(range(n))
for i in range(n - 1):
parent[i] = i + 1 # chain: 0->1->2->...->n-1
return parent
n = 1000
parent = build_chain(n)
# First find on a chain: visits n nodes
x = 0
root = x
while parent[root] != root:
root = parent[root]
# Compress
while parent[x] != root:
nxt = parent[x]; parent[x] = root; x = nxt
print('After first find, parent[0]:', parent[0]) # should be n-1
print('Second find cost: O(1) since parent[0] is now the root')จำนวนองค์ประกอบที่เชื่อมต่อกัน
การประยุกต์ใช้ DSU ที่พบได้บ่อยคือการนับ องค์ประกอบที่เชื่อมต่อกัน ในกราฟ เราเริ่มต้นตัวนับ components ให้มีค่าเท่ากับ n (หนึ่งค่าต่อหนึ่งโหนด) การ union ที่สำเร็จแต่ละครั้ง (การรวมสองเซตที่แตกต่างกัน) จะลดตัวนับลง 1 เมื่อจบการทำงาน ตัวนับจะมีค่าเท่ากับจำนวนองค์ประกอบที่แตกต่างกัน
วิธีนี้มีประสิทธิภาพมากกว่าการเรียก BFS หรือ DFS เพื่อสอบถามการเชื่อมต่อ โดยเฉพาะเมื่อมีเส้นเชื่อมเข้ามาทีละรายการ (แบบออนไลน์) DSU ประมวลผลเส้นเชื่อมแต่ละเส้นด้วยเวลาแบบเฉลี่ยตัดจำหน่ายเกือบ O(1) โดยไม่ขึ้นกับว่าเส้นเชื่อมนั้นเข้ามาเมื่อใด
class DSU:
def __init__(self, n):
self.parent = list(range(n))
self.components = n
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px == py:
return False
self.parent[px] = py
self.components -= 1
return True
dsu = DSU(7)
edges = [(0,1),(1,2),(3,4),(5,6)]
for u, v in edges:
dsu.union(u, v)
print('Components:', dsu.components) # 4: {0,1,2}, {3,4}, {5,6}, {6 alone was merged}
# Node 6 is alone => 4 total: {0,1,2},{3,4},{5,6},{6} wait
# Let me recalculate: 7 nodes, 4 edges merged 4 pairs => 7-4=3... no
# {0,1,2} one union, {3,4} one, {5,6} one => 7-3=4 components
print('Expected: 4')DSU สำหรับปัญหากราฟ: จำนวนจังหวัด
ปัญหา จำนวนจังหวัด ให้เมทริกซ์การเชื่อมโยงขนาด n×n และถามว่ามีกลุ่มเมืองที่เชื่อมต่อกันโดยตรงหรือโดยอ้อมอยู่กี่กลุ่ม นี่เป็นปัญหาองค์ประกอบที่เชื่อมต่อกันโดยตรง ซึ่ง DSU แก้ได้อย่างเป็นระเบียบ เราวนพิจารณาคู่ทั้งหมด (i, j) ที่ isConnected[i][j] == 1 และเรียก union(i, j)
หลังประมวลผลการเชื่อมต่อทั้งหมดแล้ว dsu.components คือคำตอบ วิธีนี้ง่ายและเร็วกว่าการเรียก BFS จากทุกโหนดที่ยังไม่เคยเยี่ยมชม และจัดการตัวแทนแบบเมทริกซ์ได้โดยตรงโดยไม่ต้องสร้างรายการการเชื่อมโยงก่อน
def find_provinces(isConnected):
n = len(isConnected)
parent = list(range(n))
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
def union(x, y):
px, py = find(x), find(y)
if px != py:
parent[px] = py
return True
return False
count = n
for i in range(n):
for j in range(i + 1, n):
if isConnected[i][j] == 1:
if union(i, j):
count -= 1
return count
matrix = [[1,1,0],[1,1,0],[0,0,1]]
print(find_provinces(matrix)) # 2: cities {0,1} and {2}รูปแบบการบีบอัดเส้นทาง: การลดครึ่ง
นอกเหนือจากการบีบอัดแบบสองเที่ยวแล้ว ยังมีรูปแบบแบบเที่ยวเดียวที่ง่ายกว่า เรียกว่า การลดครึ่งของเส้นทาง: ขณะเดินขึ้นไปตามสายโซ่ เราจะทำให้แต่ละโหนดชี้ไปยัง โหนดปู่ แทนที่จะชี้ไปยังโหนดแม่ วิธีนี้ลดความยาวของเส้นทางลงครึ่งหนึ่งในการเดินแต่ละครั้งโดยไม่ต้องเดินเที่ยวที่สอง และเมื่อใช้ร่วมกับ union ตามอันดับ จะมีความซับซ้อนแบบเฉลี่ยตัดจำหน่าย O(alpha(n)) เช่นเดียวกัน
การลดครึ่งของเส้นทางมักเป็นที่นิยมในการเขียนโปรแกรมแข่งขัน เพราะเป็นลูปเดียวที่กระชับและไม่มีการเรียกแบบเวียนเกิดหรือการเดินซ้ำอีกเที่ยว แต่ละขั้นตอนทำ self.parent[x] = self.parent[self.parent[x]]; x = self.parent[x]
class DSUHalving:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x):
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]] # point to grandparent
x = self.parent[x]
return x
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px == py:
return False
if self.rank[px] < self.rank[py]:
px, py = py, px
self.parent[py] = px
if self.rank[px] == self.rank[py]:
self.rank[px] += 1
return True
dsu = DSUHalving(8)
for u, v in [(0,1),(2,3),(4,5),(6,7),(0,2),(4,6),(0,4)]:
dsu.union(u, v)
print('All in one component:', dsu.find(0) == dsu.find(7))การตรวจสอบการเชื่อมต่อหลังการ union
หากต้องการตรวจสอบว่าโหนดสองโหนด connected (อยู่ในองค์ประกอบเดียวกัน) หรือไม่ ให้เรียก find(x) == find(y) หากทั้งสองการเรียกคืนค่ารากเดียวกัน แสดงว่าโหนดทั้งสองอยู่ในองค์ประกอบเดียวกัน นี่คือการสอบถามแบบ connected และเมื่อใช้การบีบอัดเส้นทาง จะใช้เวลาแบบเฉลี่ยตัดจำหน่ายเกือบ O(1)
ในโจทย์สัมภาษณ์ การสอบถามการเชื่อมต่อมักสลับปะปนกับการดำเนินการ union DSU รองรับทั้งสองอย่างแบบออนไลน์ — คุณสามารถสลับทำ union และการสอบถามได้ตามลำดับใดก็ได้ จุดนี้ทำให้ DSU แตกต่างจากอัลกอริทึมกราฟแบบสถิติเช่น BFS/DFS ซึ่งต้องทำงานใหม่หลังโครงสร้างมีการเปลี่ยนแปลงแต่ละครั้ง
class DSU:
def __init__(self, n):
self.parent = list(range(n))
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px != py:
self.parent[px] = py
def connected(self, x, y):
return self.find(x) == self.find(y)
dsu = DSU(10)
dsu.union(0, 3)
dsu.union(3, 7)
dsu.union(1, 5)
print(dsu.connected(0, 7)) # True: 0-3-7
print(dsu.connected(0, 5)) # False: different components
print(dsu.connected(1, 5)) # True: 1-5ข้อผิดพลาดที่พบบ่อยในการเขียน DSU
ข้อผิดพลาดที่พบบ่อยคือการเรียก find แล้วแก้ไข parent อย่างไม่ถูกต้อง ควรเรียก find กับสมาชิกทั้งสองตัว ก่อน ตรวจสอบความเท่ากันเสมอ มิฉะนั้นคุณอาจเปรียบเทียบโหนดกับรากของตัวเองอย่างไม่ถูกต้อง อีกข้อผิดพลาดคือการลืมว่า union ควรไม่ทำอะไรเมื่อสมาชิกทั้งสองตัวมีรากเดียวกันอยู่แล้ว
ใน Python ขีดจำกัดความลึกของการเรียกแบบเวียนเกิด (ค่าเริ่มต้น 1000) อาจทำให้เกิด RecursionError เมื่อมีสายโซ่ขนาดใหญ่และใช้ find แบบเวียนเกิด คุณอาจใช้แบบวนซ้ำสองเที่ยว เพิ่มขีดจำกัดด้วย sys.setrecursionlimit หรือใช้การลดครึ่งของเส้นทางแบบวนซ้ำเพื่อหลีกเลี่ยงการเรียกแบบเวียนเกิดที่ลึกโดยสิ้นเชิง
import sys
sys.setrecursionlimit(10000) # needed for large recursive DSU
class DSU:
def __init__(self, n):
self.parent = list(range(n))
def find(self, x):
# Safe iterative path compression
root = x
while self.parent[root] != root:
root = self.parent[root]
while self.parent[x] != root:
nxt = self.parent[x]
self.parent[x] = root
x = nxt
return root
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px == py:
return False # already same component — do nothing
self.parent[px] = py
return True
dsu = DSU(5)
print(dsu.union(0, 1)) # True: merged
print(dsu.union(0, 1)) # False: already merged — no double-countingการติดตามขนาดของ DSU
ในบางปัญหา คุณต้องทราบ ขนาด ของแต่ละองค์ประกอบ ไม่ใช่เพียงรากขององค์ประกอบนั้น ให้เพิ่มอาร์เรย์ size ที่เริ่มต้นเป็น 1 ทุกตำแหน่ง เมื่อรวมองค์ประกอบสององค์ประกอบ ให้บวกขนาดของรากที่เล็กกว่าเข้ากับรากที่ใหญ่กว่า วิธีนี้ทำให้สอบถามขนาดองค์ประกอบได้ในเวลา O(1) หลังการ union ใด ๆ
การติดตามขนาดยังเป็นพื้นฐานของ union ตามขนาด (ทางเลือกอื่นนอกเหนือจาก union ตามอันดับ): ให้เชื่อมต้นไม้ที่เล็กกว่าไว้ใต้รากของต้นไม้ที่ใหญ่กว่าเสมอ วิธีนี้รับประกันว่าความสูงของต้นไม้จะอยู่ที่ O(log n) และให้การรับประกันเชิงอสมมาตรเช่นเดียวกับ union ตามอันดับ
class DSUWithSize:
def __init__(self, n):
self.parent = list(range(n))
self.size = [1] * n
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px == py:
return
if self.size[px] < self.size[py]:
px, py = py, px # attach smaller under larger
self.parent[py] = px
self.size[px] += self.size[py]
def get_size(self, x):
return self.size[self.find(x)]
dsu = DSUWithSize(6)
for u, v in [(0,1),(1,2),(3,4)]:
dsu.union(u, v)
print('Size of component containing 0:', dsu.get_size(0)) # 3
print('Size of component containing 3:', dsu.get_size(3)) # 2
print('Size of component containing 5:', dsu.get_size(5)) # 1ตรวจสอบความเข้าใจ
ทดสอบความเข้าใจแนวคิดด้านโครงสร้างข้อมูล & อัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
ทบทวนบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า DSU รักษาเซตที่ไม่ทับซ้อนกันด้วยการดำเนินการ find และ union การบีบอัดเส้นทางทำให้ต้นไม้แบนลงโดยให้โหนดทั้งหมดที่เดินผ่านชี้ตรงไปยังราก และ ทำให้ประสิทธิภาพของ find แบบเฉลี่ยตัดจำหน่ายเกือบเป็น O(1) บทถัดไปเราจะศึกษา union ตามอันดับ ซึ่งรักษาต้นไม้ให้ตื้นจากด้านบนลงมาเพื่อให้ได้ขอบเขตแบบฟังก์ชันแอคเคอร์มันน์ผกผัน
คำถามที่พบบ่อย
บทเรียน “DSU กับการบีบอัดเส้นทาง” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “DSU กับการบีบอัดเส้นทาง” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “DSU กับการบีบอัดเส้นทาง”
นำ find ที่มีการบีบอัดเส้นทางไปใช้งาน เพื่อให้โหนดทุกตัวบนเส้นทางชี้ตรงไปยังราก และทำให้ find มีเวลาเฉลี่ยตัดจำหน่ายใกล้เคียง O(1) คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน
บทเรียน “DSU กับการบีบอัดเส้นทาง” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม
ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- DSU กับการบีบอัดเส้นทาง
- การรวมตามอันดับและขอบเขตอินเวอร์สแอกเคอร์มันน์
- การเชื่อมโยงเกินจำเป็นและการตรวจจับวงจร
- การรวมบัญชีและองค์ประกอบเชื่อมโยง