【算法】第二課學習筆記

1、最長公共子序列 LCS (Longest Common Subsequence)

1. 題目

給定兩個串序列A、B,找出兩個串的最長公共子序列LCS包含的元素和長度。html

注意:
(1) 一個序列S任意刪除若干個字符獲得新序列T,則T叫作S的子序列。
(2) 與最長公共子串(Longest Common Substring)相區別,最長公共子串要求元素相同且連續,而最長公共子序列只要求元素出現的順序一致,並不要求連續。即對於
 串A:abcde
 串B:bcdae
最長公共子串爲:bcd
最長公共子序列爲:bcde

2. 分析

該算法主要使用了動態規劃算法(DP)。java

(1). LCS解題的記號

記號

(2). 第一種狀況 xm = yn

第一種狀況
示意圖
舉例

(3). 第二種狀況 xm != yn

第二種狀況
舉例

(4). 數據結構

算法中使用到的數據結構以下:node

長度數組

方向向量

(5). 例子

示例:
X = < A,B,C,B,D,A,B >
Y = < B,D,C,A,B,A >
最長公共子序列爲:<B,C,B,A>c++

該過程生成的二維數組以下圖所示:
例子的長度數組和方向向量算法

(6). 進一步思考

圖片描述

3. 代碼

int m = 7 + 1, n = 6 + 1;
int c[m][n], b[m][n];
// 注意A、B字符串的第一個字符是不進行計算的,能夠用_來進行佔位
char A[] = "_ABCBDAB";
char B[] = "_BDCABA";
// 對第0行和第0列進行初始化
// c中 0爲初始值,1表明左,2表明上,3表明左上
for (int i = 0; i < m; i++)
    b[i][0] = c[i][0] = 0;
for (int j = 0; j < n; j++)
    b[0][j] = c[0][j] = 0;

for (int i = 1; i < m; i++){
    for (int j = 1; j < n; j++){
        if (A[i] == B[j]){
            c[i][j] = c[i - 1][j - 1] + 1;
            b[i][j] = 3;
        } else{
            if (c[i - 1][j] >= c[i][j - 1]){
                c[i][j] = c[i - 1][j];
                b[i][j] = 2;
            } else{
                c[i][j] = c[i][j - 1];
                b[i][j] = 1;
            }
        }
    }
}
// 輸出二維數組的值
for (int i = 0; i < m; i++){
    for (int j = 0; j < n; j++){
        cout << c[i][j] << "(";
        switch (b[i][j]){
            case 1: cout << "←"; break;
            case 2: cout << "↑"; break;
            case 3: cout << "↖"; break;
            default: cout << " "; break;
        }
        cout << ")  ";
    }
    cout << endl;
}

4. 推廣

LCS的應用:最長遞增子序列LIS(Longest Increasing Subsequence)
 給定一個長度爲N的數組,找出一個最長的單調遞增子序列。
 例如:給定數組{5, 6, 7, 1, 2, 8},則其最長的單調遞增子序列爲{5,6,7,8},長度爲4。數組

分析:其實此LIS問題能夠轉換成最長公子序列問題,爲何呢?數據結構

 原數組爲A {5, 6, 7, 1, 2, 8}
 排序後:A’ {1, 2, 5, 6, 7, 8}函數

由於,原數組A的子序列順序保持不變,並且排序後A’自己就是遞增的,這樣,就保證了兩序列的最
長公共子序列的遞增特性。如此,若想求數組A的最長遞增子序列,其實就是求數組A與它的排序數
組A’的最長公共子序列。性能

(此外,本題也能夠直接使用動態規劃/貪心法來求解)優化

5. 應用

求兩個序列中最長的公共子序列算法,普遍的應用在圖形類似處理、媒體流的類似比較、計算生物學
方面。生物學家經常利用該算法進行基因序列比對,由此推測序列的結構、功能和演化過程。

LCS能夠描述兩段文字之間的「類似度」,即它們的雷同程度,從而可以用來辨別抄襲。另外一方面,對
一段文字進行修改以後,計算改動先後文字的最長公共子序列,將除此子序列外的部分提取出來,這
種方法判斷修改的部分,每每十分準確。簡而言之,百度知道、百度百科都用得上。

2、最長遞增子序列 LIS (Longest Increasing Subsequence)

1. 題目

給定一個串,找出該串的最長遞增子序列LIS包含的元素和長度。

最長遞增子序列,基本定義與最長公共子序列類似,還要求該序列是遞增序列。

2. 分析

動態規劃解法

求解LIS

注意:在計算 b[i]的時候,須要遍歷 b數組在 i以前全部位置j的值,取 b[j]爲最大值且 aj<ai,此時
b[i] = b[j] + 1

3. 代碼

// a數組存儲序列元素
// b數組存儲LIS序列長度
// c數組存儲b[i]的計算來源位置的下標
int a[] = { 1, 4, 6, 2, 8, 9, 7 };
int n = 7;
int b[n], c[n];
int i, j, k;
// 初始化
for (int i = 0; i < n; i++) {
    b[i] = 1;
    c[i] = -1;
}
for (i = 1; i < n; i++) {
    k = -1;
    for (j = i - 1; j >= 0; j--) {
        if (a[j] < a[i] && b[j] > k) {
            b[i] = b[j] + 1;
            c[i] = j;
            k = b[j];
        }
    }
}
// 自定義輸出數組函數,print(int a[], int n)
cout << "array: ";
print(a, n);
cout << "LIS: ";
print(b, n);
cout << "pos: ";
print(c, n);
// stack用於存儲序列元素
// max爲最大的LIS序列的長度
int stack[n];
int top = -1, max = -1;
k = -1;
for (i = 0; i < n; i++)
    if (max < b[i]) {
        max = b[i];
        k = i;
    }
