动态规划——子序列问题汇总

1.最大连续子序列和

问题定义
问题定义
即求一个连续子序列,使的其和最大。例如对于序列{5,-3,4,2}来说,最大子序列为{5,-3,4,2},和为8。对于序列{5,-6,4,2},最大子序列为{4,2},和为6
因为是求 连续序列,所以我们以 S i S_{i} Si 表示问题状态,代表以 A i A_{i} Ai 结尾的子序列的最大和,则我们有下列动态规划递推:
动态递归式子
即若前 i i i 项的和不小于0的时候,我们可以接着扩展;否则从第 i i i 项开始另开一个新的子序列。同时,从 S i S_{i} Si 中找最大值。

实现示例

int MSS1(int* num, int len) {
	// 每个前缀Ai对应的最大和存储在数组dp[i]中
	int* dp = new int[len];
	dp[0] = num[0];
	for (int i = 1; i < len; i++) {
		// 状态转移方程
		dp[i] = max(dp[i - 1] + num[i], num[i]);
	}
	int maxsum = dp[0];
	for(int i=1;i<len;i++){
		maxsum = maxsum > dp[i] ? maxsum : dp[i];
	}
	return maxsum;
}
int MSS2(int* num, int len) {
	int sum = num[0]; // 这里的sum就代替了dp[i]来求最大值
	int maxsum = num[0];
	for (int i = 1; i < len; i++) {
		if (sum >= 0) sum += num[i];
		else sum = num[i];
		if (sum > maxsum) maxsum = sum;
	}
	return maxsum;
}

2.最长递增子序列

问题定义

最长递增子序列(Longest Increasing Subsequence,简写 LIS),给定一个序列 L = { A 1 , A 2 , . . . , A n } L=\{A_{1},A_{2},...,A_{n}\} L={A1,A2,...,An}我们需要找到一个子序列 L i n = { A k 1 A k 2 . . . A k m } L_{in} = \{ A_{k1}A_{k2}...A_{km}\} Lin={Ak1Ak2...Akm},使得 k 1 &lt; k 2 &lt; . . . &lt; k m k_{1} &lt; k_{2} &lt; ...&lt; k_{m} k1<k2<...<km A k 1 &lt; A k 2 &lt; . . . &lt; A k m A_{k1} &lt; A_{k2} &lt; ... &lt; A_{km} Ak1<Ak2<...<Akm,即求解 最长上升子序列
同样的,我们以 L i L_{i} Li 表示以 A i A_{i} Ai 结尾的子序列的最长长度,则有递推式:
在这里插入图片描述
即,对于 A i A_{i} Ai 我们将其尽可能的接到以 A j A_{j} Aj结尾的子序列上,然后求最长长度。注意每个元素在刚开始自成一个上升子序列,即初始化L[i] = 1

实现示例

int LIS(int* num, int len) {
	// 复杂度O(n^2)
	int* dp = new int[len];
	dp[0] = 1;
	for (int i = 1; i < len; i++) {
		dp[i] = 1;  // 初始化为1
		for (int j = 0; j < i; j++) {
			if (num[j] < num[i]) {
				dp[i] = max(dp[i], dp[j] + 1);
			}
		}
	}
	int maxlen = dp[0];
	for (int i = 1; i < len; i++) {
		maxlen = maxlen > dp[i] ? maxlen : dp[i];
	}
	delete[] dp;
	return maxlen;
}


3.最长公共子序列

问题定义

