学习动态规划心得
前言
最近在做题的时候,每次去做一些最大最小值,或者有多少种可能情况等问题的时候总是没有头绪,但是一看评论就是什么经典动规题目,思路清晰,而自己却很难写出来。今天就来学习一下动态规划,来弥补自己知识薄弱的地方。
动态规划(Dynamic Programming)是什么?
动态规划就是分治的思想,通俗一点就是大事化小,小事化了。并且在将大问题化解为小问题的时候,保存对这些小问题处理好的结果,供后面处理更大规模问题去使用。
动态规划的特点:
1.把原来的问题分解成了几个相似的子问题
2.所有子问题只需要解决一边
3.存储子问题的解
动态规划问题解决步骤
动态规划的本质,是对问题状态的定义和状态方程的定义。
1.状态定义
2.状态间的转移方程定义
3.状态的初始化
4.返回结果(解或者间接解)
状态定义的要求:定义的状态一定要形成递推关系
适用场景最大最小值,可不可行,是不是,方案个数等问题。
一、Fibonacci数列
先对问题进行分析
1.状态定义: F(n)
2.状态间的转移方程:F(n)=F(n-1)+F(n-2);
3.状态的初始化:(初始值) F(0)=0; F(1)=1;
4.返回结果:F(n)
代码如下(示例):
public static int Fib(int n){
if(n<=0){
return 0;
}
//从第零项开始所以多开辟一个空间
int []dp=new int[n+1];
dp[0]=0;
dp[1]=1;
for(int i=2;i<=n;i++){
//状态转移方程
//F(n)=F(n-1)+F(n-2)
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
当然我们也可以不使用数组让空间复杂度达到O(1)
public static int Fib1(int n){
if(n<=0){
return 0;
}
if(n==1||n==2){
return 1;
}
int f1=1;
int f2=1;
int f3=0;
for(int i=3;i<=n;i++){
f3=f1+f2;
f1=f2;
f2=f3;
}
return f3;
}
很多动规问题都可以转化成Fib数列解决
二、青蛙跳台阶
先是变态青蛙跳台阶,有n阶台阶,青蛙可以每次跳一个,二个…或者n个
分解问题:
1.状态定义F(n)
2.状态间转移方程定义 F(n)=2F(n-1)
3.状态的初始化 F(1)=1
4.返回结果 F(n)
public static int climbingStairs(int n){
int res=1;
for(int i=2;i<=n;i++){
//F(n)=2F(n-1)
res*=2;
}
return res;
}
之后是正常的青蛙,每次只能跳一个台阶或者两个台阶(斐波那契数列)
leetcode 70题
1.状态定义 F(n)
2.状态间转移方程定义F(n)=F(n-1)+F(n-2)
3.状态初始化 F(1)=1;
4.返回结果 F(n)
public static int climbingStairs1(int n){
if(n==1||n==2){
return n;
}
int f1=1;
int f2=2;
int f3=0;
for(int i=3;i<=n;i++){
f3=f1+f2;
f1=f2;
f2=f3;
}
return f3;
}
三、矩阵覆盖
题目链接
分析问题后依旧是斐波那契数列
1.状态定义 F(n)
2.状态间转移方程定义 F(n)=F(n-1)+F(n-2)
3.状态的初始化 F(1)=1 F(2)=2
4.返回结果 F(n)
public static int rectCover(int target) {
if(target==1||target==2){
return target;
}
int t1=1;
int t2=2;
int t3=0;
for(int i=3;i<=target;i++){
t3=t1+t2;
t1=t2;
t2=t3;
}
return t3;
}
}
或者使用数组记录
public static int rectCover1(int target){
if(target==0){
return 0;
}
int []dp=new int[target+1];
dp[0]=1;
dp[1]=2;
for(int i=2;i<target;i++){
dp[i]=dp[i-1]+dp[i-2];
}
return dp[target-1];
}
四、最大连续字数组和
leetcode 53题
这种题使用动态规划来做的话就和前面几个不相同了,因为其无法定义状态间转移方程
所以就把大问题化成先问题去解决
例如求最大连续字数组和,先求前i个最大字数组的和,最终求最大连续字数组和
时间复杂度O(n),空间复杂度O(1)做法
public int maxSubArray(int[] nums) {
int res=nums[0];
for(int i=1;i<nums.length;i++){
//比较当前值和当前值加上前一个值的大小
nums[i]=Math.max(nums[i-1]+nums[i],nums[i]);
res=Math.max(nums[i],res);
}
return res;
}
或者另外一种如果前i项最大子序和<=0就重新赋值为下一个数值,记录max最大值
public static int maxSubArray(int[] nums) {
int max=nums[0];
int count=nums[0];
for(int i=1;i<nums.length;i++){
if(count<=0){
count=nums[i];
}else {
count+=nums[i];
}
max=Math.max(max,count);
}
return max;
}
五、字符串分割
题目链接
先把大问题化成小问题 前i个字母能否成功分割
状态转移方程是 j<i&&F(j)&&[j,i-1] 即F(j)为true [j,i-1]也为true 这样可以存储true
例如:给定s=“nowcode” 已知 now 和code 在词典中,在找到cow后就可以从j+1位置找到 i位置单词是否能在词典中找到
举例说明:
例如dict 中 [le ,et ,co ,de]是单词
那么当leetcode字符串传来时
F(1) false ——— F(2) true ——— F(3)false ————F(4)true
F(5) false——— F(6) true ——— F(7)false ——— F(8) true
即判断时可以借助前面的结果来判断 例如F(4)利用F(2)为true 只需要判断后两个字母是否能组成单词即可。
public boolean wordBreak(String s, Set<String> dict) {
if(s.length()==0){
return true;
}
boolean[]array=new boolean[s.length()+1];
for(int i=1;i<=s.length();i++){
//1~i整体范围是一个单词
if(dict.contains(s.substring(0,i))){//substring 是从0到i-1位置
array[i]=true;
continue;
}
for(int j=i-1;j>0;j--){
//部分判断 一部分为前面验证过的true 判断后面的也为true
if(array[j]&&dict.contains(s.substring(j,i))){//substring 是从j位置到i-1位置
array[i]=true;
break;
}
}
}
return array[s.length()];
}
leetcode 139题
也是这样的字符串拆分 双80%
public boolean wordBreak(String s, List<String> wordDict) {
boolean []array=new boolean[s.length()+1];
for(int i=1;i<=s.length();i++){
if(wordDict.contains(s.substring(0,i))){
array[i]=true;
continue;
}
for(int j=i-1;j>0;j--){
if(array[j]&&wordDict.contains(s.substring(j,i))){
array[i]=true;
break;
}
}
}
return array[s.length()];
}
六、三角矩阵求最短路径
牛客题目链接
leetcode 120题
给定一个三角矩阵,找出自顶向下的最短路径和,每一步可以移动到下一行相邻的数字
首先确定相邻元素的概念:
1.如果在F(i,j)下一行相邻元素就是 (i+1,j)(i+1,j+1)
如果从下往上推的话,从F(i,j)位置往上的相邻位置就是(i-1,j)(i-1,j-1)
2.状态转移方程是 F(min)=Math.min(F(i-1,j),(i-1,j-1))+a(i,j)// 从相邻位置中找到最小值,之后加上当前位置坐标
注意每一行的第一列和最后一列只有一条路径
第一列F(i,0)=F(i-1,0)+a[i][0]
最后一列 F(i,i)=F(i-1,i-1)+a[i][i]
3.初始状态
F(0,0)=a[0][0]
最终思想是从上往下求(先判断边界情况,第一列只有一条路径,最后一列也只有一条路径都加上当前坐标值就好,)之后找最小路径值就可以了。
public int minimumTotal(List<List<Integer>> triangle) {
List<List<Integer>> list=new ArrayList<>();
for(int i=0;i<triangle.size();i++){
list.add(new ArrayList<>());
}
//先定义初始值 在0位置加上(0,0)坐标的值
list.get(0).add(triangle.get(0).get(0));
//之后遍历判断特殊情况 每一行的第一个 ,每一行的最后一个
for(int i=1;i<triangle.size();i++){
int count=0;//当前路径和
for(int j=0;j<=i;j++){
if(j==0){
count=list.get(i-1).get(j);
}else if(i==j){
count=list.get(i-1).get(j-1);
}else {
count=Math.min(list.get(i-1).get(j),list.get(i-1).get(j-1));
}
list.get(i).add(triangle.get(i).get(j)+count);
}
}
//之后找到list中到目标路径的最小值即可
int len=triangle.size();
int min=list.get(len-1).get(0);
for(int i=1;i<len;i++){
min=Math.min(min,list.get(len-1).get(i));
}
return min;
}
七、不同路径
leetcode 62题
找到状态转移方程F[i] [j]=F[i-1] [j]+F[i] [j-1];
定义初始状态 即边界值 F[0] [i]=1 F[j] [0]=1 即可
public int uniquePaths(int m, int n) {
int [][]path=new int[m][n];
for(int i=0;i<n;i++){
path[0][i]=1;
}
for(int j=0;j<m;j++){
path[j][0]=1;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
path[i][j]=path[i-1][j]+path[i][j-1];
}
}
return path[m-1][n-1];
}
八、不同路径II
leetcode 63题
和上题类似都是找路径个数,但是这题中间加了障碍物,
就增加了一些判断条件即可,先判断终点和起点位置是否为障碍物,之后判断路径上是否有障碍物,
其他思路和上一题是相似的。
public static int uniquePathsWithObstacles(int[][] obstacleGrid) {
List<List<Integer>> list=new ArrayList<>();
int m=obstacleGrid.length;//行数
int n=obstacleGrid[0].length;//列数
//初始化第0列
for(int i=0;i<m;i++){
list.add(new ArrayList<>());
//当前位置有障碍,无法到达
if(obstacleGrid[i][0]==1){
list.get(i).add(0);
}else {
//当前位置无障碍,但是后面位置有障碍也无法到达
if(i>0){
if(list.get(i-1).get(0)==1){
list.get(i).add(1);
}else {
list.get(i).add(0);
}
}else {
list.get(i).add(1);
}
}
}
//初始化第一行
for(int i=1;i<n;i++){
if(obstacleGrid[0][i]==1){
list.get(0).add(0);
}else {
if(list.get(0).get(i-1)==1){
list.get(0).add(1);
}else {
list.get(0).add(0);
}
}
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
if(obstacleGrid[i][j]==1){
list.get(i).add(0);
}else {
list.get(i).add(list.get(i).get(j-1)+list.get(i-1).get(j));
}
}
}
return list.get(m-1).get(n-1);
}
九、最小路径和
leetcode 64t题
和第七题一样,先判断边界条件,之后找路径最小值即可。
public int minPathSum(int[][] grid) {
int m=grid.length;//行数
int n=grid[0].length;//列数
//判断边界条件
for(int i=1;i<m;i++){
grid[i][0]=grid[i-1][0]+grid[i][0];
}
for(int j=1;j<n;j++){
grid[0][j]=grid[0][j-1]+grid[0][j];
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
grid[i][j]=Math.min(grid[i-1][j],grid[i][j-1])+grid[i][j];
}
}
return grid[m-1][n-1];
}