cout << "max: " << max << endl;
while (c[k] != -1) {
    stack[++top] = a[k];
    k = c[k];
}
stack[++top] = a[k];
cout << "LIS序列: ";
while (top != -1) {
    cout << stack[top--] << ", ";
}

4. 輸出

array: 1, 4, 6, 2, 8, 9, 7
LIS: 1, 2, 3, 2, 4, 5, 4
pos: -1, 0, 1, 0, 2, 4, 2
max: 5
LIS序列: 1, 4, 6, 8, 9

3、 KMP算法 (The Knuth-Morris-Pratt Algorithm)

1. 題目

在計算機科學中,Knuth-Morris-Pratt字符串查找算法(簡稱爲KMP算法)可在一個主文本字符串S內查找一個詞W的出現位置。此算法經過運用對這個詞在不匹配時自己就包含足夠的信息來肯定下一個匹配將在哪裏開始的發現,從而避免從新檢查先前匹配的字符。

KMP算法解決的是字符串的查找問題,即:

給定文本串text和模式串pattern,從文本串text中找出模式串pattern第一次出現的位置。

2. 分析

(1). 暴力求解 (Brute Force)

暴力求解

(2). 改進BF,使其成爲KMP

改進BF,使其成爲KMP

(3). KMP算法

算法步驟:

  • 1.先求模式串的next數組
  • 2.進行模式串和目標串的匹配

挖掘字符串比較機制

分析結論

求模式串next數組

next的遞推關係

不相等時的遞歸判斷

計算next數組代碼

KMP code

(4). 優化next數組,加快KMP匹配速度

進一步優化

優化以後的code

優化以後的next數組

(5). KMP算法性能分析

時間複雜度

時間複雜度

最差狀況

最差狀況的變種KMP

(6). 擴展

DFA

3. 代碼

(1). 暴力求解

/**
 * 暴力法解字符串匹配問題
 */
int brute_force_search(const char* s, const char* p){
    // i爲當前匹配到的原始串首位
    // j爲模式串的匹配位置
    int i=0, j=0;
    int size = strlen(p);
    int last = strlen(s) - size;
    while (i<=last && j<size){
        if(s[i+j] == p[j])
            j++;
        else{
            i++;
            j=0;
        }
    }
    if(j>=size) return i;
    return -1;
}

(2). 常規KMP

/**
 * 獲得 next 數組
 */
void get_next(char* p, int next[]){
    int len = strlen(p);
    next[0] = -1;
    int k = next[0];
    int j = 0;
    while (j < len - 1){
        // 此時, k即next[j-1],且p[k]表示前綴,p[j]表示後綴
        // 注:k==-1表示未找到k前綴與k後綴相等,首次分析可忽略
        if(k == -1 || p[j] == p[k]){
            ++j;
            ++k;
            next[j] = k;
        } else {
            // p[j]與p[k]失配,則繼續遞歸計算前綴p[next[k]]
            k = next[k];
        }
    }
}

int kmp(char s[], char p[], int next[]){
    int s_len = strlen(s);
    int p_len = strlen(p);
    int i = 0, j = -1;
    while (i<s_len && j<p_len){  
        if(j==-1 || s[i] == p[j]){
            ++i;
            ++j;
        } else {
            j = next[j];
        }
    }
    if (j >= p_len)
        return i - p_len;
    return -1;
}

(3). 變種KMP

/**
 * 獲得優化以後的next數組,滑動匹配距離更大了,便於滑動匹配
 */
void get_next_2(char* p, int next[]){
    int len = strlen(p);
    next[0] = -1;
    int k = next[0];
    int j = 0;
    while (j < len - 1){
        // 此時, k即next[j-1],且p[k]表示前綴,p[j]表示後綴
        // 注:k==-1表示未找到k前綴與k後綴相等,首次分析可忽略
        if(k == -1 || p[j] == p[k]){
            ++j;
            ++k;
            if(p[j] == p[k])
                next[j] = next[k];
            else
                next[j] = k;
        } else {
            // p[j]與p[k]失配,則繼續遞歸計算前綴p[next[k]]
            k = next[k];
        }
    }
}

int kmp(char s[], char p[], int next[]){
    int s_len = strlen(s);
    int p_len = strlen(p);
    int i = 0, j = -1;
    while (i<s_len && j<p_len){  
        if(j==-1 || s[i] == p[j]){
            ++i;
            ++j;
        } else {
            j = next[j];
        }
    }
    if (j >= p_len)
        return i - p_len;
    return -1;
}

4、 PowerString問題 (KMP算法的應用)

1. 題目

給定一個長度爲n的字符串S,若是存在一個字符串T,重複若干次T可以獲得S,那麼,S叫作週期串,T叫作S的一個週期。

如:字符串abababab是週期串,abab、ab都是它的週期,其中,ab是它的最小週期。

設計一個算法,計算S的最小週期。若是S不存在週期,返回空串。

2. 分析

使用next數組

求字符串週期

code

3. 代碼

int power_string(char* p){
    int len = strlen(p);
    if(!len) return -1;
    int next[len];
    int k = next[0] = -1;
    int j = 0;
    while (j < len - 1){
        if(k == -1 || p[j+1] == p[k]){
            ++j;
            ++k;
            next[j] = k;
        } else {
            k = next[k];
        }
    }
    int last = next[len-1];
    if(last == 0) return -1;
    if(len % (len - last) == 0) return len - last;
    return -1;
}
相關文章
相關標籤/搜索