動態規劃及其應用

                                       動態規劃

         


杭電oj做到了最大子序列和,自己的換了好多方法都超時了,網上一查需要用到動態規劃,趁此機會學習一下動態規劃
但是感覺動態規劃有點像這個遞歸哈哈哈哈

1.動態規劃,就是利用歷史記錄來避免計算的重複,而這些歷史記錄我們需要一些變量來保存,一般用到一維數組還有二維數組來保存

2.三個步驟
(1) 定義數組元素的含義,例如你的dp【i】代表的什麼含義


(2)找出數組元素之間的關係式,有點類似高中所學的數學歸納法,都是通過已知元素來推未知元素


(3)找出初始值,學過數學歸納法的都知道,雖然我們知道了數組元素之間的關係式,例如 dp[n] = dp[n-1] + dp[n-2],我們可以通過 dp[n-1] 和 dp[n-2] 來計算 dp[n],
但是,我們得知道初始值啊,例如一直推下去的話,會由 dp[3] = dp[2] + dp[1]。而 dp[2] 和 dp[1] 是不能再分解的了,,
所以我們必須要能夠直接獲得 dp[2] 和 dp[1] 的值而這,就是所謂的初始值。由了初始值,並且有了數組元素之間的關係式
,那麼我們就可以得到 dp[n] 的值了,而 dp[n] 的含義是由你來定義的,你想求什麼,就定義它是什麼,這樣,這道題也就解出來了。

 

3.
小青蛙跳臺階


 問題描述,一隻青蛙一次可以跳上一級臺階,也可以跳上二級臺階。求臺階跳上一個n級臺階總共有多少種方法。
 
 三大步驟


 一定義dp的含義,我們的問題是跳上n級臺階時的跳法,我們就定義dp【i】的含義是跳上一個n級臺階總共有dp【i】種跳法。
 
 二.找出元素之間的關係式,因爲在每一階臺階時都是從n-1階還有n-2階臺階跳上來的
 由於我們要算所有可能的跳法,所以有dp【i】=dp【i-1】+dp【i-2】;
 
 三.找出初始條件
 當 n = 1 時,dp[1] = dp[0] + dp[-1],而我們是數組是不允許下標爲負數的,所以對於 dp[1],
 我們必須要直接給出它的數值,相當於初始值,顯然,dp[1] = 1。一樣,dp[0] = 0.(因爲 0 個臺階,那肯定是 0 種跳法了)。於是得出初始值:

 1. dp[0] = 0. dp[1] = 1. | dp[0] = 0. |   |-dp[1] = 1.-| |  n <= 1
    時,dp[n] = n. |  
    
    代碼實現:

  int[] dp = new int[n+1];
        // 給出初始值
        dp[0] = 0;
        dp[1] = 1;
        // 通過關係式來計算出 dp[n]
        for(int i = 2; i <= n; i++){
            dp[i] = dp[i-1] + dp[i-2];
        }
    System.out.println(dp[i]);

    
    
 (2)
    例題二,找零錢
   


    已知不同面值的鈔票,求如 何用最少數量的鈔票組成某個金額,求可 以使用的最少鈔票數量。如果任意數量的已知面值鈔票都無法組成該金額, 則返回-1。
    輸入coins 1 3 5 money 11 輸出 3
    
    第一步,確認dp【i】的含義(i元錢時的最少硬幣方案)
    第二步,找出元素之間的關係式
    第三步,找出初始值
 

 dp【0】={0}=0
dp【1】={dp【1-arr【1】】+1}=1
dp【2】={dp【2-arr【1】】+1}=2
dp【3】={dp【3-arr【2】】+1}=1
dp【4】={dp【4-arr【2】】+1}=2
    ......
    dp【i】={dp【i-arr【j】(arr[j]<i並且是小於i中最大的面值)】+1}=


 


  代碼實現:
        

       int money = sc.nextInt();
            int[] arr = { 1, 3, 5 };
            int[] dp = new int[money + 1];// 每一個都代表一種錢數的拼湊最小值方案
            int max = 0;
            dp[0] = 0;
            for (int i = 1; i < dp.length; i++) {
                for (int j = 0; j < arr.length; j++) {
                    if (arr[j] >= i) {//找到這個臨界值,也就是小於i並且是最大的那個面值
                        if (arr[j] == i) {//如果等於i,證明就是一個硬幣就可以
                            max = j;//通過索引實現這個arr【j】
                        } else {
                            max = j - 1;
                        }
                        break;//一旦找出這個臨界值,立即跳出
                    }
                }
                if (max == -1) {
                    dp[i] = 1;
                } else {
                    dp[i] = dp[i - arr[max]] + 1;
                }

            }
            System.out.println(dp[dp.length - 1]);
        }
    }
}
    

