ระยะห่างการแก้ไข (Levenshtein)
หาความสัมพันธ์เวียนเกิดของระยะห่างการแก้ไขสำหรับการแทรก ลบ และแทนที่ แล้วเติมตาราง DP สำหรับสตริงที่มีความยาวต่างกัน
ระยะห่างการแก้ไข (Levenshtein) เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
ปัญหาระยะห่างการแก้ไข
ระยะห่างการแก้ไข (ระยะห่างเลเวนชไตน์, LeetCode 72) ถามว่า ต้องใช้การดำเนินการแทรก ลบ หรือแทนที่ อย่างน้อยกี่ครั้งเพื่อเปลี่ยนสตริงหนึ่งให้เป็นอีกสตริงหนึ่ง ตัวอย่างเช่น การเปลี่ยน 'horse' เป็น 'ros': แทนที่ 'h'→'r' (horse→rorse) ลบ 'r' (rorse→rose) และลบ 'e' (rose→ros) รวม 3 การดำเนินการ ระยะห่างการแก้ไขเป็นพื้นฐานสำคัญของเครื่องมือตรวจการสะกด การจัดเรียงลำดับ DNA และการจับคู่แบบคลุมเครือ
# Allowed operations:
# Insert: 'abc' → 'abXc' (insert X)
# Delete: 'abc' → 'ac' (delete b)
# Replace: 'abc' → 'aXc' (replace b with X)
# horse → ros: 3 operations
# 1. horse → rorse (replace h with r)
# 2. rorse → rose (delete r at index 1)
# 3. rose → ros (delete e)
print('Edit distance horse→ros: 3')
print('Edit distance intention→execution: 5')สถานะ DP และสมการเวียนเกิด
กำหนดให้ dp[i][j] = ระยะห่างการแก้ไขขั้นต่ำระหว่าง word1[:i] และ word2[:j] หาก word1[i-1] == word2[j-1] ไม่ต้องดำเนินการใด: dp[i][j] = dp[i-1][j-1] มิฉะนั้น ให้เลือกค่าต่ำสุดจากการดำเนินการสามแบบ: แทรก dp[i][j-1] + 1 ลบ dp[i-1][j] + 1 และแทนที่ dp[i-1][j-1] + 1 กรณีฐาน: dp[i][0] = i (ลบ word1 ทั้งหมด) และ dp[0][j] = j (แทรก word2 ทั้งหมด)
def edit_distance(word1, word2):
m, n = len(word1), len(word2)
dp = [[0]*(n+1) for _ in range(m+1)]
# Base cases
for i in range(m+1): dp[i][0] = i # delete all of word1
for j in range(n+1): dp[0][j] = j # insert all of word2
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] # no cost
else:
dp[i][j] = 1 + min(
dp[i][j-1], # insert
dp[i-1][j], # delete
dp[i-1][j-1] # replace
)
return dp[m][n]
print(edit_distance('horse', 'ros')) # 3
print(edit_distance('intention', 'execution')) # 5ทำความเข้าใจการดำเนินการทั้งสามแบบ
การดำเนินการทั้งสามแบบสอดคล้องโดยตรงกับการเคลื่อนที่ในตาราง DP: แทนที่ dp[i-1][j-1]+1 — เราจับคู่อักขระทั้งสองตัว แต่ต้องจ่ายค่าใช้จ่ายหนึ่งหน่วย ลบจาก word1 dp[i-1][j]+1 — นำอักขระออกจาก word1 (เลื่อนขึ้นในตาราง) แทรกลงใน word1 dp[i][j-1]+1 — แทรกอักขระเพื่อให้ตรงกับ word2 (เลื่อนไปทางซ้าย) ค่าต่ำสุดของทั้งสามค่าคือเส้นทางการแก้ไขที่เหมาะสมที่สุด
# Visualise the DP table for 'cat' → 'cut'
# dp[i][j] = min edits for word1[:i] vs word2[:j]
word1, word2 = 'cat', 'cut'
m, n = len(word1), len(word2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(m+1): dp[i][0] = i
for j in range(n+1): dp[0][j] = j
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]
else: dp[i][j]=1+min(dp[i][j-1],dp[i-1][j],dp[i-1][j-1])
print(' ', ' '.join(' '+word2))
for i, row in enumerate(dp):
print((' ' if i==0 else word1[i-1]), row)การลดการใช้พื้นที่เหลือ O(n)
ระยะห่างการแก้ไขต้องใช้เพียงแถวปัจจุบันและแถวก่อนหน้า ใช้อาร์เรย์ 1 มิติขนาด n+1 และติดตามค่า diagonal (dp[i-1][j-1]) แยกต่างหากก่อนอัปเดตแต่ละเซลล์ ประมวลผลจากซ้ายไปขวา: temp = dp[j] (ค่าเดิม = dp[i-1][j]) แล้วอัปเดต dp[j] โดยใช้ dp[j] (ลบ) dp[j-1] (แทรก) และ diagonal (แทนที่)
def edit_distance_1d(word1, word2):
m, n = len(word1), len(word2)
dp = list(range(n + 1)) # initial row: 0,1,2,...,n
for i in range(1, m + 1):
diag = dp[0] # dp[i-1][0]
dp[0] = i # dp[i][0] = i
for j in range(1, n + 1):
temp = dp[j] # dp[i-1][j] before overwrite
if word1[i-1] == word2[j-1]:
dp[j] = diag
else:
dp[j] = 1 + min(dp[j], # delete
dp[j-1], # insert
diag) # replace
diag = temp
return dp[n]
print(edit_distance_1d('horse', 'ros')) # 3
print(edit_distance_1d('intention', 'execution')) # 5การสร้างการดำเนินการแก้ไขกลับคืน
หากต้องการสร้างลำดับการแก้ไขจริงกลับคืน ให้ย้อนตามตาราง DP จาก (m, n) ในแต่ละเซลล์: หาก word1[i-1] == word2[j-1] ให้เลื่อนไปตามแนวทแยง (ไม่ต้องดำเนินการ) มิฉะนั้น ให้ตรวจว่าจากเพื่อนบ้านทั้งสาม ค่าใดเป็นค่าต่ำสุด แล้วบันทึกการดำเนินการที่สอดคล้องกัน ผลลัพธ์จะเป็นสคริปต์การแก้ไขในลำดับย้อนกลับ ให้กลับลำดับอีกครั้งเพื่อได้คำตอบสุดท้าย
def edit_ops(word1, word2):
m, n = len(word1), len(word2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(m+1): dp[i][0]=i
for j in range(n+1): dp[0][j]=j
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]
else: dp[i][j]=1+min(dp[i][j-1],dp[i-1][j],dp[i-1][j-1])
ops, i, j = [], m, n
while i>0 or j>0:
if i>0 and j>0 and word1[i-1]==word2[j-1]:
i-=1; j-=1
elif j>0 and (i==0 or dp[i][j-1]<=dp[i-1][j] and dp[i][j-1]<=dp[i-1][j-1]):
ops.append(f'Insert {word2[j-1]} at pos {i}'); j-=1
elif i>0 and (j==0 or dp[i-1][j]<=dp[i][j-1] and dp[i-1][j]<=dp[i-1][j-1]):
ops.append(f'Delete {word1[i-1]} at pos {i-1}'); i-=1
else:
ops.append(f'Replace {word1[i-1]} with {word2[j-1]}'); i-=1; j-=1
return list(reversed(ops))
for op in edit_ops('horse', 'ros'): print(op)การตรวจสอบระยะห่างหนึ่งการแก้ไข
ปัญหาสัมภาษณ์ที่ง่ายกว่า: สตริงสองชุดอยู่ห่างกันหนึ่งการแก้ไขพอดีหรือไม่ ปัญหานี้ใช้เวลา O(n) โดยไม่ต้องใช้ DP ให้เดินผ่านสตริงทั้งสองพร้อมกัน เมื่อพบอักขระไม่ตรงกัน ให้ลองการดำเนินการทั้งสามแบบ (ข้ามอักขระหนึ่งตัวใน s1 ข้ามใน s2 และข้ามทั้งสองตัว) แล้วตรวจว่าส่วนที่เหลือเหมือนกันหรือไม่ หากพบความไม่ตรงกันสองตำแหน่ง ให้คืนค่า False แนวทางแบบละโมบนี้หลีกเลี่ยง DP ขนาด O(mn) เต็มรูปแบบได้ เมื่อคุณต้องการทราบเพียงว่าระยะห่าง ≤ 1 หรือไม่
def is_one_edit_distance(s, t):
m, n = len(s), len(t)
if abs(m - n) > 1: return False
if m > n: return is_one_edit_distance(t, s) # ensure m <= n
for i in range(m):
if s[i] != t[i]:
if m == n:
return s[i+1:] == t[i+1:] # replace
else:
return s[i:] == t[i+1:] # insert into s (delete from t)
return m + 1 == n # all matched, lengths differ by 1
print(is_one_edit_distance('ab', 'acb')) # True (insert c)
print(is_one_edit_distance('ab', 'ab')) # False (zero edits)
print(is_one_edit_distance('ab', 'abc')) # True (append c)
print(is_one_edit_distance('ab', 'xyz')) # Falseการเปรียบเทียบระยะห่างการแก้ไขกับ LCS
ระยะห่างการแก้ไข (ที่ใช้การดำเนินการทั้งสามแบบ) และ LCS เป็นมุมมองเสริมกันของความคล้ายคลึงระหว่างสตริง ระยะห่างการแก้ไขนับความแตกต่าง ส่วน LCS นับความคล้ายคลึง เมื่ออนุญาตเฉพาะการแทรกและการลบ (ไม่แทนที่) ระยะห่างการแก้ไข = m + n - 2×LCS เมื่ออนุญาตการแทนที่ DP จะแตกต่างเล็กน้อย: แนวทแยงให้ค่า dp[i-1][j-1] เมื่ออักขระตรงกัน (ไม่มีค่าใช้จ่าย) หรือให้ค่า dp[i-1][j-1]+1 เมื่อแทนที่ อัลกอริทึมทั้งสองใช้เวลา O(mn)
def lcs_len(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 edit_insert_delete_only(s1, s2):
return len(s1) + len(s2) - 2 * lcs_len(s1, s2)
print(edit_insert_delete_only('sea', 'eat')) # 2
print(edit_distance('sea', 'eat')) # 2 (same here: replace not needed)การจับคู่สตริงแบบคลุมเครือ
ระยะห่างการแก้ไขเป็นพลังเบื้องหลังการจับคู่แบบคลุมเครือในโลกจริง เครื่องมือตรวจการสะกดจะแนะนำการแก้ไขที่มีระยะห่างการแก้ไข 1 หรือ 2 จากคำที่พิมพ์ ความท้าทายเมื่อใช้งานในขนาดใหญ่คือการหลีกเลี่ยงการเปรียบเทียบ O(mn × ขนาดพจนานุกรม) แนวทางแก้ไขได้แก่ต้นไม้ BK (ต้นไม้มิติสำหรับระยะห่างการแก้ไข) การทำดัชนี n-gram และอัลกอริทึมจับคู่สตริงโดยประมาณ เช่น บิแทป การเข้าใจ DP พื้นฐานช่วยให้คุณวิเคราะห์ประสิทธิภาพของเครื่องมือระดับสูงเหล่านี้ได้
def spell_suggest(typed, dictionary, max_dist=2):
'''Return words in dictionary within max_dist edits of typed.'''
suggestions = []
for word in dictionary:
if abs(len(typed) - len(word)) <= max_dist:
if edit_distance(typed, word) <= max_dist:
suggestions.append(word)
return suggestions
def edit_distance(w1, w2):
dp = list(range(len(w2)+1))
for i,c1 in enumerate(w1,1):
prev = i
for j,c2 in enumerate(w2,1):
temp = dp[j]
dp[j] = prev if c1==c2 else 1+min(dp[j],prev,dp[j-1])
prev = temp
return dp[len(w2)]
dictionary = ['horse', 'worse', 'house', 'morse', 'nurse']
print(spell_suggest('harse', dictionary)) # horse, worse, house, morseระยะห่างการแก้ไขแบบมีน้ำหนัก
ในแอปพลิเคชันบางประเภท การดำเนินการแต่ละแบบมีค่าใช้จ่ายต่างกัน ตัวอย่างเช่น การสลับอักขระที่อยู่ติดกัน (ข้อผิดพลาดในการพิมพ์ที่พบบ่อย) อาจมีค่าใช้จ่ายน้อยกว่าการแทนที่ทั้งตัว ระยะห่างดาเมอเรา–เลเวนชไตน์เพิ่มการสลับเป็นการดำเนินการแบบที่สี่ DP จึงขยายโดยตรวจสอบ dp[i-2][j-2]+1 เพิ่มเติม เมื่อ word1[i-1]==word2[j-2] และ word1[i-2]==word2[j-1] วิธีนี้จำลองข้อผิดพลาดในการพิมพ์บนแป้นพิมพ์ได้แม่นยำยิ่งขึ้น
def damerau_levenshtein(s, t):
m, n = len(s), len(t)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(m+1): dp[i][0]=i
for j in range(n+1): dp[0][j]=j
for i in range(1,m+1):
for j in range(1,n+1):
cost = 0 if s[i-1]==t[j-1] else 1
dp[i][j] = min(
dp[i-1][j]+1, # delete
dp[i][j-1]+1, # insert
dp[i-1][j-1]+cost # replace
)
# Transposition
if i>1 and j>1 and s[i-1]==t[j-2] and s[i-2]==t[j-1]:
dp[i][j] = min(dp[i][j], dp[i-2][j-2]+1)
return dp[m][n]
print(damerau_levenshtein('CA', 'ABC')) # 2
print(damerau_levenshtein('ab', 'ba')) # 1 (transposition)การจัดเรียงลำดับ DNA
ชีวสารสนเทศศาสตร์ใช้รูปแบบต่าง ๆ ของระยะห่างการแก้ไขสำหรับการจัดเรียงลำดับ DNA อัลกอริทึมนีดเดิลแมน–วุนช์เป็น DP สำหรับการจัดเรียงแบบทั่วโลกที่มีความเกี่ยวข้องอย่างใกล้ชิดกับ LCS และระยะห่างการแก้ไข โดยการตรงกันให้ค่า +1 การไม่ตรงกันให้ค่า -1 และช่องว่าง (การแทรก/ลบ) ให้ค่าปรับ อัลกอริทึมสมิธ–วอเตอร์แมนเป็นรูปแบบสำหรับการจัดเรียงเฉพาะที่ (ค้นหาสตริงย่อยที่ตรงกันดีที่สุด) ทั้งสองเป็นอัลกอริทึม DP O(mn) ที่มีโครงสร้างการเติมตารางเหมือนกัน
def needleman_wunsch(seq1, seq2, match=1, mismatch=-1, gap=-1):
m, n = len(seq1), len(seq2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(m+1): dp[i][0] = i * gap
for j in range(n+1): dp[0][j] = j * gap
for i in range(1,m+1):
for j in range(1,n+1):
score = match if seq1[i-1]==seq2[j-1] else mismatch
dp[i][j] = max(
dp[i-1][j-1] + score, # align
dp[i-1][j] + gap, # gap in seq2
dp[i][j-1] + gap # gap in seq1
)
return dp[m][n]
print(needleman_wunsch('GATTACA', 'GCATGCU')) # alignment scoreแนวทางตอบคำถามระยะห่างการแก้ไขในการสัมภาษณ์
เมื่อถูกถามเรื่องระยะห่างการแก้ไขในการสัมภาษณ์: (1) ยืนยันการดำเนินการที่อนุญาต (แทรก/ลบ/แทนที่) (2) กำหนดสถานะ DP ให้ชัดเจน (3) เขียนทั้งสามกรณีและสมการเวียนเกิดอย่างชัดเจน (4) ระบุกรณีฐาน: dp[i][0]=i และ dp[0][j]=j (5) กล่าวถึงการลดการใช้พื้นที่เหลือ O(n) (6) หากมีเวลา ให้ไล่ดูตัวอย่างเล็ก ๆ เช่น 'cat'→'cut' (แทนที่ 1 ครั้ง) เพื่อยืนยันความถูกต้อง ขอบเขตความซับซ้อนมาตรฐานคือเวลา O(mn) และพื้นที่ O(mn) → O(n)
# Clean interview solution
def min_distance(word1, word2):
m, n = len(word1), len(word2)
# O(n) space with rolling row
dp = list(range(n + 1))
for i in range(1, m + 1):
diag = dp[0] # dp[i-1][0]
dp[0] = i
for j in range(1, n + 1):
temp = dp[j]
if word1[i-1] == word2[j-1]:
dp[j] = diag
else:
dp[j] = 1 + min(dp[j], dp[j-1], diag)
diag = temp
return dp[n]
# Time: O(mn), Space: O(n)
print(min_distance('horse', 'ros')) # 3
print(min_distance('intention', 'execution')) # 5
print(min_distance('', 'abc')) # 3
print(min_distance('abc', '')) # 3ตรวจสอบความเข้าใจ
ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
สรุปบทเรียน
ในบทเรียนนี้คุณได้เรียนรู้ว่า: ระยะห่างการแก้ไข dp[i][j] = min(dp[i][j-1]+1, dp[i-1][j]+1, dp[i-1][j-1]+cost) โดย cost=0 เมื่ออักขระตรงกัน มิฉะนั้นเป็น 1 กรณีฐาน dp[i][0]=i และ dp[0][j]=j แทนการเปลี่ยนไปยังหรือออกจากสตริงว่าง และ การลดการใช้พื้นที่เหลือ O(n) ใช้อาร์เรย์ 1 มิติแบบเลื่อนร่วมกับตัวแปรแนวทแยง บทถัดไปเราจะใช้เทคนิคอาร์เรย์เลื่อนแบบเดียวกันเพื่อลดตาราง DP 2 มิติจากพื้นที่ O(mn) เหลือ O(min(m,n))
คำถามที่พบบ่อย
บทเรียน “ระยะห่างการแก้ไข (Levenshtein)” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “ระยะห่างการแก้ไข (Levenshtein)” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “ระยะห่างการแก้ไข (Levenshtein)”
หาความสัมพันธ์เวียนเกิดของระยะห่างการแก้ไขสำหรับการแทรก ลบ และแทนที่ แล้วเติมตาราง DP สำหรับสตริงที่มีความยาวต่างกัน คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน
บทเรียน “ระยะห่างการแก้ไข (Levenshtein)” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม
ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- เส้นทางไม่ซ้ำและผลรวมเส้นทางต่ำสุดบนกริด
- ลำดับร่วมที่ยาวที่สุด
- ระยะห่างการแก้ไข (Levenshtein)
- การเพิ่มประสิทธิภาพพื้นที่สำหรับ DP สองมิติ