系列文章目录
文章目录
- 系列文章目录
- 一、认识算法
- 动态规划难在哪?
- 学习目标
- 二、记忆化搜索 非常直觉的处理方式
- 注意:
- 三、70.爬楼梯 入门 模板
- 通过记忆化搜索 发现动态规划四要素
- 四、118.杨辉三角 使用答案空间处理(题目给了返回值的样式)
- 五、198.打家劫舍 记忆化搜索转化
- 六、279.完全平方数 背包问题
- 七、322.零钱兑换 背包问题
- 八、139.单词拆分
一、认识算法
不同于二分查找和堆排序,这种有明确步骤的算法,用一个不太恰当的例子,就像我们在做菜的时候,一般算法就像是先放两克盐,再放两克鸡精,动态规划更像是说先加盐少许,再加鸡精少许,最后达到好吃即可。也是将大问题拆分成小问题。
动态规划难在哪?
- 没有一个统一解的算法思想,不容易学透。
- 不同方法间的难度差距很大
学习目标
一月入门,二月上手
结合目标,以退为进
如果不是为了进大厂,不需要掌握很好,掌握基本题目即可。
二、记忆化搜索 非常直觉的处理方式
- 首先初始化一个保存记忆搜索内容的缓存,将它初始化成一些数字,这里为“-1”。
- 然后在我们执行递归的方法中,往往我们一上来先判断终止条件,这里模板上的终止条件是小于等于1,这时候我们就返回默认值。
- 然后判断是否命中记忆缓存,如果命中,直接返回缓存。 然后执行我们的状态转移方程,这里状态转移方程是
dp[n]=dp[n-1]+dp[n-2]
。 - 之后更新缓存,并返回结果。
注意:
题目如果足够简单就不用了。
因为使用了系统栈,速度较慢,可能会超时。
当我们完成了记忆化搜索的动态规划后,我们可以根据现在实现的逻辑,将其改为使用矩阵的状态转移动态规划。下面用具体例子练习。
三、70.爬楼梯 入门 模板
解题思路:
我们可以想到对于爬到第n个台阶,它有两种情况被爬到,第一种从第n-1阶台阶爬1个台阶,第二种从第n-2阶台阶爬2个台阶。所以它被爬到的不同方法,是这两种情况的总和。并且我们使用记忆化搜索(比较符合直觉)。代码如下所示:
class Solution {
HashMap<Integer,Integer> map = new HashMap<>();
public int climbStairs(int n){
if(n == 1){
return 1;
}
if(n == 2){
return 2;
}
if(map.containsKey(n)){return map.get(n);}//记忆力搜索。
int res =climbStairs(n-1)+ climbStairs(n-2);
map.put(n,res);
return res;
}
}
如果我们按照从下向上的方式上台阶的话,从0和1的位置都只有一种答案,那我们就从0和1开始填写,填写过程:2=1+0,3=1+2,4=3+2(此处数字表示位置)我们可以看出可以通过for循环完成对数组的填写,代码如下:
通过记忆化搜索 发现动态规划四要素
- 状态类型(前缀的、坐标的、区间的)
- 转移方程 比如
res =climbStairs(n-1)+ climbStairs(n-2);
- 数据初始化
- 答案位置 比如本体最终答案在n上。
四、118.杨辉三角 使用答案空间处理(题目给了返回值的样式)
class Solution {
public List<List<Integer>> generate(int numRows) {
List<List<Integer>> res = new ArrayList<>();
List<Integer> one = new ArrayList<>();
one.add(1);
res.add(one);
if(numRows ==1){
return res;
}
List<Integer> two = new ArrayList<>();
two.add(1);
two.add(1);
res.add(two);
if(numRows ==2){return res;}
for(int i=2;i<numRows ;i++){
List<Integer> line = new ArrayList<>();
line.add(1);
List<Integer> last =res.get(i-1);
for(int j=0;j<i-1;j++){
line.add(last.get(j)+ last.get(j+1));
}
line.add(1);
res.add(line);
}
return res;
}
}
五、198.打家劫舍 记忆化搜索转化
我们对一个位置的处理是否是正确的,我们只需要对比两种情况,选这个位置或者不选这个位置。
选了这个位置,相当于位置n的值与与上一个位置n-2的最优值相加,再不选这个位置的情况下,则我们相当于直接选了上一个位置n-1的最优值。
class Solution {
public int rob(int[] nums) {
if(nums.length ==0){return 0 ;}
if(nums.length ==1){return nums[0];}
int n = nums.length;
int[] dp = new int[n];
dp[0]= nums[0];
dp[1] = Math.max(nums[1], nums[0]);
for(int i=2;i<n;i++){
dp[i]= Math.max(dp[i-2]+ nums[i],dp[i -1]);
}
return dp[n-1];
}
}
六、279.完全平方数 背包问题
我们先对更好理解的但是超时的答案代码进行分析,其超时答案代码如下:
class Solution {
public int numSquares(int n) {
ArrayList<Integer> list = new ArrayList<>();
for( int i =1;i*i<=n;i++){
list.add(i * i);
}
int len = list.size();
int[][] dp = new int[len][n+1];
for(int i =0;i<len;i++){
Arrays.fill(dp[i], -1);
}
return process(list, 0, n, dp);
}
private int process(List<Integer> list, int index, int rest, int[][] dp){
if(rest == 0){
return 0;
}
if(index == list.size()){
return Integer.MAX_VALUE;
}
if(dp[index][rest] != -1){
return dp[index][rest];
}
int curr = list.get(index);
int res = Integer.MAX_VALUE;
for(int i =0;curr *i<=rest;i++){
final int process = process(list, index + 1, rest - curr * i,dp);
if(process != Integer.MAX_VALUE){
res = Math.min(res, i+process);
}
}
dp[index][rest]= res;
return res;
}
}
为了存放完全平方数,我们需要执行如下代码:
ArrayList<Integer> list = new ArrayList<>();
for( int i =1;i*i<=n;i++){
list.add(i * i);
}
为了实现记忆化搜索,我们需要执行如下代码:我们为每个完全平方数,设置了一个n+1长度的数组,也就是可以存放等于n和比n小的所有整数a所对应的一个数字,这个数字就是这个a可以被最少个数的完全平方数表示的个数。这里我们用“-1”来初始化,等于“-1”表明此时这个位置没有存储记忆。
int len = list.size();
int[][] dp = new int[len][n+1];
for(int i =0;i<len;i++){
Arrays.fill(dp[i], -1);
}
我们再看process这个函数,代码如下,首先rest==0
,说明了输入的n已经完全被完全平方数所替代,不需要用新的完全平方数来替代了,所以返回0。index == list.size()
时候,这个index如果作为下标,已经越界,需要被终止。dp[index][rest] != -1
说明这里存在记忆,可以进行调用。 int curr = list.get(index);
是用来找到index所对应的完全平方数,然后for循环是用来将rest依次减去一个完全平方数,两个完全平方数,更多完全平方数,因为一个大的完全平方数可以由几个小的完全平方数所合成,所以由大的完全平方数构成的n所使用的完全平方数个数更少,也就更符合解题目的。代码整体思路是,先把n用小的完全平方数表示出来,他的完全平方数个数肯定比较大,然后再用更大的表示,使用的完全平方数的个数会减小。
private int process(List<Integer> list, int index, int rest, int[][] dp){
if(rest == 0){
return 0;
}
if(index == list.size()){
return Integer.MAX_VALUE;
}
if(dp[index][rest] != -1){
return dp[index][rest];
}
int curr = list.get(index);
int res = Integer.MAX_VALUE;
for(int i =0;curr *i<=rest;i++){
final int process = process(list, index + 1, rest - curr * i,dp);
if(process != Integer.MAX_VALUE){
res = Math.min(res, i+process);
}
}
dp[index][rest]= res;
return res;
}
但是超时了,所以记忆化搜索在某些情况下会超时,所以我们需要用状态矩阵的方式再实现一边。因为我们是要找到最小选取数量, 我们可以借用第70.题爬楼梯的思想,对于组成输入n的最少完全平方数个数,可以由它前面的结果得来,只不过爬楼梯是求种类数,是相加,而现在是取最小值Math.min(dp[i+nn[j]], dp[i] + 1)
。代码如下:
class Solution {
public int numSquares(int n){
int nlen = (int)Math.sqrt(n)+ 1;
int[] nn = new int[nlen];
for(int i=0;i<nlen; i++){
nn[i]=(i +1)*(i + 1);
}
int[] dp = new int[n+1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0]=0;
for(int i=0;i<n; i++){
for(int j=0;j<nlen; j++){
if(i + nn[j] <= n){
dp[i + nn[j]]= Math.min(dp[i+nn[j]], dp[i] + 1);
}
}
}
return dp[n];
}
}
七、322.零钱兑换 背包问题
思路与完全平方数相同,代码如下:
class Solution {
public int coinChange(int[] coins, int amount) {
if(coins.length == 0){
return -1;
}
int [] dp = new int[amount +1];
return process(coins,amount,dp);
}
private int process(int[] coins,int amount,int[] dp){
if(amount < 0){
return -1;
}
if(dp[amount] !=0){
return dp[amount];
}
if(amount == 0){
return 0;
}
int res = Integer.MAX_VALUE;
for(int i =0;i<coins.length;i++){
if(coins[i]<=amount){
final int p1 = process(coins, amount - coins[i], dp);
if(p1!= -1){
res = Math.min(res, p1+1);
}
}
}
res = res == Integer.MAX_VALUE ? -1 : res;
dp[amount] = res;
return res;
}
}
代码改进:此时状态空间为剩余的金额,我们可以在这个状态空间中进行状态的维护,如下图所示,如果硬币面值为“1”,我们就可以根据1个长度之前的最好结果来更新状态,或者“5”同理。
所以我们在创建了状态空间之后,就可以针对所有硬币的面额尽心循环,然后从有效的位置开始直接便利所有的结果空间,并记录最优情况。注意没有有效结果的话,需要返回“-1”。