(3)

例題三,找出最短時間(杭電1260)

題目描述:

題意:有K個人買票,可以每個人買單票,給出K個單買時間;也可以和前一個人買雙票,給出K-1個買雙票時間

第一步,設定dp的含義,因爲題目中說是求k個人買票最短時間,所以我們就設dp[k]爲k個人買票花費的最短時間

第二步,找出元素之間的關係,在這裏我舉一個例子

輸入:

 單人買票時間 arr[i] 25 20 24 21
相鄰雙人買票時間  d[i] 40 25 42
dp[0]=0
dp[1]=25
dp[2]=min(25+20,40)
dp[3]=min(min(25+20,40)+24,42+25)
.....

dp[i]=min(dp[i-1]+arr[i],d[i-1]+dp[i-2])

第三步,找出初始值

dp【0】=0;

dp【1】=arr【1】;

第四步,代碼實現:


import java.util.*;

public class Main3 {
	// alphabetic order.字母序
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Scanner sc = new Scanner(System.in);
		int n = sc.nextInt();
		while (n-- > 0) {
			int k = sc.nextInt();
			int[] arr = new int[k + 1];//單人買票時間
			int d[] = new int[k];
			for (int i = 1; i < arr.length; i++) {
				arr[i] = sc.nextInt();
			}
			if (k == 1) {//k=1的時候直接就輸出
				if(arr[1]<10) {
				System.out.println("08:00:0"+arr[1]+" am");
				}else {
					System.out.println("08:00:"+arr[1]+" am");
				}
			} else {
				for (int i = 1; i < d.length; i++) {k!=1的時候就有d【】,輸入d【】的值
					d[i] = sc.nextInt();
				}
				int[] dp = new int[k + 1];
				dp[0] = 0;//初始值
				dp[1] = arr[1];//初始值
				for (int i = 2; i < dp.length; i++) {
					dp[i] = Math.min(dp[i - 1] + arr[i], d[i - 1] + dp[i - 2]);//主要的公式
				}
				int hour=dp[k]/3600;
				int min=dp[k]/60%60;
				int s=dp[k]%60;
	                int sumhour = hour + 8;
	                if (sumhour < 10) {
	                    System.out.print("0" + sumhour + ":");
	                } else {
	                    System.out.print(sumhour + ":");
	                }
	                if (min < 10) {
	                    System.out.print("0" + min + ":");
	                } else {
	                    System.out.print(min + ":");
	                }
	                if (s < 10) {
	                    System.out.print("0" + s + " ");
	                } else {
	                    System.out.print(s + " ");
	                }
	                if(sumhour>12) {//大於12就是下午
	                	System.out.print("pm");
	                }else {
	                	System.out.print("am");
	                }
	                System.out.println();
			}
		}
	}
}

(4)杭電oj(1031)

分析:如果單獨要大家編程實現最大子序列相信大家,在看了我之前寫的幾個例題的話,肯定是會做的了,但是本題的亮點就是還要求出最長子序列的第一個元素,還有最後一個思路....這需要一個特別巧妙的思路,下面讓我們一起學習這個思路吧!

解釋:

我舉出一個本題較難輸出的一組數據,跟大家一起走一下思路:設dp【i】代表的是前i個數的最大數

dp[i]=max(dp[i-1]+arr[i],arr[i]);

此時設初始最大值是arr【1】,最長子序列的首元素時arr【1】,尾元素是arr【1】,中間轉換量temp是arr【1】

arr[i]

=(5   -8  3   2   5    0)

   
dp【2】

5-8=-3(此時明顯-3比-8大,所以前兩個數的最大值是-3,所以dp【2】=-3,此時最大值還是5)

arr[2]

 

-8

dp【3】

-3+3=0(此時arr[3]的值比dp【3】大,所以arr[3]>dp[3],另temp=arr[3],因爲arr[3]大於dp,所以arr[3]可能是新的子序列的首元素,也有可能不是,所以先保存着)

arr[3]

3

dp【4】

3+2=5(此時dp還小於最大值,所以首尾元素還是5,沒有改變)

arr[4]

2

dp【5】

5+5=10(此時dp[5]大於最大值5,首尾元素要發生改變,需要判斷dp[4]+arr[5]與arr[5]的值)

arr[5]

5

dp【6】

10+0

arr[6]

0

dp【i】 dp【i-1】+arr【i】 arr[i] arr【i】

每次忘記怎麼做的時候可以做一下 5   -8  3   2   5    05  -8  3  -5   500 這兩組數據

重點:

1.

首尾元素的判斷一定是在dp[i]大於max的時候給其賦值新的數

