c++矩阵连乘的动态规划算法并输出_算法面试必修课,动态规划基础题型归纳(三)
動態規劃(Dynamic Programming,簡稱DP),是大家都覺得比較難以掌握的算法。為了應付面試,我們經常會背誦一下DP問題的源碼,其實,只要理解了思想,掌握基本的模型,然后再來點寫代碼的套路,動態規劃并沒有那么難。
先來回顧一下題型目錄。
動態規劃基本題型總結提綱目錄
1.硬幣找零,路徑規劃
2.字符串相似度/編輯距離(edit distance)
3.最長公共子序列(Longest Common Subsequence,lcs)
4.最長遞增子序列(Longest Increasing Subsequence,lis)
5.最大子序列積
6.矩陣鏈乘法
7.0-1背包問題
8.有代價的最短路徑
9.瓷磚覆蓋(狀態壓縮DP)
10.工作量劃分
11.三路取蘋果
上篇文章講到第五個題型,最大子序列積,這篇從第六個題型開始。
6.矩陣鏈乘法
先看矩陣乘法
從定義可以看出:只有當矩陣A的列數與矩陣B的行數相等時A×B才有意義。一個m×r的矩陣A左乘一個r×n的矩陣B,會得到一個m×n的矩陣C。在計算機中,一個矩陣說穿了就是一個二維數組。一個m行r列的矩陣可以乘以一個r行n列的矩陣,得到的結果是一個m行n列的矩陣,其中的第i行第j列位置上的數等于前一個矩陣第i行上的r個數與后一個矩陣第j列上的r個數對應相乘后所有r個乘積的和。
所謂矩陣鏈乘法是指當一些矩陣相乘時,如何加括號來改變乘法順序從而來降低乘法次數。例如有三個矩陣連乘:A1*A2*A3,其維數分別為:10*100,100*5,5*50.如果按照((A1*A2)A3)來計算的話,求(A1*A2)要10*100*5=5000次乘法,再乘以A3需要10*5*50=2500次乘法,因此總共需要7500次乘法。如果按照(A1(A2*A3))來計算的話,求(A2*A3)要100*5*50=25000次乘法,再乘以A1需要10*100*50=50000次乘法,因此總共需要75000次乘法??梢?#xff0c;按不同的順序計算,代價相差很大。
比如對于這個M?M?M?的矩陣鏈,
我們可以先計算M?M?然后結果乘以M?,也可以M?M?先算,然后乘以M?,為了表達方便,可以用括號表示計算順序。 矩陣鏈M?M?M?有兩種計算順序:((M?M?)M?)和(M?(M?M?))。 那么不同計算順序有什么區別? 對于((M?M?)M?):
對于(M?(M?M?)):
我們要做的就是找到讓乘法運算最少的計算順序,換言之就是找一種加括號方式,使得最后乘法運算最少。
矩陣鏈乘法問題可以表述如下:給定n個矩陣構成的一個鏈(A1*A2*A3……*An),其中i=1,2,……n,矩陣Ai的維數為p(i-1)*p(i),對于乘積A1*A2*A3……*An以一種最小化標量乘法次數的方式進行加括號。
這個問題太難理解了。
為了代入問題,先來樸素算法,這個總能看懂吧。
void mult(int a[MAXN][MAXN],int b[MAXN][MAXN],int c[MAXN][MAXN],int p,int q,int r) { int i,j,k; //先對c進行初始化 for(i=0;i<p;i++) { for(j=0;j<r;j++) { c[i][j] = 0; } } //計算矩陣乘法 for(i=0;i<p;i++) { for(j=0;j<r;j++) { for(k=0;k<q;k++) { c[i][j] += a[i][k] * b[k][j]; } } } }解決這個問題,我們可以用窮舉法,但是n很大時,這不是個好方法,其時間復雜度為指數形式。拿上面的例子來說,加括號后把矩陣鏈分成了兩部分,計算代價為兩者代價的和。因此假設這種方法的代價最少,則兩個部分的代價也是最小的,如果不是最小的,那么這種方法就不是最優的,因此矩陣鏈乘法具有最優子結構。因此我們可以利用子問題的最優解來構造原問題的一個最優解。所以,可以把問題分割為兩個子問題(A1*A2*A3……*Ak和A(k+1)*A(k+2)*A(k+3)……*An),需找子問題的最優解,然后合并這些問題的最優解。從下面的程序可以看出,其時間復雜度為n*n*n.
題目分析:
遞推關系:
設計算A[i:j],1≤i≤j≤n,所需要的最少數乘次數m[i,j],則原問題的最優值為m[1,n]。
當i=j時,A[i:j]=Ai,因此,m[i][i]=0,i=1,2,…,n
當i<j時,若A[i:j]的最優次序在Ak和Ak+1之間斷開,i<=k<j,則:m[i][j]=m[i][k]+m[k+1][j]+pi-1pkpj。由于在計算是并不知道斷開點k的位置,所以k還未定。不過k的位置只有j-i個可能。因此,k是這j-i個位置使計算量達到最小的那個位置。
綜上,有遞推關系如下:
構造最優解:
若將對應m[i][j]的斷開位置k記為s[i][j],在計算出最優值m[i][j]后,可遞歸地由s[i][j]構造出相應的最優解。s[i][j]中的數表明,計算矩陣鏈A[i:j]的最佳方式應在矩陣Ak和Ak+1之間斷開,即最優的加括號方式應為(A[i:k])(A[k+1:j)。因此,從s[1][n]記錄的信息可知計算A[1:n]的最優加括號方式為(A[1:s[1][n]])(A[s[1][n]+1:n]),進一步遞推,A[1:s[1][n]]的最優加括號方式為(A[1:s[1][s[1][n]]])(A[s[1][s[1][n]]+1:s[1][s[1][n]]])。同理可以確定A[s[1][n]+1:n]的最優加括號方式在s[s[1][n]+1][n]處斷開…照此遞推下去,最終可以確定A[1:n]的最優完全加括號方式,及構造出問題的一個最優解。
動態規劃迭代實現
用動態規劃迭代方式解決此問題,可依據其遞歸式自底向上的方式進行計算。在計算過程中,保存已解決的子問題的答案。每個子問題只計算一次,而在后面需要時只需簡單檢查一下,從而避免了大量的重復計算,最終得到多項式時間的算法。
代碼如下:
//3d1-2 矩陣連乘 動態規劃迭代實現 //A1 30*35 A2 35*15 A3 15*5 A4 5*10 A5 10*20 A6 20*25 //p[0-6]={30,35,15,5,10,20,25} #include "stdafx.h" #include <iostream> using namespace std; const int L = 7; int MatrixChain(int n,int **m,int **s,int *p); void Traceback(int i,int j,int **s);//構造最優解 int main() { int p[L]={30,35,15,5,10,20,25}; int **s = new int *[L]; int **m = new int *[L]; for(int i=0;i<L;i++) { s[i] = new int[L]; m[i] = new int[L]; } cout<<"矩陣的最少計算次數為:"<<MatrixChain(6,m,s,p)<<endl; cout<<"矩陣最優計算次序為:"<<endl; Traceback(1,6,s); return 0; } int MatrixChain(int n,int **m,int **s,int *p) { for(int i=1; i<=n; i++) { m[i][i] = 0; } for(int r=2; r<=n; r++) //r為當前計算的鏈長(子問題規模) { for(int i=1; i<=n-r+1; i++)//n-r+1為最后一個r鏈的前邊界 { int j = i+r-1;//計算前邊界為r,鏈長為r的鏈的后邊界 m[i][j] = m[i+1][j] + p[i-1]*p[i]*p[j];//將鏈ij劃分為A(i) * ( A[i+1:j] ) s[i][j] = i; for(int k=i+1; k<j; k++) { //將鏈ij劃分為( A[i:k] )* (A[k+1:j]) int t = m[i][k] + m[k+1][j] + p[i-1]*p[k]*p[j]; if(t<m[i][j]) { m[i][j] = t; s[i][j] = k; } } } } return m[1][L-1]; } void Traceback(int i,int j,int **s) { if(i==j) return; Traceback(i,s[i][j],s); Traceback(s[i][j]+1,j,s); cout<<"Multiply A"<<i<<","<<s[i][j]; cout<<" and A"<<(s[i][j]+1)<<","<<j<<endl; }7.0-1背包問題
題目描述:
有n個物品,它們有各自的體積和價值,現有給定容量的背包,如何讓背包里裝入的物品具有最大的價值總和?
為方便講解和理解,下面講述的例子均先用具體的數字代入,即:eg:number=4,capacity=8
有n個物品,每個物品有兩個屬性:一個是體積,一個是價值 ,可以表示為:(w1,v1),(w1,v1),…,(w1,vn) {(w_1,v_1 ),(w_1,v_1 ),…,(w_1,v_n )} )。同時我們還有一背包,背包的容量用W表示。現在我們將物品放入背包,放入的物品體積的總和不得超過背包的體積。
問:這個背包能裝下的最大價值。
題目分析:
?、?、將原問題分解為子問題(子問題和原問題形式相同,且子問題解求出就會被保存);
?、?、確定狀態:01背包中一個狀態就是NN個物體中第ii個是否放入體積為VV背包中;
?、?、確定一些初始狀態(邊界狀態)的值;
?、堋⒋_定狀態轉移方程,如何從一個或多個已知狀態求出另一個未知狀態的值。(遞推型)
思路:
①、確認子問題和狀態
01背包問題需要求解的就是,為了體積V的背包中物體總價值最大化,NN件物品中第ii件應該放入背包中嗎?(其中每個物品最多只能放一件)
為此,我們定義一個二維數組,其中每個元素代表一個狀態,即前ii個物體中若干個放入體積為VV背包中最大價值。數組為:f[N][V]f[N][V],其中fijfij表示前ii件中若干個物品放入體積為jj的背包中的最大價值。
②、初始狀態
初始狀態為f[0][0?V]f[0][0?V]和f[0?N][0]f[0?N][0]都為0,前者表示前0個物品(也就是空物品)無論裝入多大的包中總價值都為0,后者表示體積為0的背包啥價值的物品都裝不進去。
③、轉移函數
if (背包體積j小于物品i的體積)f[i][j] = f[i-1][j] //背包裝不下第i個物體,目前只能靠前i-1個物體裝包 elsef[i][j] = max(f[i-1][j], f[i-1][j-Vi] + Wi)最后一句的意思就是根據“為了體積V的背包中物體總價值最大化,NN件物品中第ii件應該放入背包中嗎?”轉化而來的。ViVi表示第ii件物體的體積,WiWi表示第ii件物品的價值。這樣f[i-1][j]代表的就是不將這件物品放入背包,而f[i-1][j-Vi] + Wi則是代表將第i件放入背包之后的總價值,比較兩者的價值,得出最大的價值存入現在的背包之中。
01背包問題其實就可以化簡為涂寫下面的表格,其中每個數對應數組nArr中每個元素,初始化部分為0,然后從左上角按行求解,一直求解到右下角獲取最終解nArr[5][12]。
代碼如下:
#include<iostream> using namespace std;int main() {int nArr[6][13] = {{0}};int nCost[6] = {0 , 2 , 5 , 3 , 10 , 4}; //花費int nVol[6] = {0 , 1 , 3 , 2 , 6 , 2}; //物體體積int bagV = 12;for( int i = 1; i< sizeof(nCost)/sizeof(int); i++){for( int j = 1; j<=bagV; j++){if(j<nVol[i])nArr[i][j] = nArr[i-1][j];elsenArr[i][j] = max(nArr[i-1][j] , nArr[i-1][j-nVol[i]] + nCost[i]); cout<<nArr[i][j]<<' ';} cout<<endl;}cout<<nArr[5][12]<<endl;return 0; }再來看另外一個課件上的解釋:
背包問題是動態規劃里面比較出名的題型,其變種題目很多,由于篇幅有限就不一一展開了,下面只列出背包問題的題型目錄。
一、01背包問題
這是最基本的背包問題,每個物品最多只能放一次。
二、完全背包問題
第二個基本的背包問題模型,每種物品可以放無限多次。
三、 多重背包問題
每種物品有一個固定的次數上限。
四、 混合三種背包問題
將前面三種簡單的問題疊加成較復雜的問題。
五、二維費用的背包問題
一個簡單的常見擴展。
六、分組的背包問題
一種題目類型,也是一個有用的模型。后兩節的基礎。
七、 有依賴的背包問題
另一種給物品的選取加上限制的方法。
八 、泛化物品
九、 背包問題問法的變化
試圖觸類旁通、舉一反三。
8.有代價的最短路徑
從某頂點出發,沿圖的邊到達另一頂點所經過的路徑中,各邊上權值之和最小的一條路徑叫做最短路徑。解決最短路的問題有以下算法,Dijkstra算法,Bellman-Ford算法,Floyd算法和SPFA算法等。
現在只討論Dijkstra算法。
1。指定一個節點,例如我們要計算 'A' 到其他節點的最短路徑
2.引入兩個集合(S、U),S集合包含已求出的最短路徑的點(以及相應的最短長度),U集合包含未求出最短路徑的點(以及A到該點的路徑,注意 如上圖所示,A->C由于沒有直接相連 初始時為∞)
3.初始化兩個集合,S集合初始時 只有當前要計算的節點,A->A = 0,
U集合初始時為 A->B = 4, A->C = ∞, A->D = 2, A->E = ∞,敲黑板!!!接下來要進行核心兩步驟了
4.從U集合中找出路徑最短的點,加入S集合,例如 A->D = 2
5.更新U集合路徑,if ( 'D 到 B,C,E 的距離' + 'AD 距離' < 'A 到 B,C,E 的距離' ) 則更新U
循環執行 4、5 兩步驟,直至遍歷結束,得到A 到其他節點的最短路徑
比如現在求A到E的最短加權路徑,路徑上的數值為權重。
1.選定A節點并初始化,如上述步驟3所示
2.執行上述 4、5兩步驟,找出U集合中路徑最短的節點D 加入S集合,并根據條件if ( 'D 到 B,C,E 的距離' + 'AD 距離' < 'A 到 B,C,E 的距離' )來更新U集合。
3.這時候A->B, A->C都為3,沒關系。其實這時候他倆都是最短距離,如果從算法邏輯來講的話,會先取到B點。而這個時候 if 條件變成了if ( 'B 到 C,E 的距離' + 'AB 距離' < 'A 到 C,E 的距離' ),如圖所示這時候A->B距離 其實為A->D->B
4.思路就是這樣,往后就是大同小異了
5.算法結束
大致步驟:
清除所有點的標號 全部d[i] = INF 然后將圖信息權值復制到d中 循環n次{在所有為標號的結點中,選出d值最小的結點x給x標記對于從x出發的所有邊(x,y),更新d[y] = min{d[y],d[y]+w(x,y)}代碼如下:
#include <bits/stdc++.h> using namespace std; const int maxn = 500; const int INF = 0x3f3f3f3f; int n,m,dis[maxn],Map[maxn][maxn],v[maxn]; void dijkstra(int u) {memset(v,0,sizeof v);memset(dis,INF,sizeof dis);v[u] = 1;for(int i = 1; i <= n; i++)dis[i] = min(dis[i],Map[u][i]);for(int i = 1; i <= n; i++){int x,m = INF;for(int y = 1; y <= n; y++)if(!v[y] && dis[y] <= m)m = dis[x = y];v[x] = 1;for(int y = 1; y <= n; y++)dis[y] = min(dis[y],dis[x]+Map[x][y] );} } int main() {int a,u,v,w;scanf("%d%d%d",&n,&m,&a);memset(Map, INF, sizeof Map);for(int i = 0; i < m; i++){scanf("%d%d%d",&u,&v,&w);Map[u][v] = w;Map[v][u] = w; //該圖為無向圖,若為有向圖則需要少建立一個邊信息}for(int i = 1; i <= n; i++)Map[i][i] = 0;dijkstra(a);for(int i = 1; i <= n; i++)cout<<dis[i]<<" ";return 0; }動態規劃還有最后幾個題型,下篇會都說完。再后面我會分享貪心算法、回溯算法、分治算法,排序算法。歡迎一起學習。
總結
以上是生活随笔為你收集整理的c++矩阵连乘的动态规划算法并输出_算法面试必修课,动态规划基础题型归纳(三)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: git idea 图形化_Git大全,你
- 下一篇: python中反斜杠_Python中的正