一、最长公共子序列 LCS (Longest Common Subsequence)

1. 题目

给定两个串序列A、B,找出两个串的最长公共子序列LCS包含的元素和长度。

注意:
(1) 一个序列S任意删除若干个字符得到新序列T,则T叫做S的子序列。
(2) 与最长公共子串(Longest Common Substring)相区别,最长公共子串要求元素相同且连续,而最长公共子序列只要求元素出现的顺序一致,并不要求连续。即对于
 串A:abcde
 串B:bcdae
最长公共子串为:bcd
最长公共子序列为:bcde

2. 分析

该算法主要使用了动态规划算法(DP)。

(1). LCS解题的记号

记号

(2). 第一种情况 xm = yn

第一种情况
示意图
举例

(3). 第二种情况 xm != yn

第二种情况
举例

(4). 数据结构

算法中使用到的数据结构如下:

长度数组

方向向量

(5). 例子

示例:
X = < A,B,C,B,D,A,B >
Y = < B,D,C,A,B,A >
最长公共子序列为:<B,C,B,A>

该过程生成的二维数组如下图所示:
例子的长度数组和方向向量

(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可以描述两段文字之间的“相似度”,即它们的雷同程度,从而能够用来辨别抄袭。另一方面,对
一段文字进行修改之后,计算改动前后文字的最长公共子序列,将除此子序列外的部分提取出来,这
种方法判断修改的部分,往往十分准确。简而言之,百度知道、百度百科都用得上。

二、最长递增子序列 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

三、 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;
}

四、 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;
}

kalii
452 声望29 粉丝