Dãy số chung dài nhất: Python, C++ Ví dụ

⚡ Tóm tắt thông minh

Thuật toán Longest Common Subsequence (LCC) xác định chuỗi con có thứ tự dài nhất được chia sẻ bởi hai chuỗi mà không yêu cầu các ký tự liền kề. Thuật toán lập trình động kinh điển này là nền tảng của các tiện ích diff, căn chỉnh DNA và kiểm soát phiên bản bằng cách so sánh các chuỗi một cách hiệu quả trong thời gian đa thức.

  • 📘 Khái niệm cốt lõi: Hàm Longest Common Subsequence trả về tập hợp ký tự có thứ tự dài nhất xuất hiện trong cả hai chuỗi đầu vào, đồng thời vẫn giữ nguyên thứ tự tương đối ban đầu của chúng.
  • 🐢 Cách tiếp cận ngây thơ: Phương pháp vét cạn liệt kê mọi chuỗi con của chuỗi đầu tiên và so sánh chúng với chuỗi thứ hai, chạy trong thời gian hàm mũ O(n·2^m).
  • 🔁 Phương pháp đệ quy: Một quy tắc đệ quy khớp với các ký tự cuối cùng hoặc đệ quy trên các chuỗi con nhỏ hơn, nhưng việc tính toán lại chồng chéo lên nhau.ping các vấn đề phụ lặp đi lặp lại.
  • 🧮 Lập trình năng động: Bảng dp hai chiều lưu trữ kết quả của bài toán con, mang lại giải pháp gọn gàng O(m·n) với không gian phụ trợ O(m·n).
  • 🐍 Phạm vi ngôn ngữ: Hoàn thành Python và C++ Các cách triển khai này minh họa cả phương pháp đệ quy cơ bản và bảng DP có ghi nhớ để sử dụng thực tế.
  • 🌐 Ứng dụng thực tế: Thuật toán Longest Common Subsequence (LSC) hỗ trợ các công cụ so sánh, kiểm tra đạo văn, sửa lỗi chính tả và căn chỉnh trình tự sinh học trên DNA và protein.

Trình tự con chung dài nhất

Dãy số chung dài nhất là gì?

Bài toán tìm chuỗi con chung dài nhất (LCS) nghĩa là bạn sẽ được cho hai chuỗi ký tự, mẫu hoặc dãy các đối tượng. Trong hai chuỗi ký tự hoặc mẫu này, bạn cần tìm chuỗi con dài nhất có cùng thứ tự xuất hiện trong cả hai chuỗi ký tự hoặc mẫu.

Ví dụ

Ví dụ, có hai chuỗi ký tự được cung cấp. Giả sử rằng:

Mẫu_1 = “RGBGARGA”
Mẫu_2 = “BGRARG”

  • Từ mẫu pattern_1, có thể tạo ra các chuỗi như “RGB”, “RGGA”, “RGAR”. Để tạo chuỗi, bạn cần duy trì vị trí tương đối của mỗi ký tự trong chuỗi ký tự.
  • Từ mẫu_2, ta có thể tạo ra các chuỗi như “BGR”, “BRAG”, “RARG”. Các chuỗi có thể được tạo ra miễn là chúng giữ nguyên vị trí tương đối của chuỗi gốc.

Thuật ngữ vị trí tương đối có nghĩa là trật tự.

Ví dụ, “BRG” là một chuỗi hợp lệ vì chữ “B” xuất hiện đầu tiên, sau đó là “R”, và cuối cùng là “G” trong chuỗi gốc pattern_2. Tuy nhiên, nếu chuỗi là “RBRG”, nó không hợp lệ, bởi vì trong chuỗi gốc (pattern_2), chữ “B” xuất hiện đầu tiên.

Ví dụ về chuỗi con chung dài nhất (Longest Common Subsequence)

Chúng ta có hai lựa chọn để tìm Dãy con chung dài nhất từ ​​hai dãy hoặc mảng đã cho.

  • Phương pháp ngây thơ
  • Giải pháp lập trình động: Chuỗi chung dài nhất còn được gọi là LCS.

