Længste fælles del­se­kven­ser (LCS) er et typisk problem inden for IT. Tilgange til løsning af LCS-problemet optræder ofte i in­ter­views med pro­gram­mø­rer og på al­go­rit­me­kur­ser.

Hvad er den længste fælles del­se­kvens?

Målet er at finde den længste fælles del­se­kvens i to sekvenser. Her stammer en del­se­kvens fra en sekvens. Del­se­kven­sen har samme ele­men­tæ­re ræk­ke­føl­ge, i nogle tilfælde når elementer fjernes. Lad os se på nogle eksempler for at få en bedre for­stå­el­se af prin­cip­pet:

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

Der er en vigtig forskel mellem den længste fælles del­se­kvens og den længste fælles delstreng, idet en delstreng ikke må have mellemrum. I eksemplet med ‘DAVID’ og ‘DANIEL’ ville den længste fælles delstreng være ‘DA’, da ‘I’ er adskilt af ‘V’ og ‘N’.

Er der nogen praktiske eksempler på LCS-problemet?

Problemet med den længste fælles del­se­kvens er et vigtigt emne inden for alle områder, hvor der anvendes sekvenser, der stammer fra hinanden. Der findes visse metoder til at finde LCS for at se, om der er ligheder eller forskelle, og dermed afsløre eventuel plagi­e­ring. Det velkendte værktøj ‘diff’, der kon­trol­le­rer for ændringer i kil­de­tekst­fi­ler, er baseret på LCS-problemet.

I bio­in­for­ma­tik bruges problemet med den længste fælles del­se­kvens ofte til analyse af DNA-sekvenser. DNA-baser ændrer sig med tiden på bestemte po­si­tio­ner på grund af muta­tio­ner. Til­ste­de­væ­rel­sen af en lang fælles del­se­kvens i to sekvenser tyder på et genetisk slægtskab. Dette gør det muligt for os at spore evo­lu­tio­nen mellem arter takket være den genetiske udvikling.

Sprog­for­ske­re kan også bruge problemet med den længste fælles del­se­kvens til at undersøge sproglig udvikling gennem år­hund­re­der. Hvis man har to ord fra for­skel­li­ge sprog, som har samme betydning og en lang fælles del­se­kvens, tyder det på, at de har samme rod og etymologi. Dette vil så blive betragtet som en lignende historisk udvikling.

Hvad er løs­nin­ger­ne på problemet med den længste fælles del­se­kvens?

Først og fremmest kan du løse LCS-problemet med en naiv tilgang. Dette er en enkel tilgang uden brug af op­ti­me­rin­ger eller specielle metoder. For at gøre dette beregner du blot alle del­se­kven­ser fra to sekvenser og finder den længste fælles del­se­kvens. Desværre er denne tilgang ikke særlig effektiv og er derfor kun egnet til korte sekvenser.

Nedenfor finder du tre effektive løsninger på LCS-problemet:

  1. Rekursiv tilgang
  2. Op­ti­me­ring gennem me­moi­sa­tion
  3. Dynamisk pro­gram­me­ring

Fælles for alle tilgange er, at de med hensyn til de to sekvenser adskiller sig i tre tilfælde:

  • Det sidste bogstav er det samme.
  • Det sidste bogstav er ikke det samme.
  • En af se­kven­ser­nes længde er nul.

De for­skel­li­ge tilgange adskiller sig fra hinanden med hensyn til tids­kom­plek­si­tet (asymp­to­tisk køretid) og rum­kom­plek­si­tet (hukom­mel­ses­for­brug):

Tilgang Køretid Hukom­mel­se
Naiv tilgang O(n * n²) O(n)
Rekursiv tilgang O(n²) O(1)
Op­ti­me­ring via me­moi­sa­tion O(n *m) O(n* m)
Dynamisk pro­gram­me­ring O(n *m) O(n* m)
Note

Ne­den­stå­en­de al­go­rit­mer beregner alle længden af den længste fælles del­se­kvens. Der er, hvis det er relevant, flere del­se­kven­ser af denne længde, som kan bestemmes ved hjælp af andre trin.

Rekursiv be­stem­mel­se af den længste fælles del­se­kvens

Hvis man ser på LCS-problemet, kan man se, at det har en ‘optimal un­der­struk­tur’. Det betyder, at problemet kan reduceres til del­pro­ble­mer. Som løsning kan man bruge en rekursiv tilgang. Nedenfor finder du nogle eksempler på al­go­rit­mer til at im­ple­men­te­re dette i tre af de mest al­min­de­li­ge pro­gram­me­rings­sprog.

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++

Op­ti­me­ring af den rekursive tilgang ved hjælp af me­moi­sa­tion

Når man ser på den rekursive tilgang igen, kan man se, at de over­lap­pen­de dele beregnes. Disse ka­rak­te­ri­sti­ka, kaldet ‘over­lap­pen­de del­pro­ble­mer’, er kendt fra Fibonacci-sekvenser. Også i dette tilfælde beregnes rekursive sekvenser konstant for at finde en løsning. For at gøre processen mere effektiv er det en god idé at bruge me­moi­sa­tion. Med andre ord kan man cache de del­pro­ble­mer, der allerede er beregnet, i en to­di­men­sio­nal matrix.

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++

Dynamisk pro­gram­me­ring for den længste fælles del­se­kvens

Dynamisk pro­gram­me­ring er en ikke-rekursiv metode til at løse op­ti­me­rings­pro­ble­mer ved at opdele dem i mindre del­pro­ble­mer og derefter løse dem ‘nedenfra og op’. Dynamisk pro­gram­me­ring anvendes blandt andet som en løsning til sti­fin­dings­al­go­rit­mer. Problemet med den længste fælles del­se­kvens kan også løses ved hjælp af dynamisk pro­gram­me­ring, når der anvendes en to­di­men­sio­nal matrix.

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++
Gå til ho­ved­me­nu­en