假设有两个序列 A n = { a 0 a 1 a 2 . . . a n − 1 } A_{n} = \{ a_{0}a_{1}a_{2}...a_{n-1}\} An={a0a1a2...an1} B m = { b 0 b 1 b 2 . . . b m − 1 } B_{m} = \{b_{0}b_{1}b_{2}...b_{m-1}\} Bm={b0b1b2...bm1}则我们定义一个公共子序列(非连续) Z k = { z 0 z 1 z 2 . . . z k − 1 } Z_{k} = \{z_{0}z_{1}z_{2}...z_{k-1}\} Zk={z0z1z2...zk1}即存在两组严格递增的的下标(不一定连续) i 0 i 1 . . . i k − 1 i_{0}i_{1}...i_{k-1} i0i1...ik1 j 0 j 1 . . . j k − 1 j_{0}j_{1}...j_{k-1} j0j1...jk1使得 Z k = a i 0 a i 1 . . . a i k − 1 = b j 0 b j 1 b j 2 . . . b j k − 1 Z_{k} = a_{i_{0}}a_{i_{1}}...a_{i_{k-1}} = b_{j_{0}}b_{j_{1}}b_{j_{2}}...b_{j_{k-1}} Zk=ai0ai1...aik1=bj0bj1bj2...bjk1则最长公共子序列为 Z k Z_{k} Zk,最长长度为 K K K

最长公共子序列(Longest Common Subsequence,LCS)的三个特性(从后往前):

  • a n − 1 = b m − 1 a_{n-1} = b_{m-1} an1=bm1,则 z k − 1 = a n − 1 = = b m − 1 z_{k-1} = a_{n-1} == b_{m-1} zk1=an1==bm1
    因为若找到 a i ( i &lt; n − 1 ) a_{i}(i&lt;n-1) ai(i<n1) b j ( j &lt; m − 1 ) b_{j}(j&lt;m-1) bj(j<m1) 满足条件,由于非连续递增下标的特性,还是可以顺延。
    此时, z 0 z 1 z 2 . . . z k − 2 z_{0}z_{1}z_{2}...z_{k-2} z0z1z2...zk2 可递推为 a 0 a 1 a 2 . . . a n − 2 a_{0}a_{1}a_{2}...a_{n-2} a0a1a2...an2 b 0 b 1 b 2 . . . b m − 2 b_{0}b_{1}b_{2}...b_{m-2} b0b1b2...bm2 的一个公共子序列。
  • a n − 1 ≠ b m − 1 a_{n-1} \neq b_{m-1} an1̸=bm1,则
    • z k − 1 ≠ a n − 1 z_{k-1} \neq a_{n-1} zk1̸=an1,则 z 0 z 1 z 2 . . . z k − 1 z_{0}z_{1}z_{2}...z_{k-1} z0z1z2...zk1 可递推为 a 0 a 1 a 2 . . . a n − 2 a_{0}a_{1}a_{2}...a_{n-2} a0a1a2...an2 b 0 b 1 b 2 . . . b m − 1 b_{0}b_{1}b_{2}...b_{m-1} b0b1b2...bm1 的一个公共子序列;
    • z k − 1 ≠ b m − 1 z_{k-1} \neq b_{m-1} zk1̸=bm1,则 z 0 z 1 z 2 . . . z k − 1 z_{0}z_{1}z_{2}...z_{k-1} z0z1z2...zk1 可递推为 a 0 a 1 a 2 . . . a n − 1 a_{0}a_{1}a_{2}...a_{n-1} a0a1a2...an1 b 0 b 1 b 2 . . . b m − 2 b_{0}b_{1}b_{2}...b_{m-2} b0b1b2...bm2 的一个公共子序列;

问题表示

因此,假设 C(i,j) 表示以 a i a_{i} ai b j b_{j} bj 结尾的子序列的最长公共子串的长度,数组B(i,j) 元素取自 {0,1,2} 表示问题 C(i,j) 通过哪个子问题求解,即 C(i-1,j-1),C(i-1,j)或C(i,j-1)
则我们有动态递推公式:
递推公式
例如两个字符串 X = “ABCBDAB” 和 Y = “BDCABA”,我们有下面求解数组C的过程,先求解第维再递推到高维。
在这里插入图片描述

图源来自 yysdsyl的博客

问题求解

/* 最长公共子序列问题 (LCS,非连续)*/
#include<string>
#include<iostream>

using namespace std;

#define MAXLEN 100

