本篇博客僅為對動態規划基礎問題的狀態轉移方程進行求解,然后給出對應的注釋代碼,有關題目的具體內容可在算法導論或網絡上進行查看
目錄
1.鋼管切割(最小值)
2.兩條流水線調度
3.多條流水線調度
4.最長上升子序列
5.矩陣鏈乘
6.OBST
內容
1.鋼管切割
實現解釋:
先設數組price[i]存儲着i長度鋼管切割后的最小值,p[i]存儲着i長度鋼管不切割的值,price數組既是本問題的dp數組。
經過分析可知狀態轉移方程為:
price[0] = 0;
price[i] = min(p[1]+price[i-1],p[2]+price[i-2],...p[i-1]+price[1],p[i]);
因為price[i]已經是當前情況下的最小值了,所以只需要遵循轉移方程進行代碼的完善即可。
若要計算最大值只需在計算前設置最大值為負數(保證最小),然后在判斷遞歸判斷小於即可。
坑點:
初始化和狀態轉移方程的書寫
完整代碼:
//鋼管切割最小值 #include<iostream> using namespace std; int main() { ios::sync_with_stdio(false); int length,MIN;//分別為長度和最小值 while(cin >> length) { int p[length+1]; for(int i = 1;i<=length;i++) cin >> p[i];//依據題意,i長度的價值 int price[length+1];//保存每段價值 price[0] = 0;//會使用到price[0],防止出錯初始化 for(int i = 1;i<=length;i++) { MIN = 2147483647;//獲得最小值時需要設為最大值 for(int j = 1;j<=i;j++) { if(MIN > p[j]+price[i-j]) //不斷將i長度切割為前j部分和后i-j部分 //以找到i長度切割后的最小值 { MIN = p[j] + price[i-j];//替換最小值 price[i] = MIN; } //狀態轉移方程介紹見實現解釋 } } cout << price[length] << '\n'; //輸出最長部分(i)切割后的最小值 } return 0; }
2.兩條流水線調度
實現解釋:
實現方法即得出狀態轉移方程后完善即可,設a[][i]存儲着第一二條線上各家的時間花費,t[][i]存儲着i處進行線路切換的花費,f[][i]存儲着各線在i處的最小花費。
則對每一個f[][i]應有如下的轉移方程:
f[0][1] = a[0][1];
f[1][1] = a[1][1];
f[0][i] = min(f[0][i-1]+a[0][i],f[1][i-1]+t[1][i-1]+a[0][i]);
f[1][i] = min(f[1][i-1]+a[1][i],f[0][i-1]+t[0][i-1]+a[1][i]);
前兩個為初始定義,后兩個為具體的轉換。
min函數中前一個值表示直接在當前這條線的前一個結點前進的花費,后一個表示從另一條線的前一個結點先轉移過來后再前進的花費。此時只有兩條線因此只有這兩種情況,所以比較后便可得出到達該線該位置的最小花費。
多條線的情況可參考:題解:說好的ALS呢?(后續添加鏈接)
最后比較到達n處(最后一個家)哪條線的時間花費最小即可。
坑點:
注意多次輸入后數組的初始化,以及狀態轉移方程的正確轉化即可
完整代碼:
//流水線調度基本問題 #include<iostream> using namespace std; int main() { ios::sync_with_stdio(false); int n; int fend; int i; while(cin >> n) { int a[2][n+1],t[2][n]; //兩條線上每一家花費的時間 for(i = 1;i<=n;i++) cin >> a[0][i]; for(i = 1;i<=n;i++) cin >> a[1][i]; //兩條線不同位置轉移的時間花費 for(i = 1;i<n;i++) cin >> t[0][i]; for(i = 1;i<n;i++) cin >> t[1][i]; //存儲兩條線不同位置的最小時間 int f[2][n+1]; //初始化防止錯誤 f[0][1] = a[0][1]; f[1][1] = a[1][1]; for(i = 2;i<=n;i++) { //具體狀態轉移方程介紹見實現解釋 if(f[0][i-1]+a[0][i]<f[1][i-1]+t[1][i-1]+a[0][i]) f[0][i] = f[0][i-1]+a[0][i]; else f[0][i] = f[1][i-1]+t[1][i-1]+a[0][i]; if(f[1][i-1]+a[1][i]<f[0][i-1]+t[0][i-1]+a[1][i]) f[1][i] = f[1][i-1]+a[1][i]; else f[1][i] = f[0][i-1]+t[0][i-1]+a[1][i]; } //計算兩條線分別的最后時間花費得最小值 if(f[0][n]<f[1][n]) fend = f[0][n]; else fend = f[1][n]; cout << fend << '\n'; } return 0; }
3.多條流水線調度
實現解釋:
相比兩條線的其實只是需要多判斷幾次
設a[i][j]存儲着第i條線上第j個位置的花費,t[i][j]存儲着第i條線到第j條線的調度費用,f[i][j]存儲着i條線在j處的最小花費。
則對每一個f[][i]應有如下的轉移方程:
第一個站台:f[i][1] = a[i][1];
其余站台:f[i][j] = min(f[i][j-1]+t[i][i]+a[i][j],f[1][j-1]+t[1][i]+a[i][j],...,f[n][j-1]+t[n][i]+a[i][j]);
注意站台間轉移時需要考慮自己調度到自己的情況(視題意而定)
min函數中前一個值表示直接在當前這條線的前一個結點前進的花費,后面則表示從其他線的前一個結點先轉移過來后再前進的花費,一次比較后便可得出到達當前線該位置的最小花費。
最后比較到達n處(最后一個站台)哪條線的時間花費最小即可。
坑點:
注意按照題意判斷是否要考慮自身的調度t[i][i]即可
完整代碼:
#include<iostream> #include<cstring> #include<algorithm> using namespace std; int main() { ios::sync_with_stdio(false); int num,n; int fend; int i,j; int tempf; while(cin >> num >> n)//分別為線路個數和站台個數 { int a[num][n+1],t[num][num]; for(i = 0;i<num;i++) for(j = 1;j<=n;j++) cin >> a[i][j];//i條線j站台的花費 for(i = 0;i<num;i++) for(j = 0;j<num;j++) cin >> t[i][j];//i條線到j條線的調度費用 int f[num][n+1];//一維為當前線路,二維為站台位置 for(i = 0;i<num;i++) { f[i][1] = a[i][1];//每條線第一個站台的最小花費就是第一個 } for(i = 2;i<=n;i++) { for(j = 0;j<num;j++) { tempf = 2147483647;//為了找到最小值先設定最大的 for(int k = 0;k<num;k++) {//循環判斷所有條道路的情況 if(tempf > f[k][i-1]+t[k][j]+a[j][i]) { tempf = f[k][i-1]+t[k][j]+a[j][i]; } } f[j][i] = tempf;//存儲j條線在i站台的最小值 } } tempf = f[0][n]; for(i = 1;i<num;i++) { if(tempf > f[i][n]) { tempf = f[i][n]; } } fend = tempf;//得到最大值 cout << fend << '\n'; } return 0; }
4.最長上升子序列
實現解釋:
這里介紹的其實是優化后的方案,即只存儲長度,而不是以dp[i][j]的形式存儲ij之間的最長長度,不過也是很好理解的,所以就直接分享這一篇吧。
a[i]存儲總序列的內容,dp[i]表示以i為結尾的最長子序列長度
那么首先由dp[i]開始一定是1(自己是一個序列)
后面的狀態轉移方程即:
dp[i] = max(dp[j])+1(j<i&&a[j]<a[i])
解釋:由於是上升序列,而且dp[i]是以i結尾的最長長度,因此長度增加時有兩個條件:新的數字在舊數字的后面,新的數字大於舊的數字(新數字:dp[i],舊數字:dp[j]),也是唯一的轉移方程。
坑點:
注意最后獲取最長序列時,不能直接dp[n]輸出,因為可能是在序列中間有最長的上升子序列,也需要循環判斷。
完整代碼:
#include<iostream> using namespace std; int main() { int n; cin >> n; int a[n],dp[n]; for(int i = 0;i<n;i++) { cin >> a[i]; dp[i] = 1;//自身一定是一個長度的序列 } for(int i = 1;i<n;i++) { for(int j = 0;j<i;j++) { if(a[i] > a[j])//因為是上升,所以需要只有比前面的值大才可能形成最長上升子序列 { if(dp[i] < dp[j]+1) dp[i] = dp[j] + 1;//記錄i處最長的上升序列長度 //即前面的序列長度最大長度+1即是i處的最大長度 } } } int max = dp[0];//不一定最后一個是最長的,因此需要獲取最大值 for(int i = 1;i<n;i++) { if(max < dp[i]) max = dp[i]; } cout << max << '\n'; return 0; }
5.矩陣鏈乘
實現解釋:
數據介紹:m[0]存儲第一個矩陣的行數,m[i]存儲第i個矩陣的列數和第i+1個矩陣的行數
num[i][j]記錄第i個矩陣和第j個矩陣之間的最小計算次數
cut僅是記錄切割位置所用cut[i][j]表示第ij個矩陣之間的最優切割位置
則按照矩陣乘法的知識,兩個矩陣相乘的計算次數便可直接由以下的狀態轉移方程得到:
i=j時num[i][j] = 0
i<j時num[i][j] = min(num[i][k]+num[k+1][j]+m[i-1]*m[k]*m[j])(k為[i,j)間的值,即表示以第i個矩陣到j-1個矩陣之間第k個矩陣作為分割處)
解釋:i=j自然不用計算,第二個既是當k作為分割點時總次數等於i到k個矩陣的相乘最小次數+第k+1個矩陣到第j個矩陣的相乘最小次數+合並時的產生的新次數(最左行數*分割點列數*最右行數)
然后便是正常的翻譯了。
其中對於分割方案的輸出,只需按照矩陣范圍依據cut數組獲取切割點輸出即可,具體可參考代碼。
坑點:
循環范圍的設定,注意按是否能到達設定范圍
完整代碼:
#include<iostream> using namespace std; int length; int cut[2000][2000]; int count; void printCut(int i,int j) { if(i == j) cout << 'A' << i; else { cout << '('; printCut(i,cut[i][j]); printCut(cut[i][j]+1,j); count++; cout << ')'; } } int main() { ios::sync_with_stdio(false); int n; cin >> n; length = n; int m[n+1]; for(int i = 0;i<=n;i++) cin >> m[i]; int num[n+1][n+1];//對應矩陣個數即可 for(int i = 1;i<=n;i++) num[i][i] = 0; int temp; count = 0; for(int l = 2;l<=n;l++) //l為1的情況不必討論,因為就是矩陣自己,所以從2到n依次進行 { for(int i = 1;i<=n-l+1;i++) //從第一個矩陣一直到最后一個可到達的矩陣 //這樣i表示的既是連續l個矩陣的第一個矩陣 //根據輸入,第i個矩陣的行數:i-1的值,列數:i的值 { int j = i+l-1;//當前l個矩陣的最后一個矩陣下標 num[i][j] = 2147483647;//用於比較 for(int k = i;k<=j-1;k++) //k是節點,代表第k和k+1個矩陣之間的那個下標 //所以需要到達j-1,也就是第j個矩陣前的那個位置才算結束 //由位置可知k的值為第k個矩陣的列數即划分開時需要乘的值 { temp = num[i][k]+num[k+1][j]+m[i-1]*m[k]*m[j]; //當前划分狀態的計算次數,兩個num分別為兩側矩陣鏈的次數,最后一項為合並后的新增次數 if(num[i][j]>temp) { num[i][j] = temp; cut[i][j] = k; } } } } printCut(1,n); cout << '\n'; cout << count << '\n';//切割次數 cout << num[1][n] << '\n';//計算次數 return 0; }
6.OBST
實現解釋:
實際等價於矩陣鏈乘,狀態轉移方程都是將左側和右側的最優相加后再加上合並新增的次數即可。
新增的次數對矩陣鏈乘來說就是兩個最優部分相乘的次數
對OBST來說則是左右子樹本來的搜索代價+左右子樹深度加一產生的新代價+此時根節點的搜索代價(其中深度加一的新代價和根節點的搜索代價便可合並為此時所有節點的檢索頻率之和)
解釋:深度加一時左右子樹節點的搜索代價都增加了一個結點,因此他們的代價分別增加了一次搜索概率(自己被搜索到的概率),而根節點的搜索代價即本身的搜索概率,相加既是范圍內所有結點的搜索概率和。
於是數據記錄如下:
e[i][j]存儲i到j個結點之間的最小搜索代價
w[i][j]存儲i到j個結點的總搜索概率之和(用於狀態轉移快捷增加搜索概率和)
root[i][j]存儲i到j個結點中有最小搜索代價時的根節點的腳標
狀態轉移方程即:
j = i-1時e[i][j] = 0(不存在根節點,因此也沒有搜索代價)
j >= i時e[i][j] = min(e[i][r-1]+e[r+1][j]+w[i][j])(r即某次選擇作為根節點的腳標)
對建樹方案的實現和矩陣鏈乘的實現同理,只是由於偽關鍵字的加入需要進行一些特殊處理,即如果某個方向沒有子樹則需要手動添加偽關鍵字d形成的結點,其余子樹的輸出只需做好根節點和左右的判斷即可,具體可參考代碼,方案不唯一。
坑點:
注意dp數組的初始化,初始化既是對沒檢索到情況的初始化(因此添加的值只是偽關鍵字的搜索概率)
主要難點即理解搜索子樹擴增時平均搜索的變化
完整代碼:
#include<iostream> using namespace std; const int MAX = 2147483647; double **root; int di; int n; void printOBST(int l,int r) { if(l == 1&&r == n) { di = 0; cout << "k" << root[1][n] << "為根" << '\n'; } int t,lt,rt; t = root[l][r]; if(l == t) cout << "d"<<di++ << "是k"<<t << "的左孩子" << '\n'; else if(l < t) { lt = root[l][t-1]; cout << "k"<<lt << "是k"<<t << "的左孩子" << '\n'; printOBST(l,t-1); } else return ; if(r == t) cout << "d"<<di++ << "是k"<<t << "的右孩子" << '\n'; else if(r > t) { rt = root[t+1][r]; cout << "k"<<rt << "是k"<<t << "的右孩子" << '\n'; printOBST(t+1,r); } else return ; } int main() { int maxi; double temp; while(cin >> n) { double p[n+1],q[n+1]; for(int i = 1;i<=n;i++) cin >> p[i]; for(int i = 0;i<=n;i++) cin >> q[i]; double e[n+2][n+1];//需要存儲q的內容,因此n+2 double w[n+2][n+1];//同上 root = new double *[n+1];//只是在p中選root因此n+1即可 for(int i = 0;i<=n;i++) root[i] = new double[n+1]; for(int i = 1;i<=n+1;i++) { e[i][i-1] = q[i-1]; w[i][i-1] = q[i-1]; } for(int l = 1;l<=n;l++) { for(int i = 1;i<=n-l+1;i++) { maxi = i+l-1; //i+l-1就是長度l時能到達的最大的數據下標 e[i][maxi] = MAX; w[i][maxi] = w[i][maxi-1]+p[maxi]+q[maxi]; //i--j的總概率和即等於i--j-1的加上新增的maxi處的k和d的概率 for(int j = i;j<=maxi;j++) { temp = e[i][j-1]+e[j+1][maxi]+w[i][maxi]; if(temp < e[i][maxi]) { e[i][maxi] = temp; root[i][maxi] = j; } } } } cout << "最小搜索期望為:" << e[1][n] << '\n'; cout << "樹的形狀如下:" << '\n'; printOBST(1,n); } return 0; }
