十二、動態規劃
目錄
- 動態規劃
- 12.1 動態規劃方法關鍵點:
- 12.2 找零錢問題
- 12.4 矩陣最小路徑和
- 12.5 LIS(最長上升子序列)
- 12.6 LCS
- 12.8 01背包問題
- 12.9 最優編輯
動態規劃
12.1 動態規劃方法關鍵點:
最優化原理, 也就是最優子結構性質, 這指的是最優化策略具有這樣的性質, 不論過去狀態和決策如何, 對前面的決策所形成的的狀態而言, 余下的諸決策必須構成最優決策, 簡單來說就是一個最優化決策略的子決策略總是最優子結構
無後效性, 指的是某狀態下決策的收益, 只與狀態和決策相關, 與到達該狀態方式無關
子問題的重疊性, 動態規劃將原來具有指數級時間復雜的暴力搜索算法改進成了具有多項式時間復雜度的算法, 其中的關鍵在於解決冗余, 這是動態規劃算法的根本目的;
12.2 找零錢問題
題目: 有數組penny,penny中所有的值都為正數且不重復。每個值代表一種面值的貨幣,每種面值的貨幣可以使用任意張,再給定一個整數aim(小於等於1000)代表要找的錢數,求換錢有多少種方法。
給定數組penny及它的大小(小於等於50),同時給定一個整數aim,請返回有多少種方法可以湊成aim。
測試樣例:
[1,2,4],3,3
返回:2
解法
暴力窮舉 -> 記憶法 -> 動態規劃
- 暴力窮舉法
如上例子: 取0張1元的時返回方法數r1, 取1張1元的時返回方法數r2, 取2張1元時方法數r3, 取3張1元時返回方法數r4, 累加即為 所有方法數;
而取0張1元時, 剩余可選2, 4, 邏輯相似, 遞歸求解;
import java.util.*; public class Exchange { public int countWays(int[] penny, int n, int aim) { if(penny == null || penny.length == 0){ return 0; } return process1(penny, 0, aim); } public int process1(int[] arr, int index, int aim){ int res = 0; if(index == arr.length){ res = aim == 0 ? 1 : 0; }else{ for(int i = 0; arr[index]*i <= aim; i++){ res += process1(arr, index+1, aim - arr[index]*i); } } return res; } }
- 記憶搜索法
使用一個hash表, 存儲參數為index, aim時的返回值, 等到下次index, aim相同時, 直接返回, 可以減少空間復雜度;
用空間換取時間;
import java.util.*;
public class Exchange {
public int countWays(int[] penny, int n, int aim) {
// write code here
if(penny == null || penny.length == 0 || aim < 0){
return 0;
}
int[][] map = new int[n+1][aim+1];
return process2(penny, 0, aim, map);
}
public int process2(int arr[], int index, int aim, int[][] map){
int res = 0;
if(index == arr.length){
res = aim == 0 ? 1 : 0;
}else {
for(int i = 0; arr[index]*i <= aim; i++){
int tmp = map[index + 1][aim - arr[index]*i];
if(tmp !=0){
res += tmp == -1 ? 0 : tmp;
}else{
res += process2(arr, index + 1, aim - arr[index]*i, map);
}
}
}
map[index][aim] = res == 0 ? -1 : res;
return res;
}
}
動態規劃法
申請n x (aim+1)大小的二維數組, 第一行, 只是用第一種貨幣, 記錄組成0-aim之間錢數的方法數; 然後計算下一行...
import java.util.*;
public class Exchange {
public int countWays(int[] penny, int n, int aim) {
// write code here
if(penny == null || penny.length == 0 || aim < 0){
return 0;
}
return process3(penny, n, aim);
}
public int process3(int arr[], int n, int aim){
int[][] tmp = new int[n][aim+1];
for(int i = 0; i <= aim; i++){
tmp[0][i] = i % arr[0] == 0 ? 1 : 0;
}
for(int i = 1; i < n; i++){
for(int j = 0; j <= aim; j++){
if(arr[i] > j){
tmp[i][j] = tmp[i-1][j];
}else{
tmp[i][j] = tmp[i][j - arr[i]] + tmp[i - 1][j];
}
}
}
return tmp[n - 1][aim];
}
}
12.4 矩陣最小路徑和
問題: 有一個矩陣map,它每個格子有一個權值。從左上角的格子開始每次只能向右或者向下走,最後到達右下角的位置,路徑上所有的數字累加起來就是路徑和,返回所有的路徑中最小的路徑和。
給定一個矩陣map及它的行數n和列數m,請返回最小路徑和。保證行列數均小於等於100.
測試樣例:
[[1,2,3],[1,1,1]],2,3
返回:4
解法
經典動態規劃題, 動態規劃基本思路, 通過空間換取時間, 通過初始的數據遞推出後面的值;
- 建立一個n×m的hash表存儲從左上角到當前格子走的最小路徑;
- 初始可知數據, 從左上角到達第一行的格子最小路徑, 等於第一行格子的累加; 左上角到達第一列的格子的最小路徑等於第一列格子的累加;
- 左上角到map[i][j]處的的最小路徑為 tmp[i-1][j] + map[i][j] 與 tmp[i][j-1] + map[i][j]中更小的那個值;
import java.util.*;
public class MinimumPath {
public int getMin(int[][] map, int n, int m) {
if(n < 1 || m < 1){
return 0;
}
// write code here
int[][] dp = new int[n][m];
dp[0][0] = map[0][0];
for(int i = 1; i < n; i++){
dp[i][0] = dp[i-1][0] + map[i][0];
}
for(int i = 1; i < m; i++){
dp[0][i] = dp[0][i - 1] + map[0][i];
}
for(int i = 1; i < n; i++){
for(int j = 1; j < m; j++){
dp[i][j] = map[i][j] + (dp[i][j-1] > dp[i-1][j] ? dp[i-1][j] : dp[i][j-1]);
}
}
return dp[n-1][m-1];
}
}
12.5 LIS(最長上升子序列)
問題: 這是一個經典的LIS(即最長上升子序列)問題,請設計一個盡量優的解法求出序列的最長上升子序列的長度。
給定一個序列A及它的長度n(長度小於等於500),請返回LIS的長度。
測試樣例:
[1,4,2,5,3],5
返回:3
解決
動態規劃, 思路:
建立一個長度與A長度n對應長度的數組dp, 用於存儲序列A中以每個元素為最長子序列末尾節點時的長度;
索引為0處, dp[0] = 1; 索引為i處 dp[i] = 1 + 索引i之前中比A[i]小的數中最大的dp值
根據遞推關系, 可以很容易寫出代碼
import java.util.*;
public class LongestIncreasingSubsequence {
public int getLIS(int[] A, int n) {
// write code here
if(A == null || n == 0) return 0;
int[] dp = new int[n];
for(int i = 0; i < n; i++){
int max = 0;
for(int j = 0; j < i; j++){
if(A[j] < A[i]){
max = Math.max(max, dp[j]);
}
}
dp[i] = max + 1;
}
int res = -1;
for(int i = 0; i < n; i++){
res = Math.max(res, dp[i]);
}
return res;
}
}
12.6 LCS
問題: 給定兩個字符串A和B,返回兩個字符串的最長公共子序列的長度。例如,A="1A2C3D4B56”,B="B1D23CA45B6A”,”123456"或者"12C4B6"都是最長公共子序列。
給定兩個字符串A和B,同時給定兩個串的長度n和m,請返回最長公共子序列的長度。保證兩串長度均小於等於300。
測試樣例:
"1A2C3D4B56",10,"B1D23CA45B6A",12
返回:6
解決:
動態規劃求解:
- 創建n×m的dp二維數組, dp[i][j] 表示A長度為i, B長度為j時的最大公共子序列
- 初始數據, 第一行n=0時, 若A[0] == B[j], 則dp[0][j] 及dp[0][j之後的索引] 值為1; 第一列m=0時, 若A[i] == B[0], 則dp[i][0] 及dp[i之後索引][0]值為1;
- 分兩種情況
- A[i] == B[j] 則 dp[i][j] = dp[i-1][j-1] + 1
- A[i] != B[j] 則 dp[i][j] = Math.max(dp[i][j-1], dp[i-1][j])
代碼實現如下:
public class LCS {
public int findLCS(String A, int n, String B, int m) {
// write code here
if(n == 0 || m == 0) return 0;
int[][] dp = new int[n][m];
dp[0][0] = A.charAt(0) == B.charAt(0) ? 1 : 0;
for(int i = 1; i < m; i++){
if(dp[0][i - 1] == 0){
dp[0][i] = dp[0][i - 1] + (A.charAt(0) == B.charAt(i) ? 1 : 0);
}else{
dp[0][i] = dp[0][i - 1];
}
}
for(int i = 1; i < n; i++){
if(dp[i - 1][0] == 0){
dp[i][0] = dp[i - 1][0] + (A.charAt(i) == B.charAt(0) ? 1 : 0);
}else{
dp[i][0] = dp[i-1][0];
}
}
for(int i = 1; i < n; i++){
for(int j = 1; j < m; j++){
if(A.charAt(i) == B.charAt(j)){
dp[i][j] = dp[i - 1][j - 1] + 1;
}else{
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[n-1][m-1];
}
}
代碼簡化, 創建一個(n+1)×(m+1)的二維數組;
import java.util.*;
public class LCS {
public int findLCS(String A, int n, String B, int m) {
// write code here
if(n == 0 || m == 0) return 0;
int[][] dp = new int[n + 1][m+ 1];
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(A.charAt(i - 1) == B.charAt(j - 1)){
dp[i][j] = dp[i - 1][j - 1] + 1;
}else{
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[n][m];
}
}
12.8 01背包問題
問題 一個背包有一定的承重cap,有N件物品,每件都有自己的價值,記錄在數組v中,也都有自己的重量,記錄在數組w中,每件物品只能選擇要裝入背包還是不裝入背包,要求在不超過背包承重的前提下,選出物品的總價值最大。
給定物品的重量w價值v及物品數n和承重cap。請返回最大總價值。
測試樣例:
[1,2,3],[1,2,3],3,6
返回:6
解決
方法一:
- 定義一個長度為cap+1長度的dp數組, 存儲每個容量所能獲取的最大價值
- dp[j] 的最大價值為 上一次的dp[j] 與 dp[j-w[i]] + v[i] 中的最大值
public class Backpack {
public int maxValue(int[] w, int[] v, int n, int cap) {
// write code here
int dp[] = new int[cap + 1];
for(int i = 0; i < n; i++ ){
for(int j = cap; j >= w[i]; j--){
dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
}
}
return dp[cap];
}
}
方法二:
- 創建二維數組存儲
public class Backpack {
public int maxValue(int[] w, int[] v, int n, int cap) {
// write code here
int[][] dp = new int[n+1][cap+1];
for(int i = 1; i <= n; i++){
for(int j = 1; j <= cap; j++){
dp[i][j] = dp[i-1][j];
if(j >= w[i-1]){
dp[i][j] = Math.max(dp[i][j], dp[i-1][j - w[i-1]] + v[i-1]);
}
}
}
return dp[n][cap];
}
}
12.9 最優編輯
問題: 對於兩個字符串A和B,我們需要進行插入、刪除和修改操作將A串變為B串,定義c0,c1,c2分別為三種操作的代價,請設計一個高效算法,求出將A串變為B串所需要的最少代價。
給定兩個字符串A和B,及它們的長度和三種操作代價,請返回將A串變為B串所需要的最小代價。保證兩串長度均小於等於300,且三種代價值均小於等於100。
測試樣例:
"abc",3,"adc",3,5,3,100
返回:8
解決
利用動態規劃, 基本思路如下:
構建一個(n+1)×(m+1)的dp二維數組, dp[i][j] 表示長為i的字符串A轉換成長為j的字符串B所需最小代價
初始化 dp[0][j] 和 dp[i][0] , dp[0][j] 的初始化, 0 -> j, 每次插入一個字符串; dp[i][0]的初始化, i -> 0, 每次刪除一個字符串;
dp[i][j] 的值可能來自於 dp[i-1][j-1] 、 dp[i-1][j] 、 dp[i][j-1] 這三個地方, 取其中的最小值;計算如下:
- 若A.charAt[i-1] == B.charAt[j-1], 則dp[i][j] = dp[i-1][j-1] 否則dp[i][j] = dp[i-1][j-1] + c2
- dp[i][j] = dp[i-1][j] + c1; 即刪除掉一個字符
- dp[i][j] = dp[i][j-1] + c0; 即插入一個字符
import java.util.*;
public class MinCost {
public int findMinCost(String A, int n, String B, int m, int c0, int c1, int c2) {
// write code here
int[][] dp = new int[n+1][m+1];
for(int i = 1; i <=n; i++){
dp[i][0] = c1*i;
}
for(int i = 0; i <= m; i++){
dp[0][i] = c0*i;
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(A.charAt(i - 1) == B.charAt(j - 1)){
dp[i][j] = Math.min(dp[i-1][j-1], Math.min(dp[i][j-1] + c0, dp[i-1][j] + c1));
}else{
dp[i][j] = Math.min(dp[i-1][j-1]+c2, Math.min(dp[i][j-1] + c0, dp[i-1][j]+c1) );
}
}
}
return dp[n][m];
}
}
十二、動態規劃