蓝桥杯刷题心得————动态规划

这里单独写一下动态规划专题,蓝桥杯当中一定会出一道动态规划的题目所以稍微汇总一下。

(源于蓝桥杯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 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之间有相似之处就是,有时候要区别对数字是否可重用有一些相应的操作。

你可能感兴趣的:(算法,动态规划)