介绍

动态规划并不是一种具体的算法,而是一种思想,个人觉得就是缓存+枚举,把求解的问题分成许多阶段或者多个子问题,然后按顺序求解各子问题。前一子问题的解为后一子问题提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。

所以动态规划一般用来求最优解(对子问题进行决策),求种类数(对子问题进行加和)

先分享几个经典的动态规划实现,后续再分析几个面试题

最长上升子序列

来源:LeetCode 300.最长上升子序列

描述:给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。你算法的时间复杂度应该为 O(n2) 。进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?

思路:子序列有很多,最长的长度为4

我们假设dp[i]存的是到第i个元素时,数组的最长子序列,则对应的状态转移方程为

dp[i] = max{1, dp[j] + 1 | j < i 且 arr[j] < arr[i]}

其中1为只有自己一个元素,则递增子序列的长度为1

public class Solution {public int lengthOfLIS(int[] nums) {int max = 0;int[] dp = new int[nums.length];for (int i = 0; i < nums.length; i++) {dp[i] = 1;for (int j = 0; j < i; j++) {if (nums[i] > nums[j] && (dp[j] + 1) > dp[i]) {dp[i] = dp[j] + 1;}}if (dp[i] > max) {max = dp[i];}}return max;}
}

数塔问题

来源:LeetCode 120. 三角形最小路径和描述:给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。

相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。

例如,给定三角形:

[[2],[3,4],[6,5,7],[4,1,8,3]
]

自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11) 说明:如果你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题,那么你的算法会很加分。

思路:把这个图形换一下,方便讲递推公式

[2],
[3,4],
[6,5,7],
[4,1,8,3]

我们可以从底到顶来算最优值。dp[i][j]为从最底部到第i行第j列的最小路径和,value[i][j]为第i行第j列的值,状态转移方程为

dp[i][j] = max(dp[i-1][j-1], dp[i-1][j]) + triangle[i][j]

——————————————————————————————————————

public class Solution {public int minimumTotal(List<List<Integer>> triangle) {if (triangle == null || triangle.size() == 0) {return 0;}// 这里行和列加1,是为了不用处理最下面一行的边界int[][] dp = new int[triangle.size() + 1][triangle.size() + 1];for (int i = triangle.size() - 1; i >= 0; i--) {List<Integer> rows = triangle.get(i);for (int j = 0; j < rows.size(); j++) {dp[i][j] = Math.min(dp[i + 1][j], dp[i + 1][j + 1]) + rows.get(j);}}return dp[0][0];}
}

最长公共子串

来源:LeetCode 1143. 最长公共子序列描述:给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

若这两个字符串没有公共子序列,则返回 0。示例 1:

输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace",它的长度为 3。

示例 2:

输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc",它的长度为 3。

示例 3:

输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0。

思路:这个题确实比较抽象,上图

s1=a s2=a,最长公共子串长度为1 s1=ac s2=abc,对应的公共子串长度为2

dp[i][j]为第一个字符串长度为i和第二个字符串长度为j时对应的最长公共子串 状态转移方程为

if(s1.charAt(i) == s2.charAr(j))dp[i][j] = dp[i-1][j-1] + 1;
elsedp[i][j] = Math.max(dp[i-1][j], dp[i][j -1]);

还是画图演示一下递推公式

public class Solution {public int longestCommonSubsequence(String text1, String text2) {if (text1 == null || text2 == null || text1.length() == 0 || text2.length() == 0) {return 0;}int[][] dp = new int[text1.length() + 1][text2.length() + 1];for (int i = 1; i <= text1.length(); i++) {for (int j = 1; j <= text2.length(); j++) {if (text1.charAt(i - 1) == text2.charAt(j - 1))dp[i][j] = dp[i - 1][j - 1] + 1;elsedp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);}}return dp[text1.length()][text2.length()];}
}

背包问题

是男人就看《背包九讲》,作为动态规划的入门课,《背包九讲》必不可少。这次就只分享背包九讲中最简单的01背包

