แม่แบบการแบ่งและพิชิต
สกัดแม่แบบสามขั้นตอน ได้แก่ แบ่ง พิชิต และรวม จากการเรียงลำดับแบบผสาน แล้วนำไปใช้กับรูปแบบปัญหาใหม่อย่างเป็นระบบ
แม่แบบการแบ่งและพิชิต เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
การแบ่งแยกและพิชิตคืออะไร
การแบ่งแยกและพิชิต (D&C) แก้ปัญหาด้วยการแบ่งออกเป็น ปัญหาย่อยที่เป็นอิสระต่อกัน และมีชนิดเดียวกัน จากนั้นแก้แต่ละปัญหาด้วยการเรียกซ้ำ แล้วรวมคำตอบเข้าด้วยกัน คำสำคัญคือ เป็นอิสระต่อกัน — ปัญหาย่อยไม่ใช้สถานะร่วมกัน ต่างจาก DP ที่ปัญหาย่อยทับซ้อนกัน ตัวอย่างคลาสสิก ได้แก่ การเรียงลำดับแบบผสาน การค้นหาแบบทวิภาค การเรียงลำดับแบบเร็ว จุดคู่ที่ใกล้กันที่สุด และการคูณเมทริกซ์อย่างรวดเร็ว โดยทั่วไปการแบ่งแยกและพิชิตทำเวลาได้ O(n log n) ผ่านแม่แบบสามขั้นตอน
# Divide and Conquer vs DP:
# D&C: sub-problems are INDEPENDENT (no overlap)
# DP: sub-problems OVERLAP (same sub-problem solved multiple times)
# D&C examples:
# Merge sort: split array in half, sort each, merge
# Binary search: check midpoint, recurse on one half
# Max subarray (D&C): find max in left half, right half, crossing
# Recurrence pattern:
# T(n) = 2T(n/2) + O(n) → O(n log n) [merge sort]
# T(n) = T(n/2) + O(1) → O(log n) [binary search]
# T(n) = T(n/k) + O(n) → O(n log_k n) [k-way split]แม่แบบสามขั้นตอน
อัลกอริทึมการแบ่งแยกและพิชิตทุกแบบทำตามสามขั้นตอน: (1) แบ่ง — แบ่งปัญหาออกเป็นปัญหาย่อยที่เล็กลงสองปัญหาหรือมากกว่า โดยทั่วไปแบ่งที่จุดกึ่งกลาง (2) พิชิต — แก้ปัญหาย่อยแต่ละปัญหาด้วยการเรียกซ้ำ กำหนดกรณีฐานเพื่อหยุดการเรียกซ้ำ โดยทั่วไปคือ n ≤ 1 (3) รวม — ผสานหรือรวมคำตอบของปัญหาย่อยให้เป็นคำตอบโดยรวม ความคิดสร้างสรรค์ทั้งหมดอยู่ที่ขั้นตอนการรวม ส่วนการแบ่งมักเป็นเพียงการแบ่งที่จุดกึ่งกลาง
def divide_and_conquer(arr, lo, hi):
# BASE CASE: trivial sub-problem
if lo >= hi:
return base_case_result(arr, lo, hi)
# DIVIDE: split at midpoint
mid = (lo + hi) // 2
# CONQUER: solve sub-problems recursively
left_result = divide_and_conquer(arr, lo, mid)
right_result = divide_and_conquer(arr, mid + 1, hi)
# COMBINE: merge results
return combine(left_result, right_result, arr, lo, mid, hi)
def base_case_result(arr, lo, hi): return arr[lo]
def combine(l, r, arr, lo, mid, hi): return max(l, r)การเรียงลำดับแบบผสานในฐานะตัวอย่างมาตรฐาน
การเรียงลำดับแบบผสานแสดงการแบ่งแยกและพิชิตได้อย่างสมบูรณ์แบบ: แบ่ง อาร์เรย์ที่จุดกึ่งกลาง พิชิต ด้วยการเรียงลำดับแต่ละครึ่งแบบเรียกซ้ำ รวม ด้วยการผสานครึ่งที่เรียงแล้วทั้งสองส่วนในเวลา O(n) ขั้นตอนการผสานคือส่วนที่ทำงานทั้งหมด สมการเวียนเกิดคือ T(n) = 2T(n/2) + O(n) ตามทฤษฎีบทหลักกรณีที่ 2: T(n) = O(n log n) นี่คือสมการเวียนเกิดของการแบ่งแยกและพิชิตที่สำคัญที่สุดที่ควรจดจำ
def merge_sort(arr):
# BASE CASE
if len(arr) <= 1:
return arr
# DIVIDE
mid = len(arr) // 2
# CONQUER
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
# COMBINE
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i]); i += 1
else:
result.append(right[j]); j += 1
return result + left[i:] + right[j:]
print(merge_sort([5, 3, 8, 1, 9, 2])) # [1,2,3,5,8,9]ข้อมูลอ้างอิงทฤษฎีบทหลักอย่างรวดเร็ว
ทฤษฎีบทหลักใช้แก้สมการเวียนเกิดรูปแบบ T(n) = aT(n/b) + f(n): กรณีที่ 1: f(n) = O(n^(log_b(a) - ε)) → T(n) = O(n^log_b(a)) กรณีที่ 2: f(n) = O(n^log_b(a)) → T(n) = O(n^log_b(a) × log n) กรณีที่ 3: f(n) = Ω(n^(log_b(a) + ε)) → T(n) = O(f(n)) การเรียงลำดับแบบผสาน: a=2, b=2, f(n)=O(n), n^log_2(2)=n → กรณีที่ 2 → O(n log n)
# Master Theorem quick examples:
# T(n) = 2T(n/2) + O(n) → a=2,b=2,f=n,n^log2(2)=n → Case2 → O(n log n)
# T(n) = 2T(n/2) + O(1) → a=2,b=2,f=1,n^1=n >> 1 → Case1 → O(n)
# T(n) = 2T(n/2) + O(n^2) → a=2,b=2,f=n^2,n^1 << n^2 → Case3 → O(n^2)
# T(n) = T(n/2) + O(1) → a=1,b=2,f=1,n^log2(1)=1=f → Case2 → O(log n)
# T(n) = T(n/3)+T(2n/3)+O(n) → Master doesn't apply directly → O(n log n) by recursion tree
recurrences = [
('Merge sort: 2T(n/2)+n', 'O(n log n)'),
('Binary search: T(n/2)+1', 'O(log n)'),
('Naive matrix mult: 8T(n/2)+n^2', 'O(n^3)'),
('Strassen: 7T(n/2)+n^2', 'O(n^2.81)'),
]
for r, sol in recurrences: print(r, '->', sol)อาร์เรย์ย่อยผลรวมสูงสุด: วิธีแบ่งแยกและพิชิต
วิธีแบ่งแยกและพิชิตสำหรับอาร์เรย์ย่อยผลรวมสูงสุดมีคำตอบได้สามกรณี: อยู่ทั้งหมดในครึ่งซ้าย อยู่ทั้งหมดในครึ่งขวา หรือพาดผ่านจุดกึ่งกลาง สำหรับกรณีที่พาดผ่าน ให้ขยายไปทางซ้ายจากจุดกึ่งกลางและไปทางขวาจาก mid+1 โดยหาผลรวมสูงสุดในแต่ละทิศทาง แล้วจึงรวมผลลัพธ์ วิธีแบ่งแยกและพิชิตนี้ใช้เวลา O(n log n) ซึ่งช้ากว่าวิธีของคาเดนที่ใช้เวลา O(n) แต่แสดงแม่แบบได้อย่างชัดเจน และเป็นคำถามสัมภาษณ์ที่พบบ่อยเกี่ยวกับการแบ่งแยกและพิชิต
def max_subarray_dc(nums, lo=None, hi=None):
if lo is None: lo, hi = 0, len(nums) - 1
if lo == hi: return nums[lo]
mid = (lo + hi) // 2
# Conquer
left_max = max_subarray_dc(nums, lo, mid)
right_max = max_subarray_dc(nums, mid + 1, hi)
# Cross-midpoint sum
left_sum = curr = 0
for i in range(mid, lo - 1, -1):
curr += nums[i]
left_sum = max(left_sum, curr)
right_sum = curr = 0
for i in range(mid + 1, hi + 1):
curr += nums[i]
right_sum = max(right_sum, curr)
cross_max = left_sum + right_sum
return max(left_max, right_max, cross_max)
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
print(max_subarray_dc(nums)) # 6ฟังก์ชันยกกำลัง: การยกกำลังอย่างรวดเร็ว
การยกกำลังอย่างรวดเร็ว (LeetCode 50): คำนวณ x^n ในเวลา O(log n) โดยใช้การแบ่งแยกและพิชิต หาก n เป็นเลขคู่: x^n = (x^(n/2))^2 หาก n เป็นเลขคี่: x^n = x × x^(n-1) จัดการ n ที่เป็นลบด้วย x^(-n) = 1/x^n การเรียกซ้ำแต่ละครั้งลด n ลงครึ่งหนึ่ง ดังนั้นความลึกจึงเป็น O(log n) นี่เป็นตัวอย่างที่ชัดเจนซึ่งขั้นตอนการรวมเป็นเพียงการคูณ แม้จะเรียบง่ายแต่มีประสิทธิภาพ
def my_pow(x, n):
if n < 0:
return 1 / my_pow(x, -n)
# BASE CASE
if n == 0: return 1
# DIVIDE and CONQUER
half = my_pow(x, n // 2)
if n % 2 == 0:
return half * half # even: x^n = (x^(n/2))^2
else:
return x * half * half # odd: x^n = x * (x^(n/2))^2
print(my_pow(2, 10)) # 1024
print(my_pow(2, -2)) # 0.25
print(my_pow(3, 5)) # 243
print(my_pow(0, 0)) # 1อาร์เรย์ที่เรียงแล้วเป็น BST
แปลงอาร์เรย์ที่เรียงแล้วเป็น BST (LeetCode 108) ใช้การแบ่งแยกและพิชิต โดยเลือกจุดกึ่งกลางเป็นรากเพื่อให้ความสูงสมดุล จากนั้นสร้างต้นไม้ย่อยด้านซ้ายแบบเรียกซ้ำจากครึ่งซ้าย และสร้างต้นไม้ย่อยด้านขวาจากครึ่งขวา วิธีนี้สร้าง BST ที่สมดุลตามความสูงและมีความสูงต่ำสุด O(log n) โครงสร้างการแบ่งแยกและพิชิตสอดคล้องกับการค้นหาแบบทวิภาค โดยแต่ละระดับของการเรียกซ้ำกำหนดจุดกึ่งกลางเป็นรากของช่วงย่อยปัจจุบัน
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def sorted_array_to_bst(nums):
def helper(lo, hi):
if lo > hi: return None
mid = (lo + hi) // 2
node = TreeNode(nums[mid]) # DIVIDE at midpoint
node.left = helper(lo, mid - 1) # CONQUER left
node.right = helper(mid + 1, hi) # CONQUER right
# COMBINE: already done by assignment
return node
return helper(0, len(nums) - 1)
def inorder(node):
if not node: return []
return inorder(node.left) + [node.val] + inorder(node.right)
root = sorted_array_to_bst([-10, -3, 0, 5, 9])
print(inorder(root)) # [-10,-3,0,5,9] (sorted, proving BST property)เมื่อการแบ่งแยกและพิชิตไม่ใช่ตัวเลือกที่ดีที่สุด
การแบ่งแยกและพิชิตมีต้นทุนแฝง ได้แก่ ความลึกของสแตกการเรียกฟังก์ชัน การตัดแบ่งอาร์เรย์หากไม่ใช้ดัชนี และขั้นตอนการรวม วิธีนี้เหมาะที่สุดเมื่อขั้นตอนการรวมใช้เวลา O(n) หรือน้อยกว่า เมื่อปัญหาย่อย ทับซ้อนกัน การแบ่งแยกและพิชิตจะคำนวณคำตอบซ้ำอย่างสิ้นเปลือง จึงต้องใช้ DP เมื่อขั้นตอนการรวมเป็นส่วนที่ใช้เวลามากที่สุด เช่น O(n²) การแบ่งแยกและพิชิตจะไม่ปรับปรุงประสิทธิภาพเมื่อเทียบกับวิธีพื้นฐาน ควรรู้ว่าเมื่อใดควรเลือกใช้: การแบ่งแยกและพิชิตสำหรับปัญหาย่อยที่เป็นอิสระต่อกัน และ DP สำหรับปัญหาย่อยที่ทับซ้อนกัน
# When D&C hurts:
# Fibonacci with pure D&C (no memo): T(n) = T(n-1) + T(n-2) → O(2^n)
# Sub-problems OVERLAP → use DP or memoisation instead
def fib_dc(n):
if n <= 1: return n
return fib_dc(n-1) + fib_dc(n-2) # O(2^n)!
def fib_dp(n):
a, b = 0, 1
for _ in range(n): a, b = b, a+b
return a # O(n)
print(fib_dp(30)) # fast
# fib_dc(40) would take seconds — do not run large values!การแบ่งแยกและพิชิตสำหรับการค้นหาแบบทวิภาคในเมทริกซ์ที่เรียงแล้ว
การค้นหาในเมทริกซ์สองมิติ (LeetCode 240) ที่แต่ละแถวและคอลัมน์เรียงลำดับแล้ว สามารถแก้ได้ด้วยแนวคิดการแบ่งแยกและพิชิต โดยเริ่มจากมุมขวาบน หากค่าปัจจุบัน > เป้าหมาย ให้เลื่อนไปทางซ้ายเพื่อตัดทั้งคอลัมน์ หากค่าปัจจุบัน < เป้าหมาย ให้เลื่อนลงเพื่อตัดทั้งแถว หากเท่ากัน แสดงว่าพบแล้ว อัลกอริทึมนี้ใช้เวลา O(m+n) และในทางเทคนิคไม่ใช่การแบ่งแยกและพิชิตแบบเรียกซ้ำ แต่มีแนวคิดสำคัญร่วมกันคือการตัดพื้นที่ค้นหาออกครึ่งหนึ่งในแต่ละขั้นตอน
def search_matrix(matrix, target):
if not matrix: return False
m, n = len(matrix), len(matrix[0])
row, col = 0, n - 1 # start top-right
while row < m and col >= 0:
val = matrix[row][col]
if val == target:
return True
elif val > target:
col -= 1 # eliminate this column
else:
row += 1 # eliminate this row
return False
matrix = [
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
print(search_matrix(matrix, 5)) # True
print(search_matrix(matrix, 20)) # Falseการวิเคราะห์ต้นไม้การเรียกซ้ำ
สำหรับสมการเวียนเกิดของการแบ่งแยกและพิชิตที่ไม่เข้ากับทฤษฎีบทหลัก ให้ใช้วิธี ต้นไม้การเรียกซ้ำ วาดแต่ละระดับของการเรียกฟังก์ชันแบบเรียกซ้ำ แล้วรวมงานในแต่ละระดับ การเรียงลำดับแบบผสาน: ที่ระดับ k มีปัญหาย่อยขนาด n/2^k จำนวน 2^k ปัญหาย่อย งานในแต่ละระดับ = 2^k × O(n/2^k) = O(n) จำนวนระดับทั้งหมด = log n งานทั้งหมด = O(n log n) วิธีเชิงภาพนี้ใช้ได้กับสมการเวียนเกิดทุกแบบ และช่วยสร้างสัญชาตญาณว่าเหตุใดการแบ่งแยกและพิชิตจึงมักมีเวลา O(n log n)
# Merge sort recursion tree analysis:
# Level 0: 1 problem of size n → O(n) work
# Level 1: 2 problems of size n/2 → 2*O(n/2) = O(n) work
# Level 2: 4 problems of size n/4 → 4*O(n/4) = O(n) work
# ...
# Level log(n): n problems of size 1 → n*O(1) = O(n) work
# Total levels = log(n)+1
# Total work = O(n) * O(log n) = O(n log n)
import math
n = 64
levels = int(math.log2(n)) + 1
print(f'n={n}: {levels} levels, {n}*{levels} = {n*levels} work units')
print(f'O(n log n) = O({n} * {int(math.log2(n))}) = O({n*int(math.log2(n))})')การสื่อสารวิธีแก้ปัญหาการแบ่งแยกและพิชิตในการสัมภาษณ์
เมื่อนำเสนอวิธีแก้ปัญหาแบบแบ่งแยกและพิชิตในการสัมภาษณ์: (1) ระบุสามขั้นตอนอย่างชัดเจน: “ฉันจะแบ่งที่จุดกึ่งกลาง แก้แต่ละครึ่งแบบเรียกซ้ำ แล้วรวมด้วยการผสาน” (2) ระบุกรณีฐานให้ชัดเจน (3) สร้างสมการเวียนเกิด: T(n) = 2T(n/2) + O(n) (4) ใช้ทฤษฎีบทหลักหรือต้นไม้การเรียกซ้ำเพื่อหา O(n log n) (5) กล่าวถึงกรณีที่การแบ่งแยกและพิชิตดีกว่าหรือแย่กว่าวิธีอื่น เช่น DP สำหรับปัญหาย่อยที่ทับซ้อนกัน และวิธีของคาเดนสำหรับอาร์เรย์ย่อยผลรวมสูงสุด
# D&C interview template to memorize:
def dc_template(problem, lo, hi):
# 1. BASE CASE (state it first)
if lo == hi: return solve_base(problem, lo)
# 2. DIVIDE
mid = (lo + hi) // 2
# 3. CONQUER
left = dc_template(problem, lo, mid)
right = dc_template(problem, mid + 1, hi)
# 4. COMBINE (this is where the algorithm-specific logic goes)
return combine_results(left, right, problem, lo, mid, hi)
def solve_base(p, i): return p[i]
def combine_results(l, r, p, lo, mid, hi): return max(l, r)
print('D&C template: base-divide-conquer-combine')
print('Complexity usually: T(n)=2T(n/2)+O(n) → O(n log n)')ตรวจสอบความเข้าใจอย่างรวดเร็ว
ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์เขียนโปรแกรมจากบทเรียนนี้
สรุปบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า: การแบ่งแยกและพิชิตใช้แม่แบบ: กรณีฐาน → แบ่งที่จุดกึ่งกลาง → พิชิตด้วยการเรียกซ้ำ → รวม, T(n) = 2T(n/2) + O(n) ให้ผลเป็น O(n log n) ตามทฤษฎีบทหลักกรณีที่ 2 และ การแบ่งแยกและพิชิตเหมาะที่สุดสำหรับปัญหาย่อยที่เป็นอิสระต่อกัน ขณะที่ต้องใช้ DP เมื่อปัญหาย่อยทับซ้อนกัน บทถัดไป เราจะใช้การแบ่งแยกและพิชิตเพื่อนับการผกผันในอาร์เรย์ด้วยการเรียงลำดับแบบผสานที่ปรับแก้แล้ว
คำถามที่พบบ่อย
บทเรียน “แม่แบบการแบ่งและพิชิต” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “แม่แบบการแบ่งและพิชิต” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “แม่แบบการแบ่งและพิชิต”
สกัดแม่แบบสามขั้นตอน ได้แก่ แบ่ง พิชิต และรวม จากการเรียงลำดับแบบผสาน แล้วนำไปใช้กับรูปแบบปัญหาใหม่อย่างเป็นระบบ คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน
บทเรียน “แม่แบบการแบ่งและพิชิต” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม
ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- แม่แบบการแบ่งและพิชิต
- นับจำนวนคู่กลับลำดับด้วยการเรียงลำดับแบบผสานที่ดัดแปลง
- สมาชิกเสียงข้างมาก: การลงคะแนนแบบ Boyer-Moore
- มัธยฐานของอาร์เรย์เรียงลำดับสองชุด