2.分析幾組情況,max1 = 4 = 1+4

當1+4+(-4)序列與4比較時,4大於前面,所以4有可能成爲新max的頭,所以暫存她爲temp,如果加上後面的數大於max1則,start,還有end將被改變

 

但是如果後面的值沒有超過5,那麼tenp則永遠成爲不了start

比如:

 此時後面的序列小於5,所以不行。

 

import java.util.Scanner;

public class Main5 {
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		yy: while (sc.hasNext()) {
			int nn = sc.nextInt();
			if (nn == 0) {
				break;
			}
			int[] arr = new int[nn + 1];
			int jj = 0;
			int ff = 0;
			for (int i = 1; i < arr.length; i++) {
				arr[i] = sc.nextInt();
				if (arr[i] < 0) {
					ff++;
				}
			}
			if (ff == nn) {
				System.out.println("0" + " " + arr[1] + " " + arr[arr.length - 1]);
				continue yy;
			}
			if (nn == 1) {
				System.out.println(arr[1] + " " + arr[1] + " " + arr[1]);
				continue yy;
			}
			int[] dp = new int[nn + 1];
			dp[1] = arr[1];//元素初始化
			int max = dp[1];//最大值是第一個元素
			int start = arr[1];//第一個元素是arr的第一個元素
			int temp = arr[1];//同理
			int end = arr[1];//同理
			for (int i = 2; i < dp.length; i++) {
				dp[i] = Math.max(dp[i - 1] + arr[i], arr[i]);//表達式
				if (arr[i] > dp[i - 1] + arr[i]) {//如果arr[i]>dp[i - 1] + arr[i],證明此時arr[i]可能是新的子序列的首元素
					//但不是一定,首尾元素的賦值必須在當dp[i]的值大於最大值的時候
					temp = arr[i];//先保存給一個變量
				}
				if (dp[i] > max) {//當dp的值大於最大值,他的子序列的首尾元素改變了
					if (dp[i - 1] + arr[i] > arr[i]) {//判斷首尾元素,如果原序列大於新數列的首元素(arr[i]此時相當於一個新數列的首)
						end = arr[i];//原數列的尾
						start = temp;
					} else {
						start = arr[i];//如果新數列的首大於原數列
						end = arr[i];
					}
					max = dp[i];
				}

			}
			System.out.println(max + " " + start + " " + end);

		}
	}
}

(5)杭電oj(1003)

import java.util.Scanner;

public class Main3{
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int jj=1;
        while(n-->0) {
            int nn = sc.nextInt();
            int []arr = new int[nn+1];
            for (int i = 1; i < arr.length; i++) {
                arr[i]=sc.nextInt();
            }
            int[]dp = new int[nn+1];
            dp[1]=arr[1];
            int max =  arr[1];
            int start = 1;
            int temp = 1;
            int end = 1;
            for (int i = 2; i < dp.length; i++) {
                dp[i] = Math.max(dp[i-1]+arr[i],arr[i]);
                if(arr[i]>dp[i-1]+arr[i]){
                    temp = i;
                }
                if(dp[i]>max) {
                
                    if(dp[i-1]+arr[i]>=arr[i]) {
                    	start = temp;
                    	end = i;
                    }else {
                    	start = i;
                    	end = i;
                    }
                	max = dp[i];
                    
                }
            }
            System.out.println("Case "+jj+":");
            System.out.println(max+" "+start+" "+end);
            if(n!=0) {
            	System.out.println();
            }
            jj++;
        }
    }
}

(6)問題 1557: [藍橋杯][算法提高VIP]聰明的美食家:

分析:

最長不下降子序列不連續),-對於普通的最長不下降子序列,每個數都要從頭開始遍歷,複雜度 O(n^{2}),只能處理 10^{^{4}} 以內的數據。

暫時只會這一種方法嘿嘿

代碼實現:

import java.util.*;

public class Main {
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		while (sc.hasNext()) {
		int n = sc.nextInt();
		int []arr = new int[n+1];
		int []dp = new int[n+1];
		for (int i = 1; i < arr.length; i++) {
			arr[i]=sc.nextInt();
		}
			int max = -1;
		for (int i = 1; i <arr.length; i++) {
			dp[i]=1;
			for (int j = 1; j <i; j++) {
				if(arr[j]<=arr[i]&&dp[j]+1>dp[i]) {
					dp[i]=dp[j]+1;
				}
				max = Math.max(max,dp[i]);
			}
		}	
		System.out.println(max);
		
		}
	}
}

關鍵步驟:

