0Pricing
Coding Interview Prep · บทเรียน

ลำดับร่วมที่ยาวที่สุด

กำหนดความสัมพันธ์เวียนเกิดของ LCS สำหรับสตริงสองชุด เติมตารางสองมิติ และสร้างลำดับย่อยจริงด้วยการย้อนตามตาราง

ลำดับร่วมที่ยาวที่สุด เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding 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) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “ลำดับร่วมที่ยาวที่สุด”

กำหนดความสัมพันธ์เวียนเกิดของ LCS สำหรับสตริงสองชุด เติมตารางสองมิติ และสร้างลำดับย่อยจริงด้วยการย้อนตามตาราง คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน

บทเรียน “ลำดับร่วมที่ยาวที่สุด” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม

ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. เส้นทางไม่ซ้ำและผลรวมเส้นทางต่ำสุดบนกริด
  2. ลำดับร่วมที่ยาวที่สุด
  3. ระยะห่างการแก้ไข (Levenshtein)
  4. การเพิ่มประสิทธิภาพพื้นที่สำหรับ DP สองมิติ
← กลับไปที่ Coding Interview Prep