int val[MAXLEN][MAXLEN];  // 声明数组,同时避免重叠子问题
int re[MAXLEN][MAXLEN];   // 声明数组,方便回溯求子序列
int len1, len2;

int LCS_rev(string a, string b,int i,int j) {
	// 递归写法,简洁,易于理解
	// 但是重复计算多,也可以用一个二维数组存储中间计算结果
	if (i < 0 || j < 0) return 0;
	if (a[i] == b[j]) return LCS_rev(a, b, i - 1, j - 1) + 1;
	else
		return LCS_rev(a, b, i - 1, j) > LCS_rev(a, b, i, j - 1) ? LCS_rev(a, b, i - 1, j) : LCS_rev(a, b, i, j - 1);
}
void LCS(string& a, string& b) {
	int i, j;
	len1 = a.length();  len2 = b.length();
	for (i = 0; i < len2; i++) val[0][i] = 0;
	for (i = 0; i < len1; i++) val[i][0] = 0;  // 表示一个字符串为空串
	// 递推求解
	for (i = 1; i <= len1; i++) {
		for (j = 1; j <= len2; j++) {
			if (a[i-1] == b[j-1]) { // 减1是因为字符串下标从0开始
				val[i][j] = val[i - 1][j - 1] + 1;
				re[i][j] = 0;
			}
			else if (val[i - 1][j] > val[i][j - 1]) {
				val[i][j] = val[i - 1][j];
				re[i][j] = 1;
			}
			else {
				val[i][j] = val[i][j-1];
				re[i][j] = 2;
			}
		}
	}
}
// 回溯打印公共字符串
void printfLCS(string& a,int i,int j) {
	if (i == 0 || j == 0) return; // 越界
	if (re[i][j] == 0) {
		// 先递归打印前面的
		printfLCS(a, i - 1, j - 1);
		printf("%c", a[i-1]);
	}
	else if (re[i][j] == 1) printfLCS(a, i - 1, j);
	else printfLCS(a, i, j - 1);
}
int main() {
	string s1 = "ABCBDAB";
	string s2 = "BDCABA";
	len1 = s1.length();  len2 = s2.length();
	fill(val[0], val[0] + MAXLEN*MAXLEN, -1);
	LCS(s1, s2);
	cout << "最长长度" << val[len1][len2] << endl;
	printfLCS(s1,len1,len2);
	
	system("pause");
	return 0;
}


例题练习
可参考PAT甲级 1045进行练习,解答示例

4.最长公共子串

问题描述

最长公共子串(Longest Common Substring) 是最长公共子序列的特殊情况,即
假设有两个字符串序列 A n = { a 0 a 1 a 2 . . . a n − 1 } A_{n} = \{ a_{0}a_{1}a_{2}...a_{n-1}\} An={a0a1a2...an1} B m = { b 0 b 1 b 2 . . . b m − 1 } B_{m} = \{b_{0}b_{1}b_{2}...b_{m-1}\} Bm={b0b1b2...bm1}则我们定义一个公共子串(连续) Z k = { z 0 z 1 z 2 . . . z k − 1 } Z_{k} = \{z_{0}z_{1}z_{2}...z_{k-1}\} Zk={z0z1z2...zk1}即存在两组连续递增的的下标 i 0 i 1 . . . i k − 1 i_{0}i_{1}...i_{k-1} i0i1...ik1 j 0 j 1 . . . j k − 1 j_{0}j_{1}...j_{k-1} j0j1...jk1使得 Z k = a i 0 a i 1 . . . a i k − 1 = b j 0 b j 1 b j 2 . . . b j k − 1 Z_{k} = a_{i_{0}}a_{i_{1}}...a_{i_{k-1}} = b_{j_{0}}b_{j_{1}}b_{j_{2}}...b_{j_{k-1}} Zk=ai0ai1...aik1=bj0bj1bj2...bjk1则最长公共子序列为 Z k Z_{k} Zk,最长长度为 K K K。即下标的增长值必须为1。

例如有两个字符序列:

X = <a, b, c, f, b, c>

