初识动态规划


前言

最近在做题的时候,每次去做一些最大最小值,或者有多少种可能情况等问题的时候总是没有头绪,但是一看评论就是什么经典动规题目,思路清晰,而自己却很难写出来。今天就来学习一下动态规划,来弥补自己知识薄弱的地方。


动态规划(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];
    }
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值