来源:蓝桥杯

问题描述:给定N个物品,每个物品有一个重量W和一个价值V.你有一个能装M重量的背包.问怎么装使得所装价值最大.每个物品只有一个. 输入格式   输入的第一行包含两个整数n, m,分别表示物品的个数和背包能装重量。  以后N行每行两个数Wi和Vi,表示物品的重量和价值 输出格式   输出1行,包含一个整数,表示最大价值。样例输入 3 5 2 3 3 5 4 7 样例输出 8 数据规模和约定 1<=N<=200,M<=5000.

思路:这是最简单的01背包,都不带变形的,每种物品仅有一件,可以选择放或不放。

第i件物品的重量是w[i],价值是v[i] 用dp[i][j]表示前i件物品放入一个承重为j的背包可以获得的最大价值,状态转移方程为

dp[i][j] = max{dp[i-1][j], dp[i-1][j-w[i]] + c[i]}

dp[i][j] = max{不放第i件物品,放第i件物品}

你可以照着这个状态转移方程自己写一下,我下面这种写法直接用了滚动数组,把数组从二维变成了一维,节省了空间,有兴趣的可以参考其他博客学习这种写法,本文就不深入了。

public class Main {public static void main(String[] args) {Scanner in = new Scanner(System.in);int n = in.nextInt();int m = in.nextInt();int[] widght = new int[210];int[] value = new int[210];int[] dp = new int[5010];for (int i = 0; i < n; ++i) {widght[i] = in.nextInt();value[i] = in.nextInt();}for (int i = 0; i < n; ++i) {for (int j = m; j >= widght[i]; j--) {dp[j] = Math.max(dp[j], dp[j - widght[i]] + value[i]);}}System.out.println(dp[m]);}
}

不同路径

来源:LeetCode 62不同路径

描述:一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。问总共有多少条不同的路径?

例如,上图是一个7 x 3 的网格。有多少可能的路径?说明:m 和 n 的值均不超过 100。

示例:输入: m = 3, n = 2,输出: 3 解释:从左上角开始,总共有 3 条路径可以到达右下角。

  1. 向右 -> 向右 -> 向下
  2. 向右 -> 向下 -> 向右
  3. 向下 -> 向右 -> 向右

思路:这个大家一下就会想到用递归解决,假设f(m,n)表示移动到点(m,n)的路径数,因为机器人智能向下或者向右移动,所以点(m,n)只能从点(m-1,n)和(m,n-1)移动而来,递归公式就是f(m,n)=f(m-1,n)+f(m,n-1),递归的出口呢?当然就是网格的边界了,网格边界上的点都只有一种方法,按照这种思路写出来如下代码

