例子
参考链接: link.
问题:如果我们有面值为1元、3元和5元的硬币若干枚,如何用最少的硬币凑够11元?
package com.szu.DP;
/**
* @author chen
* @create 2020-08-16 22:07
*/
/*
硬币面值:1,3,5
凑i元需要至少硬币数dp[i]
*/
public class coinCountTest {
public static void main(String[] args) {
// 面值
int[] a = {1, 3, 5};
// 凑钱数
int sum = 11;
// 最少硬币数
int[] dp = new int[sum + 1];
dp[0] = 0;
// 设置最大硬币枚数:全部都是选面值1的硬币时。
for (int i = 0; i <= sum; i++) {
dp[i] = i;
}
for (int i = 0; i <= sum; i++) {
for (int j = 0; j < 3; j++) {
// 注意此时是从小面值选到大面值:因为小选面值硬币所需的硬币数肯定比选大面值硬币所需的硬币数要大。先行小的,如果大的行得通,则用大的覆盖小的。
if (i >= a[j] && dp[i - a[j]] + 1 <= dp[i]) {
// 状态转移方程:细分出子问题,问题问的是凑够i元所需的最小硬币数dp[i],关键是硬币数dp。
// 细分出下一个子问题为dp[i]-1:“1”表示首先选择一枚硬币,“i - a[j]”表示除去该硬币之后还需凑的钱数,“dp[i -
// a[j]]”表示“除去该硬币之后还需凑的钱数”的最小硬币数。
dp[i] = dp[i - a[j]] + 1;
System.out.println("凑" + i + "元时,选择了硬币:" + a[j] + "需要最小的硬币数" + dp[i]);
}
}
}
// for (int i = 0; i < dp.length; i++) {
// //
// System.out.println("凑" + i + "元时,需要最小的硬币数" + dp[i]);
// }
}
}
通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,具有天然剪枝的功能,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
最大子序列和
-
步骤一、定义状态 -> 定义数组元素的含义
定义 dp[i] 为以 i 结尾子串的最大值 -
步骤二、状态转移方程 -> 找出数组元素间的关系式
-
步骤三、初始化 -> 找出初始条件
dp[0] = nums[0]; -
步骤四、状态压缩 -> 优化数组空间
每次状态的更新只依赖于前一个状态,就是说 dp[i] 的更新只取决于 dp[i-1] , 我们只用一个存储空间保存上一次的状态即可。 -
步骤五、选出结果
有的题目结果是 dp[i] 。
本题结果是 dp[0]…dp[i] 中最大值。
public class Solution {
public int maxSubArray(int[] nums) {
int len = nums.length;
if (len == 0) {
return 0;
}
int[] dp = new int[len];
dp[0] = nums[0];
for (int i = 1; i < len; i++) {
if (dp[i - 1] >= 0) {
dp[i] = dp[i - 1] + nums[i];
} else {
dp[i] = nums[i];
}
}
// 最后不要忘记全部看一遍求最大值
int res = dp[0];
for (int i = 1; i < len; i++) {
res = Math.max(res, dp[i]);
}
return res;
}
}
- 状态压缩 , 来看看代码,我们只需要一个变量subMax保存前面子组合的最大值,另外一个max保存全局最大值。
public int maxSubArray(int[] nums) {
if (nums == null) {
return 0;
}
int max = nums[0]; // 全局最大值
int subMax = nums[0]; // 前一个子组合的最大值,状态压缩
for (int i = 1; i < nums.length; i++) {
if (subMax > 0) {
// 前一个子组合最大值大于0,正增益
subMax = subMax + nums[i];
} else {
// 前一个子组合最大值小于0,抛弃前面的结果
subMax = nums[i];
}
// 计算全局最大值
max = Math.max(max, subMax);
}
return max;
}
区域和检索 - 数组不可变
class NumArray {
private int[] sum;
public NumArray(int[] nums) {
sum = new int[nums.length + 1];
for(int i = 0; i < nums.length; i++){
sum[i+1] = sum[i] + nums[i];
}
}
public int sumRange(int i, int j) {
return sum[j + 1] - sum[i];
}
}
/**
* Your NumArray object will be instantiated and called as such:
* NumArray obj = new NumArray(nums);
* int param_1 = obj.sumRange(i,j);
*/
连续的子数组和
给定一个包含 非负数 的数组和一个目标 整数 k,编写一个函数来判断该数组是否含有连续的子数组,其大小至少为 2,且总和为 k 的倍数,即总和为 n*k,其中 n 也是一个整数。
// 自己的想法
// class Solution {
// private int[] sum;
// public boolean checkSubarraySum(int[] nums, int k) {
// if(nums.length < 2){
// return false;
// }
// sum = new int[nums.length + 1];
// subArraySum(nums);
// for(int i = 0; i < nums.length; i++){
// for(int j = i + 1; j < nums.length; j++){
// if(k!=0 && ((sum[j+1]-sum[i]) % k == 0)){
// return true;
// }
// if(k == 0 && sum[j+1]-sum[i] == 0){
// return true;
// }
// }
// }
// return false;
// }
// public void subArraySum(int[] nums){
// for(int i = 0; i < nums.length; i++){
// sum[i+1] = sum[i] + nums[i];
// }
// }
// }
// 官方的暴力法
// public class Solution {
// public boolean checkSubarraySum(int[] nums, int k) {
// for (int start = 0; start < nums.length - 1; start++) {
// for (int end = start + 1; end < nums.length; end++) {
// int sum = 0;
// for (int i = start; i <= end; i++)
// sum += nums[i];
// if (sum == k || (k != 0 && sum % k == 0))
// return true;
// }
// }
// return false;
// }
// }
// 官方暴力法的优化:与我思路一致,但其代码更好
// public class Solution {
// public boolean checkSubarraySum(int[] nums, int k) {
// int[] sum = new int[nums.length];
// sum[0] = nums[0];
// for (int i = 1; i < nums.length; i++)
// sum[i] = sum[i - 1] + nums[i];
// for (int start = 0; start < nums.length - 1; start++) {
// for (int end = start + 1; end < nums.length; end++) {
// int summ = sum[end] - sum[start] + nums[start];
// if (summ == k || (k != 0 && summ % k == 0))
// return true;
// }
// }
// return false;
// }
// }
// 官方解法 HashMap:map的相同位置key(余数)上,存在不同的value(i或者j),表明nums的位置i与位置j的子数组的和是k的倍数。此时要找的子数组就是位置i与位置j的子数组。对于特殊情况,即第一次(map的value = 1)就碰到子数组和为n*k,如何比较,此时map.put(0, -1)发挥作用了,当子数组和为n*k的余数为0,且i == 1(map的value = 1)时,i-(-1) > 1 满足题意子数组“其大小至少为 2”的条件。
public class Solution {
public boolean checkSubarraySum(int[] nums, int k) {
int sum = 0;
HashMap <Integer, Integer> map = new HashMap <> ();
map.put(0, -1); //
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
if (k != 0)
sum = sum % k;
if (map.containsKey(sum)) {
if (i - map.get(sum) > 1)
return true;
} else
map.put(sum, i);
}
return false;
}
}
贪心算法对每个子问题的解决方案都做出选择,不能回退;动态规划则会根据以前的选择结果对当前进行选择,有回退功能。
动态规划主要运用于二维或三维问题,而贪心一般是一维问题。