舉一個例子:dp[i]代表以dp[i]結尾的最長上升序列(不連續

number【i】 13 7 9 16 38 24 37 18 44 19 21 22 63 15
dp【i】 1 1 2 3 4 4 5 4 6 5 6 7 8 3

 


 


 4.二維數組相關簡單例題


(1) 一個機器人位於一個 m x n 網格的左上角 (起始點在下圖中標記爲“Start” )。

機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記爲“Finish”)。

問總共有多少條不同的路徑?例如一個3x7的網格

    
第一步 確定dp【i】的含義(:當機器人從左上角走到(i, j) 這個位置時,一共有 dp[i] [j] 種路徑。那麼,dp[m-1] [n-1] 就是我們要的答案了。)
第二步 找出元素之間的關係
第三步 找到初始值

因爲每一步只能向右一步或者向下一步,所以有兩種方法到達
第一種是從dp【i-1】【j】向下走一步到達
第二種是從dp【i】【j-1】向右走一步到達
又因爲dp【i】【j】代表的是到達這一步的路徑方法
因爲是計算所有可能的步驟,所以是把所有可能走的路徑都加起來
所以有dp【i】【j】=dp【i-1】【j】+dp【i】【j-1】

找出初始值,因爲當i=0或者j=0時會出現負數情況所以我們的初始值是計算出所有的 dp[0] [0….n-1]
和所有的 dp[0….m-1] [0]。這個還是非常容易計算的,相當於計算機圖中的最上面一行和左邊一列。因此初始值如下:
    
 dp[0] [0….n-1] = 1; // 相當於最上面一行,機器人只能一直往左走

dp[0…m-1] [0] = 1; // 相當於最左面一列,機器人只能一直往下走   
    
    
代碼實現:
    

```java

public static void main(String[] args) {
        // TODO Auto-generated method stub
        Scanner sc = new Scanner(System.in);
        int m = sc.nextInt();
        int n = sc.nextInt();
        int [][]dp = new int[m][n];
        for (int i = 0; i < dp.length; i++) {
            for (int j = 0; j < dp[0].length; j++) {
                if(i==0|j==0) {
                    dp[i][j]=1;
                }
            }
        }
        for (int i = 1; i < dp.length; i++) {
            for (int j = 1; j < dp[0].length; j++) {
                dp[i][j]=dp[i-1][j]+dp[i][j-1];
            }
        }
        System.out.println(dp[m-1][n-1]);
    }
}
```
    


(2) 例題三
         最小路徑和問題

給定一個包含非負整數的 m x n 網格,請找出一條從左上角到右下角的路徑,使得路徑上的數字總和爲最小。

說明:每次只能向下或者向右移動一步。
輸入:
**arr=[1,3,1]
          [1,5,1]
          [4,2,1]**
第一步 定義dp的含義    
 由於我們的目的是從左上角到右下角,最小路徑和是多少,那我們就定義 dp[i] [j]的含義爲:。   
  當機器人從左上角走到(i, j) 這個位置時,最下的路徑和是 dp[i] [j]。那麼,dp[m-1] [n-1] 就是我們要的答案了

第二步 找出元素之間的關係
到達dp【i】【j】
第一種方法是從dp[i-1][j]向下走一步下來
第二種方法是從dp[i][j-1]向右走一步
選擇一種,使得dp[i] [j] 的值是最小的,顯然有
dp[i][j]=min{dp[i-1][j]+arr[i][j],dp[i][j-1]+arr[i][j]}

第三步 找出初始值
dp[0] [j] = arr[0] [j] + dp[0] [j-1]; // 相當於最上面一行,機器人只能一直往左走

dp[i] [0] = arr[i] [0] + dp[i-1] [0];  // 相當於最左面一列,機器人只能一直往下走
    
代碼實現:    

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Scanner sc = new Scanner(System.in);
        int m = sc.nextInt();
        int n = sc.nextInt();
        int [][]arr = new int[m][n];
        int [][]dp = new int[m][n];
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr[0].length; j++) {
                arr[i][j]=sc.nextInt();
            }
        }
        dp[0][0]=arr[0][0];
        for (int j = 1; j < dp[0].length; j++) {
            dp[0][j]=dp[0][j-1]+arr[0][j];
        }
        for (int i = 1; i < dp.length; i++) {
            dp[i][0]=dp[i-1][0]+arr[i][0];
        }
        for (int i = 1; i < dp.length; i++) {
            for (int j = 1; j < dp[0].length; j++) {
                dp[i][j]=Math.min(dp[i-1][j]+arr[i][j], dp[i][j-1]+arr[i][j]);
            }
        }
        System.out.println(dp[m-1][n-1]);
        }
    
}  

(3)  杭電oj2084:

分析:

