Unit 4 递归和动态规划

Unit 4 递归和动态规划

Q1:斐波那契数列+青蛙跳台阶问题
Q2:剪绳子问题 or 整数拆分
Q3:二进制中1的个数
Q4:数字字符串翻译成英文的方法个数(解码方法)
Q5:棋盘路径和最大
Q6:n个骰子的点数
Q7:跳跃游戏
Q8:分割等和子集(背包问题)

unit 4 Q1:斐波那契数列

剑指offer 10 Leetcode 509 难度:简单
描述:斐波那契数列 F(n)=F(n-1)+F(n-2):{1,1,2,3,5,8,…}
time:2019/08/30
思路:
1.暴力递归。时间复杂度O(2^N)

    public int f1(int n){
        if(n<1)
            return 0;
        if(n==1||n==2)
            return 1;
        return f1(n-1)+f1(n-2);
    }

缺点:求F(10)要求F(8)和F(9),求F(9)要求F(7)和F(8)…像F(8)就被求了两次。且重复的节点数随n的增大呈指数级递增。

2.(重要)顺序通过前面的两个求后面的值。时间复杂度O(N)
date:2019/11/12
根据F(1)、F(2)求F(3),再根据F(2)、F(3)求F(4)。。。最后求到F(n)

    //顺序非递归版
    public int f2(int n){
        if(n<1)
            return n;
        if(n==1||n==2)
            return 1;
        int f_i_2=1; //f[1]=1
        int f_i_1=1; //f[2]=1
        int f_i=f_i_1+f_i_2;
        for(int i=3;i<=n;i++){
            f_i=f_i_1+f_i_2;
            f_i_2=f_i_1;//都往右移了
            f_i_1=f_i;//都往右移了
        }
        return f_i;
    }

附:青蛙跳台阶问题
题目:一只青蛙一次可以跳一级台阶,也可以跳两级台阶。求该青蛙跳上一个n级台阶总共有多少种跳法。
解:
把n级台阶时的跳法看成n的函数,记为F(n)。

  • 只有一级台阶,青蛙只跳一次,F(1)=1
  • 只有两级台阶,青蛙可以分两次跳,每次跳1级;也可以一次跳两级,F(2)=2
  • n>2时,第一次跳的时候有两种等可能的选择
  1. 第一次跳一级,则还剩(n-1)级台阶,跳法数为F(n-1)
  2. 第一次跳两级,则还剩(n-2)级台阶,跳法数为F(n-2)

因此n级台阶的跳法数F(n)=F(n-1)+F(n-2),即斐波那契数列。

 

unit 4 Q2:剪绳子问题 or 整数拆分

剑指offer 14 Leetcode 343 难度:中等
给订一根长度为n(n>1)的绳子,把绳子剪成m(m>1)段,将每段绳子长度相乘,求其最大乘积。
例:当n=8时,剪成长度为2,3,3的三段,此时得到的最大乘积为18
date:2019/12/8
思路
方法一:动态规划,时间复杂度O(n^2),空间复杂度O(n)
1.创立一个数组F,长度为n+1。F[n]代表长度为n的绳子被剪成若干段后的最大乘积。
F(n)=max(F[i]*F[n-i])。通过之前的F[1]、F[2]…F[n-1]求F[n]

    public int cutRope(int n){
        //三种初始情况
        if(n<=1) return -1;//出错了
        if(n==2) return 1;//至少得砍一刀
        if(n==3) return 2;//1*2=2

        int[] F=new int[n+1];
        F[0]=0;
        F[1]=1;//被剪的就剩一段的时候就是1
        F[2]=2;//绳长为2,不剪了,长度为2时最大乘积为2
        F[3]=3;//绳长为3,不剪了,长度为3时最大乘积为3
        for(int i=4;i<n+1;i++){
            int max=0;
            for(int j=1;j<=i/2;j++){
                int cur=F[j]*F[i-j];
                max=(cur>max)?cur:max;//更新max
            }
            F[i]=max;
        }
        return F[n];
    }

