今天在leetcode上做了幾個簡單的動態規劃的題目,也算是對動態規劃有個基本的了解了。現在對動態規劃這個演算法做乙個簡單的總結。
動態規劃英文 dynamic programming,是求解決策過程最優化的數學方法,後來沿用到了程式設計領域。
動態規劃的大致思路是把乙個複雜的問題轉化成乙個分階段逐步遞推的過程,從簡單的初始狀態一步一步遞推,最終得到複雜問題的最優解。
動態規劃解決問題的過程分為兩步:
尋找狀態轉移方程
利用狀態轉移方程式自底向上求解問題
《漫畫:什麼是動態規劃?(整合版)》
話不多說,直接看看題目。
題目描述
假設你正在爬樓梯。需要 n 階你才能到達樓頂。
每次你可以爬 1 或 2 個台階。你有多少種不同的方法可以爬到樓頂呢?
注意:給定 n 是乙個正整數。
示例 1:
輸入: 2
輸出: 2
解釋: 有兩種方法可以爬到樓頂。
1. 1 階 + 1 階
2. 2 階
示例 2:
輸入: 3
輸出: 3
解釋: 有三種方法可以爬到樓頂。
1. 1 階 + 1 階 + 1 階
2. 1 階 + 2 階
3. 2 階 + 1 階
爬樓梯問題是動態規劃演算法中非常經典的一道題目,出場率十分高。現在我嘗試循序漸進地把這個問題講清楚。
思路我們設定乙個函式f(n)來表示走到第n級台階走法的數量,現在假設有10級台階。現在就會出現兩種情況:
我們是從第9級,跨1級上來,到第10級
我們是從第8級,跨2級上來,到第10級
其實對於任何第n級台階,都會出現這兩種情況,即第n級的前一步是走了1級或者兩級。
所以如果我們統計f(10)的話,可以發現f(10) = f(9) + f(8),即到第10級的走法等於到第9級的走法加上到第8級的走法。同理可得,f(9) = f(8) + f(7),f(8) = f(7) + f(6)等等等等……
所以我們就得到了動態規劃步驟1中的所說的所謂的狀態轉移方程:f(n) = f(n-1) + f(n-2).
一直到最底層,當只有1級台階時,f(1) = 1;當只有2級台階時f(2) = 2.
到這裡,直覺告訴我們可以用遞迴來解決這個問題。
遞迴法
public int climbstairs (int n)
if (n == 1)
if (n == 2)
return climbstairs(n - 1) + climbstairs(n - 2);
}
但是遞迴法有個問題,時間複雜度比較高。我們可以看一下下圖:
遞迴的過程可以構造出一棵二叉樹,可以看出求解f(n)過程中,會訪問\(2^n\)次f()函式,即時間複雜度為\(o(2^n)\).並且,遞迴的過程中包含著大量的重複操作,二叉樹越往下走,重複操作越多,上圖中相同顏色標出的節點就是表示重複的操作。
那怎麼解決這個問題呢?現在我們就要搬出動態規劃的步驟2了,採用自底向上的方法求解問題。
剛才的遞迴法,我們是從第10級台階往下,計算f(9)和f(8),再計算f(9)需要的f(8)和f(7),以及f(8)需要的f(7)和f(6),依次往下,體現在二叉樹上,就是從最頂上的節點往下構造出這棵二叉樹。
現在我們轉化思路,自底向下構造。我們現在已經知道了f(1)=1和f(2)=2,所以我們可以知道f(3) = f(2) + f(1) = 3,進一步地,我們可以知道f(4) = f(3) + f(2) = 5,等等等等……
按照這個方法,我們可以設定乙個陣列,依次往裡面填數就可以了。時間複雜度為\(o(n)\)。
動態規劃法
public int climbstairs (int n) // 防止陣列越界
int step = new int[n + 1];
step[1] = 1;
step[2] = 2;
for (int i = 3; i <= n; i++)
return step[n];
}
題目描述
給定乙個整數陣列nums
,找到乙個具有最大和的連續子陣列(子陣列最少包含乙個元素),返回其最大和。
示例:
輸入: [-2,1,-3,4,-1,2,1,-5,4],
輸出: 6
解釋: 連續子陣列 [4,-1,2,1] 的和最大,為 6。
思路
廢話不多說,我們直接走動態規劃的流程。第一步就是尋找狀態轉移方程。
其實這個狀態轉移方程有點像高中數學裡面的數列的通項公式,數列的通項公式可以通過各種各樣的方法找出來,什麼規律法、累加累乘什麼的,我們這裡找動態規劃的狀態轉移方程就比較類似於規律法找通項公式,這個通項公式就是第n項與前若干項之間的關係。
我們看這個題目,我們遍歷一遍陣列,假如我們現在正站在第i個元素,如何通過第i個元素的值和前面若干個元素的值來找到所謂的最大子序和呢?
最大子序和,我們當然是想讓乙個子序中正數越多越好,負數越少越好。所以假如我們現在有乙個子序,它是和最大子序的候選人,我們就希望這個子序的後面的元素是正數,從而可以繼續增加這個子序的和。換位思考一下,現在我們是乙個元素,前面有乙個子序,我們就希望前面這個子序的和是正的,我加入這個子序不就抱了大腿嗎,要是前面這個子序的和是負的,那完了,我加入前面的子序還要自損一部分功力,還不如單幹呢,我自己就當乙個子序。
前面的解釋,自我感覺還是比較形象的,現在讓這個解釋與動態規劃的程式設計實現結合起來。
我們定義乙個陣列dp
,dp[i]
是以第i個元素為結尾的一段最大子序和。求dp[i]
時,假設前面dp[0]
~dp[i-1]
都已經求出來了,dp[i-1]
表示的是以i-1
為結尾的最大子序和,若dp[i-1]
小於0,則dp[i]
加上前面的任意長度的序列和都會小於不加前面的序列(即自己本身乙個元素是以自己為結尾的最大自序和)。
所以狀態轉移方程相當於是乙個判斷函式。
if (dp[i - 1] > 0) else
第二步是利用狀態轉移方程自底向上求解,這和上一道題目類似,按照順序往陣列裡面填值。
原始碼
public int maxsubarray (int nums) else
max = math.max(dp[i],max);
}return max;
}
題目描述
你是乙個專業的小偷,計畫偷竊沿街的房屋。每間房內都藏有一定的現金,影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統,如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。
給定乙個代表每個房屋存放金額的非負整數陣列,計算你在不觸動警報裝置的情況下,能夠偷竊到的最高金額。
示例 1:
輸入: [1,2,3,1]
輸出: 4
解釋: 偷竊 1 號房屋 (金額 = 1) ,然後偷竊 3 號房屋 (金額 = 3)。
偷竊到的最高金額 = 1 + 3 = 4 。
示例 2:
輸入: [2,7,9,3,1]
輸出: 12
解釋: 偷竊 1 號房屋 (金額 = 2), 偷竊 3 號房屋 (金額 = 9),接著偷竊 5 號房屋 (金額 = 1)。
偷竊到的最高金額 = 2 + 9 + 1 = 12 。
思路
廢話不多說,我們直接走動態規劃的流程。第一步就是尋找狀態轉移方程。
再重複一遍,狀態轉移矩陣是第n項與前若干項之間的關係。
現在我們是乙個小偷,站在第i家的屋頂,我們是偷,還是不偷呢?這是個問題。
rob(i) = math.max( rob(i - 2) + currenthousevalue, rob(i - 1) )
第二步是利用狀態轉移矩陣自底向上求解問題。
我們定義乙個陣列dp
,dp[i]
是以第i個元素為結尾的偷竊到的最大金額。求dp[i]
時,假設前面dp[0]
~dp[i-1]
都已經求出來了。
原始碼
public int rob(int nums)
return dp[nums.length];
}
在利用動態規劃求解問題的過程中,比較難的是找到狀態轉移方程,之前多次提到,狀態轉移方程是第n項與前若干項之間的關係。這是我個人的一點理解,求動態規劃的第i項時可以假設前面的若干項都是已知的了。比如第一題爬樓梯,就是當前項和前兩項的關係,最大子序和是當前項取決於前一項的正負,打家劫舍也是看當前項和前兩項的關係。
找到這種關係後,需要轉化思路,自底向上編寫程式,這樣才能降低時間複雜度,才是真正的動態規劃。
Leetcode初級演算法
不是很難的一道動態規劃的題,感覺做多了就記住了。class solution return dp n 此題想法就是,只要後面買的減去前面買的能大於0,就算在內,每次買完和max比較,大於max就記錄為max,如果買的sum小於0了,重新開始買,sum記為0 class solution if sum...
初級演算法探索 陣列篇(一)
問題 從排序陣列中刪除重複項 給定乙個排序陣列,你需要在原地刪除重複出現的元素,使得每個元素只出現一次,返回移除後陣列的新長度。不要使用額外的陣列空間,你必須在原地修改輸入陣列並在使用 o 1 額外空間的條件下完成。示例 1 給定陣列 nums 1,1,2 函式應該返回新的長度 2,並且原陣列 nu...
初級演算法探索 陣列篇(九)
問題 兩數之和 給定乙個整數陣列和乙個目標值,找出陣列中和為目標值的兩個數。你可以假設每個輸入只對應一種答案,且同樣的元素不能被重複利用。示例 給定 nums 2,7,11,15 target 9 因為 nums 0 nums 1 2 7 9 所以返回 0,1 理解同一元素不能重複使用 3,3 ta...