动态规划-01背包系列题,选取动态规划中采用01背包模型解题思路的几个例题
1.01背包问题解题思路
2.01背包问题实现
3.01背包系列-最多任务工资
4.01背包系列-数列不相邻数字最大和
5.01背包系列-数列找到指定和子数列
1.01背包问题解题思路
从右到左进行解析,分析第n个物品的状态,第n个物品只有“放”和“不放“俩种状态,在“放”状态下,某些变量发生变化,以及放情况下,与前面子问题之间的联系。在“不放”状态下,即考虑前n-1个物品。
每个物品都有俩种状态,动态规划,相当于列出所有情况,从中挑选子问题的最优解。
所以首先要从后往前分析问题,给出状态转移方程,然后判定边界情况,然后从前往后实现代码,填充中间数组(一维数组,二维数组)。
2.01背包问题实现
题目:有一个包和n个物品,包的容量为m,每个物品都有各自的体积和价值。问当这n个物品放在包里而物品的体积总数不超过包的容量m时,如何让背包具有最大价值?
分析:
1.建模:使用数组分别存储这n个物品的体积和价值
v[n] = {v1,v2,v3,….vn};
w[n] = {w1,w2,w3,..wn};
2.确定子问题:n个物品加入容量为m的包里f(n,m),第n个物品有俩种选择,放或不放,如果放,包的剩余容量为m-vn,价值为wn + f(n-1,m-vn)
如果不放,包的剩余容量为m,价值为f(n-1,m)
3.状态转移方程:f(n,m) = max{wn+f(n-1,m-vn),f(n-1,m)}
4.边界情况:
- n == 0 retutn 0
- m ==0 return 0
- m < 0 return f(n-1,m)
5.动态规划二维数组(画图更直观)
dp[i][j] = max{dp[i-1][j-vi]+wi,dp[i-1][j]}
dp[i][j]表示前i件物品中,选择若干件放在容量为j的包里的最大价值1
6..代码实现
代码中给出递归法和动态规划法,用以比较
public class Bags {
public static void main(String[] args){
int[] weight = {3,4,7,9,13};
int[] volume = {2,4,3,5,7};
int m = 11;
int n = weight.length-1;
int res = getMaxValue(volume,weight,m,n);
System.out.println(res);
int res_dp = getMaxValue_DP(volume,weight,m);
System.out.println(res_dp);
}
/**
* 使用二维数组保存中间结果
* @param
* @param m
* @return
*/
private static int getMaxValue_DP(int[] volume, int[] weight, int m) {
if (volume.length == 0)
return 0;
if (m == 0)
return 0;
int[][] dp = new int[volume.length][m];
//填充第一列
for (int i = 0; i < dp.length; i++){
if (volume[i] <= 1){
dp[i][0] = dp[i][0] < weight[i] ? weight[i] : dp[i][0];
}
}
//填充第一行
for (int i = 0; i < m; i++){
if (volume[0] <= i+1){
dp[0][i] = weight[0];
}
}
//从左到右,从上到下依次填充
for (int i = 1; i < weight.length; i++){
for (int j = 1; j < m; j++){
int a = dp[i-1][j];
int b = Integer.MIN_VALUE;
if (j-volume[i] >= 0)
b = dp[i-1][j-volume[i]]+weight[i];
dp[i][j] = Math.max(a,b);
}
}
return dp[volume.length-1][m-1];
}
/**
* 递归方法-获得最大质量
* 存在重叠子问题,空间复杂度,时间复杂度高
* @param
* @param m
* @return
*/
private static int getMaxValue(int[] volume, int[] weight, int m,int n) {
if (m == 0 || n == 0)
return 0;
if (volume[n] > m)
return getMaxValue(volume, weight, m, n - 1);
int a = getMaxValue(volume, weight, m, n - 1);//不放
int b = getMaxValue(volume, weight, m - volume[n], n - 1) + weight[n];//放
return Math.max(a,b);
}
}
说明:此题中动态规划先填充第一行,第一列,然后依次从左到右,从上到下,填充二维数组中其他部位。
3.最多任务工资
问题:每个任务都有开始时间和结束时间,完成每个任务都有相应的工资,但执行每个任务不能有时间冲突,求所能赚取的最大工资数。
分析:同样和上面解题思路一致
1.建模:
使用数组分别存储n个任务的开始时间,结束时间,以及工资
start[n] = {s1,s2,…sn};
end[n] = {e1,e2,…en};
money[n] = {m1,m2,..mn};
2.确定子问题:
执行n个任务f(n),第n个任务有俩种状态,做或不做。
如果做,f(n) = money(n) + f(pre[n])
如果不做,f(n) = f(n-1)
pre[n]表示是在不冲突情况下,第n个任务的前一个可执行任务
3.状态转移方程:
f(n) = max{money(n) + f(pre[n]),f(n-1)}
4.边界情况:
n == 0 return 0
pre[n] < 0 return f(n-1)
5.动态规划-数组(一维数组)
6.代码实现:
package com.zd.dp;
import java.util.Arrays;
/**
* 工作任务问题
* 求赚取的最大金额数
*/
public class Task {
public static void main(String[] args){
int n = 5;
int[] start = {1,2,3,5,6};
int[] end = {3,5,6,9,10};
int[] money = {5,10,4,12,10};
int res = getMaxMoney(n,start,end,money);
System.out.println(res);
int res_dp = getMaxMoney_dp(n,start,end,money);
System.out.println(res_dp);
}
/**
* 使用动态规划解决
* @param n
* @param start
* @param end
* @param money
* @return
*/
private static int getMaxMoney_dp(int n, int[] start, int[] end, int[] money) {
if (n == 1)
return money[0];
int[] pre = new int[n];//上一个可以执行的最近任务
for (int i = n-1; i >= 0; i--){
pre[i] = -1;
for (int j = i-1; j >= 0; j--){
if (end[j] <= start[i]) {
pre[i] = j;
break;
}
}
}
int[] dp = new int[n];
for (int i = 1; i < n; i++){
int a = money[i];
int b = dp[i-1];
if (pre[i] != -1)
a += dp[pre[i]];
dp[i] = Math.max(a,b);
}
return dp[n-1];
}
private static int getMaxMoney(int n, int[] start, int[] end, int[] money) {
if (n == 0)
return 0;
int[] pre = new int[n];//每个任务上一个可以执行的任务
for (int i = n-1; i >= 0; i--){
pre[i] = -1;
for (int j = i-1; j >= 0; j--){
if (end[j] <= start[i]) {
pre[i] = j;
break;
}
}
}
System.out.println(Arrays.toString(pre));
int res = getMaxMoneys(n-1,money,pre);
return res;
}
private static int getMaxMoneys(int i, int[] money, int[] pre) {
if (i < 0)
return 0;
int a = getMaxMoneys(i-1,money,pre);//不做
int b = getMaxMoneys(pre[i],money,pre)+money[i];//做
return Math.max(a,b);
}
}
4.不相邻数字最大和
问题:在数列中选择数字,需要保证不能选择相邻数字,要求所选的数字和最大
分析:在前n个数字中,第n个数字有俩种选择,选或不选,如果选,下一个可选位置从n-2处开始。
状态转移方程:f(n) = max{f(n-1),a[n-1]+f(n-2)}
边界情况:
n==1 return a[0]
n==2 return Math.max(a[0],a[1])
代码实现:
package com.zd.dp;
/**
* 从数组中选出不相邻的数字,要求所选的数字和最大
*/
public class SelectMaxSum {
public static void main(String[] args){
int[] array = {5,6,1,2,7};
int n = 5;
int res = getMax(array,n-1);
System.out.println(res);
int res_dp = getMax_dp(array,n);
System.out.println(res_dp);
}
private static int getMax_dp(int[] array, int n) {
int[] dp = new int[n];
dp[0] = array[0];
dp[1] = Math.max(array[0],array[1]);
for (int i = 2; i < n; i++)
dp[i] = Math.max(array[i] + dp[i-2],dp[i-1]);
return dp[n-1];
}
private static int getMax(int[] array, int n) {
if (n == 0)
return array[0];
if (n == 1)
return Math.max(array[0],array[1]);
int a = array[n] + getMax(array,n-2);
int b = getMax(array,n-1);
return Math.max(a,b);
}
}
5.判断指定和的子序列是否存在
问题:给定值,判断在数列中是否存在和为给定值的子数列
分析:
状态转移方程:
f(n,m) = f(n-1,m) || f(n-1,m-a[n-1])
边界情况:
m == 0 return true
m < 0 return false
n < 0 return false
代码实现:
package com.zd.dp;
/**
* 给定值,判断数列中是否存在和为给定值的子数列
*/
public class IsSumOfValue {
public static void main(String[] args){
int[] a = {5,6,1,2,7,4};
int sum = 9;
int i = a.length;
boolean res = isSumOfValue(a,sum,i-1);
System.out.println(res);
boolean res_dp = isSumOfValue_dp(a,sum,i);
System.out.println(res_dp);
}
/**
* 动态规划解决
* @param a
* @param sum
* @param i
* @return
*/
private static boolean isSumOfValue_dp(int[] a, int sum, int n) {
boolean[][] dp = new boolean[n][sum];
//填充第一行
for (int i = 0; i < sum; i++){
dp[0][i] = a[0] == i ? true:false;
}
//填充第一列
for (int i = 1; i < n; i++){
dp[i][0] = true;
}
//从左到右,从上到下依次填充二维数组
for (int i = 1; i < n; i++){
for (int j = 1; j < sum; j++){
boolean A = false;//选
boolean B = dp[i-1][j];//不选
if (sum - a[i] > 0)
A = dp[i-1][sum-a[i]];
dp[i][j] = A || B;
}
}
boolean res = false;
for (int i = 0; i < n; i++){
res = res || dp[i][sum-1];
}
return res;
}
/**
* 递归解决
* @param a
* @param sum
* @param i
* @return
*/
private static boolean isSumOfValue(int[] a, int sum, int i) {
if (sum == 0)
return true;
if (sum < 0)
return false;
if (i < 0)
return false;
boolean res = isSumOfValue(a,sum-a[i],i-1) || isSumOfValue(a,sum,i-1);
return res;
}
}
总结:分析过程即是从后往前分析,解题是从前往后解题,动态规划过程,即是填充数组元素过程。重点是确定状态转移方程,有时候边界情况也不好确定。