動態規劃初識(從dfs到dfs優化到動態規劃順推和逆推)
思想:動態規劃是通過組合子問題來解決問題的,是用於求解包含重疊子問題的最優化問題的方法。
入門題目:數字三角形
題目描述:給出了一個數字三角形。從三角形的頂部到底部有很多條不同路徑。對於每條路徑,把路徑上面的數加起來可以得到一個和,你的任務就是找到最大的和。
注意:路徑上的每一步只能從一個數走到下一層上和它最近的左邊的那個數或者右邊的那個數。
如:
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
一、深度優先搜尋
此題可以從深度優先搜尋入手,深度搜索的概念簡單說就是竟可能深入一條路徑直到遇到障礙,返回上一步繼續下一路徑。因此要想深入,(
程式碼如下:
#include <iostream> using namespace std; int tri[410][410]; int N; static int m=0,l=0; int dfs(int x,int y) { m++; cout<<x<<","<<y<<" "; if(x>N || y>N) return 0; else { l++; return max(dfs(x+1,y),dfs(x+1,y+1))+tri[x][y]; } } int main() { cin>>N; memset(&tri,0,sizeof(tri)); for(int i=1;i<=N;i++) for(int j=1;j<=i;j++) cin>>tri[i][j]; int maxsum = dfs(1,1); cout<<maxsum<<" m: "<<m<<" l: "<<l<<endl; return 0; }
程式通過第一個點(1,1)出發,不斷向下遞迴找到最大和。程式中使用全域性變數m表示dfs函式呼叫次數,l表示max函式呼叫次數,並且通過cout得到dfs呼叫順序。
輸入輸出案例一:
分析:
dfs函式呼叫7次(由輸出可知max()函式內部為逗號運算子,先執行右邊部分),max呼叫三次:
路徑一:1,1 2,2 3,3 3,2 (先從1,1一直到3,3;然後回到2,2轉到3,2;最後max比較得到2,2)
路徑二:2,1 3,2 3,1 (2,2回到1,1然後轉到2,1;然後到3,2得到要比較的第一個值;然後回到2,1到3,1得到比較的第二個值,使用max進行比較得到2,1
最後再將2,1和2,2使用max函式進行比較得到最後的最大和為15。
輸入輸出案例二:
此輸入輸出案例分析和上面類似,但是我們可以發現重複計算的地方為4,3;3,2;4,2;相比上面案例重複計算量增加了,因此我們可以想象,當三角形足夠大時時間複雜度會相當高,此時我們需要做一些優化,將計算過的點通過陣列儲存起來,可以大大節省時間。
二、深度優先搜尋+記憶優化
通過將計算所得結果儲存於陣列,當需要計算x,y點的資料是首先判斷其res是否已經計算過,是則直接返回,否則繼續計算。
程式碼如下:
/*dfs+優化*/
#include <iostream>
using namespace std;
int tri[410][410];
int res[410][410];
int N;
static int m=0,l=0;
int dfs(int x,int y)
{
m++;
cout<<x<<","<<y<<" ";
if(res[x][y] != -1) return res[x][y];
if(x>N || y>N) return 0;
else
{
l++;
return res[x][y] = max(dfs(x+1,y),dfs(x+1,y+1))+tri[x][y];
}
}
int main()
{
cin>>N;
memset(&tri,0,sizeof(tri));
memset(&res,-1,sizeof(res));
for(int i=1;i<=N;i++)
for(int j=1;j<=i;j++)
cin>>tri[i][j];
int maxsum = dfs(1,1);
cout<<maxsum<<" m: "<<m<<" l: "<<l<<endl;
return 0;
}
輸入輸出案例一:
相比沒優化的dfs演算法,此時m只需要13次,而max只要6次。因此經過優化的dfs演算法要優於dfs演算法。
三、動態規劃順推
經過上面的dfs,我們可以知道要想得到最大路徑和只要res陣列最後一行中最大元素即為最大路徑和。而每一個點[x][y]的最大路徑和只可能來自於[x-1][y]或者[x-1][y-1]兩個地方。因此對於x,y點我們只需要考慮上一行兩個點大小即可,所以有:
res[x][y] = max(res[x-1][y],res[x-1][y-1])+tri[x][y]
所以我們只需要從第一個點開始逐步求得res[][]陣列,最後res[][]陣列最後一行最大值即為所求結果。
程式碼:
/**********dp正向計算**********/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int tri[410][410];
int res[410][410];
int main()
{
int n,i,j;
cin>>n;
memset(res,0,sizeof(res));
for(i=0;i<n;i++){
for(j=0;j<=i;j++)
cin>>tri[i][j];
}
res[0][0] = tri[0][0];
for(i=1;i<n;i++)
for(j=0;j<=i;j++)
res[i][j] = max(res[i-1][j],res[i-1][j-1])+tri[i][j];
//max_element為STL庫求最大元素的函式
cout<<*max_element(res[n-1],res[n-1]+n)<<endl;
return 0;
}
四、動態規劃逆推
上面介紹了順推,也可以使用逆推,從下到上,把最下面一層當做開始第一層,計算res[x][y]狀態陣列時,其值只可能來自於res[x+1][y]和res[x+1][y+1]兩個狀態,因此有如下狀態轉移方程:
res[x][y] = max(res[x+1][y],res[x+1][y+1])+tri[x][y]
到最後一個res的時候就是最大的路徑和。
程式碼:
/**********dp反向計算**********/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int tri[410][410];
int res[410][410];
int main()
{
int n,m,i,j;
cin>>n;
memset(res,0,sizeof(res));
for(i=0;i<n;i++){
for(j=0;j<=i;j++)
cin>>tri[i][j];
}
for(i=n-1;i>=0;i--)
for(j=0;j<=i;j++)
res[i][j]=tri[i][j]+max(res[i+1][j],res[i+1][j+1]);
cout<<res[0][0]<<endl;
return 0;
}
總結:dfs和dp還是有區別的,dfs主要是一種遍歷方法,而dp是一種思想方法,dp主要是將要求解的內容分解為小問題,然後通過狀態轉移得到結果;dfs只是深度優先遍歷各節點判斷結果。