Y = <a, b, f, c, a, b>

X和Y的 Longest Common Sequence为<a, b, c, b>,长度为4

X和Y的 Longest Common Substring为 <a, b>,长度为2

问题表示

类似于问题3,假设 C(i,j) 表示以 a i a_{i} ai b j b_{j} bj 结尾的子序列的最长公共子串的长度,数组B(i,j) 元素取自 {0,1,2} 表示问题 C(i,j) 通过哪个子问题求解,即 C(i-1,j-1),C(i-1,j)或C(i,j-1)
则我们有动态递推公式(注意这里下标从1开始):
递推公式

实现示例

/* 最长公共子串 */
#include<string>
#include<iostream>

using namespace std;

#define MAXLEN 100
int val[MAXLEN][MAXLEN];

int max = -1;  // 最长公共子串长度
int indexa;  // 公共子串在主串a中的结束位置

void LCString(string a, string b) {
	int i, j;
	for (i = 0; i < a.length(); i++) val[i][0] = 0;
	for (i = 0; i < b.length(); i++) val[0][i] = 0;
	for (i = 1; i <= a.length(); i++) {
		for (j = 1; j <= b.length(); j++) {
			if (a[i - 1] == b[j - 1]) 
				val[i][j] = val[i - 1][j - 1] + 1;
			else val[i][j] = 0;
			if (val[i][j] > max) {
				max = val[i][j];
				indexa = i;
			}
		}
	}
}
void printfLCString(string a) {
	for (int i = indexa - max; i < indexa; i++) printf("%c", a[i]);
	printf("\n");
}
int main() {
	string s1 = "abcabacababac";
	string s2 = "ababc";
	LCString(s1, s2);
	printf("最长长度%d\n", max);
	printfLCString(s1);
	system("pause");
	return 0;
}

方法2

也有稍微暴力一点的算法,复杂度都是 O ( m ∗ n ) O(m*n) O(mn)
在这里插入图片描述
如上图,对于字符串 A=“shaohui” 和字符串 B=“ahui” ,固定字符串A,然后先将字符串B的头部和字符串A的尾巴对齐,然后开始向右移动字符串B,每个求出它们交集的最长公共子串的长度。
(注意,上叙述的头部指的是字符串最右面的位置,计算机中对应字符串的最后一位)
当然,考虑字符串的头部为下标0的位置,即固定A,将B从右往左移动也是可以的,即上图的顺序是从右下到左上,下面考虑这种的代码实现。

实现示例

int LCString_Vio(string a, string b) {
	// 稍微暴力一点的
	int lena = a.length(), lenb = b.length();
	int maxl = 0; // 最长长度
	int i, j;
	for (i = lena - 1; i >= 0; i--) {
		int tmplen = 0; // 当前的最短距离
		int iter = lena - 1 - i; // 当前递归的长度
		if (iter > lenb - 1) iter = lenb - 1;
		for (j = 0; j < iter; j++) {
			tmplen += (a[i + j] == b[j]);
		}
		if (tmplen > maxl) maxl = tmplen;
	}
	return maxl;
}

此算法不需要动态规划数组来存储中间值,示例图来自 hackbuteer1的Blog

5.最小编辑距离

问题描述

编辑距离(Edit Distance),又称Levenshtein距离,是指两个字符串间,由一个转成另外一个所需的最少编辑操作次数。当然,允许的编辑只包括单个字符 替换、单个字符 插入和单个字符的 删除。一般来说,编辑距离越小,两个字符串的相似度越大。

例如将字符串 “kitten” 修改为 字符串"sitting" 则需要3次但字符编辑操作,如下:

  • sitten ( k -> s )
  • sittin (e -> i )
  • sitting (_ -> g )

因此它们的编辑距离为3.

问题表示

