# 基礎動態規劃 ## 基本觀念 動態規劃(Dynamic Programming, DP) Dynamic的意思是動態的,而Programming則是以表格紀錄,所以DP其實就是以變動的表格來求解。 DP的應用很廣、變化很多,在程式競賽中常常遇到這類題目,而且DP有許多困難而精妙的(變態)優化方法,很多題目都需要花時間好好體會,所以此章只會先介紹一些典型的例題,後續的變化及優化方法將在進階動態規劃討論。 ## 想法 動態規劃和分治法相同,都是將問題分成子問題,再將子問題的解合併成最後的解,設計DP都是從尋找遞迴式開始,大致可分為三步驟。 1. 定義子問題。 2. 找出問題與子問題之間的遞迴的方式進行計算。 3. 找出子問題的計算順序來避免以遞迴的方式進行計算。 ## 例題 動態規劃其實和遞迴枚舉類似,但重要的是動態規劃以表格將算過的數據紀錄下來,藉此大幅優化執行時間,這裡舉個費式數列的例子: 如果依照遞迴的想法,可快速寫出下列程式: ```cpp= #include<iostream> using namespace std; long long fib(int n){ if(n<3)return 1; return fib(n-1)+fib(n-2); } int main(){ int n; cin>>n; cout<<fib(n)<<'\n'; return 0; } ``` 這程式可以跑,只要不輸入太大的數字,因為這遞迴函式會一個呼叫兩個,兩個呼叫四個,複雜度接近2的N次方。 但是我們真的需要這麼多的運算嗎? ```graphviz digraph FIB{ nodesep=0.1 "fib(5)"->{"fib(3) " "fib(4)"} "fib(3) "->{"fib(1)" "fib(2)"} "fib(4)"->{"fib(2) " " fib(3) "} " fib(3) "->{" fib(1) " " fib(2) "} } ``` 觀察上圖之例子可知,我們其實重複執行了需多運算,如果能將每次運算的值都處存下來,便可節省許多的運算時間,這時我們可以將程式改寫如下。 ```cpp= #include<iostream> using namespace std; long long dp[100005]; long long fib(int n){ if(dp[n]!=-1)return dp[n]; return dp[n]=fib(n-1)+fib(n-2); } int main(){ for(int i=0;i<100005;++i)dp[i]=-1; dp[1]=1,dp[2]=1; int n; cin>>n; cout<<fib(n)<<'\n'; return 0; } ``` 透過一個dp[]陣列將運算過後的值存起來,並以-1來表示此數值還未計算過,但透過觀察可發現第n項的值是由n-1項和n-2項所構成的,那麼就可以用一個for迴圈由小到大計算數值來求第n項的值,進而縮減成下列程式碼: ```cpp= #include<iostream> using namespace std; long long fib[100005]; int main(){ fib[1]=1,fib[2]=1; int n; cin>>n; for(int i=3;i<=n;++i) fib[i]=fib[i-1]+fib[i-2]; cout<<fib[n]<<'\n'; return 0; } ``` 那麼,在了解DP的基礎概念後,就能進入一系列的例題了。 ### 例題1 題目連結:[Frog1](https://atcoder.jp/contests/dp/tasks/dp_a) 思路:這題最主要的思路定義一狀態dp(i)為走到第i格時 程式碼: ### 例題2 題目連結:[Frog2](https://atcoder.jp/contests/dp/tasks/dp_b) 思路: 程式碼: ### 1D1D ### 2D1D ## 習題 ###### tags: `APCS與競賽入門`