Giải pháp đơn giản có độ phức tạp về thời gian lớn hơn và không phải là giải pháp tối ưu. Bằng cách sử dụng Giải pháp Lập trình Động (DP), chúng ta khắc phục được vấn đề về độ phức tạp.

Phương pháp ngây thơ

Phương pháp đơn giản (Naive method) là cách tiếp cận dễ dàng đối với vấn đề, bất kể độ phức tạp về thời gian và các yếu tố tối ưu hóa khác. Nó bao gồm "phương pháp vét cạn", nhiều vòng lặp và các lời gọi đệ quy trong hầu hết các trường hợp. Thuật ngữ "vét cạn" có nghĩa là xem xét tất cả các mẫu có thể có cho một vấn đề nhất định.

Ví dụ

Từ ví dụ trên về mẫu1 và mẫu2, chúng ta giả sử mẫu1 có chiều dài m và mẫu2 có chiều dài n. Để kiểm tra mọi trường hợp có thể xảy ra, chúng ta cần đánh giá mọi dãy con có thể có của mẫu 1 bằng mẫu 2.

Đây là một chuỗi ký tự đơn giản gồm 4 chữ cái “ABCD”. Ví dụ, chúng ta cần tạo một dãy từ “ABCD”. Chúng ta có thể chọn lấy một ký tự hoặc không. Điều đó có nghĩa là, với mỗi ký tự, chúng ta có hai lựa chọn:

  • Ký tự sẽ được thêm vào phần tiếp theo.
  • Ký tự sẽ không được thêm vào dãy sau.

Ở đây, hình ảnh hiển thị tất cả các chuỗi mà chúng ta có thể tạo từ chuỗi “ABCD”.

Trình tự ABCD theo phương pháp đơn giản

Chuỗi có 1 ký tự:

Chuỗi ký tự đơn giản theo phương pháp đơn giản

Chuỗi có 2 ký tự:

Phương pháp đơn giản gồm hai chuỗi ký tự

Chuỗi có 3 ký tự:

Phương pháp đơn giản với chuỗi ba ký tự

Từ sơ đồ trên, có 14 dãy. Nếu ta không lấy bất kỳ chữ cái nào, về cơ bản là một chuỗi rỗng, thì tổng số dãy sẽ là 15. Hơn nữa, chính chuỗi “ABCD” cũng là một dãy. Vì vậy, tổng số dãy là 16.

Vì vậy, có thể tạo ra 2^4 hoặc 16 chuỗi con từ “ABCD”. Sau đó, một chuỗi có độ dài là m sẽ có tổng số dãy con là 2^m.

Với mỗi chuỗi con, chúng ta cần kiểm tra toàn bộ mẫu2. Việc này sẽ mất thời gian O(n). O(n) là độ phức tạp của hàm tính toán thời gian thực thi.

Vì vậy, độ phức tạp thời gian tổng thể trở thành O(n*2^m). Ví dụ như trên, ta có giá trị của m=8 và n=5.

Dưới đây là các bước của Phương pháp Naive:

Bước 1) Lấy một chuỗi từ mẫu 1.
Bước 2) Hãy ghép chuỗi từ bước 1 với mẫu 2.
Bước 3) Nếu trùng khớp thì lưu chuỗi tiếp theo.
Bước 4) Nếu vẫn còn các chuỗi trong mẫu 1, hãy quay lại bước 1.
Bước 5) In dãy con dài nhất.

Cấu trúc tối ưu

Thuật ngữ "cấu trúc con tối ưu" có nghĩa là có thể tìm ra giải pháp tối ưu bằng cách giải các bài toán con. Ví dụ, trong ví dụ trên, chúng ta có mẫu 1 và mẫu 2.

Bước 1) Lấy hai ký tự đầu tiên từ mỗi mẫu.

Bước 2) Lấy ký tự thứ ba đến thứ năm từ mỗi mẫu.

Bước 3) Tiếp tục tương tự với các ký tự còn lại.

Cấu trúc đệ quy của bài toán LCS

Cấu trúc đệ quy của bài toán LCS

Chúng ta tìm LCS (Common Score) trên chuỗi con (chuỗi được tạo ra từ chuỗi gốc). Sau đó, chúng ta ghi lại độ dài của LCS của các chuỗi con đó.

