浅谈我对动态规划的一点理解---大家准备好小板凳,我要开始吹牛皮了~~~
前言
作為一個退役狗跟大家扯這些東西,感覺確實有點。。。但是,針對網上沒有一篇文章能夠很詳細的把動態規劃問題說明的很清楚,我決定還是拿出我的全部家當,來跟大家分享我對動態規劃的理解,我會盡可能的把所遇到的動態規劃的問題都涵蓋進去,博主退役多年,可能有些地方會講的不完善,還望大家多多貢獻出自己的寶貴建議,共同進步~~~
概念
首先我們得知道動態規劃是什么東東,百度百科上是這么說的,動態規劃(dynamic programming)是運籌學的一個分支,是求解決策過程(decision process)最優化的數學方法。20世紀50年代初美國數學家R.E.Bellman等人在研究多階段決策過程(multistep decision process)的優化問題時,提出了著名的最優化原理(principle of optimality),把多階段過程轉化為一系列單階段問題,利用各階段之間的關系,逐個求解,創立了解決這類過程優化問題的新方法——動態規劃。1957年出版了他的名著《Dynamic Programming》,這是該領域的第一本著作。
小伙伴們估計看到這段話都已經蒙圈了吧,那么動態規劃到底是什么呢?這么說吧,動態規劃就是一種通過把原問題分解為相對簡單的子問題的方式求解復雜問題的方法。動態規劃常常適用于有重疊子問題和最優子結構性質的問題。
舉個例子,我們都是從學生時代過來的人,學生時代我們最喜歡的一件事是什么呢?就是等待寒暑假的到來,因為可以放假了啊^-^,嘻嘻,誰會不喜歡玩呢~~可是呢,放假之前我們必須經歷的一個過程就是期末考試,期末沒考好,回家肯定是要挨板子的,所以我們就需要去復習啦,而在復習過程中我們是不是要去熟記書中的每一個知識點呢,書是一個主體,考試都是圍繞著書本出題,所以我們很容易知道書本不是核心,書本中的若干個知識點才是核心,然后那個若干個知識點又可以拆解成無數個小知識點,是不是發現有點像一棵倒立的樹呢,但是呢,當我們要運用這些知識點去解題時,每一道題所涉及的知識點,其實就是這些知識點的一個排列組合的所有可能結果的其中一種組合方式,這個能理解的了嘛?
對這個排列組合,我們舉個例子,比如小明爸爸叫小明去買一包5元錢的香煙,他給了一張5元的,三張2元的,五張1元的紙幣,問小明有幾種付錢的方式?這個選擇方式我們很容易就知道,我們可以對這些可能結果進行一個枚舉。
這個組合方式有很多種,我們可以對其進行一個分類操作:
當我們只用一張紙幣的時候:一張5元紙幣
當我們需要用兩張紙幣的時候:結果不存在
當我們需要用三張紙幣的時候:兩張2元紙幣和一張1元紙幣
當我們需要用四張紙幣的時候:一張2元紙幣,三張一元紙幣
當我們需要用五張紙幣的時候:五張一元紙幣
從上面的分類分析來看,我們知道,排列組合的方式共有四種,而我如果問你,我現在需要花費的紙幣張數要最小,我們應該選取哪種方式呢,很顯然我們直接選取第一種方法,使用一張5元的紙幣就好了啊,這個就是求最優解的問題啦,也就是我們今天需要研究的問題,動態規劃問題
相信大家到了這里,對動態規劃應該有了初步的認識吧,我也很高興帶大家一起暢游算法的美妙,那么請繼續聽我吹牛皮吧,啦啦啦~~~
基本思想
若要解一個給定問題,我們需要解其不同部分(即子問題),再合并子問題的解以得出原問題的解。 通常許多子問題非常相似,為此動態規劃法試圖僅僅解決每個子問題一次,從而減少計算量: 一旦某個給定子問題的解已經算出,則將其記憶化存儲,以便下次需要同一個子問題解之時直接查表。 這種做法在重復子問題的數目關于輸入的規模呈指數增長時特別有用。
簡單來說,還是拿上面的例子來講,比如說你做一道數學難題,難題,無非就是很難嘛,但是我們需要做的就是把這道難題解出來,對于一個數學水平很菜的選手來講,做出一道難題是不是會感覺非常困難呢?其實換個角度來看待這個問題,一道難題其實是由若干個子問題構成,而每一個子問題也許會是一些很基礎的問題,一個入門級的問題,類似于像1+1=2這樣的問題,相信大家只要有所接觸都能熟練掌握,而針對這些難題,我們也應該去考慮把它進行一個分解,我現在腦邊還能回憶起中學老師說過的話,做不來的題目你可以把一些解題過程先寫出來,把最基本的思路寫出來,寫著寫著說不定答案就出來了呢?相信看完我這篇文章的人水平都能再上一個臺階,不僅如此,對于當前的全球熱潮Artificial Intelligence也是如此,看似非常復雜繁瑣的算法,把它進行一個拆解,其實就是若干個數學公式的組合,最根本的來源還是基礎數學,所以啊,學好數學,未來是光明的~~~
分治與動態規劃
共同點:二者都要求原問題具有最優子結構性質,都是將原問題分而治之,分解成若干個規模較小(小到很容易解決的程序)的子問題.然后將子問題的解合并,形成原問題的解.
不同點:分治法將分解后的子問題看成相互獨立的,通過用遞歸來做。
?動態規劃將分解后的子問題理解為相互間有聯系,有重疊部分,需要記憶,通常用迭代來做。
問題特征
最優子結構:當問題的最優解包含了其子問題的最優解時,稱該問題具有最優子結構性質。
重疊子問題:在用遞歸算法自頂向下解問題時,每次產生的子問題并不總是新問題,有些子問題被反復計算多次。動態規劃算法正是利用了這種子問題的重疊性質,對每一個子問題只解一次,而后將其解保存在一個表格中,在以后盡可能多地利用這些子問題的解。
我認為可能會和回溯的部分問題有點類似,有興趣的同學可以自行閱讀一下我曾經寫過的文章回溯算法入門及經典案例剖析(初學者必備寶典)
解題步驟
1.找出最優解的性質,刻畫其結構特征和最優子結構特征,將原問題分解成若干個子問題;
把原問題分解為若干個子問題,子問題和原問題形式相同或類似,只不過規模變小了。子問題都解決,原問題即解決,子問題的解一旦求出就會被保存,所以每個子問題只需求解一次。
2.遞歸地定義最優值,刻畫原問題解與子問題解間的關系,確定狀態;
在用動態規劃解題時,我們往往將和子問題相關的各個變量的一組取值,稱之為一個“狀態”。一個“狀態”對應于一個或多個子問題, 所謂某個“狀態”下的“值”,就是這個“狀 態”所對應的子問題的解。所有“狀態”的集合,構成問題的“狀態空間”。“狀態空間”的大小,與用動態規劃解決問題的時間復雜度直接相關。
3.以自底向上的方式計算出各個子問題、原問題的最優值,并避免子問題的重復計算;
定義出什么是“狀態”,以及在該“狀態”下的“值”后,就要找出不同的狀態之間如何遷移――即如何從一個或多個“值”已知的 “狀態”,求出另一個“狀態”的“值”(遞推型)。
4.根據計算最優值時得到的信息,構造最優解,確定轉移方程;
狀態的遷移可以用遞推公式表示,此遞推公式也可被稱作“狀態轉移方程”。
實例分析
1.01背包問題
有N件物品和一個容量為V的背包。第i件物品的費用是c[i],價值是w[i]。求解將哪些物品裝入背包可使這些物品的費用總和不超過背包容量,且價值總和最大。?
f[i][v]表示前i件物品恰放入一個容量為v的背包可以獲得的最大價值。則其狀態轉移方程便是:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。
將前i件物品放入容量為v的背包中”這個子問題,若只考慮第i件物品的策略(放或不放),那么就可以轉化為一個只牽扯前i-1件物品的問題。如果不放第i件物品,那么問題就轉化為“前i-1件物品放入容量為v的背包中”;如果放第i件物品,那么問題就轉化為“前i-1件物品放入剩下的容量為v-c[i]的背包中”,此時能獲得的最大價值就是f [i-1][v-c[i]]再加上通過放入第i件物品獲得的價值w[i]。
對01背包不清楚的或者有興趣閱讀的同學請移步至這里
下面貼下01背包的模板
void backtrack(int i,int cp,int cw) {if(i>n){if(cp>bestp){bestp=cp;for(i=1;i<=n;i++) bestx[i]=x[i];}}else{for(int j=0;j<=1;j++) {x[i]=j;if(cw+x[i]*w[i]<=c) {cw+=w[i]*x[i];cp+=p[i]*x[i];backtrack(i+1,cp,cw);cw-=w[i]*x[i];cp-=p[i]*x[i];}}} }最終我們可以去得到答案:
int n,c,bestp;//物品個數,背包容量,最大價值 int p[10000],w[10000],x[10000],bestx[10000];//物品的價值,物品的重量,物品的選中情況 int main() {bestp=0; cin>>c>>n;for(int i=1;i<=n;i++) cin>>w[i];for(int i=1;i<=n;i++) cin>>p[i];backtrack(1,0,0);cout<<bestp<<endl; }2.矩陣連乘
給定n個可連乘的矩陣{A1, A2, …,An},根據矩陣乘法結合律,可有多種不同計算次序,每種次序有不同的計算代價,也就是數乘次數。例如給定2個矩陣A[pi,pj]和B[pj,pk],A×B為[pi,pk]矩陣,數乘次數為pi×pj×pk。將矩陣連乘積Ai…Aj簡記為A[i:j] ,這里i≤j。考察計算A[i:j]的最優計算次序,設這個計算次序在矩陣Ak和Ak+1之間將矩陣鏈斷開,i≤k<j,則A[i:j]的計算量=A[i:k]的計算量+A[k+1:j]的計算量+A[i:k]和A[k+1:j]相乘的計算量。計算A[i:j]的最優次序所包含的計算矩陣子鏈A[i:k]和A[k+1:j]的次序也是最優的。即矩陣連乘計算次序問題的最優解包含著其子問題的最優解,這種性質稱為最優子結構性質,問題具有最優子結構性質是該問題可用動態規劃算法求解的顯著特征。
舉個例子:
給出N個數,每次從中抽出一個數(第一和最后一個不能抽),該次的得分即為抽出的數與相鄰兩個數的乘積。一直這樣將每次的得分累加直到只剩下首尾兩個數為止,問最小得分。
實現過程如下:
#define maxn 105 int dp[maxn][maxn],a[maxn]; int main() {int n;cin>>n;int i,j,k,len;memset(dp,0,sizeof(dp)); //len是設置步長,也就是j減i的值 for(i=0;i<n;i++) cin>>a[i];for(i=0;i<n-2;i++) dp[i][i+2]=a[i]*a[i+1]*a[i+2];//如果只有三個數就直接乘起來 for(len=3;len<n;len++){for(i=0;i+len<n;i++){ j=i+len;for(k=i+1;k<j;k++){if(dp[i][j]==0) dp[i][j]=dp[i][k]+dp[k][j]+a[i]*a[k]*a[j];else dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]+a[i]*a[k]*a[j]);}}}cout<<dp[0][n-1]<<endl;return 0; }3.最長公共子序列與最長公共子串
子串應該比較好理解,至于什么是子序列,這里給出一個例子:有兩個母串
- cnblogs
- belong
比如序列bo, bg, lg在母串cnblogs與belong中都出現過并且出現順序與母串保持一致,我們將其稱為公共子序列。最長公共子序列(Longest Common Subsequence, LCS),顧名思義,是指在所有的子序列中最長的那一個。子串是要求更嚴格的一種子序列,要求在母串中連續地出現。在上述例子的中,最長公共子序列為blog(cnblogs, belong),最長公共子串為lo(cnblogs, belong)。
對于母串X=<x1,x2,?,xm>X=<x1,x2,?,xm>, Y=<y1,y2,?,yn>Y=<y1,y2,?,yn>,求LCS與最長公共子串。
暴力解法:
假設 m<nm<n, 對于母串XX,我們可以暴力找出2m2m個子序列,然后依次在母串YY中匹配,算法的時間復雜度會達到指數級O(n?2m)O(n?2m)。顯然,暴力求解不太適用于此類問題。
動態規劃:
假設Z=<z1,z2,?,zk>Z=<z1,z2,?,zk> 是XX 與YY 的LCS, 我們觀察到
- 如果xm=ynxm=yn ,則zk=xm=ynzk=xm=yn ,有Zk?1Zk?1 是Xm?1Xm?1 與Yn?1Yn?1 的LCS;
- 如果xm≠ynxm≠yn ,則ZkZk 是XmXm 與Yn?1Yn?1 的LCS,或者是Xm?1Xm?1 與YnYn 的LCS。
因此,求解LCS的問題則變成遞歸求解的兩個子問題。但是,上述的遞歸求解的辦法中,重復的子問題多,效率低下。改進的辦法——用空間換時間,用數組保存中間狀態,方便后面的計算。這就是動態規劃(DP)的核心思想了。
DP求解LCS
用二維數組c[i][j]記錄串x1x2?xix1x2?xi與y1y2?yjy1y2?yj的LCS長度,
用i,j遍歷兩個子串x,y,如果兩個元素相等就+1 ,不等就用上一個狀態最大的元素
實現過程如下:
int lcs(string str1, string str2, vector<vector<int>>& vec) {int len1 = str1.size();int len2 = str2.size();vector<vector<int>> c(len1 + 1, vector<int>(len2 + 1, 0));for (int i = 0; i <= len1; i++) {for (int j = 0; j <= len2; j++) {if (i == 0 || j == 0) {c[i][j] = 0;}else if (str1[i - 1] == str2[j - 1]) {c[i][j] = c[i - 1][j - 1] + 1;vec[i][j] = 0;}else if (c[i - 1][j] >= c[i][j - 1]){c[i][j] = c[i - 1][j];vec[i][j] = 1;}else{c[i][j] = c[i][j - 1];vec[i][j] = 2;}}}return c[len1][len2]; }void print_lcs(vector<vector<int>>& vec, string str, int i, int j) {if (i == 0 || j == 0){return;}if (vec[i][j] == 0){print_lcs(vec, str, i - 1, j - 1);printf("%c", str[i - 1]);}else if (vec[i][j] == 1){print_lcs(vec, str, i - 1, j);}else{print_lcs(vec, str, i, j - 1);} }int _tmain(int argc, _TCHAR* argv[]) {string str1 = "123456";string str2 = "2456";vector<vector<int>> vec(str1.size() + 1, vector<int>(str2.size() + 1, -1));int result = lcs(str1, str2, vec);cout << "result = " << result << endl;print_lcs(vec, str1, str1.size(), str2.size());getchar();return 0; }DP求解最長公共子串
前面提到了子串是一種特殊的子序列,因此同樣可以用DP來解決。定義數組的存儲含義對于后面推導轉移方程顯得尤為重要,糟糕的數組定義會導致異常繁雜的轉移方程。考慮到子串的連續性,將二維數組c[i,j]c[i,j]用來記錄具有這樣特點的子串——結尾為母串x1x2?xix1x2?xi與y1y2?yjy1y2?yj的結尾——的長度。
區別就是因為是連續的,如果兩個元素不等,那么就要=0了而不能用之前一個狀態的最大元素
最長公共子串的長度為 max(c[i,j]),?i∈{1,?,m},j∈{1,?,n}max(c[i,j]),?i∈{1,?,m},j∈{1,?,n}。
實現過程如下:
int lcs_2(string str1, string str2, vector<vector<int>>& vec) {int len1 = str1.size();int len2 = str2.size();int result = 0; //記錄最長公共子串長度vector<vector<int>> c(len1 + 1, vector<int>(len2 + 1, 0));for (int i = 0; i <= len1; i++) {for (int j = 0; j <= len2; j++) {if (i == 0 || j == 0) {c[i][j] = 0;}else if (str1[i - 1] == str2[j - 1]) {c[i][j] = c[i - 1][j - 1] + 1;vec[i][j] = 0;result = c[i][j] > result ? c[i][j] : result;}else {c[i][j] = 0;}}}return result; }void print_lcs(vector<vector<int>>& vec, string str, int i, int j) {if (i == 0 || j == 0){return;}if (vec[i][j] == 0){print_lcs(vec, str, i - 1, j - 1);printf("%c", str[i - 1]);}else if (vec[i][j] == 1){print_lcs(vec, str, i - 1, j);}else{print_lcs(vec, str, i, j - 1);} } int _tmain(int argc, _TCHAR* argv[]) {string str1 = "123456";string str2 = "14568";vector<vector<int>> vec(str1.size() + 1, vector<int>(str2.size() + 1, -1));int result = lcs_2(str1, str2, vec);cout << "result = " << result << endl;print_lcs(vec, str1, str1.size(), str2.size());getchar();return 0; }4.走金字塔
給定一個由n行數字組成的數字三角型,如圖所示。設計一個算法,計算從三角形的頂至底的一條路徑,使該路徑經過的數字總和最大。路徑上的每一步都只能往左下或右下走,給出這個最大和。
? ? ? ? 7?
? ? ? 3 ?8?
? ? 8 ?1 ?0?
? 2 ?7 ?4 ?4?
4 ?5 ?2 ?6 ?5
對于這種問題,我們可以有正向和反向兩種思考方式。正向思考這個問題,dp[i][j]表示從第一行第一列到第i行第j列最大的數字總和;反向思考這個問題,dp[i][j]表示從第i行第j列到最后一行最大的數字總和。反向思考的代碼要簡潔一些
正向思考:
int triangle[110][110],dp[110][110]; int main() {int N;cin>>N;memset(dp,0,sizeof(dp));memset(triangle,0,sizeof(triangle));for(int i=1;i<=N;i++){for(int j=1;j<=i;j++){cin>>triangle[i][j];}}dp[1][1]=triangle[1][1];for(int i=2;i<=N;i++){for(int j=1;j<=i;j++){if(j!=1) dp[i][j]=max(dp[i][j],dp[i-1][j-1]+triangle[i][j]);if(j!=i) dp[i][j]=max(dp[i][j],dp[i-1][j]+triangle[i][j]);}}int max=-1;for(int i=1;i<=N;i++){if(dp[N][i]>max) max=dp[N][i];}cout<<max<<endl;return 0; }反向思考:
int triangle[110][110],dp[110][110]; int main() {int N;cin>>N;memset(dp,0,sizeof(dp));memset(triangle,0,sizeof(triangle));for(int i=1;i<=N;i++){for(int j=1;j<=i;j++){cin>>triangle[i][j];}}for(int i=1;i<=N;i++){dp[N][i]=triangle[N][i];}for(int i=N-1;i>=1;i--){for(int j=1;j<=i;j++){dp[i][j]=max(dp[i+1][j]+triangle[i][j],dp[i+1][j+1]+triangle[i][j]);}}cout<<dp[1][1]<<endl;return 0; }5.最長遞增子序列(LIS)
最長遞增子序列的定義和最長公共子序列的定義差不多,只不過最長遞增子序列是針對單個數組的。
舉個例子,給出一序列,求出該序列的最長上升子序列的最大長度。
我們可以這么考慮,用數組a[]存儲序列,b[i]表示以a[i]為結尾的序列的最大長度。
因此要求出b[i]的最大值,即求出max{b[0],b[1]....b[i-1]}的最大值,那么b[i]的最大值為max{b[0],b[1]....b[i-1]}+1; 即可寫出狀態方程:b[i]=max{b[0],b[1].....b[j]}+1;(0<=j<i&&a[j]<a[i]),然后求出數組b[]中的最大值即為所求。 int main(void) {int i,j,n;int a[1001];int b[1001];int max;scanf("%d",&n);for(i=0;i<n;i++){scanf("%d",&a[i]);b[i]=1;}for(i=0;i<n;i++){max=0;for(j=0;j<i;j++){if(a[i]>a[j]&&b[j]>max){max=b[j];}}b[i]=max+1;}max=0;for(i=0;i<n;i++){if(max<b[i])max=b[i];}printf("%d\n",max);return 0; }其實還有更好的方法:
實現過程如下:
#define MAX_N 1010 #define INF 10010 int main() {int i;int n;cin>>n;int a[1010];for(i=0;i<n;i++){cin>>a[i];}int dp[MAX_N];fill(dp,dp+n,INF);for(i=0;i<n;i++){*lower_bound(dp,dp+n,a[i])=a[i];}cout<<lower_bound(dp,dp+n,INF)-dp<<endl; }6.最大子段和
給定n個整數組成的序列a1,a2,…,an,求該序列子段和的最大值。最大子段和不能是負數,當序列中均為負數時定義最大子段和為0。例如{-2, 11, -4, 13, -5, -2}的最大子段和為20。首先可以想到用分治的方法解決這個問題,如果將給定的序列a[1..n]分成長度相等的兩段a[1..n/2]和a[n/2+1:n],分別求出這兩段的最大子段和。則該給定序列的最大子段和有三種情況:
和a[1…n/2]的最大子段和相同;
和a[n/2+1…n]的最大子段和相同;
最大子段和包含兩部分。
前兩種情形我們可以用遞歸方法求出,第三種情形可以分別求出兩部分的最大子段和值再相加。序列的最大子段和即為這三種情形的最大值。
實現過程如下:
int maxsub(int a[],int low,int high) {if(low==high) return a[low];int s1,s2,s3,s31,s32,i,j,sum;int mid=(low+high)/2;s1=maxsub(a,low,mid);s2=maxsub(a,mid+1,high);i=mid;s31=a[mid];while((s31+a[i-1]>s31)&&(i>low)){s31+=a[i-1];i--;}j=mid+1;s32=a[mid+1];while((s32+a[j+1]>s32)&&(j<high)){s32+=a[j+1];j++;}sum=s31+s32;if(sum<s1) sum=s1;if(sum<s2) sum=s2; return sum; }int main() {int a[6]={-2,11,-4,13,-5,-2};cout<<max(0,maxsub(a,0,5))<<endl; }使用動態規劃的解法能夠獲得更好的復雜度。若記b[j]=max(a[i]+a[i+1]+..+a[j]),其中1<=i<=j,并且1<=j<=n。注意一點,b[j]一定包括了a[j]。所求的最大子段和為max b[j],1<=j<=n。由b[j]的定義可易知,當b[j-1]>0時b[j]=b[j-1]+a[j],否則b[j]=a[j]。故b[j]的動態規劃遞歸式為:b[j]=max(b[j-1]+a[j],a[j]),1<=j<=n。
int maxsub(int a[],int n) {int sum=0,b=0;for(int i=0;i<=n;i++) {if(b>0) b+=a[i];else b=a[i];if(b>sum) sum=b;}return sum; }int main() {int a[6]={-2,11,-4,13,-5,-2};cout<<maxsub(a,5)<<endl; }7.最大子矩陣和
求一個最大為100*100矩陣中的子矩陣中元素之和的最大值
主要思想為將其轉化為一維數組求最大子段和,如果最優解左起第i列,右止于第j列,那么我們相當于把這些列的對應位加和,成為一列,并對改列求最大子段和即可(降維思想)。
實現過程如下:
#define maxn 105 #define inf 0x3f3f3f3fint array[maxn][maxn]; int f[maxn][maxn][maxn];int main() {int n;cin>>n;for(int i=1;i<=n;i++){for(int j=1;j<=n;j++){scanf("%d", &array[i][j]);}}memset(f, 0, sizeof(f));int ans=-inf;for(int i=1;i<=n;i++){for(int j=1;j<=n;j++){int sum=0;for(int k=j;k<=n;k++){sum+=array[i][k];f[i][j][k]=max(f[i-1][j][k]+sum,sum);//i是指行,j是起始列,k是終結列,f存的值為在ijk范圍內的元素和最大值ans=max(ans,f[i][j][k]);}}}cout<<ans<<endl;return 0; }8.凸多邊形最優三角剖分
用多邊形頂點的逆時針序列表示凸多邊形,即P={V0,V1,…,Vn}表示具有n+1條邊的凸多邊形。給定凸多邊形P,以及定義在由多邊形的邊和弦組成的三角形上的權函數w。要求確定該凸多邊形的三角剖分,使得即該三角剖分中諸三角形上權之和為最小。若P={V0,V1……Vn}的最優三角剖分T包含三角形V0VkVn,則T的權為三個部分權之和:三角形V0VkVn的權,多邊形{V0,V1……Vk}的權和多邊形{Vk,Vk+1……Vn}的權之和。可以斷言,由T確定的這兩個子多邊形的三角剖分也是最優的。設t[i][j]為凸多邊形{Vi-1,Vi……Vj}的最優三角剖分所對應的最優權值,則P的最優權值為t[1][n],有:
實現過程如下:
#define N 6 int weight[][N]= { {0,2,2,3,1,4}, {2,0,1,5,2,3}, {2,1,0,2,1,4}, {3,5,2,0,6,2}, {1,2,1,6,0,1}, {4,3,4,2,1,0} }; int t[N][N]; //t[i][j]表示多邊形{Vi-1VkVj}的最優權值 int s[N][N]; //s[i][j]記錄Vi-1到Vj最優三角剖分的中間點Kint get_weight(const int a, const int b, const int c) {return weight[a][b]+weight[b][c]+weight[c][a]; }void minweight() {int minval;for(int i=1;i<N;i++) t[i][i]=0;for(int r=2;r<N;r++){for(int i=1;i<N-r+1;i++){int j=i+r-1;minval=9999; for (int k=i;k<j;k++){t[i][j]=t[i][k]+t[k+1][j]+get_weight(i-1,k,j);if(t[i][j]<minval) {minval=t[i][j];s[i][j]=k; }}t[i][j]=minval; //取得多邊形Vi-1Vj的最小劃分權值 }} }void backtrack(int a, int b) {if (a==b) return;backtrack(a,s[a][b]);backtrack(s[a][b]+1,b); cout<<"最優三角:"<<"v"<<a-1<<"v"<<s[a][b]<<"v"<<b<<endl; }int main() {minweight();cout<<"result:"<<t[1][N-1]<<endl;backtrack(1,5); }9.最優二叉搜索樹
在《算法導論》中對于這個問題有詳細的描述。如果在二叉樹中查找元素不考慮概率及查找不成功的情況下,可以采用紅黑樹或者平衡二叉樹來搜索。而現實生活中,查找的關鍵字是有一定的概率的,就是說有的關鍵字可能經常被搜索,而有的很少被搜索,而且搜索的關鍵字可能不存在,為此需要根據關鍵字出現的概率構建一個二叉樹。比如輸入法中針對用戶習慣可以自動調整詞頻以減少用戶翻查次數,使得經常用的詞匯被放置在前面,這樣就能有效地加快查找速度。給定一個由n個互異的關鍵字組成的有序序列K={k1<k2<k3<,……,<kn}和它們被查詢的概率P={p1,p2,p3,……,pn},構造一棵二叉查找樹使得查詢所有元素的總的代價最小。對于一個搜索樹,當搜索的元素在樹內時,表示搜索成功。當不在樹內時,表示搜索失敗,用一個虛葉子節點來標示搜索失敗的情況,因此需要n+1個虛葉子節點{d0<d1<……<dn},對于應di的概率序列是Q={q0,q1,……,qn}。其中d0表示搜索元素小于k1,dn表示搜索元素大于kn。di(0<i<n)表示搜索節點在ki和k(i+1)之間。因此有公式:
由每個關鍵字和每個虛擬鍵被搜索的概率,可以確定在一棵給定的二叉查找樹內一次搜索的期望代價。設一次搜索的實際代價為檢查的節點個數,即所發現的節點的深度加上1。所以一次搜索的期望代價為:
需要注意的是一棵最優二叉查找樹不一定是一棵整體高度最小的樹,也不一定總是把最大概率的關鍵字放在根部。對于這一點可以很容易找到反例。定義e[i,j]為搜索一棵包含關鍵字ki,……,kj的最優二叉查找樹的期望代價,當j=i-1時,說明此時只有虛擬鍵di-1,故e[i,i-1] = qi-1;當j≥i時,需要從ki,……,kj中選擇一個根kr,然后用關鍵字ki,……,kr-1來構造一棵最優二叉查找樹作為左子樹,用關鍵字kr+1,……,kj來構造一棵最優二叉查找樹作為右子樹。定義一棵有關鍵字ki,……,kj的子樹概率的總和為:
當一棵樹成為一個節點的子樹時子樹中每個節點深度都增加1,期望搜索代價增加量為子樹中所有節點概率的總和。因此如果kr是一棵包含關鍵字ki,……,kj的最優子樹的根,則有:
用root[i,j]來記錄關鍵字ki,……,kj的子樹的根,另外為了防止重復計算,用二維數組來保存w(i,j)的值,其中w[i,j] = w[i,j-1]+pj+qj。
實現過程如下:
#define N 5 #define MAX 999999.99999 void optimal_binary_search_tree(float *p,float *q,int n,float e[N+2][N+1],int root[N+1][N+1]); void Construct_Optimal_BST(int root[N+1][N+1],int i,int j,bool flag);int main() {float p[N+1]={0,0.15,0.10,0.05,0.10,0.20};float q[N+1]={0.05,0.10,0.05,0.05,0.05,0.10};float e[N+2][N+1];int root[N+1][N+1];int i,j;optimal_binary_search_tree(p,q,N,e,root);cout<<"最優二叉查找樹的代價為: "<<e[1][N]<<endl;cout<<"最優二叉查找樹的結構描述如下:"<<endl;Construct_Optimal_BST(root,1,N,0); }void optimal_binary_search_tree(float *p,float *q,int n,float e[N+2][N+1],int root[N+1][N+1]) {int i,j,k,r;float t;float w[N+2][N+1];for(i=1;i<=N+1;++i) {e[i][i-1]=q[i-1];w[i][i-1]=q[i-1];}for(k=1;k<=n;++k) //自底向上尋找最優子樹 {for(i=1;i<=n-k+1;i++){j=i+k-1;e[i][j]=MAX;w[i][j]=w[i][j-1]+p[j]+q[j];for(r=i;r<=j;r++) {t=e[i][r-1]+e[r+1][j]+w[i][j];if(t<e[i][j]){e[i][j]=t;root[i][j]=r;}}}} }void Construct_Optimal_BST(int root[N+1][N+1],int i,int j,bool flag) { if(flag==0) { cout<<"k"<<root[i][j]<<" is root"<<endl; flag=1; } int r=root[i][j]; //如果左子樹是葉子 if(r-1<i) { cout<<"d"<<r-1<<" is the left child of "<<"K"<<r<<endl; } //如果左子樹不是葉子 else { cout<<"k"<<root[i][r-1]<<" is the left child of "<<"K"<<r<<endl; Construct_Optimal_BST(root,i,r-1,1); } //如果右子樹是葉子 if(r+1>j) { cout<<"d"<<j<<" is the right child of "<<"K"<<r<<endl; } //如果右子樹不是葉子 else { cout<<"k"<<root[r+1][j]<<" is the right child of "<<"K"<<r<<endl; Construct_Optimal_BST(root,r+1,j,1); } }10.雙調歐幾里得旅行商問題
這個問題是《算法導論》中的思考題。給定平面上n個點,確定一條連接各點的最短閉合旅程。這個解的一般形式為NP的。J.L. Bentley建議通過只考慮雙調旅程來簡化問題。這種旅程即為從最左點開始,嚴格地從左到右直至最右點,然后嚴格地從右到左直至出發點。在這種情況下,存在確定的最優雙調路線的O(n*n)時間的算法。
例如,a圖是最短閉合路線,但是它不是雙調的。b圖是最短的閉合雙調路線。
首先將各點按照x坐標從小到大排列,定義從Pi到Pj的路徑為從Pi開始,從右到左一直到P1,然后從左到右一直到Pj。在這個路徑上,會經過P1到Pmax(i,j)之間的所有點且每個點只經過一次。定義d(i,j)為滿足這一條件的最短路徑,我們只考慮i>=j的情況。同時定義dist(i,j)為點Pi到Pj之間的直線距離。根據題意我們需要求的是d(n,n)。
關于子問題d(i,j)的求解,分三種情況:
當j<i-1時,由定義可知,點Pi-1一定在路徑Pi-Pj上,又由于j<i-1,因此Pi的左邊的相鄰點一定是Pi-1,故d(i,j)=d(i-1,j)+dist(i-1,i)。
當j=i-1時,與Pi相鄰的那個點可能是P1到Pi-1中的任何一個,故d(i,i-1)=min{d(k,i-1)+dist(i,k)},1<=k<i-1。
當j=i時,同理,d(i,i)=min{d(k,i)+dist(i,k)},1<=k<i。
實現過程如下:
#define INF 0x3f3f3f3f int n; double dis[550][550],dp[505][505]; struct Node { int x,y; }node[1500];bool cmp(Node a,Node b) { if(a.x!=b.x) return a.x<b.x; else return a.y<b.y; } void get_dis() { int x1,x2,y1,y2; for(int i=1;i<=n;++i){ x1=node[i].x;y1=node[i].y; for(int j=i;j<=n;++j){ x2=node[j].x;y2=node[j].y; dis[j][i]=dis[i][j]=sqrt((double)((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2))); } } } int main() { int x,y; while(scanf("%d",&n)!=EOF){ memset(dis,0,sizeof(dis)); for(int i=1;i<=n;++i){ scanf("%d%d",&x,&y); node[i]=(Node){x,y}; } sort(node+1,node+n+1,cmp); get_dis(); dp[2][1]=dp[1][2]=dis[1][2]; dp[2][2]=2*dis[1][2]; for(int i=3;i<=n;++i){ for(int j=1;j<i-1;++j){ dp[j][i]=dp[i][j]=dp[i-1][j]+dis[i][i-1]; } dp[i][i-1]=dp[i-1][i]=dp[i][i]=INF; for(int k=1;k<i-1;++k){ if(dp[i][i-1]-(dp[k][i-1]+dis[k][i])>1e-3){ dp[i-1][i]=dp[i][i-1]=dp[k][i-1]+dis[k][i]; } } for(int k=1;k<i;++k){ if(dp[i][i]-(dp[k][i]+dis[k][i])>1e-3){ dp[i][i]=dp[k][i]+dis[k][i]; } } } printf("%.2f\n",dp[n][n]); } return 0; }11.最大不相交子段和
求兩個不相交子段加起來的最大值。
將數組a[]從下標k處劃分成兩部分a[1-k]和a[k+1,N],用m1表示a[1-k]的最大字段和,m2表示a[k+1,N]的最大字段和,則 S = Max { m1, m2 } ( 其中k屬于區間[2-N-1] ,k不能取1和N,因為題目中j<p)
通過正向遍歷數組a求m1,逆向遍歷數組a求m2,最后根據 S = Max { m1,m2 } 枚舉k即可。
const int INF = -0xfffffff; const int Max = 100000;// 對象 int n; int a[Max+1]; int b[Max+2]; int m1[Max+2], m2[Max+2]; // m1[i]:Max sum of sub array [0,i] of a// m2[i]:Max sum of sub array [i,n] of a int maxSum; // resint main() {while( scanf( "%d", &n ) && n ) {m1[0] = m2[n+1] = INF;memset( b, 0, sizeof(b) );for( int i = 1; i <= n; ++ i ) {scanf( "%d", &a[i] );if( b[i-1] >= 0 )b[i] = b[i-1] + a[i];else b[i] = a[i];m1[i] = b[i] > m1[i-1] ? b[i] : m1[i-1];}for( int i = n; i >= 1; -- i ) {if( b[i+1] >= 0 )b[i] = b[i+1] + a[i];elseb[i] = a[i];m2[i] = b[i] > m2[i+1] ? b[i] : m2[i+1];}maxSum = INF;for( int i = 1; i < n; ++ i ) {if( m1[i] + m2[i+1] > maxSum )maxSum = m1[i] + m2[i+1];}printf( "%d\n", maxSum );}return 0; }12.最長回文子串
回文是指正著讀和倒著讀,結果一些樣,比如abcba或abba。
舉個例子,我們要在一個字符串中要到最長的回文子串。
暴力求解:
最容易想到的就是暴力破解,求出每一個子串,之后判斷是不是回文,找到最長的那個。
求每一個子串時間復雜度O(N^2),判斷子串是不是回文O(N),兩者是相乘關系,所以時間復雜度為O(N^3)。
string findLongestPalindrome(string &s) {int length=s.size();//字符串長度int maxlength=0;//最長回文字符串長度int start;//最長回文字符串起始地址for(int i=0;i<length;i++)//起始地址for(int j=i+1;j<length;j++)//結束地址 {int tmp1,tmp2;for(tmp1=i,tmp2=j;tmp1<tmp2;tmp1++,tmp2--)//判斷是不是回文 {if(s.at(tmp1)!=s.at(tmp2))break;}if(tmp1>=tmp2&&j-i>maxlength){maxlength=j-i+1;start=i;}}if(maxlength>0)return s.substr(start,maxlength);//求子串return NULL; }動態規劃
回文字符串的子串也是回文,比如P[i,j](表示以i開始以j結束的子串)是回文字符串,那么P[i+1,j-1]也是回文字符串。這樣最長回文子串就能分解成一系列子問題了。這樣需要額外的空間O(N^2),算法復雜度也是O(N^2)。
首先定義狀態方程和轉移方程:
P[i,j]=0表示子串[i,j]不是回文串。P[i,j]=1表示子串[i,j]是回文串。
string findLongestPalindrome(string &s) {const int length=s.size();int maxlength=0;int start;bool P[50][50]={false};for(int i=0;i<length;i++)//初始化準備 {P[i][i]=true;if(i<length-1&&s.at(i)==s.at(i+1)){P[i][i+1]=true;start=i;maxlength=2;}}for(int len=3;len<=length;len++)//子串長度for(int i=0;i<=length-len;i++)//子串起始地址 {int j=i+len-1;//子串結束地址if(P[i+1][j-1]&&s.at(i)==s.at(j)){P[i][j]=true;maxlength=len;start=i;}}if(maxlength>=2)return s.substr(start,maxlength);return NULL; }中心擴展
中心擴展就是把給定的字符串的每一個字母當做中心,向兩邊擴展,這樣來找最長的子回文串。算法復雜度為O(N^2)。
但是要考慮兩種情況: 1、像aba,這樣長度為奇數。 2、想abba,這樣長度為偶數。 實現過程如下: string findLongestPalindrome(string &s) {const int length=s.size();int maxlength=0;int start;for(int i=0;i<length;i++)//長度為奇數 {int j=i-1,k=i+1;while(j>=0&&k<length&&s.at(j)==s.at(k)){if(k-j+1>maxlength){maxlength=k-j+1;start=j;}j--;k++;}}for(int i=0;i<length;i++)//長度為偶數 {int j=i,k=i+1;while(j>=0&&k<length&&s.at(j)==s.at(k)){if(k-j+1>maxlength){maxlength=k-j+1;start=j;}j--;k++;}}if(maxlength>0)return s.substr(start,maxlength);return NULL; }Manacher法
Manacher法只能解決例如aba這樣長度為奇數的回文串,對于abba這樣的不能解決,于是就在里面添加特殊字符。我是添加了“#”,使abba變為a#b#b#a。這個算法就是利用已有回文串的對稱性來計算的,具體算法復雜度為O(N),我沒看出來,因為有兩個嵌套的for循環。
具體原理利用已知回文串的左半部分來推導右半部分首先,在字符串s中,用rad[i]表示第i個字符的回文半徑,即rad[i]盡可能大,且滿足:
s[i-rad[i],i-1]=s[i+1,i+rad[i]]
很明顯,求出了所有的rad,就求出了所有的長度為奇數的回文子串.
至于偶數的怎么求,最后再講.
假設現在求出了rad[1..i-1],現在要求后面的rad值,并且通過前面的操作,得知了當前字符i的rad值至少為j.現在通過試圖擴大j來掃描,求出了rad[i].再假設現在有個指針k,從1循環到rad[i],試圖通過某些手段來求出[i+1,i+rad[i]]的rad值.
根據定義,黑色的部分是一個回文子串,兩段紅色的區間全等.
因為之前已經求出了rad[i-k],所以直接用它.有3種情況:
①rad[i]-k<rad[i-k]
如圖,rad[i-k]的范圍為青色.因為黑色的部分是回文的,且青色的部分超過了黑色的部分,所以rad[i+k]肯定至少為rad[i]-k,即橙色的部分.那橙色以外的部分就不是了嗎?這是肯定的.因為如果橙色以外的部分也是回文的,那么根據青色和紅色部分的關系,可以證明黑色部分再往外延伸一點也是一個回文子串,這肯定不可能,因此rad[i+k]=rad[i]-k.為了方便下文,這里的rad[i+k]=rad[i]-k=min(rad[i]-k,rad[i-k]).
?
②rad[i]-k>rad[i-k]
如圖,rad[i-k]的范圍為青色.因為黑色的部分是回文的,且青色的部分在黑色的部分里面,根據定義,很容易得出:rad[i+k]=rad[i-k].為了方便下文,這里的rad[i+k]=rad[i-k]=min(rad[i]-k,rad[i-k]).
根據上面兩種情況,可以得出結論:當rad[i]-k!=rad[i-k]的時候,rad[i+k]=min(rad[i]-k,rad[i-k]).
注意:當rad[i]-k==rad[i-k]的時候,就不同了,這是第三種情況:
如圖,通過和第一種情況對比之后會發現,因為青色的部分沒有超出黑色的部分,所以即使橙色的部分全等,也無法像第一種情況一樣引出矛盾,因此橙色的部分是有可能全等的,但是,根據已知的信息,我們不知道橙色的部分是多長,因此就把i指針移到i+k的位置,j=rad[i-k](因為它的rad值至少為rad[i-k]),等下次循環的時候再做了.
整個算法就這樣.
至于時間復雜度為什么是O(n),我已經證明了,但很難說清楚.所以自己體會吧.
上文還留有一個問題,就是這樣只能算出奇數長度的回文子串,偶數的就不行.怎么辦呢?有一種直接但比較笨的方法,就是做兩遍(因為兩個程序是差不多的,只是rad值的意義和一些下標變了而已).但是寫兩個差不多的程序是很痛苦的,而且容易錯.所以一種比較好的方法就是在原來的串中每兩個字符之間加入一個特殊字符,再做.如:aabbaca,把它變成(#a#a#b#b#a#c#a#),左右的括號是為了使得算法不至于越界。這樣的話,無論原來的回文子串長度是偶數還是奇數,現在都變成奇數了.
13.KMP算法
KMP算法用于字符串匹配,kmp算法完成的任務是:給定兩個字符串O和f,長度分別為n和m,判斷f是否在O中出現,如果出現則返回出現的位置。常規方法是遍歷a的每一個位置,然后從該位置開始和b進行匹配,但是這種方法的復雜度是O(nm)。kmp算法通過一個O(m)的預處理,使匹配的復雜度降為O(n+m)。
在這里我只能簡單的進行個小結,有興趣的同學可以參考我之前寫過的一篇文章KMP算法學習(詳解)
樸素匹配算法需要兩個指針i,j都遍歷一遍字符串,故復雜度m*n
KMP算法i指針不回溯,j指針的回溯參考next數組,體現了動態規劃的思想
原理如下:
藍色表示匹配,紅色為失配
分析藍色部分
如果存在最長公共前后綴的話,比如這樣:
就可以在下次匹配的時候用,這樣避免了i的回溯
next數組的意義:當模式匹配串T失效的時候,next數組對應的元素知道應該使用T串的哪個元素進行下一輪匹配
實現過程如下:
void get_next(string T, int *next) {int i = 1; //后綴int j = 0; //前綴next[1] = 0;while (i < T[0]) //T[0]表示字符串長度 {if (j == 0 || T[i] == T[j]){i++;j++;next[i] = j;}elsej = next[j];} }int KMP(string S, string T, int pos) {int i = pos; //標記主串S下標int j = 1; //匹配串下標int next[255];get_next(T, next);while (i <= S[0] && j <= T[0]) //0位置都放字符串長度 {if (j == 0 || S[i] == T[j]){i++;j++;}elsej = next[j]; //j退回到合適位置,i不用再回溯了if (j > T[0]) //如果存在j在匹配完最后一個元素后又++了,所以會大于長度return i - T[0]; //i的位置減去匹配串的長度就是匹配串出現的位置elsereturn 0;} }會出現一種特殊情況:
S = “aaaabcde”
T = "aaaaax"
這樣的話next數組為012345,實際上由于前面都是a,直接調到第一個a就可以了,期望的next數組為000005
這樣next數組構造改為12-15行
改進方案:
void get_next(string T, int *next) {int i = 1; //后綴int j = 0; //前綴next[1] = 0;while (i < T[0]) //T[0]表示字符串長度 {if (j == 0 || T[i] == T[j]){i++;j++;if (T[i] != T[j])next[i] = j;elsenext[i] = next[j];}elsej = next[j];} }14.硬幣找零問題
假設有幾種硬幣,如1 5 10 20 50 100,并且數量無限。請找出能夠組成某個數目的找零所使用最少的硬幣數。
解法:
用待找零的數值k描述子結構/狀態,記作sum[k],其值為所需的最小硬幣數。對于不同的硬幣面值coin[0...n],有sum[k] = min(sum[k-coin[0]] , sum[k-coin[1]], ...)+1。對應于給定數目的找零total,需要求解sum[total]的值。
注意要從前往后算,從后往前算無法保存狀態,需要遞歸,效率很低,就不是動態規劃了
#define MaxNum pow(2,31) - 1 int main() {int n;while (cin >> n){vector<int> c(n + 1, 0);for (int i = 1; i <= n; i++){if (i == 1 || i == 5 || i == 10 || i == 20 || i == 50 || i == 100){c[i] = 1;continue;}int curMin = MaxNum;if (i - 1 > 0)curMin = c[i - 1] < curMin ? c[i - 1] : curMin;if (i - 5 > 0)curMin = c[i - 5] < curMin ? c[i - 5] : curMin;if (i - 10 > 0)curMin = c[i - 10] < curMin ? c[i - 10] : curMin;if (i - 20 > 0)curMin = c[i - 20] < curMin ? c[i - 20] : curMin;if (i - 50 > 0)curMin = c[i - 50] < curMin ? c[i - 50] : curMin;if (i - 100 > 0)curMin = c[i - 100] < curMin ? c[i - 100] : curMin;c[i] = curMin + 1;}cout << c[n] << endl;}system("pause");return 0; }15.找平方個數最小
給一個正整數?n,?找到若干個完全平方數(比如1,?4,?9,?...?)使得他們的和等于?n。你需要讓平方數的個數最少。
給出?n?=?12,?返回?3?因為?12?=?4?+?4?+?4。
給出?n?=?13,?返回?2?因為?13?=?4?+?9。
實現過程如下:
int findMin(int n) {int *result = new int(n + 1);result[0] = 0;for (int i = 1; i <= n; i++){int minNum = i;for (int j = 1;; j++){if (i >= j * j){int tmp = result[i - j*j] + 1;minNum = tmp < minNum ? tmp : minNum;}elsebreak;}result[i] = minNum;}return result[n]; }int main() {int n;while (cin >> n)cout << findMin(n) << endl; }16.N*N方格內的走法問題
有一個n*n的方格,從左上角走到右下角有多少種最短路徑的走法?
若不加最短路徑則n^(n-1)種走法。加上了最短路徑就是說橫向的距離為n-1,縱向的距離為n-1.總共的距離是2(n-1)步走到。
實現過程如下:
int main() {int n;while (cin >> n){vector<vector<int>> dp(n+1, vector<int>(n+1, 1));for (int i = 1; i <= n;i++){for (int j = 1; j <= n;j++){dp[i][j] = dp[i][j - 1] + dp[i - 1][j];}}cout << dp[n][n] << endl;} }17.樓層拋珠問題
某幢大樓有100層。你手里有兩顆一模一樣的玻璃珠。當你拿著玻璃珠在某一層往下扔的時候,一定會有兩個結果,玻璃珠碎了或者沒碎。這幢大樓有個臨界樓層。低于它的樓層,往下扔玻璃珠,玻璃珠不會碎,等于或高于它的樓層,扔下玻璃珠,玻璃珠一定會碎。玻璃珠碎了就不能再扔。現在讓你設計一種方式,使得在該方式下,最壞的情況扔的次數比其他任何方式最壞的次數都少。也就是設計一種最有效的方式。
例如:有這樣一種方式,第一次選擇在60層扔,若碎了,說明臨界點在60層及以下樓層,這時只有一顆珠子,剩下的只能是從第一層,一層一層往上實驗,最壞的情況,要實驗59次,加上之前的第一次,一共60次。若沒碎,則只要從61層往上試即可,最多只要試40次,加上之前一共需41次。兩種情況取最多的那種。故這種方式最壞的情況要試60次。仔細分析一下。如果不碎,我還有兩顆珠子,第二顆珠子會從N+1層開始試嗎?很顯然不會,此時大樓還剩100-N層,問題就轉化為100-N的問題了。
那該如何設計方式呢?
根據題意很容易寫出狀態轉移方程:N層樓如果從n層投下玻璃珠,最壞的嘗試次數是:
那么所有層投下的最壞嘗試次數的最小值即為問題的解:。其中F(1)=1.
實現過程如下:
int max(int a, int b) {return (a > b)? a : b; }int dp[101]; //N<=100; int floorThr(int N) {for (int i = 2; i <= N; i++){dp[i] = i;for (int j = 1; j<i; j++){int tmp = max(j, 1 + dp[i - j]); //j的遍歷相當于把每層都試一遍if (tmp<dp[i])dp[i] = tmp;}}return dp[N]; }int main() {dp[0] = 0;dp[1] = 1;int dis = floorThr(100);cout << dis << endl;system("Pause"); }18.字符串相似度/編輯距離(edit distance)
許多程序會大量使用字符串。對于不同的字符串,我們希望能夠有辦法判斷其相似程度。我們定義了一套操作方法來把兩個不相同的字符串變得相同,具體的操作方法為:
1.修改一個字符(如把“a”替換為“b”)。
2.增加一個字符(如把“abdd”變為“aebdd”)。
3.刪除一個字符(如把“travelling”變為“traveling”)。
比如,對于“abcdefg”和“abcdef”兩個字符串來說,我們認為可以通過增加/減少一個“g“的方式來達到目的。上面的兩種方案,都僅需要一次操作。把這個操作所需要的次數定義為兩個字符串的距離,給定任意兩個字符串,你是否能寫出一個算法來計算出它們的距離?
不難看出,兩個字符串的距離肯定不超過它們的長度之和(我們可以通過刪除操作把兩個串都轉化為空串)。雖然這個結論對結果沒有幫助,但至少可以知道,任意兩個字符串的距離都是有限的。
我們還是應該集中考慮如何才能把這個問題轉化成規模較小的同樣的問題。如果有兩個串A=xabcdae和B=xfdfa,它們的第一個字符是相同的,只要計算A[2,…,7]=abcdae和B[2,…,5]=fdfa的距離就可以了。但是如果兩個串的第一個字符不相同,那么可以進行如下的操作(lenA和lenB分別是A串和B串的長度):
1.刪除A串的第一個字符,然后計算A[2,…,lenA]和B[1,…,lenB]的距離。
2.刪除B串的第一個字符,然后計算A[1,…,lenA]和B[2,…,lenB]的距離。
3.修改A串的第一個字符為B串的第一個字符,然后計算A[2,…,lenA]和B[2,…,lenB]的距離。
4.修改B串的第一個字符為A串的第一個字符,然后計算A[2,…,lenA]和B[2,…,lenB]的距離。
5.增加B串的第一個字符到A串的第一個字符之前,然后計算A[1,…,lenA]和B[2,…,lenB]的距離。
6.增加A串的第一個字符到B串的第一個字符之前,然后計算A[2,…,lenA]和B[1,…,lenB]的距離。
在這個題目中,我們并不在乎兩個字符串變得相等之后的字符串是怎樣的。所以,可以將上面6個操作合并為:
1.一步操作之后,再將A[2,…,lenA]和B[1,…,lenB]變成相同字符串。
2.一步操作之后,再將A[1,…,lenA]和B[2,…,lenB]變成相同字符串。
3.一步操作之后,再將A[2,…,lenA]和B[2,…,lenB]變成相同字符串。
這樣,很快就可以完成一個遞歸程序。
代碼實現如下:
int calStringDis(string strA, int pABegin,int pAEnd,string strB, int pBBegin,int pBEnd) { if (pABegin > pAEnd) { if (pBBegin > pBEnd) return 0; else return pBEnd - pBBegin + 1; } if (pBBegin > pBEnd) { if(pABegin > pAEnd) return 0; else return pAEnd - pABegin + 1; } if (strA[pABegin] == strB[pBBegin]) { return calStringDis(strA,pABegin+1,pAEnd,strB,pBBegin+1,pBEnd); } else { int t1 = calStringDis(strA,pABegin+1,pAEnd,strB,pBBegin+2,pBEnd); int t2 = calStringDis(strA,pABegin+2,pAEnd,strB,pBBegin+1,pBEnd); int t3 = calStringDis(strA,pABegin+2,pAEnd,strB,pBBegin+2,pBEnd); return minValue(t1,t2,t3)+1; } }在遞歸的過程中,有些數據被重復計算了。
很經典的可使用動態規劃方法解決的題目,和計算兩字符串的最長公共子序列相似。
設Ai為字符串A(a1a2a3 … am)的前i個字符(即為a1,a2,a3 … ai)
設Bj為字符串B(b1b2b3 … bn)的前j個字符(即為b1,b2,b3 … bj)
設 L(i,j)為使兩個字符串和Ai和Bj相等的最小操作次數。
當ai==bj時 顯然 L(i,j) = L(i-1,j-1)
當ai!=bj時?
若將它們修改為相等,則對兩個字符串至少還要操作L(i-1,j-1)次
? 若刪除ai或在bj后添加ai,則對兩個字符串至少還要操作L(i-1,j)次
? 若刪除bj或在ai后添加bj,則對兩個字符串至少還要操作L(i,j-1)次
? 此時L(i,j) = min( L(i-1,j-1), L(i-1,j), L(i,j-1) ) + 1?
顯然,L(i,0)=i,L(0,j)=j, 再利用上述的遞推公式,可以直接計算出L(i,j)值。
代碼實現如下:
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];elsec[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; }19.N皇后問題
N皇后問題是一個以國際象棋為背景的問題:如何能夠在 NxN 的國際象棋棋盤上放置八個皇后,使得任何一個皇后都無法直接吃掉其他的皇后?為了達到此目的,任兩個皇后都不能處于同一條橫行、縱行或斜線上。
我們可以通過下面的圖標來展示回溯法的過程
從而更加有助于我們的理解
我們以4x4為例:
我們在試探的過程中,皇后的放置需要檢查他的位置是否和已經放置好的皇后發生沖突,為此需要以及檢查函數來檢查當前要放置皇后的位置,是不是和其他已經放置的皇后發生沖突
假設有兩個皇后被放置在(i,j)和(k,l)的位置上,明顯,當且僅當|i-k|=|j-l| 時,兩個皇后才在同一條對角線上。
(1)先從首位開始檢查,如果不能放置,接著檢查該行第二個位置,依次檢查下去,直到在該行找到一個可以放置一個皇后的地方,然后保存當前狀態,轉到下一行重復上述方法的檢索。
(2)如果檢查了該行所有的位置均不能放置一個皇后,說明上一行皇后放置的位置無法讓所有的皇后找到自己合適的位置,因此就要回溯到上一行,重新檢查該皇后位置后面的位置。
具體的實現代碼如下:
#define max 4 //sum用于描述解的可能的個數,每當輸出一次復合要求的位置 //sum的數量就會被+1 int queen[max], sum=0; /* max為棋盤最大坐標 */void show() /* 輸出所有皇后的坐標 */ {int i;printf("(");//i代表行數,queen[i]代表當前行元素所處的列數,//注意此處下標是從0開始的for(i = 0; i < max; i++){printf(" %d", queen[i]+1);}printf(")\n");//每次輸出一種解的時候,那么他的解的數量就會增加1sum++; }//此函數用于判斷皇后當前皇后是否可以放在此位置 int PLACE(int n) /* 檢查當前列能否放置皇后 */ {//queen[i] == queen[n]用于保證元素不能再同一列//abs(queen[i] - queen[n]) == abs(n - i)用于約束元素不能再同一行且不能再同一條斜線上int i;for(i = 0; i < n; i++) /* 檢查橫排和對角線上是否可以放置皇后 */{if(queen[i] == queen[n] || abs(queen[i] - queen[n]) == abs(n - i)){return 0;}}return 1; }//核心函數,回溯法的思想 void NQUEENS(int n) /* 回溯嘗試皇后位置,n為橫坐標 */ {int i;for(i = 0; i < max; i++){//首先將皇后放在第0列的位置,對于第一次來說是肯定成立的//所以第一次將皇后放在第0行0列的位置queen[n] = i; /* 將皇后擺到當前循環到的位置 */if(PLACE(n)){if(n == max - 1){show(); /* 如果全部擺好,則輸出所有皇后的坐標 */}else{NQUEENS(n + 1); /* 否則繼續擺放下一個皇后 */}}} }int main() {NQUEENS(0); /* 從橫坐標為0開始依次嘗試 */printf("\n");printf("總共的解法有%d種\n", sum);return 0; }習題練習推薦
- N皇后問題是一個經典的問題,在一個N*N的棋盤上放置N個皇后,每行一個并使其不能互相攻擊(同一行、同一列、同一斜線上的皇后都會自動攻擊),求出有多少種合法的放置方法。輸出N皇后問題所有不同的擺放情況個數。---九度OJ1254
- 將一堆正整數分為2組,要求2組的和相差最小.---51Nod 1007 參考題解在這里,關于01背包問題更多題目推薦參考這里
- 符號三角形的 第1行有n個由“+”和”-“組成的符號 ,以后每行符號比上行少1個,2個同號下面是”+“,2個異 號下面是”-“ 。計算有多少個不同的符號三角形,使其所含”+“ 和”-“ 的個數相同 。---hdu2510
- 給出兩個字符串,求出這樣的一個最長的公共子序列的長度:子序列中的每個字符都能在兩個原串中找到, 而且每個字符的先后順序和原串中的先后順序一致。---POJ1458
- 求出最長上升子序列的長度。---百練2757
- 從三角形頂部數字走,每次只能走到這個數字的左下角或者右下角的數字,直到底部,計算走過的線路的數字之和,求這個和的最大值。---POJ1163。
- 給出兩個串,分別為a,b,問a串在b串中出現了幾次?(其實位置不同,就算不同的串)。---hdu1686
更多題目推薦未來待續更新
參考文獻
- 動態規劃百度百科:https://baike.baidu.com/item/動態規劃/529408?fr=aladdin
- 回溯算法入門及經典案例剖析(初學者必備寶典):http://www.cnblogs.com/ECJTUACM-873284962/p/8447050.html#_nav_14
- 動態規劃:http://www.doc88.com/p-6408257243193.html
- 麻省理工學院公開課:算法導論 ---動態規劃,最長公共子序列:http://open.163.com/movie/2010/12/L/4/M6UTT5U0I_M6V2U1HL4.html
轉載于:https://www.cnblogs.com/ECJTUACM-873284962/p/8644075.html
總結
以上是生活随笔為你收集整理的浅谈我对动态规划的一点理解---大家准备好小板凳,我要开始吹牛皮了~~~的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安装Wincc Flexible 200
- 下一篇: 计算机等级考试湖北准考证查询,2016年