1. 概念
動態規劃和分治法類似,把原問題劃分成若干個子問題,不同的是,分治法,子問題間相互獨立,動態規劃,子問題不獨立。
動態規劃解題一般分爲三個步驟:
- 表示狀態
分析問題的狀態時,不要分析整體,只分析最後一個階段即可!因爲動態規劃問題都是劃分爲多個階段的,各個階段的狀態表示都是一樣,而我們的最終答案在就是在最後一個階段。每一種狀態都是使用數組的位數來表示的。一般我們會先使用數組的第一維來表示階段,然後再根據需要通過增加數組維數,來添加其他狀態。
- 找出狀態轉移方程
狀態轉移指的是,當前階段的狀態如何通過之前計算過的階段狀態得到。
- 初始化邊界
初始化邊角狀態,以及第一個階段的狀態。
整個動態規劃,最難的就是定義狀態。一旦狀態定義出來,表明你已經抽象出了子問題,可以肢解原來的大問題了。
2. 相關算法題
2.1 斐波那契數列
斐波那契數列指的是這樣一個數列:1、1、2、3、5、8、13、21、34、…… 其規律很明顯,從第3個數開始,每個數都等於它前兩個數的和。
遞歸解法
//遞歸方式
public static int fibonacci(int i){
if(i < 2) return i;
return fibonacci(i - 1) + fibonacci(i - 2);
}
使用遞歸的方式,會有好很多的重複計算
使用動態規劃的方式,來避免重複計算
解題思路
定義一個一維數組dp,dp[ i ] 代表斐波那契數列第i個數字,轉移方程爲dp[ i ] = dp[ i - 1 ] + dp[ i - 2 ],初始化dp[ 0 ] = 1,dp[ 1 ] = 1,之後,從2開始循環,直到計算出要求的第n個數字結束。
// 動態規劃
public static int fibonacci2(int i){
//定義一個一維數組dp,dp[i]表示斐波那契數列第i個數字
int[] dp = new int[i + 1];
dp[0] = 0;
dp[1] = 1;
for(int j = 2; j < i + 1; j++){
dp[j] = dp[j - 1] + dp[j - 2];
}
return dp[i];
}
與之類似的還有:跳臺階問題:每次只能跳一個或者兩個臺階,跳到n層臺階上有幾種方法 。
2.2 矩陣連乘問題
給定n個矩陣{A1,A2,…,An},其中Ai與Ai+1是可乘的,i=1,2…,n-1。如何確定計算矩陣連乘積的計算次序,使得依此次序計算矩陣連乘積需要的數乘次數最少。
例如,給定三個連乘矩陣{A1,A2,A3}的維數分別是10100,1005和550,採用(A1A2)A3,乘法次數爲101005+10550=7500次,而採用A1(A2A3),乘法次數爲100550+10100*50=75000次乘法,顯然,最好的次序是(A1A2)A3,乘法次數爲7500次。
矩陣乘法
矩陣A 和矩陣B,只有當A的列數和B的行數相等的時候纔可以相乘,假如A(m x n),B(n x k),A x B 得到一個m 行 k列的矩陣,具體乘法是拿A的第一行與B的第一列中各個元素的乘積的和爲結果矩陣的第一行第一列的數,然後拿A的第一行和B的第二列的各個元素的乘積作爲第一行第二列的數…。
解題思路
定義一個二維數組m,m[ i ] [ j ] 表示第i個矩陣到第j個矩陣連乘的最小次數,則最優值就是m[ 1 ] [ n ]。假設A1A2…An的一個最優加括號把乘積在Ak和Ak+1間分開,則前綴子鏈A1…Ak的加括號方式必定爲A1…Ak的一個最優加括號,後綴子鏈同理。
所以m[ i ] [ j ] = min( m[ i ] [ k ] + m [ k + 1 ] + p(i - 1) * p(k) *p(j) )
( p(i - 1) 、p(k)、p(j) 分別是第i個矩陣的行數,第k個矩陣的列數,第j個矩陣的列數),如果i = j,m[ i ] [ j ] = 0
一開始並不知道k的確切位置,需要遍歷所有位置以保證找到合適的k來分割乘積。
對於一組矩陣:A1(30x35),A2(35x15),A3(15x5),A4(5x10),A5(10x20),A6(20x25) 個數N爲6
那麼p數組保存它們的行數和列數:p={30,35,15,5,10,20,25}共有N+1即7個元素
p[0],p[1]代表第一個矩陣的行數和列數,p[1],p[2]代表第二個矩陣的行數和列數…p[5],p[6]代表第六個矩陣的行數和列數
輔助表m: m[i][j]代表從矩陣Ai,Ai+1,Ai+2…直到矩陣Aj最小的相乘次數,比如A[2][5]代表A2A3A4A5最小的相乘次數,即最優的乘積代價。我們看上圖,從矩陣A2到A5有三種斷鏈方式:A2{A3A4A5}、{A2A3}{A4A5}、{A2A3A4}A5,這三種斷鏈方式會影響最終矩陣相乘的計算次數,我們分別算出來,然後選一個最小的,就是m[2][5]的值,同時保留斷開的位置k在s數組中。
代碼
public class MartixChain {
/**
*
* @param p p爲矩陣鏈,p[0],p[1]代表第一個矩陣的行數和列數,p[1],p[2]代表第二個矩陣的行數和列數
* @return 返回最優值
*/
public static int martixChain(int[] p){
//計算矩陣的個數
int n = p.length - 1;
//m[i][j],表示第i個矩陣到第j個矩陣連乘的最小相乘次數
int[][] m = new int[n + 1][n + 1];
//s[i][j]=k,表示,第i個矩陣到第j個矩陣的連乘,從第k個矩陣分割,相乘次數最小
int[][] s = new int[n + 1][n + 1];
//初始化二維數組,將對角線位置上的值設爲0,本身就是0,可以上略
for(int i = 0; i < n + 1; i++){
m[i][i] = 0;
}
for(int r = 2; r < n + 1; r++){//表示r個矩陣相乘
// i表示從第i個矩陣開始,r個矩陣連乘。n-r,表示r個矩陣相乘,最左邊的矩陣是第n-r+1個,
// 如果超過這個,最後一組r個矩陣就不是r個了,也就是i的左邊界
for(int i = 1; i <= n - r + 1; i++){
//j 表示從第i個矩陣開始,長度爲r的矩陣鏈的最後一個矩陣
int j = i + r - 1;
//先以i進行劃分
m[i][j] = m[i + 1][j] + p[i - 1] * p[i] *p[j];
//記錄劃分位置
s[i][j] = i;
//尋找使矩陣相乘次數最小的劃分點
for(int k = i + 1; k < j; k++){
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][n];
}
public static void main(String[] args) {
int[] p = new int[]{5, 7, 4, 3, 5};
System.out.println(martixChain(p)); // 264
}
}
2.3 劍指 Offer 42. 連續子數組的最大和
輸入一個整型數組,數組裏有正數也有負數。數組中的一個或連續多個整數組成一個子數組。求所有子數組的和的最大值。
要求時間複雜度爲O(n)。
示例1:
輸入: nums = [-2,1,-3,4,-1,2,1,-5,4]
輸出: 6
解釋: 連續子數組 [4,-1,2,1] 的和最大,爲 6。
解題思路
使用動態規劃算法,定義一個數組dp,dp[i]代表以nums[i]結尾的連續子數組的最大和。如果dp[i - 1] >= 0,dp[i]=dp[i-1]+nums[i],如果
dp[i - 1] < 0,dp[i] = nums[i]。dp[0] = nums[0]
public static int maxSubArray(int[] nums){
if(nums == null || nums.length == 0) return 0;
//dp[i]表示以nums[i]結尾的連續子數組的最大和
int[] dp = new int[nums.length];
dp[0] = nums[0];
int res = nums[0];
for(int i = 1; i < nums.length; i++){
if(dp[i - 1] < 0){
dp[i] = nums[i];
}else {
dp[i] = dp[i - 1] + nums[i];
}
res = Math.max(res, dp[i]);
}
return res;
}
這道題,也可以不使用數組,只使用一個變量來記錄以nums[i]結尾的連續子數組的最大和
public static int maxSubArray2(int[] nums){
if(nums == null || nums.length == 0) return -1;
int max = nums[0];
int res = nums[0];
for (int i = 1; i < nums.length; i++) {
if(max < 0){
max = nums[i];
}else{
max = max + nums[i];
}
res = Math.max(max, res);
}
return res;
}
2.4 01揹包問題
有n個物品,它們有各自的體積和價值,現有給定容量的揹包,如何讓揹包裏裝入的物品具有最大的價值總和?
eg:number=4,capacity=8
i(物品編號) | 1 | 2 | 3 | 4 |
---|---|---|---|---|
w(體積) | 2 | 3 | 4 | 5 |
v(價值) | 3 | 4 | 5 | 6 |
解題思路
- 表示狀態
V[i ,j]
,表示當前揹包容量 j,前 i 個物品最佳組合獲得的最大的價值 - 找出狀態轉移方程
揹包容量不足以裝物品i(w(i) 大於揹包容量)則裝入前i個物品得到的最大價值和裝入前i - 1個物品的最大價值是相同的,即V[i ,w] = V[i - 1,w]
揹包容量可以裝入物品i,可以裝也可以不裝,所以V[i,j] = max { V[i - 1 ,j - w(i)] + v(i) , V[i - 1,j] }
爲什麼揹包容量足夠的情況下,還需要 V(i,j)=max{V(i-1,j),V(i-1,j-w(i))+v(i)}?
V(i-1,j-w(i))+v(i)
表示裝了第i個物品後背包中的最大價值,所以當前揹包容量 j 中,必定有w(i)個容量給了第i個揹包。
因此只剩餘j-w(i)個容量用來裝除了第i件物品的其他所有物品。
V(i-1,j-w(i))是前i-1個物品裝入容量爲j-w(i)的揹包中最大價值。
注意,這裏有一個問題。前i-1個物品裝入容量爲j-w(i)的揹包中最大價值+物品i的價值。可能不如將,前i-1個物品裝入容量爲j的揹包中得到的價值大。也就是說,可能出現 V(i-1,j) > (V(i-1,j-w(i))+v(i))
比如說,將第i個物品放入揹包,可能會導致前面更有價值的物品放不進揹包。因此,還不如不把第i個物品放進去,把空間讓出來,從而能讓前i-1個物品中更有價值的物品能夠放入揹包。從而讓V(i,j)取得最大的值。
所以我們需要 max{V(i-1,j),V(i-1,j-w(i))+v(i)},來作爲把前i個物品裝入容量爲j的揹包中的最優解。
根據上面的例子,重量分別爲2,3,4,5 ,價值分別爲3,4,5,6,商品編號是1,2,3,4
表中橫座標代表容量j,縱座標代表商品編號i,表中數字代表在揹包容量爲j時,前i個物品最佳組合獲得的最大價值dp[ i ][ j ]。物品最小的重量爲2,所以容量爲0、1時,表格中都爲0,並且縱座標爲0的那一列,也全爲0,因爲0個商品,價值爲0。當容量爲2時,1號商品,重量爲2,可以放,也可以不放,因爲1號商品前面沒有商品了,所以前1個物品,獲得的最大價值爲3,不管容量爲幾都爲3,看2號商品,揹包容量爲2時,沒法放2好商品,所以dp[ 2 ][ 2 ] = dp[ 1 ][ 2 ],當揹包容量爲3時, 2號商品可以放,也可以不放,主要看價值,dp[ 1 ][ 3 ] = 2,
而3號商品的價值爲3,所以應該放如揹包,這樣,揹包容量爲3時,就不放1號商品了,dp[ 2 ][ 3 ] = max { dp[ 1 ] [ 3 -2] + v(2) ,dp [ 1 ][ 3 ] } = 3。
只要上面的圖理解了,以及dp[ i ] [ j ] 代表的含義理解了,01揹包問題就很簡單了。
代碼實現
public class ZeroOnePack {
/**
*
* @param n 物品數量
* @param v 揹包容量
* @param weights 物品的重量
* @param values 物品的價值
* @return 返回容量爲v的揹包所能獲得的最大價值
*/
public static void zeroOnePack(int n, int v, int[] weights, int[] values){
//初始化動態規劃數組,dp[i][j]代表揹包容量爲j時,前i個物品最佳組合所能獲得的最大價值
int[][] dp = new int[n + 1][v + 1];
//爲了便於理解,將dp[i][0]和dp[0][j]均置爲0,從1開始計算
for(int i = 1; i < n + 1; i++){
for(int j = 1; j < v + 1; j++){
//如果第i件物品的重量大於揹包容量j,則不裝入揹包
//由於weight和value數組下標都是從0開始,故注意第i個物品的重量爲weight[i-1],價值爲value[i-1]
if(weights[i - 1] > j){
dp[i][j] = dp[i - 1][j];
}else {
dp[i][j] = Math.max(dp[i - 1][j - weights[i - 1]] + values[i - 1],dp[i - 1][j]);
}
}
}
//獲得的最大商品重量
int maxValue = dp[n][v];
System.out.println("獲得的最大商品價值爲" + maxValue);
//通過回溯法算出哪些商品被裝到揹包裏了。從dp[n][v]開始,如果dp[n][v]=dp[n-1][v]
//說明,第n件商品沒有被裝到揹包裏,如果不相等,說明,被裝到揹包裏了,之後,減去
//被裝入商品的重量,再判斷容量爲當前容量時物品是否裝入揹包了(重複上述過程)。
int j = v;
for(int i = n; i > 0; i--){
//如果dp[i][j] > dp[i - 1][j],說明,第i件商品是放入揹包的
if(dp[i][j] > dp[i - 1][j]){
System.out.print(i + " ");
j = j - weights[i - 1];
}
if(j == 0){
break;
}
}
}
}
2.5 面試題66. 構建乘積數組
給定一個數組 A[0,1,…,n-1],請構建一個數組 B[0,1,…,n-1],其中 B 中的元素 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。
示例:
輸入: [1,2,3,4,5]
輸出: [120,60,40,30,24]
提示:
所有元素乘積之和不會溢出 32 位整數
a.length <= 100000
解題思路
題目中求的是每一個元素左邊的元素和右邊的元素的乘積,不包括當前元素,可以分別求出每一個元素左邊元素的乘積和右邊元素的乘積,並且計算的時候,後一個左邊元素的乘積是前一個數左邊元素的乘積乘以前一個元素,比如3的左邊元素的乘積,是2的左邊元素的乘積乘以2,同理,每一個數的右邊元素的乘積,是這個數後邊的元素的乘積乘以後邊的元素。
代碼
class Solution {
public int[] constructArr(int[] a) {
//用動態規劃的思想解題,先算出每一個元素從第一個元素開始到當前元素之前的一個元素的
//乘積,再算出從數組末尾開始,每一個元素到當前元素的前一個元素的乘積,將前面算出的
//結果一一相乘就是答案
int n = a.length;
if(n == 0) return new int[0];
int[] left = new int[n];
//第一個數的左邊的數的乘積是1
left[0] = 1;
//算出每一元素左邊的乘積(不包括自己)
for(int i = 1; i < n; i++){
left[i] = left[i - 1] * a[i - 1];
}
//算出每一個元素右邊的乘積(不包括自己),可以定義一個temp變量代替數組,temp每一個數的右邊元素的乘積
//最後一個數的右邊元素的乘積是1
int temp = 1;
int[] res = new int[n];
res[n - 1] = left[n - 1];
for(int i = a.length - 2; i >= 0; i--){
temp = temp * a[i + 1];
res[i] = left[i] * temp;
}
return res;
}
}
如有不足之處,歡迎指正,謝謝!