在用動態規劃考慮數塔問題時可以自頂向下的分析,自底向上的計算

從頂點出發時到底向左走還是向右走應取決於是從左走能取到最大值還是從右走能取到最大值,只要左右兩道路徑上的最大值求出來了才能作出決策。同樣的道理下一層的走向又要取決於再下一層上的最大值是否已經求出才能決策。這樣一層一層推下去,直到倒數第二層時就非常明瞭。

所以第一步對第五層的8個數據,做如下四次決策:
如果經過第四層2,則在第五層的19和7中肯定是19;
如果經過第四層18,則在第五層的7和10中肯定是10;
如果經過第四層9,則在第五層的10和4中肯定是10;
如果經過第四層5,則在第五層的4和16中肯定是16;
經過一次決策,問題降了一階。5層數塔問題轉換成4層數塔問題,如此循環決策…… 最後得到1階的數塔問題
還要確定dp[i][j]的含義是到達這個點的最大值

代碼實現:-

import java.util.Scanner;

public class Main3 {
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		int n = sc.nextInt();
		while (n-- > 0) {
			int i = sc.nextInt();
			int[][] arr = new int[i][2 * i - 1];
			int kk = (2 * i - 1) / 2;
			for (int j = 0; j < arr.length; j++) {
				int ff = j;
				for (int j2 = kk; j2 < arr[0].length; j2 += 2) {
					arr[j][j2] = sc.nextInt();
					ff--;
					if (ff >= 0) {

					} else {
						break;
					}
				}
				kk--;
			}
			int[][] dp = new int[i][2 * i - 1];// 每條路的最大值,每個節點的最大值
			int jj = i - 1;
			int jjj = 1;
			int ff = 0;
			ff = jj;
			yy: for (int j = arr.length - 1; j < arr.length; j++) {
				for (int j2 = 0; j2 < arr[0].length; j2 += 2) {
					dp[j][j2] = arr[j][j2];
				}
				break yy;
			}
			for (int j = dp.length - 2; j >= 0; j--) {
				for (int j2 = jjj; j2 < dp[0].length; j2 += 2) {

					dp[j][j2] = Math.max(arr[j][j2] + dp[j + 1][j2 - 1], arr[j][j2] + dp[j + 1][j2 + 1]);
					ff--;
					if (ff <= 0) {
						break;
					}

				}
				jj--;
				ff = jj;
				jjj++;
			}

			System.out.println(dp[0][(2*i-2)/2]);
		}
	}
}

  揹包問題:

一、01揹包

內容:有n件物品和容量爲m的揹包 給出i件物品的重量以及價值 求解讓裝入揹包的物品重量不超過揹包容量 且價值最大 

特點:每個物品只有一件供你選擇放還是不放

(4)藍橋杯(01揹包問題):

  

有n個物品,它們有各自的體積和價值,現有給定容量的揹包,如何讓揹包裏裝入的物品具有最大的價值總和?

爲方便講解和理解,下面講述的例子均先用具體的數字代入,即:eg:number=4,capacity=8
                 

i(物品編號) 1   2 3 4
w(體積) 2 3 4 5
v(價值) 3 4 5 6
  1. 揹包問題的解決過程:

在解決問題之前,爲描述方便,首先定義一些變量:wi表示第 i 個物品的價值,ci表示第 i 個物品的體積,定義dp(i,j)(i個物品放入有j個空間的包),當前揹包容量 j,前 i 個物品最佳組合對應的價值,同時揹包問題抽象化:

尋找遞推關係式,面對當前商品有兩種可能性:

2.

第一種:當包容量小於第i個物體的體積,這時所得價值(dp[i][j])爲:dp[i][j]=dp[i-1][j];   

包的容量比該商品體積小,裝不下,此時的價值與前i-1個的價值是一樣的

因爲不放進去,所以說當前價值(dp[i][j])爲前i-1個物品的價值f[i-1][j]:當前i個物品用j個空間所擁有的價值就等於前i-1個物品用j個空間的價值,因爲物品i沒有放進去,

所以就相當於繼承dp[i-1][j]了,所以dp[i][j]=dp[i-1][j]

3.

第二種:第i件放進去,這時所得價值爲:dp[i][j]={dp[i-1][j-c[i]](沒裝之前的前i-1的)+w[i]}裝之後

裝了也不一定達到當前最優價值,因爲這件物品可能體積(c【i】)大,又沒有價值,如果c【i】越大,那麼沒裝這件物品之前的體積就越小(j-c【i),裝的可能會少,所以在裝與不裝之間選擇最優的一個,即dp(i,j)=maxdp(i-1,j)dp(i-1,j-c(i))+w(i)};