为了定义子问题的状态,我们假设字符序列 A [ 1 , . . . , i ] A[1,...,i] A[1,...,i] B [ 1 , . . . , j ] B[1,...,j] B[1,...,j] 分别是字符串 A 和 B 的前i、j个字符组成的子串,由于在A中删除一个字符来匹配B,就相当于在B中插入一个字符来匹配A,即这两个操作可以相互转换,所以我们考虑只操作一个字符串,即固定字符串B,操作字符串A。

同时我们定义dp[i][j]是字符序列A[1,…,i]和B[1,…,j]的编辑距离,则有

  • 插入操作
    在这里插入图片描述
    d p [ i ] [ j ] = d p [ i ] [ j − 1 ] + 1 dp[i][j] = dp[i][j-1] + 1 dp[i][j]=dp[i][j1]+1

  • 删除操作
    在这里插入图片描述
    d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + 1 dp[i][j] = dp[i-1][j] + 1 dp[i][j]=dp[i1][j]+1

  • 修改操作
    在这里插入图片描述
    d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 dp[i][j] = dp[i-1][j-1] + 1 dp[i][j]=dp[i1][j1]+1

  • 不进行操作,
    在这里插入图片描述
    当然,对于不同的操作,我们可以赋予不同的权重。
    所以,我们能得到动态递归方程
    在这里插入图片描述
    其中, a i ≠ b j a_{i}≠b_{j} ai̸=bj 表示不相等时取1,相等时取0。字符串小标从1开始。

实现示例

/* 最小编辑距离 (Edit Distance) */
#include<string>
#include<iostream>

using namespace std;

#define INF 100 // 最大字符串长度
int dp[INF][INF];  // 中间结果数组
int way[INF][INF];  // 三种方式:0(不动),1(插入),2(删除),3(修改)

void MinEditDistance(string a, string b) {
	int lena = a.length(), lenb = b.length();
	int i, j;
	// 初始化,固定b,操作a
	for (i = 0; i < lenb; i++) {
		dp[i][0] = i;  // 删除a
		way[i][0] = 2;
	}
	for (i = 0; i < lenb; i++) {
		dp[0][i] = i;  // 插入a
		way[0][i] = 1;
	}
	// 递推
	for (i = 1; i <= lena; i++) {
		for (j = 1; j <= lenb; j++) {
			if (a[i - 1] == b[j - 1]) {
				dp[i][j] = dp[i - 1][j - 1]; way[i][j] = 0;
			}
			else {
				int op1 = dp[i][j - 1] + 1;		// 插入
				int op2 = dp[i - 1][j] + 1;		//删除
				int op3 = dp[i - 1][j - 1] + 1; // 修改
				dp[i][j] = op1 < op2 ? op1 : op2;
				way[i][j] = op1 < op2 ? 1 : 2;
				dp[i][j] = dp[i][j] < op3 ? dp[i][j] : op3;
				way[i][j] = dp[i][j] < op3 ? way[i][j] : 3;
			}
		}
	}
	printf("最小编辑距离%d\n", dp[lena][lenb]);
}
void printfOP(string a, string b,int i,int j) {
	// 打印操作
	if (i == 0 || j == 0) return;
	if (way[i][j] == 0) printfOP(a, b, i - 1, j - 1);
	else if (way[i][j] == 1) { // 插入
		printfOP(a, b, i, j - 1);
		printf("在%c后面插入%c\n", a[i - 1], b[j - 1]);
	}
	else if (way[i][j] == 2) { // 删除
		printfOP(a, b, i - 1, j);
		printf("删除%c\n", a[i - 1]);
	}
	else if (way[i][j] == 3) { // 修改
		printfOP(a, b, i - 1, j - 1);
		printf("修改%c为%c\n", a[i - 1], b[j - 1]);
	}
}
int main() {
	string s1 = "kitten";
	string s2 = "sitting";
	MinEditDistance(s1, s2);
	printfOP(s1, s2,s1.length(),s2.length());
	system("pause");
	return 0;
}

上述图源 BlackStorm的博客
最小编辑距离在自然语言处理中可以用来度量两句话的相似程度【NLP_Stanford课堂】最小编辑距离

后续

待补充。。。。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值