class Solution {public int uniquePaths(int m, int n) {// 在网格边界的格子只能有一种走法if (m == 1 || n == 1) {return 1;}// m,n这个位置只能从(m - 1 , n)和(m, n - 1)移动而来return uniquePaths(m - 1, n) + uniquePaths(m, n - 1);}
}

其实这个代码效率还是很低的,因为有很多重复的计算,如下图

当m和n为(3,3)时,(2,2)被计算了2次,而且m和n越大,重复计算的次数最多,我们可以把已经算出来的值保存一下,这样下次再用的时候就不用算了,直接取就行,叫做备忘录算法,grid[m][n]表示走到(m,n)这个点时的路径数。

class Solution {public static int[][] grid = new int[110][110];public int uniquePaths(int m, int n) {if (grid[m][n] != 0)return grid[m][n];if (m == 1 || n == 1) {return 1;}return grid[m][n] = uniquePaths(m - 1, n) + uniquePaths(m, n - 1);}
}

当值不为0的时候说明已经被算过了,直接取就行了,否则就得计算并保存结果,这样效率提高了不少,但是如果m和n特别大,递归层数过多时会造成堆栈溢出的,该怎么办?这个时候就得用到动态规划了

递归是从上至下开始计算的,有没有可能从下而上的计算呢?,如先算出(1,2)和(2,1),然后就能算出(2,2)了,我们得按照一定的规律计算,保证在算(2,2)之前,(1,2)和(2,1)已经算完了,我们只要按行从左到右计算,或者按列从上到下即可

dp[i][j]表示到达第i行第j列的路径数,所以状态转移方程为

dp[i][j] = dp[i][j-1] + dp[i-1][j]

——————————————————————————————————————

class Solution {public static int[][] grid = new int[110][110];public int uniquePaths(int m, int n) {for (int i = 1; i <= n ; i++) {for (int j = 1; j <= m ; j++) {if (i == 1 || j == 1)grid[i][j] = 1;elsegrid[i][j] = grid[i][j-1] + grid[i-1][j];}}return grid[n][m];}
}

减绳子

来源:《剑指offer》第二版

描述:给你一根长度为n的绳子,请把绳子剪成m段 (m和n都是整数,n>1并且m>1)每段绳子的长度记为k[0],k[1],…,k[m].请问k[0]k[1]…*k[m]可能的最大乘积是多少?例如,当绳子的长度为8时,我们把它剪成长度分别为2,3,3的三段,此时得到的最大乘积是18.

思路:定义函数f(n)为长度为n的绳子剪成若干段后各段长度乘积的最大值。在剪第一刀的时候,我们有n-1种可能的选择,也就是剪出来的第一段绳子的长度分别为1,2...n-1。因此f(n)=max(f(i)*f(n-i)),其中0<i<n。这是一个从上至下的递归公式,递归会有很多重复的子问题。我们可以从下而上的顺序计算,也就是说我们先得到f(2),f(3),再得到f(4),f(5),直到得到f(n)

假设dp[i]表示长度为i的绳子能得到的最大乘积,则状态转移方程为

dp[i] = max(dp[i], dp[j] * dp[i-j])

——————————————————————————————————————

public class Solution {public int maxNumAfterCutting(int n) {if (n < 2)return 0;// 绳子长度为2时,只能剪成1和1if (n == 2)return 1;// 只可能为长度为1和2的2段或者长度都为1的三段,最大值为2if (n == 3)return 2;// 当长度大于3时,长度为3的段的最大值是3int product[] = new int[n+1];product[0] = 0;product[1] = 1;product[2] = 2;product[3] = 3;int max = 0;for (int i = 4; i <= n; i++) {max = 0;for (int j = 1; j <= i / 2; j++) {int sum = product[j] * product[i - j];if (sum > max) {max = sum;product[i] = max;}}}return product[n];}}

代码中第一个for循环变量i是顺序递增的,这意味着计算顺序是自下而上的。因此再求f(i)之前,对于每一个j(0<i<j)而言,f(j)都已经求解出来了,并且保存在product[j]里。为了求解f(i),我们需要求出所有可能的f(i)*f(i-j)并比较得出他们的最大值,这就是代码中第二个for循环的功能

这个面试题又比第一个面试题难了一点,因为第一个面试题仅仅是将一个大问题划分成几个子问题,并没有根据局部解进行决策得到最优解,而这个面试题体现了决策的过程

接雨水

来源:LeetCode 42. 接雨水

描述:给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水

上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)

示例:输入: [0,1,0,2,1,0,1,3,2,1,2,1]输出: 6

思路:思路还是比较简单,对每一个柱子能存多少水求和即可,这样只需要获取这个柱子左边的最高高度和这个柱子右边的最高高度,2者的最小值减去柱子的高度就是这个柱子的存水量

class Solution {public int trap(int[] height) {int sum = 0;for (int i = 0; i < height.length; i++) {int maxLeft = 0, maxRight = 0;// 对i这个柱子,左边柱子的最高值for (int left = 0; left < i; left++) {maxLeft = Math.max(maxLeft, height[left]);}// 对i这个柱子,右边柱子的最高值for (int right = i + 1; right < height.length ; right++) {maxRight = Math.max(maxRight, height[right]);}// i这个柱子能存的水量int temp = Math.min(maxLeft, maxRight) - height[i];if (temp > 0)sum += temp;}return sum;}
}

每次都要算某个柱子的左右最值,时间复杂度是O(n2),能不能把算左右最值的效率提高呢?这就用到动态规划了,假如说

我们用dp[i],表示到第i个柱子(包括第i个柱子)左边的最大值,height[i]为第i个柱子的高度,右边同理,则状态转移方程为

dp[i] = max(dp[i-1], height[i])

——————————————————————————————————————

class Solution {public int trap(int[] height) {int sum = 0;int len = height.length;if (len == 0)return 0;int[] maxLeft = new int[len];int[] maxRight = new int[len];maxLeft[0] = height[0];for (int i = 1; i < len; i++) {maxLeft[i] = Math.max(height[i], maxLeft[i-1]);}maxRight[len - 1] = height[len - 1];for (int i = len - 2; i >= 0; i--) {maxRight[i] = Math.max(height[i] ,maxRight[i+1]);}for (int i = 0; i < height.length; i++) {sum += Math.min(maxLeft[i], maxRight[i]) - height[i];}return sum;}
}

这样时间复杂度就变成O(n)了

分割等和子集

来源:LeetCode 416. 分割等和子集

描述:给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

注意:

每个数组中的元素不会超过 100 数组的大小不会超过 200 示例 1:

输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].

示例 2:

输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.

思路:很典型的01背包,背包的容量为元素和的一半,最后看背包是否能填满即可 之前已经说了01背包的状态转移方程

第i件物品的重量是w[i],价值是v[i] 用dp[i][j]表示前i件物品放入一个承重为j的背包可以获得的最大价值,状态转移方程为

dp[i][j] = max{dp[i-1][j], dp[i-1][j-w[i]] + c[i]}

dp[i][j] = max{不放第i件物品,放第i件物品}

在这个题目中,承重和价值都是这个值的大小,因为上一次例子用到了压缩数组的写法,这次就换一种写法,完全按照状态转移方程来

public class Solution {public boolean canPartition(int[] nums) {int sum = 0;for (int i = 0; i < nums.length; i++) {sum += nums[i];}// 奇数直接falseif ((sum & 1) == 1) {return false;}int target = sum >> 1;int[][] dp = new int[nums.length][target + 1];for (int i = 0; i < nums.length; i++) {for (int j = 0; j <= target; j++) {if (j >= nums[i]) {if (i == 0) {dp[i][j] = nums[i];} else {dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - nums[i]] + nums[i]);}}}}return dp[nums.length - 1][target] == target;}
}

感谢(づ ̄3 ̄)づ╭❤~ 完整阅读!

希望对您有所帮助 (*^▽^*)

动态顺序字符串基本操作实验_掌握套路,你也会用动态规划相关推荐