方法二:贪心算法,时间复杂度O(1),空间复杂度O(1)
根据常理,一个数字拆开之后的乘积要大于原数字;将原数字拆的越细越均匀,乘积越大。
若原数字为n,可将n表示为 n=x * a+b,x是拆成的小段,b是剩下的一个。
原问题就转化为,已知正数n,求满足n=x * a+b条件下(x^a) * b的最大值
经过枚举,我们得到以下规律:

  • 1.将小段x的长度定为3的时候,小段乘积会最大;
  • 2.余数b=2时,不再将其拆成1 * 1,直接在(3^a)基础上再乘2即可
  • 3.余数b=1时,取出一个小段和1合并成4,剪成2* 2,即乘积为(3^(a-1)) * 2 * 2
    (设n=4,若不取出合并,1 * 3=3;若取出合并,2 * 2=4>3)
  • 4.余数b=0时,(3^a)即可
    public int cutRope_greedy(int n){
        //三种初始情况
        if(n<=1) return -1;//出错了
        if(n==2) return 1;//至少得砍一刀
        if(n==3) return 2;//1*2=2

        int a=n/3,b=n%3;
        if(b==0)
            return (int)Math.pow(3,a);
        if(b==1)
            return (int)Math.pow(3,a-1)*2*2;
        else
            //b==2
            return (int)Math.pow(3,a)*2;
    }

 

unit 4 Q3:二进制中1的个数

剑指offer 15 Leetcode 338 难度:中等
输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
date:2019/12/9
思路
Leetcode题解中的一道奇技淫巧,做到了时、空复杂度均为O(n)
对于十进制的数,其对应的二进制数中,总有两个规律:

  • 二进制表示中,奇数一定比前面的偶数多一个1,在最低位多一个1.
    (例:0的二进制=0,1的二进制=1。2的二进制=10,3的二进制11。)
  • 二进制表示中,对偶数除以2,除前和除后的1的个数一样多。因为偶数最低位是0,除二相当于整个偶数右移一位,抹去最低位的0,1的个数不变。
    (例:2的二进制=10,4的二进制=100,8的二进制=1000。3的二进制=11,6的二进制=110,12的二进制=1100)

因此设置数组F[n],F[n]表示n的二进制数中1的个数。开始情况,0的二进制=0,0个1,F[0]=0

分析到这,通过前面的求后面的,最后返回F[n],是不是有动态规划内味儿了。

    public int count1InBinary(int n){
        int[] F=new int[n+1];//F[n]代表n对应二进制数的1的个数
        F[0]=0;//0的二进制有0个1
        for(int i=1;i<n+1;i++){
            //奇数。比前面偶数的最低位多一个1
            if(i%2==1)
                F[i]=F[i-1]+1;
            //偶数,和除以2的数的1的个数一样多
            else
                F[i]=F[i/2];
        }
        return F[n];
    }

 

unit 4 Q4:数字字符串翻译成英文的方法个数(解码方法)

剑指 46 Leetcode 91 难度:中等
一条包含字母 A-Z 的消息通过以下方式进行了编码:
‘A’ -> 1
‘B’ -> 2

‘Z’ -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数。

示例 1:
输入: “12”
输出: 2
解释: 它可以解码为 “AB”(1 2)或者 “L”(12)。

示例 2:
输入: “226”
输出: 3
解释: 它可以解码为 “BZ” (2 26), “VF” (22 6), 或者 “BBF” (2 2 6) 。

