1. 程式人生 > >“最大連續子序列和”、“最大遞增子序列”、“最大公共子序列”、“最長公共子串”問題總結

“最大連續子序列和”、“最大遞增子序列”、“最大公共子序列”、“最長公共子串”問題總結

一、最大連續子序列和(最大子序列)

最大子序列是要找出由陣列成的一維陣列中和最大的連續子序列。比如{5,-3,4,2}的最大子序列就是 {5,-3,4,2},它的和是8,達到最大;而 {5,-6,4,2}的最大子序列是{4,2},它的和是6。

思路:只要前i項的和還沒有小於0那麼子序列就一直向後擴充套件,否則丟棄之前的子序列開始新的子序列,同時我們要記下各個子序列的和,最後找到和最大的子序列。

程式設計注意點:maxSum儲存當前最大和;currSum儲存當前序列和;curBegin儲存當前序列和的起始位置


int maxSubSum(const vector<int> & arr,int &begin,int &end)
{
    int maxSum=0;
    int currSum=0;
    int newbegin=0;
    for(int i=0;i<arr.size();i++)
    {
        currSum+=arr[i];
        if(currSum>maxSum)
        {
            maxSum=currSum;
            begin=newbegin;
            end=i;
        }
        if(currSum<0)
        {
            currSum=0;
            newbegin=i+1;
        }
    }
    return maxSum;
}

二、最大遞增子序列

動態規劃法:

設長度為N的陣列為{a0,a1, a2, ...an-1),則假定以aj結尾的陣列序列的最長遞增子序列長度為L(j),則L(j)={ max(L(i))+1, i<j且a[i]<a[j] }。也就是說,我們需要遍歷在j之前的所有位置i(從0到j-1),找出滿足條件a[i]<a[j]的L(i),求出max(L(i))+1即為L(j)的值。最後,我們遍歷所有的L(j)(從0到N-1),找出最大值即為最大遞增子序列。時間複雜度為O(N^2)。

例如給定的陣列為{5,6,7,1,2,8},則L(0)=1, L(1)=2, L(2)=3, L(3)=1, L(4)=2, L(5)=4。所以該陣列最長遞增子序列長度為4,序列為{5,6,7,8}。

int LIS(int *arr, int n)
{
	int *f = new int[n];
	f[0] = 1;
	for (int i = 1; i < n; i++)
	{
		f[i] = 1;
		for (int j = 0; j < i; j++)
		{
			if (arr[i]>arr[j] && f[j]>f[i]-1)   //注意因為可能f[i]是更新後的,因此需減去1比較
				f[i] = f[j] + 1;
		}
	}
	return f[n-1];
}

三、最大公共子序列(Longest Common Subsequence, LCS)

如果字串一的所有字元按其在字串中的順序出現在另外一個字串二中,則字串一稱之為字串二的子串。注意,並不要求子串(字串一)的字元必須連續出現在字串二中。請編寫一個函式,輸入兩個字串,求它們的最長公共子序列,並打印出最長公共子序列。

例如:輸入兩個字串BDCABA和ABCBDAB,字串BCBA和BDAB都是是它們的最長公共子序列,則輸出它們的長度4,並列印任意一個子序列。

動態規劃法:

考慮最長公共子序列問題如何分解成子問題,設A=“a0,a1,…,am-1”,B=“b0,b1,…,bn-1”,並Z=“z0,z1,…,zk-1”為它們的最長公共子序列。不難證明有以下性質:

(1) 如果am-1==bn-1,則zk-1=am-1=bn-1,且“z0,z1,…,zk-2”是“a0,a1,…,am-2”和“b0,b1,…,bn-2”的一個最長公共子序列;

(2) 如果am-1!=bn-1,則若zk-1!=am-1時,蘊涵“z0,z1,…,zk-1”是“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一個最長公共子序列;

(3) 如果am-1!=bn-1,則若zk-1!=bn-1時,蘊涵“z0,z1,…,zk-1”是“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一個最長公共子序列。

      這樣,在找A和B的公共子序列時,如果有am-1==bn-1,則進一步解決一個子問題,找“a0,a1,…,am-2”和“b0,b1,…,bm-2”的一個最長公共子序列;如果am-1!=bn-1,則要解決兩個子問題,找出“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一個最長公共子序列和找出“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一個最長公共子序列,再取兩者中較長者作為A和B的最長公共子序列。

       求解:
       引進一個二維陣列c[][],用c[i][j]記錄X[i]與Y[j] 的LCS 的長度,b[i][j]記錄c[i][j]是通過哪一個子問題的值求得的,以決定輸出最長公共字串時搜尋的方向。
      我們是自底向上進行遞推計算,那麼在計算c[i,j]之前,c[i-1][j-1],c[i-1][j]與c[i][j-1]均已計算出來。此時我們根據X[i] == Y[j]還是X[i] != Y[j],就可以計算出c[i][j]。

      問題的遞迴式寫成:

      回溯輸出最長公共子序列過程:

 

       演算法分析:
      由於每次呼叫至少向上或向左(或向上向左同時)移動一步,故最多呼叫(m + n)次就會遇到i = 0或j = 0的情況,此時開始返回。返回時與遞迴呼叫時方向相反,步數相同,故演算法時間複雜度為Θ(m + n)。