  1. 数据结构动态顺序字符串基本操作实验_技术连载:数据结构 - 栈

    栈 只有一个口,可以进数据也可以出数据,一种典型的先进后出.后进先出的数据结构: 栈 栈的基本操作 入栈 入栈操作需要考虑当前栈是否以及满了,如果满了是选择扩容还是返回入栈失败: 入栈成功则需要更新栈 ...

  2. java动态数组储存敌机_如何使用参数通过graphql将动态数组字符串存储为neo4j中的节点属性?...

    我正在使用Neo4j(3.2.3)和graphql . 我试图通过graphql在用户的单个属性中添加多个电话号码(动态)作为字符串数组 . 因此,用户可以拥有与他/她想要添加的电话号码一样多的电话号 ...

  3. Python_基础语法_字符串基本操作__声明方式_占位符_切片_转义字符_加密解密(6)

    目录: 一.字符串介绍 1.字符的渲染 2.字符的编码 3.不可变类型 二.字符串的基本操作 1.字符串声明 2.字符串占位符号 3.字符串索引切片(包含练习) 4.字符串转义 三.字符串的高级操作 ...

  4. java中动态输入_根据特定输入动态生成字符串的代码 - java

    我想根据特定的输入动态生成字符串 如果输入= 1,则字符串输出= 00000 如果它= 10,则输出= 0000a 输入= 16,输出= 00010 等等 . 生成的字符串是从0到9的数字和从a到f的 ...