date:2020/02/16
思路:
先递归求解。再想办法用动态规划实现递归的想法。思路来源参考
这道题我注释写的挺详细,思路就懒狗了,直接看代码吧。

  • 递归方法:
	//递归实现
    public int numDecodings(String s) {
        if(s==null||s.equals(""))
            return 0;
        //字符串 to 字符串数组
        String[] strArray=s.split("");
        //字符串数组 to int数组
        int[] intArr=new int[strArray.length];
        for(int i=0;i<strArray.length;i++){
            intArr[i]=Integer.parseInt(strArray[i]);
        }
        //从左向右遍历
        return digui(intArr,0);
    }

    public int digui(int[] nums,int i){
        //递归中止条件
        //最后一层再下1,定为1
        if(i==nums.length)
            return 1;
        //最后一位,如果最后一位是0,该位不能单独表达一个字母,情况个数0.
        if(i==nums.length-1)
            if(nums[i]==0)
                return 0;
            else
                return 1;
        //以0位开始的一个数字,无论如何都不能单独表达一个字母
        if(nums[i]==0)
            return 0;
        //进入递归
        int res;//就是暂存本层digui(nums,i)的值
        if(nums[i]*10+nums[i+1]<=26)
            //nums[i]不仅能自己组成字母,还能跟后面一位一起组成字母
            //num[i]自己单独字母的情况 + num[i]和num[i+1]一起组成字母的情况
            res=digui(nums,i+1)+digui(nums,i+2);
        else
            //nums[i]只能自己组成字母
            res=digui(nums,i+1);
        return res;
    }
  • 动态规划
	//动态规划实现递归
	    public int numDecodings(String s) {
        if(s==null||s.equals(""))
            return 0;
        //字符串 to 字符串数组
        String[] strArray=s.split("");
        //字符串数组 to int数组
        int[] intArr=new int[strArray.length];
        for(int i=0;i<strArray.length;i++){
            intArr[i]=Integer.parseInt(strArray[i]);
        }
        int len=intArr.length;
        //-----------------动态规划部分--------------------
        //设立动态规划数组。长度是(len+1)
        int[] dp=new int[len+1];
        dp[len]=1;//
        //最后一位如果是0,不能单独成字母,则dp[len-1]是0
        dp[len-1]=(intArr[len-1]==0)?0:1;
        //从右向左遍历,从倒数第二位开始
        for(int i=len-2;i>=0;i--){
            //遍历到的数字是0,不能单独成字母,要他无用
            if(intArr[i]==0){
                dp[i]=0;
                continue;
            }
            //遍历到的intArr[i]不仅自己能组字母,还能跟后面的一起组个字母
            if(10*intArr[i]+intArr[i+1]<=26)
                dp[i]=dp[i+1]+dp[i+2];//两种情况的次数都算进去
            else
                //就自己组字母一种情况
                dp[i]=dp[i+1];
        }
        //又回到最初的起点
        return dp[0];
    }

 

unit 4 Q5:棋盘路径和最大(礼物的最大价值)

剑指offer 47 牛客链接
在这里插入图片描述
date:2020/02/17
思路:
设F(i,j)为走到board[i][j]位置时的最大和。可以得到递归式:
F[ i ][ j ]=max(F[ i-1 ][ j ],F[ i ][ j-1 ])+board[ i ][ j ]
递归做法:

    public int getMost(int[][] board) {
        // write code here
        if(board==null||board.length==0)
            return -1;
        int maxI=board.length,maxJ=board[0].length;
        return digui(board,maxI-1,maxJ-1);
    }
    
    //digui(i,j)返回的是到达(i,j)时所能得到的最大值
    public int digui(int[][] board,int i,int j){
        int maxI=board.length,maxJ=board[0].length;//maxI:行的最大值。maxJ:列的最大值
        if(i>=maxI||j>maxJ)
            return -1;
        int up=0,left=0;
        if(i-1>=0)
            up=digui(board,i-1,j);
        if(j-1>=0)
            left=digui(board,i,j-1);
        int res=board[i][j]+Math.max(up,left);
        return res;
    }

动态递归做法:

    public int getMost(int[][] board) {
        // write code here
        if(board==null||board.length==0)
            return -1;
        int maxI=board.length,maxJ=board[0].length;
        int[][] dp=new int[maxI][maxJ];
        for(int i=0;i<maxI;i++){
            for(int j=0;j<maxJ;j++){
                int up=0,left=0;
                if(i-1>=0)
                    up=dp[i-1][j];
                if(j-1>=0)
                    left=dp[i][j-1];
                dp[i][j]=board[i][j]+Math.max(up,left);
            }
        }
        return dp[maxI-1][maxJ-1];
    }

 

unit 4 Q6:n个骰子的点数

剑指offer 60 Leetcode:简单(我觉得给个困难都不过分)
把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。