Bây giờ, đây là một tính chất thú vị khác đó là trùng lặpping vấn đề phụMột vấn đề được cho là có sự chồng chéo.ping Các bài toán con nếu đề bài có thể được chia thành các bài toán con nhỏ và được sử dụng nhiều lần trong chương trình.

Sơ đồ dưới đây cho thấy thuật toán đệ quy đã gọi hàm có cùng tham số nhiều lần.

Sự chồng chéo cấu trúc phụ tối ưuping các vấn đề con

Ví dụ, hãy nhìn vào cây đệ quy. Trong ô màu đậm, bạn có thể nhận thấy sự chồng chéo.ping các vấn đề con. (“RG”, “RA”), (“RG”, “R”) và các vấn đề khác được gọi nhiều lần.

Để tối ưu hóa điều này, chúng ta có phương pháp tiếp cận như sau: Lập trình năng động (ĐP).

Phương pháp đệ quy tìm dãy con chung dài nhất

Đồ thị ở trên minh họa phương pháp đệ quy. Mỗi hàm đệ quy đều có một trường hợp cơ sở để thoát khỏi vòng lặp đệ quy hoặc bắt đầu trả về từ ngăn xếp của nó.

Để thực hiện việc này, chúng ta sẽ sử dụng trường hợp cơ bản. Vì vậy, thuật toán giống như sau:

  • Nếu tất cả các phần tử trước phần tử cuối cùng đều trùng khớp, thì tăng độ dài lên một và quay lại.
  • Truyền hai mẫu vào hàm và lấy giá trị lớn nhất trong các giá trị trả về.
  • Nếu một mẫu có độ dài bằng 0 thì chúng ta không có chuỗi con nào để so sánh. Trả về XNUMX trong trường hợp này. Đây là trường hợp cơ bản của đệ quy.

Biệt danh Code:

def lcs:
    input: pattern_1, pattern_2, len_1, len_2
    if len_1 or len_2 is zero:
        return 0
    if pattern_1[len_1 - 1] equals pattern_2[len_2 - 1]:
        return 1 + lcs(pattern_1, pattern_2, len_1 - 1, len_2 - 1)
    else:
        return max(lcs(pattern_1, pattern_2, len_1 - 1, len_2),
                   lcs(pattern_1, pattern_2, len_1, len_2 - 1))

Triển khai tại C++

#include<iostream>
#include<bits/stdc++.h>
using namespace std;
int lcs(string pattern_1, string pattern_2, int len_1, int len_2) {
  if (len_1 == 0 || len_2 == 0)
    return 0;
  if (pattern_1[len_1 - 1] == pattern_2[len_2 - 1]) {
    return 1 + lcs(pattern_1, pattern_2, len_1 - 1, len_2 - 1);
  } else {
    return max(lcs(pattern_1, pattern_2, len_1 - 1, len_2), lcs(pattern_1, pattern_2, len_1, len_2 - 1));
  }
}
int main() {
  string pattern_1, pattern_2;
  pattern_1 = "RGBGARGA";
  pattern_2 = "BGRARG";
  cout<<"Length of LCS is: "<<lcs(pattern_1, pattern_2, pattern_1.size(), pattern_2.size())<<endl;
}

Đầu ra:

Length of LCS is: 5

Triển khai tại Python

def lcs(pattern_1, pattern_2, len_1, len_2):
    if len_1 == 0 or len_2 == 0:
        return 0
    if pattern_1[len_1 - 1] == pattern_2[len_2 - 1]:
        return 1 + lcs(pattern_1, pattern_2, len_1 - 1, len_2 - 1)
    else:
        return max(lcs(pattern_1, pattern_2, len_1 - 1, len_2),
                   lcs(pattern_1, pattern_2, len_1, len_2 - 1))

pattern_1 = "RGBGARGA"
pattern_2 = "BGRARG"
print("Length of LCS is: ", lcs(pattern_1, pattern_2, len(pattern_1), len(pattern_2)))

Đầu ra:

Length of LCS is:  5

Phương pháp lập trình động tìm dãy con chung dài nhất (LCS)