/** 
找出兩個字串的最長公共子序列的長度
** author :liuzhiwei  
** data   :2011-08-15
**/ 
#include "stdio.h"
#include "string.h"
#include "stdlib.h"
int LCSLength(char* str1, char* str2, int **b)
{
	int i,j,length1,length2,len;
	length1 = strlen(str1);
	length2 = strlen(str2);
 
	//雙指標的方法申請動態二維陣列
	int **c = new int*[length1+1];      //共有length1+1行
	for(i = 0; i < length1+1; i++)
		c[i] = new int[length2+1];      //共有length2+1列
 
	for(i = 0; i < length1+1; i++)
		c[i][0]=0;        //第0列都初始化為0
	for(j = 0; j < length2+1; j++)
		c[0][j]=0;        //第0行都初始化為0
 
	for(i = 1; i < length1+1; i++)
	{
		for(j = 1; j < length2+1; j++)
		{
			if(str1[i-1]==str2[j-1])   //由於c[][]的0行0列沒有使用,c[][]的第i行元素對應str1的第i-1個元素
			{
				c[i][j]=c[i-1][j-1]+1;
				b[i][j]=0;          //輸出公共子串時的搜尋方向
			}
			else if(c[i-1][j]>c[i][j-1])
			{
				c[i][j]=c[i-1][j];
				b[i][j]=1;
			}
			else
			{
				c[i][j]=c[i][j-1];
				b[i][j]=-1;
			}
		}
	}
	
	len=c[length1][length2];
	for(i = 0; i < length1+1; i++)    //釋放動態申請的二維陣列
		delete[] c[i];
	delete[] c;
	return len;
}
void PrintLCS(int **b, char *str1, int i, int j)
{
	if(i==0 || j==0)
		return ;
	if(b[i][j]==0)
	{
		PrintLCS(b, str1, i-1, j-1);   //從後面開始遞迴,所以要先遞迴到子串的前面,然後從前往後開始輸出子串
		printf("%c",str1[i-1]);        //c[][]的第i行元素對應str1的第i-1個元素
	}
	else if(b[i][j]==1)
		PrintLCS(b, str1, i-1, j);
	else
		PrintLCS(b, str1, i, j-1);
}
 
int main(void)
{
	char str1[100],str2[100];
	int i,length1,length2,len;
	printf("請輸入第一個字串:");
	gets(str1);
	printf("請輸入第二個字串:");
	gets(str2);
	length1 = strlen(str1);
	length2 = strlen(str2);
	//雙指標的方法申請動態二維陣列
	int **b = new int*[length1+1];
	for(i= 0; i < length1+1; i++)
		b[i] = new int[length2+1];
	len=LCSLength(str1,str2,b);
	printf("最長公共子序列的長度為:%d\n",len);
	printf("最長公共子序列為:");
	PrintLCS(b,str1,length1,length2);
	printf("\n");
	for(i = 0; i < length1+1; i++)    //釋放動態申請的二維陣列
		delete[] b[i];
	delete[] b;
	system("pause");
	return 0;
}

這裡的輸出列印函式這裡我不是很明白==

四、最長公共子串

最長公共子串 和 最長公共子序列 是有區別的:

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

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

     X和Y的 最長公共子序列 為<a, b, c, b>,長度為4

     X和Y的 最長公共子串 為 <a, b>長度為2

    其實Substring問題是Subsequence問題的特殊情況,也是要找兩個遞增的下標序列

    <i1, i2, ...ik> 和 <j1, j2, ..., jk>使

     xi1 == yj1

    xi2 == yj2

    ......

    xik == yjk

    與Subsequence問題不同的是,Substring問題不光要求下標序列是遞增的,還要求每次

   遞增的增量為1, 即兩個下標序列為:

   <i, i+1, i+2, ..., i+k-1> 和 <j, j+1, j+2, ..., j+k-1>

    類比Subquence問題的動態規劃解法,Substring也可以用動態規劃解決

 令c[i][j]表示x[i]和y[j]為結尾的相同子串的最大長度:

 如  X = <y, e, d, f>,   Y = <y, e, k, f>,則:

   c[1][1] = 1

   c[2][2] = 2

   c[3][3] = 0

   c[4][4] = 1

不難得出遞推關係:

   如果xi == yj, 則 c[i][j] = c[i-1][j-1]+1

   如果xi ! = yj,  那麼c[i][j] = 0

最後求Longest Common Substring的長度等於   max{  c[i][j],  1<=i<=n, 1<=j<=m}

int LC_substring(char *str1, char *str2)
{
	int len1 = strlen(str1);
	int len2 = strlen(str2);
	int **c = new int*[len1 + 1];
	int i, j;
	for (i = 0; i < len1 + 1; i++)
		c[i] = new int[len2 + 1];
	for (i = 0; i < len1 + 1; i++)
		c[i][0] = 0;
	for (j = 0; j < len2 + 1; j++)
		c[0][j] = 0;

	int max = 0;    //記錄最長公共子串長度
	int x, y;       //記錄公共子串末尾分別在str1,str2的位置
	for (i = 1; i < len1 + 1; i++)
	{
		for (j = 1; j < len2 + 1; j++)
		{
			if (str1[i - 1] == str2[j - 1])
				c[i][j] = c[i - 1][j - 1] + 1;
			else c[i][j] = 0;
			if (c[i][j]>max)
			{
				max = c[i][j];
				x = i;
				y = j;
			}
		}
	}

	for (i = 0; i < len1 + 1; i++)
		delete[] c[i];
	delete[] c;

	return max;
}

注意上述程式中未輸出列印最大子串,可以在更新max值處標記當前最大子串末尾分別在str1與str2中的位置m,n;最後根據max,m,n將子串打印出。