date:2020/02/23

思想:
在这里插入图片描述
递归实现:

class Solution {
    //n个骰子,点数之和最小为n*1=n,最大为n*6=6n,共有(6n-n+1)=(5n+1)个可能的点数和
    //点数和n存res[0],点数和(n+1)存res[1]...点数和(n+i)存res[i]中...点数和s存res[s-n]中
    //double[] res=new double[5n+1];
    int N;
    public double[] twoSum(int n) {
        N=n;
        //n个骰子,点数和s范围是n~6n
        double[] res=new double[5*n+1];
        double sum=Math.pow(6,n);
        for(int s=n;s<=6*n;s++){
            res[s-n]=F(n,s)/sum;//结果除以所有可能的次数
        }
        return res;
    }
    //第n个骰子点数1,则点数和为F(n-1,s-1);点数2,点数和为F(n-1,s-2)...点数6,点数和为F(n-1,s-6)
    //由于前(n-1)个骰子点数和最小为(n-1),累加公式时后面的点数和要大于等于n-1
    //F(n,s)=F(n-1,s-1)+F(n-1,s-2)+F(n-1,s-3)+F(n-1,s-4)+F(n-1,s-5)+F(n-1,s-6)
    //n个骰子,点数之和s
    public double F(int n,int s){
        //F(1,1) F(1,2) F(1,3) F(1,4) F(1,5) F(1,6)都得返回1
        if(n==1)
            if(s>=1&&s<=6)
                return 1;
            else
                return 0;
        if(n>s)
            return 0;
        double result=0;
        for(int S=s-1;S>=s-6&&S>=n-1;S--)
            result+=F(n-1,S);
        return result;
    } 
}

动态规划实现:

class Solution {
    public double[] twoSum(int n) {
        double[][] dp=new double[n+1][6*n+1];//代替F(n,s),其中s的最大值=6*n
        //F(1,1) F(1,2) F(1,3) F(1,4) F(1,5) F(1,6)都是1,其他F(1,s)都是0
        for(int j=1;j<=6;j++)
            dp[1][j]=1;
        //根据F(n,s)=F(n-1,s-1)+F(n-1,s-2)+F(n-1,s-3)+F(n-1,s-4)+F(n-1,s-5)+F(n-1,s-6)
        //其中n要大于s
        for(int i=2;i<=n;i++){
            for(int s=i;s<=6*i;s++){
                //求dp[i][s] 即求F(i,s)
                int temp=0;
                for(int S=s-1;S>=s-6&&S>=i-1;S--)
                    temp+=dp[i-1][S];
                dp[i][s]=temp;
            }
        }
        //对每一个数除以6^n,就是它的概率
        double sum=Math.pow(6,n);
        for(int i=0;i<dp.length;i++)
            for(int j=0;j<dp[0].length;j++)
                dp[i][j]=dp[i][j]/sum;
        //返回结果。n个骰子的点数和s的范围是n~6n。dp[n]是n个骰子的各种点数和可能情况的概率
        double[] res=new double[5*n+1];
        for(int s=n;s<=6*n;s++)
            res[s-n]=dp[n][s];
        return res;
    }
}

 

unit 4 Q7:跳跃游戏

Leetcode 55 难度:中等
思路:
给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个位置。

示例 1:
输入: [2,3,1,1,4]
输出: true
解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。

示例 2:
输入: [3,2,1,0,4]
输出: false
解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。

date:2020/03/04

方法一:递归
我自己能想到的。

class Solution {
    public boolean canJump(int[] nums) {
        return digui(nums,0);
    }
    public boolean digui(int[] nums,int i){
        int len=nums.length;
        //结束条件,找到最后一个了
        if(i==len-1)
            return true;
        int cur=nums[i];//当前值,能跳几步
        //boolean isArrived=false;
        for(int n=1;n<=cur&&i+n<len;n++){
        	//这次跳n步找到最后一个了没
            boolean isCurArrived=digui(nums,i+n);
            if(isCurArrived)
                return true;
        }
        return false;
    }
}