Lập trình động nghĩa là tối ưu hóa phương pháp đệ quy thông thường. Ví dụ, nếu nhìn vào đồ thị của phương pháp đệ quy hoặc phương pháp đơn giản, ta có thể thấy có nhiều lời gọi hàm giống hệt nhau. Phương pháp lập trình động ghi lại tất cả các phép tính vào một mảng và sử dụng lại chúng khi cần thiết.

Chúng ta sẽ sử dụng một mảng 2 chiều có kích thước mxn, trong đó m và n là độ dài của mẫu 1 và mẫu 2. Đối với một mảng 2D, chúng ta có thể sử dụng cấu trúc dữ liệu Danh sách trong Python hoặc cấu trúc dữ liệu vectơ/mảng trong C++.

Biệt danh Code Đối với LCS sử dụng DP:

LCS(pattern_1, pattern_2):
    m = length of pattern_1 + 1
    n = length of pattern_2 + 1
    dp[n][m]
    for i in range 0 to n + 1:
        for j in range 0 to m + 1:
            if i or j equals to 0:
                dp[i][j] = 0
            else if pattern_1[i] == pattern_2[j]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return dp[n][m]

Đây là bảng LCS được sử dụng làm cấu trúc dữ liệu mảng 2D cho phương pháp lập trình động.

Phương pháp lập trình động của bảng LCS 2D

Chúng ta hãy cùng thảo luận về logic mà chúng ta đã sử dụng ở đây. Các bước như sau:

Bước 1) Nếu i hoặc j bằng 0, ta lấy một chuỗi rỗng từ hai chuỗi đã cho và cố gắng tìm các chuỗi con chung. Tuy nhiên, vì chuỗi con ta lấy là chuỗi rỗng, nên độ dài của chuỗi con là 0.

Bước 2) Nếu hai ký tự trùng khớp, chúng ta sẽ gán giá trị cho chỉ số (i,j) bằng cách tăng LCS đã tính toán trước đó, LCS này nằm ở chỉ số (i-1,j-1) (từ hàng trước).

Bước 3) Nếu không khớp, ta sẽ lấy LCS lớn nhất của hai chỉ số liền kề. Và bằng cách này, ta cần điền đầy đủ các giá trị vào mảng 2D.

Bước 4) Cuối cùng, chúng ta sẽ trả về giá trị của ô cuối cùng của mảng 2D.

Về cơ bản, tất cả các giá trị trong mảng 2D đều chứa độ dài của các dãy con chung. Trong số đó, ô cuối cùng chứa độ dài của dãy con chung dài nhất.

Triển khai tại C++

