ลำดับร่วมที่ยาวที่สุด
กำหนดความสัมพันธ์เวียนเกิดของ LCS สำหรับสตริงสองชุด เติมตารางสองมิติ และสร้างลำดับย่อยจริงด้วยการย้อนตามตาราง
ลำดับร่วมที่ยาวที่สุด เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
ลำดับย่อยคืออะไร
ลำดับย่อยของสตริงเกิดจากการลบอักขระบางตัว (หรือไม่ลบเลย) โดยไม่เปลี่ยนลำดับของอักขระที่เหลืออยู่ ตัวอย่างเช่น 'ACE' เป็นลำดับย่อยของ 'ABCDE' แต่ 'AEC' ไม่ใช่ (ลำดับไม่ถูกต้อง) ลำดับย่อยร่วมที่ยาวที่สุด (LCS) ของสตริงสองชุดคือลำดับย่อยที่ยาวที่สุดซึ่งปรากฏอยู่ในสตริงทั้งสอง 'ABCBDAB' และ 'BDCABA' มี LCS เป็น 'BCBA' หรือ 'BDAB' ซึ่งมีความยาว 4
# Subsequence vs Substring
# 'ACE' is a subsequence of 'ABCDE' (skip B, D)
# 'ACE' is NOT a substring of 'ABCDE' (must be contiguous)
# LCS examples:
# LCS('ABCBDAB', 'BDCABA') = 4 ('BCBA' or 'BDAB')
# LCS('AGGTAB', 'GXTXAYB') = 4 ('GTAB')
# LCS('ABC', 'AC') = 2 ('AC')
print('Subsequence check: ACE in ABCDE')
text = 'ABCDE'
pattern = 'ACE'
i = 0
for ch in text:
if i < len(pattern) and ch == pattern[i]: i += 1
print('Found:', i == len(pattern)) # Trueการอนุมานความสัมพันธ์เวียนเกิดของ LCS
กำหนดให้ dp[i][j] = ความยาวของ LCS ของ text1[:i] และ text2[:j] หากอักขระตรงกัน (text1[i-1] == text2[j-1]) ให้ขยาย LCS อีก 1: dp[i][j] = dp[i-1][j-1] + 1 หากไม่ตรงกัน ให้เลือกค่าที่ดีกว่าจากการข้ามอักขระของสตริงใดสตริงหนึ่ง: dp[i][j] = max(dp[i-1][j], dp[i][j-1]) กรณีฐานคือ dp[0][j] = dp[i][0] = 0 (LCS ที่เทียบกับสตริงว่างมีค่าเป็น 0)
def lcs_length(text1, text2):
m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i-1] == text2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1 # extend match
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) # skip one
return dp[m][n]
print(lcs_length('ABCBDAB', 'BDCABA')) # 4
print(lcs_length('AGGTAB', 'GXTXAYB')) # 4
print(lcs_length('ABC', 'AC')) # 2การติดตามตาราง LCS
สำหรับ text1='ABCD' และ text2='ACBD': เริ่มต้นด้วยค่า 0 ทั้งหมด เมื่ออักขระตรงกัน (A-A, C-C, B-B หากอยู่ในตำแหน่งที่ถูกต้อง, D-D) ให้ใช้ dp[i][j] = dp[i-1][j-1] + 1 มิฉะนั้นให้เลือกค่าสูงสุดจากช่องด้านซ้ายหรือด้านบน การอ่านตารางที่เติมเสร็จแล้วจะแสดงให้เห็นว่าการเคลื่อนที่แนวทแยงสอดคล้องกับอักขระที่ตรงกันอย่างไร ค่าสุดท้าย dp[4][4] ให้ความยาวของ LCS
def lcs_trace(text1, text2):
m, n = len(text1), len(text2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if text1[i-1] == text2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
# Print table
print(' ', ' '.join(text2))
for i, row in enumerate(dp):
label = ' ' if i == 0 else text1[i-1]
print(label, row)
return dp[m][n]
lcs_trace('ABCD', 'ACBD')การสร้าง LCS จริงกลับคืน
หากต้องการกู้คืนสตริง LCS จริง ให้ย้อนตามตาราง DP จาก dp[m][n] หาก text1[i-1] == text2[j-1] อักขระนี้อยู่ใน LCS — ให้บันทึกอักขระนั้น แล้วเลื่อนไปตามแนวทแยงที่ (i-1, j-1) หาก dp[i-1][j] > dp[i][j-1] ให้เลื่อนขึ้น มิฉะนั้นให้เลื่อนไปทางซ้าย เมื่อสิ้นสุดให้กลับลำดับอักขระที่รวบรวมไว้ เนื่องจากคุณย้อนตามตารางมา ลำดับขั้นตอนการสร้างกลับคืนนี้ใช้เวลา O(m+n)
def lcs_reconstruct(text1, text2):
m, n = len(text1), len(text2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if text1[i-1] == text2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
# Backtrack
result = []
i, j = m, n
while i > 0 and j > 0:
if text1[i-1] == text2[j-1]:
result.append(text1[i-1])
i -= 1; j -= 1
elif dp[i-1][j] > dp[i][j-1]:
i -= 1
else:
j -= 1
return ''.join(reversed(result))
print(lcs_reconstruct('ABCBDAB', 'BDCABA')) # BCBA or BDABการลดการใช้พื้นที่เหลือ O(n)
ตาราง LCS ต้องใช้เพียงแถวปัจจุบันและแถวก่อนหน้า คุณสามารถใช้อาร์เรย์ 1 มิติขนาด n+1 และใช้ตัวแปร diagonal เก็บค่าที่เคยอยู่ใน dp[i-1][j-1] ก่อนที่จะถูกเขียนทับ ให้ประมวลผลจากซ้ายไปขวาในแต่ละแถว หลังประมวลผลแต่ละเซลล์ ค่า dp[j] ที่อัปเดตแล้วจะเป็นค่าของแถวปัจจุบัน และคุณต้องบันทึกค่าก่อนหน้าไว้ใน diagonal ก่อนเขียนทับ
def lcs_o1_space(text1, text2):
m, n = len(text1), len(text2)
dp = [0] * (n + 1) # represents previous row
for i in range(1, m + 1):
diag = 0 # dp[i-1][j-1]
for j in range(1, n + 1):
temp = dp[j] # save current (will become diagonal for next j)
if text1[i-1] == text2[j-1]:
dp[j] = diag + 1
else:
dp[j] = max(dp[j], dp[j-1])
diag = temp
return dp[n]
print(lcs_o1_space('ABCBDAB', 'BDCABA')) # 4
print(lcs_o1_space('AGGTAB', 'GXTXAYB')) # 4ความสัมพันธ์ระหว่าง LCS กับระยะห่างการแก้ไข
LCS มีความสัมพันธ์อย่างใกล้ชิดกับระยะห่างการแก้ไข (ระยะห่างเลเวนชไตน์) หากทราบ LCS คุณสามารถคำนวณระยะห่างการแก้ไขขั้นต่ำโดยใช้เฉพาะการแทรกและการลบได้: edit_dist = m + n - 2 * LCS(s1, s2) อักขระแต่ละตัวจาก s1 ที่ไม่อยู่ใน LCS ต้องถูกลบ และอักขระแต่ละตัวจาก s2 ที่ไม่อยู่ใน LCS ต้องถูกแทรก การแทนที่ไม่ถูกนับในที่นี้ เนื่องจากเราอนุญาตเฉพาะการแทรกและการลบ แต่สูตรนี้มีประโยชน์สำหรับปัญหาที่เกี่ยวข้อง
def lcs_length(s1, s2):
m, n = len(s1), len(s2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] + 1
else: dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
def min_edits_insert_delete(s1, s2):
lcs = lcs_length(s1, s2)
return len(s1) + len(s2) - 2 * lcs
print(min_edits_insert_delete('ABCD', 'ANCD')) # 2 (delete B, insert N)
print(min_edits_insert_delete('horse', 'ros')) # 5การลบเพื่อทำให้สตริงสองชุดเท่ากัน
การลบเพื่อทำให้สตริงสองชุดเท่ากัน (LeetCode 583) ถามถึงจำนวนการลบขั้นต่ำเพื่อทำให้สตริงสองชุดเท่ากัน อักขระที่เก็บไว้ต้องเป็นลำดับย่อยร่วม ดังนั้นคุณจึงต้องทำให้ LCS ยาวที่สุดและลบทุกอย่างที่เหลือ คำตอบคือ m + n - 2 * LCS(s1, s2) ซึ่งเทียบเท่ากับระยะห่างการแก้ไขโดยใช้การแทรกและการลบข้างต้น การมองปัญหาในรูปของ LCS เป็นเทคนิคการลดรูปที่ทรงพลัง
def min_distance(word1, word2):
m, n = len(word1), len(word2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
lcs = dp[m][n]
return m + n - 2 * lcs # deletions needed
print(min_distance('sea', 'eat')) # 2 (delete s, delete t)
print(min_distance('leetcode', 'etco')) # 4สตริงย่อยร่วมที่ยาวที่สุด
อย่าสับสนระหว่าง LCS (ลำดับย่อย) กับ สตริงย่อยร่วมที่ยาวที่สุด สตริงย่อยต้องอยู่ติดกัน ดังนั้นเมื่ออักขระไม่ตรงกัน จำนวนจะกลับไปเป็น 0 แทนที่จะเลือกค่าสูงสุดจากเพื่อนบ้าน สมการเวียนเกิดจึงเปลี่ยนเป็น: หากอักขระตรงกัน dp[i][j] = dp[i-1][j-1] + 1 มิฉะนั้น dp[i][j] = 0 ให้ติดตามค่าสูงสุดที่พบในทุกเซลล์
def longest_common_substring(s1, s2):
m, n = len(s1), len(s2)
dp = [[0]*(n+1) for _ in range(m+1)]
max_len = 0
for i in range(1, m+1):
for j in range(1, n+1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
max_len = max(max_len, dp[i][j])
# else dp[i][j] stays 0 (reset)
return max_len
# LCS (subseq) vs substring:
print('LCS subseq:', lcs_length('ABCBDAB', 'BDCABA')) # 4 (BCBA)
print('LCS substring:', longest_common_substring('ABCBDAB', 'BDCABA')) # 2 (BD or AB)LCS สำหรับการเปรียบเทียบลำดับ
LCS ถูกใช้อย่างแพร่หลายในเครื่องมือ diff (เช่น diff ของยูนิกซ์) เพื่อเปรียบเทียบไฟล์ สคริปต์การแก้ไขระหว่างไฟล์สองไฟล์ได้มาจาก LCS: บรรทัดใน LCS ไม่เปลี่ยนแปลง บรรทัดเพิ่มเติมจากไฟล์ที่ 1 จะถูกลบ และบรรทัดเพิ่มเติมจากไฟล์ที่ 2 จะถูกแทรก การเข้าใจ LCS ช่วยให้คุณเห็นภาพว่าระบบควบคุมรุ่นติดตามการเปลี่ยนแปลงอย่างไร และเหตุใดจึงเกิดข้อขัดแย้งขณะผสาน
def diff(old_lines, new_lines):
'''Simple diff using LCS to find unchanged lines.'''
m, n = len(old_lines), len(new_lines)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1,m+1):
for j in range(1,n+1):
if old_lines[i-1]==new_lines[j-1]: dp[i][j]=dp[i-1][j-1]+1
else: dp[i][j]=max(dp[i-1][j],dp[i][j-1])
# Backtrack to produce diff
output, i, j = [], m, n
while i>0 or j>0:
if i>0 and j>0 and old_lines[i-1]==new_lines[j-1]:
output.append(' '+old_lines[i-1]); i-=1; j-=1
elif j>0 and (i==0 or dp[i][j-1]>=dp[i-1][j]):
output.append('+ '+new_lines[j-1]); j-=1
else:
output.append('- '+old_lines[i-1]); i-=1
return list(reversed(output))
for line in diff(['a','b','c'], ['a','x','c']): print(line)ลำดับเหนือร่วมที่สั้นที่สุด
ลำดับเหนือร่วมที่สั้นที่สุด (LeetCode 1092) ถามหาสตริงที่สั้นที่สุดซึ่งมีทั้ง s1 และ s2 เป็นลำดับย่อย อักขระ LCS แต่ละตัวจะปรากฏเพียงครั้งเดียวในลำดับเหนือร่วม ส่วนอักขระที่ไม่อยู่ใน LCS จากสตริงทั้งสองต้องถูกรวมไว้ ความยาว = m + n - LCS(s1, s2) หากต้องการสร้างกลับคืน ให้ใช้การย้อนตามตาราง LCS แบบเดียวกัน แต่รวมอักขระจากสตริงทั้งสองที่ตำแหน่งไม่ตรงกันไว้ด้วย
def shortest_common_supersequence(s1, s2):
m, n = len(s1), len(s2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1,m+1):
for j in range(1,n+1):
if s1[i-1]==s2[j-1]: dp[i][j]=dp[i-1][j-1]+1
else: dp[i][j]=max(dp[i-1][j],dp[i][j-1])
# Reconstruct
result, i, j = [], m, n
while i>0 and j>0:
if s1[i-1]==s2[j-1]: result.append(s1[i-1]); i-=1; j-=1
elif dp[i-1][j]>dp[i][j-1]: result.append(s1[i-1]); i-=1
else: result.append(s2[j-1]); j-=1
while i>0: result.append(s1[i-1]); i-=1
while j>0: result.append(s2[j-1]); j-=1
return ''.join(reversed(result))
print(shortest_common_supersequence('abac', 'cab')) # 'cabac' length 5ความซับซ้อนของ LCS และเคล็ดลับการสัมภาษณ์
อัลกอริทึม LCS แบบคลาสสิกใช้เวลา O(m×n) และพื้นที่ O(m×n) โดยลดพื้นที่ได้เหลือ O(min(m,n)) ด้วยเทคนิคอาร์เรย์เลื่อน เคล็ดลับสำคัญในการสัมภาษณ์: (1) กำหนดให้ชัดเจนว่าสถานะ DP แทนอะไร ก่อนเขียนโค้ด (2) จัดการกรณีตรงกันและไม่ตรงกันแยกจากกัน (3) เมื่อถูกขอให้สร้างลำดับกลับคืน ให้อธิบายการย้อนตามตารางก่อนเขียนโค้ด (4) กล่าวถึงลำดับเพิ่มร่วมที่ยาวที่สุด (LIS) ในฐานะปัญหา 1 มิติที่เกี่ยวข้อง ซึ่งแก้ได้ในเวลา O(n log n) ด้วยการเรียงแบบอดทน
# LCS: O(mn) time, O(min(m,n)) space with rolling array
# Longest Increasing Subsequence (related but 1D):
from bisect import bisect_left
def lis_length(nums):
'''Patience sorting: O(n log n) LIS length.'''
tails = []
for num in nums:
pos = bisect_left(tails, num)
if pos == len(tails): tails.append(num)
else: tails[pos] = num
return len(tails)
print(lis_length([10, 9, 2, 5, 3, 7, 101, 18])) # 4 (2,3,7,101 or 2,5,7,18)ตรวจสอบความเข้าใจ
ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
สรุปบทเรียน
ในบทเรียนนี้คุณได้เรียนรู้ว่า: LCS ใช้ dp[i][j] = dp[i-1][j-1]+1 เมื่ออักขระตรงกัน มิฉะนั้นใช้ max(dp[i-1][j], dp[i][j-1]) ลำดับจริงสร้างกลับคืนด้วยการย้อนตามแนวทแยงเมื่ออักขระตรงกัน และย้อนเข้าหาเพื่อนบ้านที่มีค่ามากกว่าเมื่อไม่ตรงกัน และ LCS เป็นพื้นฐานของระยะห่างการแก้ไข การดำเนินการลบ ลำดับเหนือร่วมที่สั้นที่สุด และเครื่องมือ diff บทถัดไปเราจะอนุมานสมการเวียนเกิดของระยะห่างการแก้ไข (เลเวนชไตน์) ซึ่งเพิ่มการแทนที่เข้าไปในกรอบงานของ LCS
คำถามที่พบบ่อย
บทเรียน “ลำดับร่วมที่ยาวที่สุด” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “ลำดับร่วมที่ยาวที่สุด” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “ลำดับร่วมที่ยาวที่สุด”
กำหนดความสัมพันธ์เวียนเกิดของ LCS สำหรับสตริงสองชุด เติมตารางสองมิติ และสร้างลำดับย่อยจริงด้วยการย้อนตามตาราง คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน
บทเรียน “ลำดับร่วมที่ยาวที่สุด” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม
ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- เส้นทางไม่ซ้ำและผลรวมเส้นทางต่ำสุดบนกริด
- ลำดับร่วมที่ยาวที่สุด
- ระยะห่างการแก้ไข (Levenshtein)
- การเพิ่มประสิทธิภาพพื้นที่สำหรับ DP สองมิติ