LeetCode探索初级算法 - 动态规划

今天在LeetCode上做了几个简单的动态规划的题目,也算是对动态规划有个基本的了解了。现在对动态规划这个算法做一个简单的总结。

什么是动态规划

动态规划英文 Dynamic Programming,是求解决策过程最优化的数学方法,后来沿用到了编程领域。

动态规划的大致思路是把一个复杂的问题转化成一个分阶段逐步递推的过程,从简单的初始状态一步一步递推,最终得到复杂问题的最优解。

动态规划解决问题的过程分为两步:

  1. 寻找状态转移方程
  2. 利用状态转移方程式自底向上求解问题

在这里先向大家推荐一篇文章,也是讲动态规划的,用漫画的形式讲解,生动活泼,浅显易懂。

《漫画:什么是动态规划?(整合版)》

例题

话不多说,直接看看题目。

1.爬楼梯

题目描述

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1:

输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

示例 2:

输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶

爬楼梯问题是动态规划算法中非常经典的一道题目,出场率十分高。现在我尝试循序渐进地把这个问题讲清楚。

思路

我们设置一个函数F(N)来表示走到第N级台阶走法的数量,现在假设有10级台阶。现在就会出现两种情况:

  1. 我们是从第9级,跨1级上来,到第10级
  2. 我们是从第8级,跨2级上来,到第10级

其实对于任何第N级台阶,都会出现这两种情况,即第N级的前一步是走了1级或者两级。

所以如果我们统计F(10)的话,可以发现F(10) = F(9) + F(8),即到第10级的走法等于到第9级的走法加上到第8级的走法。同理可得,F(9) = F(8) + F(7),F(8) = F(7) + F(6)等等等等……

所以我们就得到了动态规划步骤1中的所说的所谓的状态转移方程:F(N) = F(N-1) + F(N-2).

一直到最底层,当只有1级台阶时,F(1) = 1;当只有2级台阶时F(2) = 2.

到这里,直觉告诉我们可以用递归来解决这个问题。

递归法
public int climbStairs (int n) {
if (n < 1) {
return 0;
}
if (n == 1) {
return 1;
}
if (n == 2) {
return 2;
} return climbStairs(n - 1) + climbStairs(n - 2);
}

但是递归法有个问题,时间复杂度比较高。我们可以看一下下图:

递归的过程可以构造出一棵二叉树,可以看出求解F(N)过程中,会访问\(2^N\)次F()函数,即时间复杂度为\(O(2^N)\).并且,递归的过程中包含着大量的重复操作,二叉树越往下走,重复操作越多,上图中相同颜色标出的节点就是表示重复的操作。

那怎么解决这个问题呢?现在我们就要搬出动态规划的步骤2了,采用自底向上的方法求解问题。

刚才的递归法,我们是从第10级台阶往下,计算F(9)和F(8),再计算F(9)需要的F(8)和F(7),以及F(8)需要的F(7)和F(6),依次往下,体现在二叉树上,就是从最顶上的节点往下构造出这棵二叉树。

现在我们转化思路,自底向下构造。我们现在已经知道了F(1)=1和F(2)=2,所以我们可以知道F(3) = F(2) + F(1) = 3,进一步地,我们可以知道F(4) = F(3) + F(2) = 5,等等等等……

按照这个方法,我们可以设置一个数组,依次往里面填数就可以了。时间复杂度为\(O(N)\)。

动态规划法
public int climbStairs (int n) {
if (n == 1){
return 1;
}// 防止数组越界
int[] step = new int[n + 1];
step[1] = 1;
step[2] = 2;
for (int i = 3; i <= n; i++) {
step[i] = step[i - 1] + step[i - 2];
}
return step[n];
}

2. 最大子序和

题目描述

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

思路

废话不多说,我们直接走动态规划的流程。第一步就是寻找状态转移方程。

其实这个状态转移方程有点像高中数学里面的数列的通项公式,数列的通项公式可以通过各种各样的方法找出来,什么规律法、累加累乘什么的,我们这里找动态规划的状态转移方程就比较类似于规律法找通项公式,这个通项公式就是第N项与前若干项之间的关系。