方法二:贪心算法
贪心算法,遍历到nums[i]意味着:能通过某种方法走到i这个位置。
如果能走到4位置,一定有某种办法走到0,1,2,3。
每轮遍历找到当前能走到的最远位置;当i>max说明走到目前为止不能走到的地方,循环中止。

class Solution {
    public boolean canJump(int[] nums) {
        int max=0;//当前能走到最远的位置
        for(int i=0;i<nums.length;i++){
            //走到目前为止不能走到的地方了,所以出错了
            if(i>max)
                return false;
            //如果能走到i这个位置,则从i开始还能接着走到(i+nums[i])。
            //与当前max比较,取较大者为当前能走到最远的位置
            max=Math.max(max,i+nums[i]);
        }
        return true;
    }
}

 

unit 4 Q8:分割等和子集

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

注意:
每个数组中的元素不会超过 100;数组的大小不会超过 200

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

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

思路:

本题思路可以转换成:能否找到一组数,使他们的和刚好等于数组和的一半
在这里插入图片描述
以上图dp矩阵为例:
dp矩阵大小为 len*(target+1),其中目标和target=sum/2
d[i][j]表示:从arr[0] -> arr[i]能挑出若干个,其和刚好等于j
例:d[2][3]:从arr[0] -> arr[2]能挑出若干个,他们的和刚好等于3

初始工作
(1) 第一列的target为0,不挑数字就可以使和为0,所以第一列dp[i][0]全部为true
(2) 处理arr[0],arr[0]只能使dp矩阵中第一行中满足j=arr[0]的为true。故置dp[0][arr[0]]为true

状态转移
以下两种情况置dp[i][j]为true:

  1. 本列之前就成功过了:dp[i-1][j]==true这次不选就能成功,置dp[i][j]为true
  2. 就差arr[i]就成功了:int curNeedDiff=j-arr[i]; 如果dp[i-1][curNeedDiff]==true
    curNeedDiff:当前还需要多少。
    加上这次arr[i]刚好凑够target,置dp[i][j]为true
代码:
class Solution {
    public boolean canPartition(int[] arr) {
        int len=arr.length;
        if(len<=1)
            return false;//数组长度为1和0都不能成功分割
        int sum=0,target;
        for(int num:arr){
            sum+=num;
        }
        if(sum%2==0)
            target=sum/2;
        else
            return false;//总数为奇数
        boolean[][] dp=new boolean[len][target+1];
        //第一列,target=0,全部都为true
        //int i;
        for(int i=0;i<len;++i)
            dp[i][0]=true;
        //处理arr[0],arr[0]只能使dp矩阵中第一行中满足j=arr[i]的为true
        dp[0][arr[0]]=true;
        //从第二行开始遍历(需要借助上一层的状态且上一行已做过初始处理)
        for(int i=1;i<len;++i){
            for(int j=1;j<target+1;++j){
                //之前就成功了
                int curNeedDiff=j-arr[i];//目标值减去当前值还需要多少
                if(dp[i-1][j])
                    dp[i][j]=true;
                else if(curNeedDiff>=0&&dp[i-1][curNeedDiff])
                    dp[i][j]=true;
            }
        }
        return dp[len-1][target];//图的右下角,最后一个位置 
    }
}

简化版:
从后往前,相当于第i轮遍历的dp都是在第i-1轮的dp结果上做的

class Solution {
    public boolean canPartition(int[] arr) {
        int len=arr.length;
        if(len<=1)
            return false;//数组长度为1和0都不能成功分割
        int sum=0,target;
        for(int num:arr){
            sum+=num;
        }
        if(sum%2==0)
            target=sum/2;
        else
            return false;//总数为奇数
        //dp简化版本:dp[j]能找到若干个数,使其和为k
        boolean[] dp=new boolean[target+1];
        dp[0]=true;
        for(int i=0;i<len;++i){
            for(int j=target;j>=0;--j){
                if(dp[j])//上一轮为true,这一轮必为true
                    continue;
                int curNeedDiff=j-arr[i];
                if(curNeedDiff>=0 && dp[curNeedDiff])
                    dp[j]=true;
            }
        }
        return dp[target];
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值