Levenshtein distance - Bài toán tìm kiếm chuỗi tương tự

💡
Khoảng những năm 2005 - 2006, mình cần xây dựng một hệ thống search engine. Lúc đó mình đã dùng thư viện Lucene.Net để hỗ trợ indexing cho full text search. Tuy nhiên, mình gặp vấn đề phải giải quyết đối với những mistyping query, và mình đã tìm ra thuật toán này . Levenshtein distance được đặt theo tên Vladimir Levenshtein, người đã đề ra khái niệm này vào năm 1965.

Đặt vấn đề

Giả như người dùng muốn tìm kiếm chuỗi “command, nhưng anh ta gõ nhầm thành “comnand“, làm cách nào hệ thống vẫn hiểu để trả về kết quả chính xác?

Khoảng cách Levenshtein

Là số bước ít nhất để biến một chuỗi \(A\) thành chuỗi \(B\) thông qua 3 phép biến đổi:

  • Deletion: Xóa một ký tự

  • Insertion: Thêm 1 ký tự

  • Substitution: Thay 1 ký tự bằng một ký tự khác

Ví dụ: Khoảng cách của “GILY“ và “GEELY“ là 2, vì để biến “GILY” thành “GEELY” cần có 2 bước:

  • Thay I thành E: GILYGELY

  • Chèn thêm E vào: GELYGEELY

Mô tả thuật toán tính khoảng cách Levenshtein

Ý tưởng chính của thuật toán là ta sẽ đi xây dựng một bảng (mảng 2 chiều) để lưu trữ số thao tác tối thiểu cần thực hiện để chuyển một đoạn của chuỗi thứ nhất thành một đoạn của chuỗi thứ hai. Bảng này được tính theo ý tưởng của thuật toán quy hoạch động (dynamic programming).

Mô tả thuật toán

  • Đầu vào là chuỗi \(A[0 \dots m-1]\) và \(B[0 \dots n-1]\) với \(m,n\) lần lượt là độ dài của chuỗi\(A\) và chuỗi \(B\).

  • Khởi tạo bảng \(D\):

    • Cho \(i:0 \to m:\) \(D[i][0] = i\)

    • Cho \(j:0 \to n:\) \(D[0][j] = j\)

    • Với \(D[i][j]\) là khoảng cách Levenshtein giữa:

      • \(A[0\dots i-1]\): chuỗi con khởi đầu từ vị trí \(0\) đến vị trí \(i-1\) có độ dài \(i\)

      • \(B[0 \dots j-1]\): chuỗi con khởi đầu từ vị trí \(0\) đến vị trí \(j-1\) có độ dài \(j\)

    • Công thức đệ quy để tính \(D[i][j]\):

$$D[i][j] = \begin{cases} 0 \quad \text{if } i=0, j=0,\\ i \quad \text{if } j=0,\\ j \quad \text{if } i=0,\\ \min \begin{cases} D[i-1][j]+1 \quad \text{(delete)},\\ D[i][j-1]+1 \quad \text{(insertion)},\\ D[i-1][j-1]+c \quad \text{(substitution, if A[i-1]=B[j-1] then c=0, otherwise c=1)} \end{cases} \end{cases}$$

  • Duyệt qua từng ký tự của \(A\) và \(B\) để tính \(D[i][j]\) dựa trên công thức bên trên

  • \(D[m][n]\) chính là khoảng cách Levenshtein của \(A\) và \(B\)

Demo