  5. python将字符串逆序_为什么说Python是一门伟大的入门语言?(附免费教程)

    Python 是一门伟大的入门语言.作为一门伟大的编程语言,一定要具备一些特征,其中有五项特征是非常重要的: 非常棒的首次体验:就像书的开始,首先一定要能够"沉迷",学习新知识一定 ...

  6. 操作系统实验一 Linux基本操作|实验二 进程管理

    由于当时没存代码,只有实验文档代码截图,文末也可直接获取实验文档. 操作系统实验 目录 实验一 Linux基本操作 实验二进程管理 实验一 Linux基本操作 1实验目的 1.熟悉在Linux操作系统 ...

  7. python创建一个字符串_Python字符串基本操作

    一.任务描述 本实验任务主要对Python字符串进行一些基本操作,通过完成本实验任务,要求学生熟练掌握Python字符串的基本操作,并对Python字符串基本操作进行整理并填写工作任务报告. 二.任务 ...

  8. 大学计算机文档基本操作实验的效果,上海工程技术大学计算机实验报告5

    实验05 PowerPoint的基本操作 姓名 班级 学号 实验日期2010-12-20 课程名称 计算机应用基础 指导教师 杨孜茁 成绩 实验名称:PowerPoint的基本操作 实验目的: 1. ...

  9. 网络安全_密码学实验_对称加密算法DES

    网络安全_密码学实验_对称密码非对称密码_加密算法DES RSA 一.实验环境 二.对称加密DES 1.理解DES算法原理 2.加密过程 3.解密过程 三.运行结果 一.实验环境 PyCharm 20 ...

最新文章

  1. 【并行计算-CUDA开发】从零开始学习OpenCL开发(一)架构
  2. 学习Guava Cache知识汇总
  3. pxe+kickstart 实现基于网络的无人值守安装操作系统
  4. Redis的zset有多牛?请把耳朵递过来
  5. java8的路径_什么是路径?
  6. 【AI视野·今日CV 计算机视觉论文速览 第151期 part2】Tue, 6 Aug 2019
  7. 视频处理简单实例 [OpenCV 笔记2]
  8. 复习zabbix配置agent过程
  9. Web 端的测试 Selenium 用法必备
  10. [2018.07.10 T2]不回文
  11. 【预测模型-ELAMN预测】基于遗传算法优化ELMAN神经网络实现数据回归预测matlab代码
  12. 关于ResourceBundle国际化的一些思考
  13. Mac 下修改eclipse内存设置
  14. NLP入门从入门到实战 实体命名识别 +中文预处理之繁简体转换及获取拼音
  15. 读《谁说大象不能跳舞》
  16. Word文件打开之后有只读限制
  17. 【5G安全系列】AS层PDCP完整性保护与加密流程
  18. Dev-C++安装OpenCV
  19. 转到计算机科学与技术专业的申请书,转专业的申请书范文(精选5篇)
  20. Centos7快速单机搭建ceph(Octopus版)

热门文章

  1. eclipse全局搜索_Eclipse如何进行全局搜索和单页面搜索?
  2. 30 分钟学会如何使用 Shiro
  3. 【面试经验分享】Java 面试中的那些潜规则
  4. 【深入Java虚拟机JVM 07】JVM如何判断对象已死
  5. 算法--生成m个指定范围的不重复随机数的三种方法分析(Java实现)
  6. 02-JDBC连接MySQL数据库【查询数据】
  7. SpringBoot之使用RabbitMQ实现延迟队列
  8. Java里的数组介绍
  9. linux 下用ecipse 作用oracle 的客户端
  10. shiro表单登录认证及退出(自定义form认证器)