我们看这个题目,我们遍历一遍数组,假如我们现在正站在第i个元素,如何通过第i个元素的值和前面若干个元素的值来找到所谓的最大子序和呢?

最大子序和,我们当然是想让一个子序中正数越多越好,负数越少越好。所以假如我们现在有一个子序,它是和最大子序的候选人,我们就希望这个子序的后面的元素是正数,从而可以继续增加这个子序的和。换位思考一下,现在我们是一个元素,前面有一个子序,我们就希望前面这个子序的和是正的,我加入这个子序不就抱了大腿吗,要是前面这个子序的和是负的,那完了,我加入前面的子序还要自损一部分功力,还不如单干呢,我自己就当一个子序。

前面的解释,自我感觉还是比较形象的,现在让这个解释与动态规划的编程实现结合起来。

我们定义一个数组dp[]dp[i]以第i个元素为结尾的一段最大子序和。求dp[i]时,假设前面dp[0]~dp[i-1]都已经求出来了,dp[i-1]表示的是以i-1为结尾的最大子序和,若dp[i-1]小于0,则dp[i]加上前面的任意长度的序列和都会小于不加前面的序列(即自己本身一个元素是以自己为结尾的最大自序和)。

所以状态转移方程相当于是一个判断函数。

if (dp[i - 1] > 0) {
dp[i] = dp[i - 1] + nums[i];
} else {
dp[i] = nums[i];
}

第二步是利用状态转移方程自底向上求解,这和上一道题目类似,按照顺序往数组里面填值。

源码
public int maxSubArray (int[] nums) {
if (nums.length == 0) return 0;
if (nums.length == 1) return nums[0];
int[] dp = new int[nums.length];
dp[0] = nums[0];
int max = dp[0];
for (int i = 1; i < dp.length; i++) {
if (dp[i - 1] > 0) {
dp[i] = dp[i - 1] + nums[i];
} else {
dp[i] = nums[i];
} max = Math.max(dp[i],max);
}
return max;
}

3. 打家劫舍

题目描述

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

示例 1:

输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

输入: [2,7,9,3,1]
输出: 12
解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。

思路

废话不多说,我们直接走动态规划的流程。第一步就是寻找状态转移方程。

再重复一遍,状态转移矩阵是第N项与前若干项之间的关系。

现在我们是一个小偷,站在第i家的屋顶,我们是偷,还是不偷呢?这是个问题。

  • 如果偷,那前面一家(i-1)我就不能偷,我当前偷到的最大值就是偷完前(i-2)家的最大值加上我偷这一家的钱。
  • 如果不偷,我当前偷到的最大值就是偷完前(i-1)加的最大值,然后我就去下一家再看看。

所以状态转移矩阵就可以用如下一个公式表示:

rob(i) = Math.max( rob(i - 2) + currentHouseValue, rob(i - 1) )

第二步是利用状态转移矩阵自底向上求解问题。

我们定义一个数组dp[]dp[i]以第i个元素为结尾的偷窃到的最大金额。求dp[i]时,假设前面dp[0]~dp[i-1]都已经求出来了。

源码
public int rob(int[] nums) {
if (nums.length == 0) return 0;
int[] dp = new int[nums.length + 1];
dp[0] = 0;
dp[1] = nums[0];
for (int i = 2; i <= nums.length; i++) {
dp[i] = Math.max(dp[i-1],dp[i-2] + nums[i-1]);
}
return dp[nums.length];
}

总结

在利用动态规划求解问题的过程中,比较难的是找到状态转移方程,之前多次提到,状态转移方程是第N项与前若干项之间的关系。这是我个人的一点理解,求动态规划的第i项时可以假设前面的若干项都是已知的了。比如第一题爬楼梯,就是当前项和前两项的关系,最大子序和是当前项取决于前一项的正负,打家劫舍也是看当前项和前两项的关系。

找到这种关系后,需要转化思路,自底向上编写程序,这样才能降低时间复杂度,才是真正的动态规划。

