这里单独写一下动态规划专题,蓝桥杯当中一定会出一道动态规划的题目所以稍微汇总一下。
(源于蓝桥杯2020届一道题目)
1.某市市长获得了若干批口罩,每一批口罩的数目如下:(如果你把以下文字复制到文本文件中,请务必检查复制的内容是否与文档中的一致。在试题目录下有一个文件 mask.txt,内容与下面的文本相同)
9090400,8499400,5926800,8547000,4958200,4422600,5751200,4175600,6309600
,5865200,6604400,4635000,10663400,8087200,4554000
现在市长要把口罩分配给市内的 2 所医院。由于物流限制,每一批口罩只能全部分配给其中一家医院。市长希望 2 所医院获得的口罩总数之差越小越好。请你计算这个差最小是多少?
首先求最值那么一般思路都是往dp方向上去靠,同时这道题又是最经典动态规划中的背包问题,首先先要判断数是否可重用,不可重用就是子集背包问题,可重用就是完全背包问题,这两者之间有微妙的差别
蓝桥杯算法模板常用套路
Ps:就是这其中内层循环的差别,该题题解如下,j应该是从后往前反向遍历,因为每个物品(或者说数字)只能用一次,以免之前的结果影响其他的结果。最值问题一般是(dp动态规划,那么这道题是一道变形的背包问题)
对于背包容量为sum/2的有i批个口罩,口罩数为nums[i],那么最大差值是多少
static int[] dp = new int[98090000];
static int sum = 0;
static int[] nums = {0,9090400,8499400,5926800,8547000,4958200,4422600,5751200,4175600,6309600,
5865200,6604400,4635000,10663400,8087200,4554000};
public static void main(String[] args) {
//动态规划之子集背包问题
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
System.out.println(sum);
int v = sum / 2;
for (int i = 1; i < nums.length; i++) {
for (int j = v; j >= nums[i]; j--) {
//j >= nums[i]说明背包空间充足仍然可以继续存放
dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
System.out.println(dp[v]);
System.out.println(sum - dp[v]*2);
}
2.再来看一道题目,输入一个只包含正整数的非空数组nums,请你写一个算法,判断这个数组是否可以背分割成两个子串,使得两个子集的元素和相等。
输入nums[1,5,11,5],算法返回true,因为nums可以分割成[1,5,5]和[11]
输入nums[1,3,2,5],算法返回false,因为nums无论如何都不能分割成两个和相等的子集
可以把问题转换成背包问题,对于容量为sum/2的背包和i个物品,其他重量为nums[i],是否存在一种装法,恰好将背包装满,能转化为背包问题就直接套用模板
static int sum = 0;
static int[] nums = {0,1,5,11,5};
static boolean[] dp = new boolean[22];
public static void main(String[] args) {
System.out.println(canPartition(nums));
}
public static boolean canPartition(int[] nums) {
dp[0] = true;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
if(sum % 2 != 0) return false;
int v = sum / 2;
for (int i = 1; i < nums.length; i++) {
for (int j = v ; j >= nums[i]; j--) {
//j >= nums[i]判断是否可以放入背包
dp[j] = dp[j]||dp[j - nums[i]];
}
}
return dp[v];
}
3.那么接下来我们看一道完全背包问题,零钱兑换,给定不同面额的硬币coins和一个总金额amount,写一个函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
比如输入amount = 5,coins = [1,2,5],算法应该返回4,因为有如下四种方式可以凑出目标金额:
5 = 5
5 = 2 + 2 + 1
5 = 2 + 1 + 1 + 1
5 = 1 + 1 + 1 + 1 + 1
我们可以把这个问题转换成背包问题,对于背包容量为5的背包有i个硬币,硬币的大小为coins[i],数量为无限个。(无限个意味着可以重复使用硬币,参照背包问题和完全背包的套路模板,其实区别就是在此是否可重用),对于内层循环就是背包容量进行循环,前者是从z后往前(防止重复使用影响节后),后者是从前往后循环。
public class bagdemo1test {
public static void main(String[] args) {
int amount = 5;
int[] coins = new int[]{1,2,5};
System.out.println(moneychange(amount, coins));
}
public static int moneychange(int amount,int[] coins) {
//base case
//dp[0] = 1,
int[] dp = new int[amount + 1];
dp[0] = 1;
for (int i = 0; i < coins.length; i++) {
for (int j = 1; j <= amount; j++) {
if(j - coins[i] >= 0)
dp[j] = dp[j] + dp[j - coins[i]];
}
}
return dp[amount];
}
}
来做三道连续的dp数组题目来巩固一下动态规划
4.leetcode打家劫舍题目1
Ps:这道题要求求最值,首先先找状态和选择,状态很明显就是可选择的房屋,那么选择就是选择该房子或者不选择该房子,根据套路模板
for(状态1 in 状态1 的所有取值)
for(状态2 in 状态2 的所有取值)
dp[i] = 计算(选择1,选择2)
//关键就是在于对dp数组的定义并且这个计算,指的是要对于题目而言dp数组的定义来进行计算
那么可以观察该题目,因为题目的dp[i]只与dp[i + 1]和dp[i + 2]的值有关,所以可以将空间复杂度降为O(1),如下所示.
public class Demo2 {
//状态 可选择的房屋数
//选择:选择当前房屋或者不选择当前房屋
public int rob(int[] nums) {
int n = nums.length;
int dp_i = 0;
int dp_i_1 = 0;
int dp_i_2 = 0;
for (int i = nums.length - 1; i >= 0; i--) {
dp_i = Math.max(dp_i_1, dp_i_2 + nums[i]);
dp_i_2 = dp_i_1;
dp_i_1 = dp_i;
}
return dp_i;
}
}
5.Leetcode打家劫舍2
这道题和上一题类似,但是房屋是环形的,其实可以就只有三种情况
1.要么第一个和最后一个都不取
2.第一个取,最后一个不取
3.最后一个取,第一个不取
后两者已经将第一种情况涵括,将后两者的情况进行求最值即可。
public class Demo3 {
/*
* 环形考虑三种情况,1.要么都不选择第一个和最后一个
* 2.选择第一个不选最后一个
* 3.选择最后一个不选第一个
* 相当于在三种情况下选择一个最大值
* 后两者的情况已经包含第一种情况
*
* */
public int rob(int[] nums) {
int n = nums.length;
if(n == 1)return nums[0];
return Math.max(robRange(nums, n - 1, 1), robRange(nums, n - 2, 0));
}
/*
* 其实和上一道题差不多,就是要对参数进行更改就行
* */
public int robRange(int[] nums,int end,int start) {
int n = nums.length;
int dp_i = 0;
int dp_i_1 = 0;
int dp_i_2 = 0;
for (int i = end; i >= start; i--) {
dp_i = Math.max(dp_i_1, dp_i_2 + nums[i]);
dp_i_2 = dp_i_1;
dp_i_1 = dp_i;
}
return dp_i;
}
}
6.Leetcode打家劫舍3
那么这道题涵盖递归和动态规划,首先树形结构一定要考虑递归,并且对于递归树,一定要对其进行剪枝,用一个Hashmap<TreeNode,Integer> memo备忘录取存放值,如果备忘录中存在值就可以取出。
public class Demo4 {
/*
* 首先看到树肯定是有递归,并且递归树有重复的值,一定要剪枝。
*
* */
//memo备忘录
static HashMap<TreeNode, Integer> memo = new HashMap<TreeNode, Integer>();
public int rob(TreeNode root) {
if(root == null) return 0;
if(memo.containsKey(root)) return memo.get(root);
//进行递归
//选择装或不装,装的话就跳下一个房屋中去,不装的话值不发生改变
int do_it = root.val
+ (root.left == null ? 0 : rob(root.left.left) + rob(root.left.right))
+ (root.right == null? 0 : rob(root.right.left) + rob(root.right.right));
int not_do = rob(root.right) + rob(root.left);
int res = Math.max(do_it, not_do);
memo.put(root, res);
return res;
}
}
总结:那么我们来探讨一下动态规划和回溯算法之间的关系
先说一下回溯算法,dfs和回溯算法对于这两者而言,我个人也是模棱两可,而对于套路模板中,我所发现的动态规划和dfs之间有相似之处就是,有时候要区别对数字是否可重用有一些相应的操作。