#include<iostream>
using namespace std;
int lcs(string pattern_1, string pattern_2) {
  int m = pattern_1.size();
  int n = pattern_2.size();
  // dp will store solutions as the iteration goes on
  int dp[n + 1][m + 1];
  for (int i = 0; i < n + 1; i++) {
    for (int j = 0; j < m + 1; j++) {
      if (i == 0 || j == 0) {
        dp[i][j] = 0;
      } else if (pattern_2[i - 1] == pattern_1[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[n][m];
}
int main() {
  string pattern_1 = "RGBGARGA";
  string pattern_2 = "BGRARG";
  cout<<"Length of LCS: "<<lcs(pattern_1, pattern_2)<<endl;
}

Đầu ra:

Length of LCS: 5

Triển khai tại Python

def lcs(pattern_1, pattern_2):
    m = len(pattern_1)
    n = len(pattern_2)
    # dp will store solutions as the iteration goes on
    dp = [[None] * (n + 1) for item in range(m + 1)]
    for i in range(m + 1):
        for j in range(n + 1):
            if i == 0 or j == 0:
                dp[i][j] = 0
            elif pattern_1[i - 1] == pattern_2[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]

pattern_1 = "RGBGARGA"
pattern_2 = "BGRARG"
print("Length of LCS: ", lcs(pattern_1, pattern_2))

Đầu ra:

Length of LCS: 5

Vì vậy, cả hai chuỗi đều có dãy con chung dài nhất có độ dài 5.

Tóm lại, trong phương pháp quy hoạch động (DP), chúng ta chỉ tính toán mỗi tác vụ một lần. Trong phương pháp đệ quy, có thể xảy ra sự chồng chéo.ping các vấn đề con.

Trong Thuật toán lập trình động này, chúng tôi đang sử dụng ma trận 2D. Sẽ có hai chuỗi cho trước (giả sử cả hai đều có độ dài n). Khi đó không gian cần thiết trong mảng là nx n. Nếu chuỗi đủ lớn, chúng ta sẽ cần phiên bản giải pháp DP được tối ưu hóa bộ nhớ.

Logic đơn giản hóa được thực hiện trong mã là:

  • Khai báo mảng 2D DP[m][n].
  • Điền vào hàng đầu tiên và cột đầu tiên của mảng DP bằng 0.
  • Lấy i và j cho lần lặp.
  • Nếu pattern1[i] bằng pattern2[j], thì cập nhật DP[i][j] = DP[i-1][j-1] + 1.
  • Nếu pattern1[i] không bằng pattern2[j], thì DP[i][j] sẽ là giá trị lớn nhất giữa DP[i-1][j] và DP[i][j-1].
  • Tiếp tục cho đến khi i và j đạt tới m và n.
  • Phần tử cuối cùng, DP[m-1][n-1], sẽ chứa độ dài.

Ở đây, nó được gọi là DP[m-1][n-1] vì chỉ số mảng bắt đầu từ 0.

Câu Hỏi Thường Gặp

Các thuật toán học máy sử dụng LCS như một đặc trưng tương đồng trong phân loại văn bản, đánh giá chuỗi-đến-chuỗi và phát hiện đạo văn mã. Nó cũng là nền tảng của các chỉ số kiểu BLEU và ROUGE dùng để chấm điểm văn bản được tạo ra so với các kết quả tham chiếu.

Đúng vậy. Các trợ lý lập trình AI như GitHub Copilot và GPT có thể tạo ra các phiên bản lập trình đệ quy và lập trình động của LCS. Python, C++, hoặc là JavaHọ cũng có thể thêm tính năng ghi nhớ kết quả, in ra chuỗi con thực tế hoặc chuyển đổi mã sang dạng lặp theo yêu cầu.

Chuỗi con phải liên tục, trong khi dãy con chỉ cần giữ nguyên thứ tự. Ví dụ: “ABCDE”, “ACD” là một dãy con hợp lệ nhưng không phải là chuỗi con, trong khi “BCD” vừa là chuỗi con vừa là dãy con.

Phiên bản lập trình động chạy trong thời gian và không gian O(m·n), trong đó m và n là độ dài của hai chuỗi đầu vào. Phiên bản đệ quy thông thường chạy trong thời gian O(2^(m+n)) theo cấp số mũ trong trường hợp xấu nhất.

LCS cung cấp sức mạnh cho các tiện ích so sánh tập tin, hợp nhất Git, căn chỉnh trình tự DNA và protein trong tin sinh học, phát hiện đạo văn, kiểm tra chính tả và các công cụ đồng bộ hóa dữ liệu cần phải bảo toàn thứ tự chia sẻ của các bản ghi.

Bảng chuẩn yêu cầu không gian O(m·n). Tối ưu hóa hai hàng trượt giúp giảm không gian xuống O(min(m, n)) khi bạn chỉ cần độ dài, mặc dù việc tái tạo chuỗi con thực tế vẫn cần toàn bộ bảng.

Đúng vậy, đệ quy thuần túy hoạt động tốt với các chuỗi ngắn nhưng sẽ tính toán lại cùng một bài toán con nhiều lần và trở nên không thực tế khi chuỗi dài hơn 20 đến 25 ký tự. Việc thêm ghi nhớ hoặc bảng quy hoạch động sẽ khôi phục lại điều này. trachiệu suất bảng.

Đúng vậy. Ý tưởng DP mở rộng đến k chuỗi bằng cách sử dụng bảng k chiều với thời gian và không gian O(n^k). Biến thể này xuất hiện trong các công cụ so sánh nhiều tệp và căn chỉnh nhiều chuỗi trong tin sinh học.

Tóm tắt bài viết này với: