Pikimad ühised alajadad (LCS) on IT-vald­kon­nas tüüpiline probleem. LCS-probleemi la­hen­da­mise lä­he­ne­mis­vii­sid esinevad sageli prog­ram­mee­ri­jate töö­in­terv­juu­des ja algo­ritmide kursustel.

Mis on pikim ühine alajada?

Eesmärk on leida kahe jada pikim ühine alajada. Siin on alajada tuletatud jadast. Alajadal on sama ele­men­tide järjekord, mõnel juhul, kui elemente on eemal­da­tud. Vaadakem mõningaid näiteid, et saada parem ülevaade põ­hi­mõt­test:

Jada X Jada Y LCS(X, Y)
FATHER MATTER ATER
MOTHER HERBAL HER
DAVID DANIEL DAI
Note

Pikima ühise alajada ja pikima ühise alamjada vahel on oluline erinevus, sest alamjada ei tohi sisaldada lünki. Näites „DAVID” ja „DANIEL” oleks pikim ühine alamjada „DA”, kuna „I” on eraldatud tähtedega „V” ja „N”.

Kas on olemas prak­tilisi näiteid LCS-probleemi kohta?

Pikima ühise alajada probleem on oluline küsimus kõikides vald­kon­da­des, kus ka­su­ta­takse üks­tei­sest tu­le­ne­vaid jadasid. On olemas teatud viisid LCS-i leid­miseks, et näha, kas on sarnasusi või erinevusi, ja omakorda avastada või­ma­likku pla­giee­ri­mist. Tuntud utiliit „diff”, mis kont­rol­lib muudatusi al­li­ka­teks­ti­fai­li­des, põhineb LCS-prob­lee­mil.

Bioin­for­maa­ti­kas ka­su­ta­takse DNA jär­jes­tuste ana­lüü­si­misel sageli pikima ühise ala­jär­jes­tuse probleemi. DNA alused muutuvad aja jooksul mu­tat­sioo­nide tõttu teatud kohtades. Kahe jär­jes­tuse pika ühise ala­jär­jes­tuse olemasolu viitab ge­nee­ti­li­sele su­gu­lu­sele. See võimaldab meil ge­nee­ti­lise arengu abil jälgida lii­ki­de­va­he­list evo­lut­siooni.

Kee­le­tead­la­sed võivad kasutada pikima ühise alajada probleemi ka keelelise arengu uuri­miseks läbi sajandite. Kui teil on kaks erinevast keelest pärit sõna, millel on sama tähendus ja pikk ühine alajada, viitab see sellele, et neil on sama juur ja etü­mo­loo­gia. Seda võiks seega pidada sarnaseks aja­loo­li­seks arenguks.

Millised on la­hen­dused pikima ühise alajada prob­lee­mile?

Esiteks võite LCS-probleemi lahendada naiivse lä­he­ne­mis­vii­siga. See on lihtne lä­he­ne­mis­viis, mis ei kasuta op­ti­mee­ri­mist ega spet­siaal­seid meetodeid. Selleks arvutate lihtsalt välja kõik kahe jada alajadad ja leiate pikima ühise alajada. Kahjuks ei ole see lä­he­ne­mis­viis väga efek­tiivne ja sobib seetõttu ainult lühikeste jadade puhul.

Allpool leiate kolm tõhusat lahendust LCS-prob­lee­mile:

  1. Re­kur­siivne lä­he­ne­mine
  2. Op­ti­mee­ri­mine me­moi­sat­siooni abil
  3. Dü­naa­mi­line prog­ram­mee­ri­mine

Kõikidel lä­he­ne­mis­vii­si­del on ühine see, et kahe jär­jes­tuse puhul erinevad need kolmel juhul:

  • Viimane täht on sama.
  • Viimane täht ei ole sama.
  • Ühe jär­jes­tuse pikkus on null.

Need lä­he­ne­mis­vii­sid erinevad üks­tei­sest ajalise keerukuse (asümp­too­ti­line tööaeg) ja ruumilise keerukuse (mälu ka­su­ta­mine) poolest:

Lä­he­ne­mis­viis Kestus Mälu
Naivne lä­he­ne­mis­viis O(n * n²) O(n)
Re­kur­siivne lä­he­ne­mine O(n²) O(1)
Op­ti­mee­ri­mine me­moi­sat­siooni abil O(n *m) O(n* m)
Dü­naa­mi­line prog­ram­mee­ri­mine O(n *m) O(n* m)
Note

Allpool esitatud algo­rit­mid arvutavad kõik pikima ühise alajada pikkuse. Vajaduse korral on olemas mitu sellise pikkusega alajada, mis saab määrata teiste sammude abil.

Pikima ühise alajada re­kur­siivne määramine

LCS-probleemi vaadates võib näha, et sellel on „op­ti­maalne alus­st­ruk­tuur”. See tähendab, et probleemi saab jagada alamp­rob­leemi­deks. La­hen­dus­ena võib kasutada re­kur­siiv­set lä­he­ne­mist. Allpool on toodud mõned näited algo­ritmi­dest, millega seda kolme le­vi­nui­mas prog­ram­mee­ri­mis­kee­les rakendada.

Python

def lcs(X, Y, m, n):
    # Base case
    if m == 0 or n == 0:
        return 0
    # Last elements are equal
    elif X[m-1] == Y[n-1]:
        return 1 + lcs(X, Y, m-1, n-1)
    # Last elements differ
    else:
        return max(lcs(X, Y, m, n-1), lcs(X, Y, m-1, n))
# Let's test
X, Y = "DANIEL", "DAVID"
lcs_len = lcs(X, Y, len(X), len(Y))
print(f"Length of LCS is: {lcs_len}")
python

Java

import java.io.*;
class LCS {
    public static int lcs(String A, String B, int m, int n)
    {
        // Base case
        if (m == 0 || n == 0)
            return 0;
        // Last elements are equal
        if (A.charAt(m - 1) == B.charAt(n - 1))
            return 1 + lcs(A, B, m - 1, n - 1);
        // Last elements differ
        else
            return Math.max(lcs(A, B, m, n - 1),
             lcs(A, B, m - 1, n));
    }
    
    // Let's test
    public static void main(String[] args)
        {
            String X = "DAVID";
            String Y = "DANIEL";
            int lcsLength = LCS.lcs(X, Y, X.length(), Y.length());
            System.out.println("Length of LCS is: " + lcsLength);
        }
}
java

C++

#include <iostream>
using namespace std;
int lcs(string X, string Y, int m, int n)
{
    // Base case
    if (m == 0 || n == 0) {
        return 0;
    }
    // Last elements are equal
    if (X[m - 1] == Y[n - 1]) {
        return 1 + lcs(X, Y, m - 1, n - 1);
    }
    // Last elements differ
    else {
        return max(lcs(X, Y, m, n - 1), lcs(X, Y, m - 1, n));
    }
}
// Let's test
int main()
{
    // Initialize variables
    string X = "DAVID";
    string Y = "DANIEL";
    // Compute and output length of LCS
    cout << "Length of LCS is " << lcs(X, Y, X.size(), Y.size());
    return 0;
}
c++

Re­kur­siivse lä­he­ne­mise op­ti­mee­ri­mine me­moi­sat­siooni abil

Kui vaadata uuesti re­kur­siiv­set lä­he­ne­mist, näete, et ar­vu­ta­takse kattuvad osad. Need omadused, mida ni­me­ta­takse „kat­tu­va­teks alamp­rob­leemi­deks”, on tuntud Fibonacci jadas. Ka sel juhul ar­vu­ta­takse lahenduse leid­miseks pidevalt re­kur­siiv­seid jadasid. Protsessi tõ­hus­ta­miseks tasub kasutada me­moi­sat­siooni. Teisisõnu, võite juba arvutatud alamp­rob­lee­mid sal­ves­tada ka­he­mõõt­me­lisse maatriksi.

Python

def lcs(X, Y, m, n, table):
    
    # Base case
    if (m == 0 or n == 0):
        return 0
    # Already computed value at given position
    if (table[m][n] != -1):
        return table[m][n]
    # Last elements are equal
    if X[m - 1] == Y[n - 1]:
        table[m][n] = 1 + lcs(X, Y, m - 1, n - 1, table)
    # Last elements differ
    else:
        table[m][n] = max(lcs(X, Y, m, n - 1, table), lcs(X, Y, m - 1, n, table))
    return table[m][n]
# Let's test
X = "DAVID"
Y = "DANIEL"
m, n = len(X), len(Y)
# Initialize table fields to `-1`
table = [[-1 for i in range(n + 1)] for j in range(m + 1)]
// Compute and output length of LCS
print(f"Length of LCS is: {lcs(X, Y, m, n, table)}")
python

Java

import java.io.*;
class LCS {
    public static int lcs(String X, String Y, int m, int n, int[][] table) {
        // Base case
        if (m == 0 || n == 0) {
            return 0;
        }
        // Already computed value at given position
        if (table[m][n] != -1) {
            return table[m][n];
        }
        // Last elements are equal
        if(X.charAt(m - 1) == Y.charAt(n - 1)) {
            table[m][n] = 1 + lcs(X, Y, m - 1, n - 1, table);
            return table[m][n];
        }
        // Last elements differ
        else {
            table[m][n] = Math.max(lcs(X, Y, m, n - 1, table),lcs(X, Y, m - 1, n, table));
            return table[m][n];
        }
    }
    // Let's test
    public static void main(String args[]){
        // Initialize variables
        String X = "DAVID";
        String Y = "DANIEL";
        int m = X.length();
        int n = Y.length();
        int[][] table = new int[m + 1][n + 1];
        
        // Initialize table fields to `-1`
        for(int i=0; i < m + 1; i++) {
            for(int j=0; j < n + 1; j++) {
                table[i][j] = -1;
            }
        }
        // Compute and output length of LCS
        int lcsLength = lcs(X, Y, m, n, table);
        System.out.println("Length of LCS is: " + lcsLength);
    }
}
java

C++

#include <bits/stdc++.h>
using namespace std;
int lcs(char *X, char* Y, int m, int n, vector<vector<int>>& table)
{
    // Base case
    if (m == 0 || n == 0)
        return 0;
    // Already computed value at given position
    if (table[m][n] != -1) {
        return table[m][n];
    }
    // Last elements are equal
    if (X[m - 1] == Y[n - 1]) {
        table[m][n] = 1 + lcs(X, Y, m - 1, n - 1, table);
        return table[m][n];
    }
    // Last elements differ
    else { 
        table[m][n] = max(lcs(X, Y, m, n - 1, table), lcs(X, Y, m - 1, n, table));
        return table;
    }
}
// Let's test
int main()
{
    // Initialize variables
    char X[] = "DAVID";
    char Y[] = "DANIEL";
    int m = strlen(X);
    int n = strlen(Y);
    // Initialize table with `-1`
    vector<vector<int>> table(m + 1, vector<int>(n + 1, -1));
    
    // Compute and output length of LCS
    cout << "Length of LCS is: " << lcs(X, Y, m, n, table);
    return 0;
}
c++

Dy­naa­mi­line prog­ram­mee­ri­mine pikima ühise alajada jaoks

Dü­naa­mi­line prog­ram­mee­ri­mine on mit­te­süs­te­maa­ti­line viis op­ti­mee­ri­mis­üles­an­nete la­hen­da­miseks, jagades need väik­se­ma­teks alamp­rob­leemi­deks ja la­hen­da­des need seejärel „alt ülespoole”. Muu hulgas ka­su­ta­takse dü­naa­mi­list prog­ram­mee­ri­mist la­hen­dus­ena tee­deot­singu algo­ritmide jaoks. Ka pikima ühise alajada probleemi saab lahendada dü­naa­mi­lise prog­ram­mee­ri­mise abil, kasutades ka­he­mõõt­me­list maat­rik­sit.

Python

def lcs(X , Y, m, n): 
    
    # Initialize dynamic programming table fields to `None`
    table = [[None] * (n + 1) for _ in range(m + 1)]
    
    # Compute table values
    for i in range(m + 1):
        for j in range(n + 1):
            # Base case
            if i == 0 or j == 0 :
                table[i][j] = 0
            # Last elements are equal
            elif X[i - 1] == Y[j - 1]:
                table[i][j] = table[i - 1][j - 1]+ 1
            # Last elements differ
            else:
                table[i][j] = max(table[i - 1][j] , table[i][j - 1])
    
    return table[m][n]
# Let's test
X = "DAVID"
Y = "DANIEL"
# Compute and output length of LCS
lcs_len = lcs(X, Y, len(X), len(Y))
print(f"Length of LCS is: {lcs_len}")
python

Java

import java.io.*;
class LCS {
    public static int lcs(String X, String Y, int m, int n)
    {
        // Initialize dynamic programming table fields
        int table[][] = new int[m + 1][n + 1];
        for (int i = 0; i <= m; i++) {
            for (int j = 0; j <= n; j++) {
                // Base case
                if (i == 0 || j == 0)
                    table[i][j] = 0;
                // Last elements are equal
                else if (X.charAt(i - 1) == Y.charAt(j - 1))
                    table[i][j] = table[i - 1][j - 1] + 1;
                // Last elements differ
                else
                    table[i][j] = Math.max(table[i - 1][j], table[i][j - 1]);
            }
        }
        return table[m][n];
    }
    // Let's test
    public static void main(String args[]){
        // Initialize variables
        String X = "DAVID";
        String Y = "DANIEL";
        int m = X.length();
        int n = Y.length();
        // Compute and output length of LCS
        int lcsLength = lcs(X, Y, m, n);
        System.out.println("Length of LCS is: " + lcsLength);
    }
}
java

C++

#include <bits/stdc++.h>
using namespace std;
int lcs(string X, string Y, int m, int n) {
	// Initialize dynamic programming table
	int table[m + 1][n + 1];
	// Compute table values
	for (int i = 0; i <= m; i++) {
		for (int j = 0; j <= n; j++) {
			// Base case
			if (i == 0 || j == 0)
				table[i][j] = 0;
			// Last elements are equal
			else if (X[i - 1] == Y[j - 1])
				table[i][j] = table[i - 1][j - 1] + 1;
			// Last elements differ
			else
				table[i][j] = max(table[i - 1][j], table[i][j - 1]);
		}
	}
	return table[m][n];
}
// Let's test
int main() {
  // Initialize variables
  string X = "DAVID";
  string Y = "DANIEL";
  int m = X.size();
  int n = Y.size();
  // Compute and output length of LCS
  cout << "Length of LCS is " << lcs(X, Y, m, n);
  return 0;
}
c++
Go to Main Menu