動態規劃一(Dynamic Programming)
1.引入
動態規劃的思想是從頂向下、從最後到最初考慮;
碼程式碼時從底向上、從頭到尾開始寫起。
1.1.給定一排硬幣,面值不等,均大於0,則如何選擇硬幣,使得選擇的硬幣都不相鄰,同時選擇出來的硬幣的總值最大?
從頂向下考慮:
假設計數到第n枚硬幣時(就是已經討論完到底要不要選取第n枚硬幣後),當前選取的所有硬幣的總價值是F[n];
從選不選擇最後一枚硬幣考慮,有兩種選擇,第一種不選擇最後一枚硬幣,則F[n]=F[n-1],選擇完前n枚硬幣的總價值和選擇完前n-1枚硬幣的總價值是一樣的;
另一種是選擇最後一枚硬幣,則最後的總價值是F[n]=F[n-2]+val[n].其中val[n]是第n枚硬幣的價值。
根據這個規律就可以得到F[n]的表示式
F[0]=0; 就是如果沒有硬幣,就什麼值都沒有
F[1]=val[1] 就是如果只有一枚硬幣,那總價值就是這個硬幣的價值
F[n]=max(F[n-1],F[n-2]+val[n])
虛擬碼
演算法 CoinRow(val[1...n])
//算出間隔硬幣的最大硬幣價值
//輸入:一排硬幣的價值val[n]
//輸出:最大硬幣價值F[n]
//虛擬碼中,陣列下角標從1開始計
F[0]=0;F[1]=val[1];
for i←2 to n do
F[n]=max(F[n-1],F[n-2]+val[i])
return F[n]
JAVA描述:
import java.util.Scanner; public class DPcoin { public static void main(String[] args) { System.out.print("input n:"); Scanner input=new Scanner(System.in); int n=input.nextInt(); int[] coins=new int[n]; System.out.printf("input %d coins's values\n",n); for(int i=0;i<n;i++) coins[i]=input.nextInt(); System.out.println("the coins:"); System.out.println(coins); int f[]=new int[n]; f[0]=coins[0]; f[1]=coins[1]; for(int i=2;i<n;i++) f[i]=Math.max(coins[i]+f[i-2], f[i-1]); System.out.printf("the utmost value: %d",f[n-1]); input.close(); } }
1.2 給定一個金額,再給定一個硬幣的價值列表,計算怎麼用最少的硬幣來描述這個價格。
題目解釋:
有的人可能會覺得這個問題沒有意義,因為比如9塊錢,我直接先取一個5塊錢再取4個1塊錢好了,比如32塊錢,我直接取一個20一個10塊錢2個1塊錢好了,就是從大面值的開始算;
但是這是預設使用人民幣的情況,如果這個國家發行的硬幣面值是1、4、5元,要組合出一個8元呢?按照從大到小的順序就是先找1個5元,再找3個1元總共4個硬幣,但是實際上最優解是2個4元錢。
現在來分析問題:
先來看1.1中最核心的式子:
F[n]=max(F[n-1],F[n-2]+val[i])
1.1的約束條件是選擇硬幣不相鄰,現在約束條件是選擇硬幣的步長有要求(從硬幣面值的集合中進行選擇),假設硬幣有4種面值{1,4,5,10};
我們假設湊成面值sum需要的最少硬幣數為F[sum],則我們有4種方式到達面值sum.選擇的最後一個硬幣面值為:
1 此時F[sum]=F[sum-1]+1,即湊成【面值為sum-1】的最少硬幣數再加1
4 此時F[n]=F[sum-4]+1
5 此時F[n]=F[sum-5]+1
10 此時F[n]=F[sum-10]+1
我們選擇這當中最小的一個:
F[n]=(F[sum-1],F[sum-4],F[sum-5],F[sum-10])+1
虛擬碼:
演算法 ChangeMaking(sum,coins[1...n])
//從面值為coins的硬幣集合中,用最少的硬幣,湊出總價值為sum的面值
//輸入:總價值,硬幣面值的陣列
//輸出:使用的硬幣數F[n]
F[0]=0;F[1]=1;
for i←2 to sum do
//i迴圈的總面值數,即從2塊錢一直算到sum塊錢需要的最少硬幣數
tmp←∞;
for j←1 to n do
//j迴圈的是硬幣的類別
//到達面值sum前有4種選擇,4種中有一個花費了最少的硬幣數,最少硬幣數用tmp儲存
if sum≥coins[j]
tmp←min(F[sum-coins[j]],tmp)
else
break
F[i]←tmp+1
return F[n]
**JAVA描述**:
import java.util.Scanner;
public class ChangeMaking {
public static void main(String[] args) {
System.out.println("input the amount of change:");
Scanner input=new Scanner(System.in);
int sum=input.nextInt();
int coins[]= {1,4,5,10,20};
int F[]=new int[sum+1];
F[0]=0;F[1]=1;
int tmp,j;
for(int i=2;i<=sum;i++) {
j=0;tmp=Integer.MAX_VALUE;
while(j<coins.length&&i>=coins[j]) {
tmp=Math.min(F[i-coins[j]], tmp);
j++;
}
F[i]=++tmp;
}
System.out.printf("coin cnt:%d",F[sum]);
input.close();
}
}
1.3 m*n的方格中有一些硬幣,硬幣的面值不一,從左上角一直向下或向右走走到右下角,怎麼選擇路徑使得收集的硬幣總額最大?
從頂向下考慮:
假設累計到最右下角時的最大硬幣總額為F[m][n],則F[m][n]可以表示為:
F[m][n]=max(F[m-1][n],F[m][n-1])+coins[m][n-1]
同時除了第一排和第一列的格子,其他地方的格子累計最大硬幣總額也可以表示為:
F[i][j]=max(F[i-1][j],F[i][j-1])+coins[i][j-1]
即如果我要計算走到第i行第j列格子時,可以得到的最大硬幣總額,我要先算這個格子上邊和左邊的最大硬幣總額,取更大的那個,然後再加上自己當前格子上的硬幣的錢,就可以得到走到這個地方可以得到的最大硬幣面額。
虛擬碼:
演算法 CollectionCoins(coins[m][n])
//計算從(1,1)走到(m,n)能收穫的最大硬幣總額
//輸入:一個m*n格子上硬幣的面值
//輸出:最大硬幣總額 F[m][n]
F[1][1]=coins[1][1]
//先計算出第一排的結果
for i←2 to n do
F[1][i]←F[1][i-1]+coins[1][i]
for i←2 to m do
F[i][2]←F[i][1]+coins[i][2] //每一排第一個F值單獨計算
for j←2 to n do
F[i][j]←max(F[i-1][j],F[i][j-1])+coins[i][j]
return F[m][n]
JAVA實現:
public class CollectCoins {
public static void main(String[] args) {
int coins[][]= {
{3,4,1,5,0,7,6},
{5,4,8,2,4,1,7},
{6,0,9,1,4,3,1,}
};
int F[][]=new int[coins.length][coins[0].length];
F[0][0]=coins[0][0];F[1][0]=F[0][0]=coins[1][0];
//初始化F第一排
for(int i=1;i<coins[0].length;i++) {
F[0][i]=F[0][i-1]+coins[0][i];
}
for(int i=1;i<coins.length;i++) {
F[i][0]=F[i-1][0]+coins[i][0];
for(int j=1;j<coins[0].length;j++) {
F[i][j]=Math.max(F[i-1][j], F[i][j-1])+coins[i][j];
}
}
System.out.print(F[coins.length-1][coins[0].length-1]);
}
}
總結:
首先關鍵的是求F表示式之間的關係。然後可以發現,前面三個題中的動態規劃其實都將所有過程中的最優解解出來了:
比如第一題,其實求出了取到第1、2、3…n個硬幣時可以得到的最大面額F[i];
第二題,先求總額為1塊錢時的最少硬幣組合、再求總額為2塊錢時的最少硬幣組合、…最後求到總額為sum塊錢時的最少硬幣組合;
第三個問題其實將表格中每個位置可以得到的最大硬幣總額都求出來了。