這裏需要解釋一下,爲什麼能裝的情況下,需要這樣求解(這纔是本問題的關鍵所在!):

可以這麼理解,如果要到達dp(i,j)這一個狀態有幾種方式?

肯定是兩種,第一種是第i件商品沒有裝進去,第二種是第i件商品裝進去了。沒有裝進去很好理解,就是dp(i-1,j);裝進去了怎麼理解呢?如果裝進去第i件商品,那麼裝入之前是什麼狀態,肯定是V(i-1,j-w(i))。由於最優性原理(上文講到),V(i-1,j-w(i))就是前面決策造成的一種狀態,後面的決策就要構成最優策略。兩種情況進行比較,得出最優。
4.

填表,首先初始化邊界條件,V(0,j)=V(i,0)=0;

然後一行一行的填表:

    如,i=1,j=1,w(1)=2,v(1)=3,有j<w(1),故V(1,1)=V(1-1,1)=0;
    又如i=1,j=2,w(1)=2,v(1)=3,有j=w(1),故V(1,2)=max{ V(1-1,2),V(1-1,2-w(1))+v(1) }=max{0,0+3}=3;
    如此下去,填到最後一個,i=4,j=8,w(4)=5,v(4)=6,有j>w(4),故V(4,8)=max{ V(4-1,8),V(4-1,8-w(4))+v(4) }=max{9,4+6}=10……

分析:

可以一行一行的分析

當i=1,j=1,2,3,4,5,6,7,8(揹包容量一點點擴大時)的時候,其實也就是遍歷了當i=1的時候,每一個揹包容量下他的最大價值dp[1][j];

以便dp[2][j-ci]+wi的使用,因爲dp[2][j-ci]代表了沒裝之前(前兩個物品並且揹包容量是j-ci)時的最大價值,遍歷完i=2,再i++並重復這個過程

5.

代碼實現:


import java.math.BigInteger;
import java.util.*;
import java.util.Scanner;

public class Main {
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		int n = sc.nextInt();
		int v = sc.nextInt();
		int[] mi = new int[n + 1];
		int[] ji = new int[n + 1];
		int [][]dp = new int[n+1][v+1];
		for (int i = 1; i <= n; i++) {
			mi[i] = sc.nextInt();
			ji[i] = sc.nextInt();
		}
		for (int i = 1; i < dp.length; i++) {
			for (int j = 1; j <=v; j++) {
				if(j-mi[i]>=0) {
					dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-mi[i]]+ji[i] );
				}else {
					dp[i][j]=dp[i-1][j];
				}
			}
		}
		System.out.println(dp[n][v]);
	}
}

二維數組缺點是隻能處理數據較小時 , 不能處理數據大的數據。

沒必要用二維數組,因爲外層循環已經代表了第幾個物品,所以只用後面的包容量就夠了,變成一維數組。

轉換成一維數組,dp【i】代表第i容量時最優解。

狀態轉移方程dp【i】=max(dp【i】,dp【i-m【i】】+w【i】)

代碼實現:

