1、最長公共子序列、最長公共子串
最長公共子序列(Longest-Common-Subsequence,LCS)
dp[i][j]:dp[i][j]表示長度分別為i和j的序列X和序列Y構成的LCS的長度
dp[i][j] = 0,如果i=0 或 j=0
dp[i][j] = dp[i-1][j-1] + 1,如果 X[i-1] = Y[i-1]
dp[i][j] = max{ dp[i-1][j], dp[i][j-1] },如果 X[i-1] != Y[i-1]
LCS長度為 dp[Xlen][Ylen]

int dp[100][100]; // 存儲LCS長度, 下標i,j表示序列X,Y長度 void LCS_dp(char * X, char * Y) { int i, j; int xlen = strlen(X); int ylen = strlen(Y); // dp[0-xlen][0] & dp[0][0-ylen] 都已初始化0 for(i = 1; i <= xlen; ++i) { for(j = 1; j <= ylen; ++j) { if(X[i-1] == Y[j-1]) { dp[i][j] = dp[i-1][j-1] + 1; } else if(dp[i][j-1] > dp[i-1][j]) { dp[i][j] = dp[i][j-1]; } else { dp[i][j] = dp[i-1][j]; } } } printf("len of LCS is: %d\n", dp[xlen][ylen]); i = xlen; j = ylen; int k = dp[i][j]; char lcs[100] = {'\0'}; while(i && j) { if(X[i-1] == Y[j-1] && dp[i][j] == dp[i-1][j-1] + 1) { lcs[--k] = X[i-1]; --i; --j; } else if(X[i-1] != Y[j-1] && dp[i-1][j] > dp[i][j-1]) { --i; } else { --j; } } printf("%s\n",lcs); }
最長公共子串(Longest-Common-Substring,LCS)
dp[i][j]:表示X[0-i]與Y[0-j]的最長公共子串長度
dp[i][j] = dp[i-1][j-1] + 1,如果 X[i] == Y[j]
dp[i][j] = 0,如果 X[i] != Y[j]
初始化:i==0或者j==0,如果X[i] == Y[j],dp[i][j] = 1;否則dp[i][j] = 0。
最長公共子串的長度為max(dp[i][j])。

// 最長公共子串 DP int dp[100][100]; void LCS_dp(char * X, char * Y) { int xlen = strlen(X); int ylen = strlen(Y); int maxlen = 0; int maxindex = 0; for(int i = 0; i < xlen; ++i) { for(int j = 0; j < ylen; ++j) { if(X[i] == Y[j]) { if(i && j) { dp[i][j] = dp[i-1][j-1] + 1; } if(i == 0 || j == 0) { dp[i][j] = 1; } if(dp[i][j] > maxlen) { maxlen = dp[i][j]; maxindex = i + 1 - maxlen; } } } } if(maxlen == 0) { printf("NULL LCS\n"); return; } printf("The len of LCS is %d\n",maxlen); int i = maxindex; while(maxlen--) { printf("%c",X[i++]); } printf("\n"); }
2、數組中最長遞增子序列:如在序列1,-1,2,-3,4,-5,6,-7中,最長遞增序列為1,2,4,6。
時間復雜度O(N^2)的算法:
LIS[i]:表示數組前i個元素中(包括第i個),最長遞增子序列的長度
LIS[i] = max{ 1, LIS[k]+1 }, 0 <= k < i, a[i]>a[k]

