动态规划及其应用

                                       动态规划

         


杭电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]);}}

大家看出区别了吗,没错,将逆序改成顺序,因为我们需要递推的是当前药品_更新后_发生改变的状态。所以要顺序求解。

 

                                                  总结

详解了几道简单的一维数组还有二维数组,陆续将不断更新相关动态规划的题目

 

 

 

 

 

 

 

 

 

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