LeetCode探索初级算法 - 动态规划的更多相关文章

  1. LeetCode初级算法--动态规划01:爬楼梯

    LeetCode初级算法--动态规划01:爬楼梯 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.net ...

  2. LeetCode初级算法(动态规划+设计问题篇)

    目录 爬楼梯 买卖股票的最佳时机 最大子序和 打家劫舍 动态规划小结 Shuffle an Array 最小栈 爬楼梯 第一想法自然是递归,而且爬楼梯很明显是一个斐波拉切数列,所以就有了以下代码: c ...

  3. leetcode探索中级算法

    leetcode探索中级答案汇总: https://leetcode-cn.com/explore/interview/card/top-interview-questions-medium/ 1)数 ...

  4. leetcode探索高级算法

    C++版 数组和字符串 正文 链表: 正文 树与图: 树: leetcode236. 二叉树的最近公共祖先 递归(先序) leetcode124二叉树最大路径和 递归 图: leetcode 547朋 ...

  5. LeetCode初级算法--数组01:只出现一次的数字

    LeetCode初级算法--数组01:只出现一次的数字 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn. ...

  6. LeetCode初级算法--数组02:旋转数组

    LeetCode初级算法--数组02:旋转数组 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.net/ ...

  7. LeetCode初级算法--字符串01:反转字符串

    LeetCode初级算法--字符串01:反转字符串 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.ne ...

  8. LeetCode初级算法--链表01:反转链表

    LeetCode初级算法--链表01:反转链表 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.net/ ...

  9. LeetCode初级算法--链表02:合并两个有序链表

    LeetCode初级算法--链表02:合并两个有序链表 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn. ...

随机推荐

  1. Tomcat源码分析 (二)----- Tomcat整体架构及组件

    前言 Tomcat的前身为Catalina,而Catalina又是一个轻量级的Servlet容器.在美国,catalina是一个很美的小岛.所以Tomcat作者的寓意可能是想把Tomcat设计成一个优 ...

  2. android ——Tablayout

    Tabs make it easy to explore and switch between different views. 通过TabLayout可以在一个活动中通过滑动或者点击切换到不同的页面 ...

  3. Python3 反射

    反射 python面向对象中的反射:通过字符串的形式操作对象相关的属性 hasattr(obj,name) # hasattr(obj, name) # 判断一个对象是否有指定的属性name,返回Tr ...

  4. 从原理层面掌握@ModelAttribute的使用(使用篇)【一起学Spring MVC】

    每篇一句 每个人都应该想清楚这个问题:你是祖师爷赏饭吃的,还是靠老天爷赏饭吃的 前言 上篇文章 描绘了@ModelAttribute的核心原理,这篇聚焦在场景使用上,演示@ModelAttribute ...

  5. 对博弈活动中蕴含的信息论原理的讨论,以及从熵角度看不同词素抽象方式在WEBSHELL文本检测中的效果区别

    1. 从赛马说起 0x1:赛马问题场景介绍 假设在一场赛马中有m匹马参赛,令第i匹参赛马获胜的概率为pi,如果第i匹马获胜,那么机会收益为oi比1,即在第i匹马上每投资一美元,如果赢了,会得到oi美元 ...

  6. 在一个jsp页面内实现简单计算器

    首先创建一个calculate.jsp 这是用Javascript代码来验证,代码如下: <script type="text/javascript"> functio ...

  7. Java - 手动解析不带引号的JSON字符串

    目录 1 需求说明 2 解析代码 2.1 实现思路 2.2 详细代码 2.3 测试样例 1 需求说明 项目中遇到了一批不带引号的类JSON格式的字符串: {Name:Heal,Age:20,Tag:[ ...

  8. Spring-Boot:拦截器注解范例

    package com.example.aop; import java.lang.annotation.Documented; import java.lang.annotation.Element ...

  9. @WebInitParam注解

    Servlet注解——@WebInitParam多个InitParam的写法 使用@WebInitParam配置多个InitParam,使某些页面不被拦截.在过滤器Filter.java下添加注解:@ ...

  10. js 设计模式&&query

    1. 语法: try{           //需要执行的代码      }catch(e){           //错误处理 e程序遇到错误时的报错信息      } 2.惰性函数: 函数在第一次 ...