int LIS(int a[], int length) { int *LIS = new int[length]; for(int i = 0; i < length; ++i) { LIS[i] = 1; //初始化默認長度 for(int j = 0; j < i; ++j) //前面最長的序列 if(a[i] > a[j] && LIS[j]+1 > LIS[i]) LIS[i] = LIS[j]+1; } int max_lis = LIS[0]; for(int i = 1; i < length; ++i) if(LIS[i] > max_lis) max_lis = LIS[i]; return max_lis; //取LIS的最大值 }
時間復雜度O(NlogN)的算法:
輔助數組b[],用k表示數組b[]目前的長度,算法完成后k的值即為LIS的長度。
初始化:b[0] = a[0],k = 1
從前到后掃描數組a[],對於當前的數a[i],比較a[i]和b[k-1]:
如果a[i]>b[k-1],即a[i]大於b[]最后一個元素,b[]的長度增加1,b[k++]=a[i];
如果a[i]<b[k-1],在b[1]...b[k]中二分查找第一個大於a[i]的數b[j],修改b[j]=a[i]。
LIS的長度為k

//修改的二分搜索算法,若要查找的數w在長為len的數組b中存在則返回下標 //若不存在,則返回b數組中的第一個大於w的那個元素的下標 int BiSearch(int *b, int len, int w) { int left = 0, right = len-1; int middle; while(left <= right) { middle = (left+right)/2; if(b[middle] > w) right = middle - 1; else if(b[middle] < w) left = middle + 1; else return middle; } //返回b數組中的剛剛大於w的那個元素的下標 return (b[middle]>w) ? middle : middle+1; } int LIS(int *array, int n) { int *B = new int[n]; int len = 1; B[0] = array[0]; for(int i=1; i<n; ++i) { if(array[i] > B[len-1]) { B[len] = array[i]; ++len; } else { int pos = BiSearch(B, len, array[i]); B[pos] = array[i]; } } delete []B; return len; }
3、計算字符串的相似度(編輯距離)
為了判斷字符串的相似程度,定義了一套操作方法來把兩個不相同的字符串變得相同,具體的操作方法為: 1.修改一個字符。2.增加一個字符。3.刪除一個字符。
比如,對於“abcdefg”和“abcdef”兩個字符串來說,可以通過增加/減少一個“g“的方式來達到目的。上面的兩種方案,都僅需要一次操作。把這個操作所需要的次數定義為兩個字符串的距離,給定任意兩個字符串,寫出一個算法來計算出它們的距離。
設 L(i,j)為使兩個字符串和Ai和Bj相等的最小操作次數。
當ai==bj時 顯然 L(i,j) = L(i-1,j-1)
當ai!=bj時 L(i,j) = min( L(i-1,j-1), L(i-1,j), L(i,j-1) ) + 1

int minValue(int a, int b, int c) { int t = a <= b ? a:b; return t <= c ? t:c; } int calculateStringDistance(string strA, string strB) { int lenA = (int)strA.length()+1; int lenB = (int)strB.length()+1; int **c = new int*[lenA]; for(int i = 0; i < lenA; i++) c[i] = new int[lenB]; for(int i = 0; i < lenA; i++) c[i][0] = i; for(int j = 0; j < lenB; j++) c[0][j] = j; c[0][0] = 0; for(int i = 1; i < lenA; i++) { for(int j = 1; j < lenB; j++) { if(strB[j-1] == strA[i-1]) c[i][j] = c[i-1][j-1]; else c[i][j] = minValue(c[i][j-1], c[i-1][j], c[i-1][j-1]) + 1; } } int ret = c[lenA-1][lenB-1]; for(int i = 0; i < lenA; i++) delete [] c[i]; delete []c; return ret; }
4、8*8的棋盤上面放着64個不同價值的禮物,每個小的棋盤上面放置一個禮物(禮物的價值大於0),一個人初始位置在棋盤的左上角,每次他只能向下或向右移動一步,並拿走對應棋盤上的禮物,結束位置在棋盤的右下角,請設計一個算法使其能夠獲得最大價值的禮物。
動態規划算法:
dp[i][j] 表示到棋盤位置(i,j)上可以得到的最大禮物值
dp[i][j] = max( dp[i][j-1] , dp[i-1][j] ) + value[i][j] (0<i,j<n)

int GetMaxValue(int **dp, int **value) { int i, j, n = 8; dp[0][0] = value[0][0]; for(i = 1; i < n; i++) { dp[i][0] = dp[i-1][0] + value[i][0]; } for(j = 1; j < n; j++) { dp[0][j] = dp[0][j-1] + value[0][j]; } for(i = 1; i < n; i++) { for(j = 1; j < n; j++) { dp[i][j] = max(dp[i][j-1] , dp[i-1][j]) + value[i][j]; } } return dp[n-1][n-1]; }
5、給定一個整數數組,求這個數組中子序列和最大的最短子序列,如數組a[]={1,2,2,-3,-5,5}子序列和最大為5,最短的為a[5]。
動態規划
sum[i] = max(sum[i-1]+a[i], a[i]) (sum[0]=a[0],1<=i<=n)
len[i] = max(len[i-1]+1, 0) (len[0]=0,1<=i<=n)

void max_sub(int a[], int size) { int *sum = new int[size]; int *len = new int[size]; int temp_sum = 0; sum[0] = a[0]; len[0] = 0; for(int i = 1; i < size; i++) { temp_sum = sum[i-1] + a[i]; if(temp_sum > a[i]) { sum[i] = temp_sum; len[i] = len[i-1]+1; } else { sum[i] = a[i]; len[i] = 0; } } int index = 0; for(int i = 1; i < size; i++) { if(sum[i] > sum[index]) index = i; else if(sum[i] == sum[index] && len[i] < len[index]) index = i; } printf("Max sub sum is %d, from %d to %d",sum[index],index-len[index],index); delete []sum; delete []len; }
6、子數組的最大和
狀態方程:
Start[i] = max{A[i], Start[i-1]+A[i]}
All[i] = max{Start[i], All[i-1]}

int MaxSum(int *A, int n) { int * All = new int[n]; int * Start = new int[n]; All[0] = Start[0] = A[0]; for(int i=1; i<n; ++i) { Start[i] = max(A[i], A[i]+Start[i-1]); All[i] = max(Start[i], All[i-1]); } int max = All[n-1]; delete []All; delete []Start; return max; }
因為Start[i-1]只在計算Start[i]時使用,而且All[i-1]也只在計算All[i]時使用,所以可以只用兩個變量就夠了,節省空間。

int MaxSum(int *A, int n) { int All = A[0]; int Start = A[0]; for(int i=1; i<n; ++i) { Start = max(A[i], A[i]+Start); All = max(Start, All); } return All; }
7、在數組中,數字減去它右邊的數字得到一個數對之差。求所有數對之差的最大值。例如在數組{2, 4, 1, 16, 7, 5, 11, 9}中,數對之差的最大值是11,是16減去5的結果。
思路:假設f[i]表示數組中前i+1個數的解,前i+1個數的最大值為m[i]。則狀態轉移方程:
f[i] = max(f[i-1], m[i-1] - a[i]), m[i] = max(m[i-1],a[i])。問題的解為f[n-1]。

int MaxDiff_Solution1(int *pArray, int nLen) { if(pArray == NULL || nLen <= 1) return 0; int *f = new int[nLen]; int *m = new int[nLen]; f[0] = 0; //1個數的情況 m[0] = pArray[0]; for(int i = 1; i < nLen; i++) { f[i] = max(f[i-1], m[i-1] - pArray[i]); m[i] = max(m[i-1], pArray[i]); } return f[nLen - 1]; }
上述代碼用了兩個輔助數組,其實只需要兩個變量,前i個數的情況只與前i-1個數的情況有關。在“子數組的最大和問題”中,也使用過類似的技術。

int MaxDiff_Solution2(int *pArray, int nLen) { if(pArray == NULL || nLen <= 1) return 0; int f = 0; int m = pArray[0]; for(int i = 1; i < nLen; i++) { f = max(f, m - pArray[i]); m = max(m, pArray[i]); } return f; }
8、從一列數中篩除盡可能少的數使得從左往右看,這些數是從小到大再從大到小的。
雙端 LIS 問題,用 DP 的思想可解,目標規划函數 max{ b[i] + c[i] - 1 }, 其中 b[i] 為從左到右,0--i 個數之間滿足遞增的數字個數;c[i] 為從右到左,n-1--i個數之間滿足遞增的數字個數。最后結果為 n-max 。

/* a[] holds the original numbers b[i] holds the number of increasing numbers from a[0] to a[i] c[i] holds the number of increasing numbers from a[n-1] to a[i] */ int double_lis(int a[], int n) { int *b = new int[n]; int *c = new int[n]; // update array b from left to right for(int i = 0; i < n; ++i) { b[i] = 1; for(int j = 0; j < i; ++j) if(a[i] > a[j] && b[j]+1 > b[i]) b[i] = b[j] + 1; } // update array c from right to left for (int i = n-1; i >= 0; --i) { c[i] = 1; for(int j = n-1; j > i; --j) if(a[i] > a[j] && c[j]+1 > c[i]) c[i] = c[j] + 1; } int max = 0; for (int i = 0; i < n; ++i ) { if (b[i]+c[i] > max) max = b[i] + c[i]; } max = max-1; //delete the repeated one delete []b; delete []c; return n-max; }
9、從給定的N個正數中選取若干個數之和最接近M
解法:轉換成01背包問題求解,從正整數中選取若干個數放在容量為M的背包中。

#include <stdio.h> const int MAX = 10010; int f[MAX]; int g[MAX][MAX]; int main() { //從數組value中選中若干個數之和最接近V int value[] = {2,9,5,7,4,11,10}; int V = 33; //子集和 int N = sizeof(value)/sizeof(value[0]); for(int i = 0; i <= V; ++i) //初始化:沒要求和一定是V { f[i] = 0; } for(int i = 0; i < N; ++i) { for(int v = V; v >= value[i]; --v) { if(f[v] < f[v-value[i]] + value[i] ) //選value[i] { f[v] = f[v-value[i]] + value[i]; g[i][v] = 1; } else //不選value[i] { f[v] = f[v]; g[i][v] = 0; } } } printf("%d\n",f[V]); int i = N; //輸出解 int v = V; while(i-- > 0) { if(g[i][v] == 1) { printf("%d, ",value[i]); v -= value[i]; } } printf("\n"); return 0; }
從給定的N個正數中選取若干個數之和為M

#include <iostream> #include <list> using namespace std; void find_seq(int sum, int index, int * value, list<int> & seq) { if(sum <= 0 || index < 0) return; if(sum == value[index]) { printf("%d ", value[index]); for(list<int>::iterator iter = seq.begin(); iter != seq.end(); ++iter) { printf("%d ", *iter); } printf("\n"); } seq.push_back(value[index]); find_seq(sum-value[index], index-1, value, seq); //放value[index] seq.pop_back(); find_seq(sum, index-1, value, seq); //不放value[index] } int main() { int M; list<int> seq; int value[] = {2,9,5,7,4,11,10}; int N = sizeof(value)/sizeof(value[0]); for(int i = 0; i < N; ++i) { printf("%d ",value[i]); } printf("\n"); scanf("%d", &M); printf("可能的序列:\n"); find_seq(M, N-1, value, seq); return 0; }
10、將一個較大的錢,不超過1000的人民幣,兌換成數量不限的100、50、10、5、2、1的組合,請問共有多少種組合呢?
解法:01背包中的完全背包問題(即每個物品的數量無限制)
dp[i][j]:表示大小為j的價值用最大為money[i]可表示的種類數

#define NUM 7 int money[NUM] = {1, 2, 5, 10, 20, 50, 100}; // 動態規划解法(完全背包) int NumOfCoins(int value) { int dp[7][1010]; for(int i = 0; i <= value; ++i) dp[0][i] = 1; for(int i = 1; i < NUM; ++i) { for(int j = 0; j <= value; ++j) { if(j >= money[i]) dp[i][j] = dp[i][j-money[i]] + dp[i-1][j]; else dp[i][j] = dp[i-1][j]; } } return dp[6][value]; }
11、撈魚問題:20個桶,每個桶中有10條魚,用網從每個桶中抓魚,每次可以抓住的條數隨機,每個桶只能抓一次,問一共抓到180條的排列有多少種。
分析:看看這個問題的對偶問題,抓取了180條魚之后,20個桶中剩下了20條魚,不同的抓取的方法就對應着這些魚在20個桶中不同的分布,於是問題轉化為將20條魚分到20個桶中有多少中不同的分類方法(這個問題當然也等價於180條魚分到20個桶中有多少種不同的方法)。
dp[i][j]:前i個桶放j條魚的方法共分為11種情況:前i-1個桶放j-k(0<=k<=10)條魚的方法總和。我們可以得到狀態方程:f(i,j) = sum{ f(i-1,j-k), 0<=k<=10}

/*撈魚:將20條魚放在20個桶中,每個桶最多可以放10條,求得所有的排列方法 /*自底向上DP f(i,j) = sum{ f(i-1,j-k), 0<=k<=10 } /*該方法中測試 20個桶 180條魚,與遞歸速度做對比 */ void CatchFish() { int dp[21][200]; // 前i個桶放j條魚的方法數 int bucketN = 20; int fishN = 20; memset(dp,0,sizeof(dp)); for(int i = 0; i <= 10; ++i) // 初始化合法狀態 { dp[1][i] = 1; } for(int i = 2; i <= bucketN; ++i) // 從第二個桶開始 { for(int j = 0; j <= fishN; ++j) { for(int k = 0; k <= 10 && j-k >= 0; ++k) { dp[i][j] += dp[i-1][j-k]; } } } printf("%d\n",dp[bucketN][fishN]); }
12、n個骰子的點數:把n個骰子扔在地上,所有骰子朝上一面的點數之和為S。輸入n,打印出S的所有可能的出現的值。
F(k,n) 表示k個骰子點數和為n的種數,k表示骰子個數,n表示k個骰子的點數和
對於 k>0, k<=n<=6*k
F(k,n) = F(k-1,n-6) + F(k-1,n-5) + F(k-1,n-4) + F(k-1,n-3) + F(k-1,n-2) + F(k-1,n-1)
對於 n<k or n>6*k
F(k,n) = 0
當k=1時, F(1,1)=F(1,2)=F(1,3)=F(1,4)=F(1,5)=F(1,6)=1

void SumOfDices() { int dp[21][6*20+1]; // k個骰子,和為n的種類數,不超過20個骰子 int number = 3; // 骰子數 int face = 6; // 面數,6面 memset(dp,0,sizeof(dp)); for(int i = 1; i <= 6; ++i) // 初始化1個骰子的情況 { dp[1][i] = 1; } for(int i = 2; i <= number; ++i) // 從第二個骰子開始 { for(int j = i; j <= face * i; ++j) // i個骰子的點數從i到i*6 { for(int k = 1; k <= face && j-k >= 0; ++k) { dp[i][j] += dp[i-1][j-k]; } } } for(int i = 0; i <= number * face; ++i) { printf("Sum = %d, Number is %d\n",i,dp[number][i]); } }
13、給定三個字符串A,B,C;判斷C能否由AB中的字符組成,同時這個組合后的字符順序必須是A,B中原來的順序,不能逆序;例如:A:mnl,B:xyz;如果C為mnxylz,就符合題意;如果C為mxnzly,就不符合題意,原因是z與y順序不是B中順序。
DP求解:定義dp[i][j]表示A中前i個字符與B中前j個字符是否能組成C中的前(i+j)個字符,如果能標記true,如果不能標記false; 有了這個定義,我們就可以找出狀態轉移方程了,初始狀態dp[0][0] = 1:
dp[i][j] = 1 如果 dp[i-1][j] == 1 && C[i+j-1] == A[i-1]
dp[i][j] = 1 如果 dp[i][j-1] == 1 && C[i+j-1] == B[j-1]

#include <iostream> using namespace std; char A[201]; char B[201]; char C[401]; int dp[201][201]; // dp[i][j] 表示A前i個字符與B前j個字符是否能構成C前i+j個字符 int main() { memset(dp,0,sizeof dp); scanf("%s %s %s", A, B, C); int lenA = strlen(A); int lenB = strlen(B); dp[0][0] = 1; for(int i = 0; i <= lenA; ++i) { for(int j = 0; j <= lenB; ++j) { if(i > 0 && (dp[i-1][j] == 1) && (C[i+j-1] == A[i-1])) { dp[i][j] = 1; } if(j > 0 && (dp[i][j-1] == 1) && (C[i+j-1] == B[j-1])) { dp[i][j] = 1; } } } printf("%s\n",dp[lenA][lenB] ? "yes" : "no"); return 0; }