// 0,1揹包:一維法
	public static int bag2(int W, int[] w, int[] v) {
		int n = w.length - 1;// 第一個值,不算
		int[] dp = new int[v + 1];
		for (int i = 1; i <= n; i++)
		    for (int j = v; j >= m[i]; j--)
                         dp[j] = Math.max(dp[j], dp[j - m[i]] + w[i]);
 
		return f[v]; // 最優解

注意:這裏第二個for循環爲什麼從大到小?

一維數組,必須逆序查找

dp[i][j]=max(dp[i-1][j],dp[i-1][j-m[i]]+w[i]); 

壓縮空間:

dp[j]=max(dp[j],dp[j-m[i]]+w[i]);

這個方程非常重要,基本上所有揹包問題相關的問題的方程都是它衍生出來。

爲什麼逆序:

逆序的關鍵就在於這個狀態轉移方程,dp[i][j]只與dp[i-1][j]dp[i-1][j-m[i]]+w[i]有關,即只與i-1時刻狀態有關,所以我們只需要用一維數組dp【】來保存i-1時刻的狀態;

如果正序遍歷:

其面前的dp【0】,dp【1】,dp【2】......dp【v】都已經改變過了,裏面存的都不是i-1時刻的值,這樣求dp【j】時利用dp【j-m【i】】必定是錯的值,數組遍歷逆序大多是這個原因。

分析一下,因爲,m【i】是定值,j從0開始遞增,那麼j-m【i】的差值必定在j前面,是改變過的值,變成了i時刻,所以必須要逆序;

如果逆序遍歷,保證了i時刻dp【j-m【i】】是i-1時刻的狀態,因爲j是遞減的,m【i】爲定值

那麼每一個(j-m【i】)的差值都是上一個狀態的(i-1)狀態。

 

揹包問題二(多重揹包問題):

二、多重揹包

內容:有n件物品和容量爲m的揹包 給出i件物品的重量以及價值 還有數量 求解讓裝入揹包的物品重量不超過揹包容量 且價值最大 

特點:每個物品都有了一定的數量

例題:急!災區的食物依然短缺!
爲了挽救災區同胞的生命,心繫災區同胞的你準備自己採購一些糧食支援災區,現在假設你一共有資金n元,而市場有m種大米,每種大米都是袋裝產品,其價格不等,並且只能整袋購買。
請問:你用有限的資金最多能採購多少公斤糧食呢?

輸入數據首先包含一個正整數C,表示有C組測試用例,每組測試用例的第一行是兩個整數n和m(1<=n<=100, 1<=m<=100),分別表示經費的金額和大米的種類,然後是m行數據,每行包含3個數p,h和c(1<=p<=20,1<=h<=200,1<=c<=20),分別表示每袋的價格、每袋的重量以及對應種類大米的袋數。

Output

對於每組測試數據,請輸出能夠購買大米的最多重量,你可以假設經費買不光所有的大米,並且經費你可以不用完。每個實例的輸出佔一行。

Sample Input

1

8 2

2 100 4

4 100 2

Sample Output

400

題目思路:

如果01揹包問題解決,那麼多重揹包問題就不會太難,多重揹包問題在01揹包問題上新增了條件,就是每一樣物品有多少件,而01揹包只有一件,所以在解決多重揹包時還要解決的問題是這件物品要拿多少件時纔是最優,在兩個for循環中再來一個for循環遍歷件數,件數控制在小於給定的限制件數,並且j-k*m【i】要大於等於0

                        int dp[] = new int[v + 1];
			for (int i = 1; i <= nn; i++) {
				for (int j = v; j >= 0; j--) {//優化    int j = v; j >= m[i]; j--
					if(j>=m[i]){
						for (int j2 = 0; j2 <= k[i] && j - j2 * m[i] >= 0; j2++) {
							dp[j] = Math.max(dp[j], dp[j - j2 * m[i]] + j2 * w[i]);
						}	
				                    }else{
                                                        dp[j]=dp[j]

			}
			System.out.println(dp[v]);

優化:

                       int dp[] = new int[v + 1];
		for (int i = 1; i <= nn; i++) {
			for (int j = v; j >= 0; j--) {//優化    int j = v; j >= m[i]; j--				
				for (int j2 = 0; j2 <= k[i] && j - j2 * m[i] >= 0; j2++) {
					dp[j] = Math.max(dp[j], dp[j - j2 * m[i]] + j2 * w[i]);
						}
				}
			}
			System.out.println(dp[v]);

因爲比m【i】小的值還是等於上一狀態也就是dp【i-1】【j】的狀態,所以可以不用再次賦值,完成了優化。


之前自己的錯誤思想是每一件物品儘可能的多拿,比如有20的體積的揹包,這個大衣的體積是6,那麼我就拿三件,那麼價值也能乘3,這不就更多嗎,但是dp[i][j]是由dp[i-1][j]和dp[i-1][j-k*m[i]]決定,如果想讓dp[i][j]大,那麼dp[i-1][j-k*m[i]]就要大,如果拿三件,那麼j-k*m[i]的值就小,即揹包之前的容量被壓縮的小了。剩2,假如一個鑽石的體積是3,價值999999,剩2的容量就拿不了鑽石,而拿三件大衣價值才三四十塊錢,所以,要一件一件的遞增,遞增到兩件大衣時,剩容量8,不就可以多拿幾個鑽石,這多好啊。所以要看看到底拿幾件時才能達到最優解。而不是一下儘可能多拿

二進制優化做法:

        首先,可以把它轉化爲一個01揹包的問題。每個物品有s件,我們可以把它差分成s份,每份物品當做不同的個體,即只能選一次,這就轉化爲了01揹包物品,但是這樣的話,複雜度還是很大。

        繼續優化,一個物品的數量是s的話,只要把s拆分成一些數字,使它們能夠表示出1-s中任意一個數字,就可以,沒必要把它拆成s個1。

        那麼這樣的數字最少需要多少個呢?最少需要log(s)個,向上取整。
比如7,它最少需要3個數字來表示:
即 1(2^0=1 ), 2(2^1=2), 4(2^2=4)。原因:每個數字有2種可能選或不選,那麼可以表示的不同數字個數就是 2 * 2 * 2 = 8。但是還需要注意一個問題,就是有些數字可能能夠表示出來一些大於s的數字,但是這件物品最多隻有s件,那麼就需要特殊處理一下最後一個數。
        比如10,若用1,2, 4, 8表示,可能會表示出來大於10的數字,例如:4+8=12。那麼如果最後一個數字加上前面數的總和會大於s,就將它替換爲剩下的物品個數,即將8替換爲3,這時正好能表示出1-s所有的數,-> 1, 2,4可以表示7以內的所有數,這些數加上3就可以表示10以內的所有數啦。
——參考揹包九講

        // 多重揹包:一維法-二進制優化

  public static int bag4(int W, int[] w, int[] v, int[] num) {
            int n = w.length;
            int[] f = new int[W + 1];
            int[] vv = new int[n*W];
            int[] ww = new int[n*W];
            // 複製w和v
            for(int i=0;i<n;i++)
            {
                vv[i] = v[i];
                ww[i] = w[i];
            }
            k = n;
            for(int i=0;i<n;i++)
            {
                s = num[i]
                for(int i=1;i<=s;i*=2)   //二進制優化
                    vv[k]=v[i]*i,ww[k++]=w[i]*i,s-=i;
                if(s>0)
                    vv[k]=v[i]*s,ww[k++]=w[i]*s;
            }
     
            for (int i = 1; i <= k; i++)  //01揹包
                for (int j = W; j >= ww[i]; j--)
                            f[j] = Math.max(f[j], f[j - ww[i]] + vv[i]);
     
            return f[W]; // 最優解
        }

全部代碼實現:

import java.util.*;

public class Main {
			
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		int n = sc.nextInt();
		while (n-- > 0) {
			int v = sc.nextInt();
			int nn = sc.nextInt();
			int[] m = new int[nn + 1];
			int[] w = new int[nn + 1];
			int[] k = new int[nn + 1];
			for (int i = 1; i <= nn; i++) {
				m[i] = sc.nextInt();
				w[i] = sc.nextInt();
				k[i] = sc.nextInt();
			}
			int b = 0;
			int dp[] = new int[v + 1];
			for (int i = 1; i <= nn; i++) {
				for (int j = v; j >= 0; j--) {//優化    int j = v; j >= m[i]; j--
					if (j >= m[i]) {
						for (int j2 = 0; j2 <= k[i] && j - j2 * m[i] >= 0; j2++) {
							dp[j] = Math.max(dp[j], dp[j - j2 * m[i]] + j2 * w[i]);
						}
					}else {
						dp[j]=dp[j];
					}
				}
			}
			System.out.println(dp[v]);
		}
	}
}

三、完全揹包

內容:有n件物品和容量爲m的揹包 給出i件物品的重量以及價值 求解讓裝入揹包的物品重量不超過揹包容量 且價值最大 

特點: 每個物品可以無限選用

完全揹包跟多重揹包的不同點就是完全揹包沒有限制件數,只要(j-件數*m【i】)大於等於0就可以

                        int dp[] = new int[v + 1];
			for (int i = 1; i <= nn; i++) {
				for (int j = v; j >= 0; j--) {//優化    int j = v; j >= m[i]; j--
					if (j >= m[i]) {
						for (int j2 = 0;  j - j2 * m[i] >= 0; j2++) {
							dp[j] = Math.max(dp[j], dp[j - j2 * m[i]] + j2 * w[i]);
						}
					}else {
						dp[j]=dp[j];
					}
				}
			}
			System.out.println(dp[v]);

優化: 轉換成從小到大,因爲每個物品可以用無限次,只要不準超過重量,就不在乎之前i(上一層)是否用過。

// 完全揹包:一維法-正序
	public static int bag3(int W, int[] w, int[] v) {
		int n = w.length - 1;// 第一個值,不算
		int[] dp = new int[v + 1];
		for (int i = 1; i <= n; i++)
			for (int j = m[i]; j <= v; j++)
				dp[j] = Math.max(dp[j], dp[j - m[i]] + w[i]);
		return dp[v]; // 最優解
	}

完全揹包代碼段:

    for(int i=1;i<=n;i++)
        for(int j=w[i];j<=V;j++)
            f[j]=max(f[j],f[j-w[i]]+c[i]);

而這是01揹包的核心代碼段:

for(int i=1;i<=n;i++){
    for(int j=m;j>=s[i];j--){
        f[j]=max(f[j],f[j-s[i]]+v[i]);}}

大家看出區別了嗎,沒錯,將逆序改成順序,因爲我們需要遞推的是當前藥品_更新後_發生改變的狀態。所以要順序求解。

 

                                                  總結

詳解了幾道簡單的一維數組還有二維數組,陸續將不斷更新相關動態規劃的題目

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章