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时,第一次跳的时候有两种等可能的选择
- 第一次跳一级,则还剩(n-1)级台阶,跳法数为F(n-1)
- 第一次跳两级,则还剩(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:
- 本列之前就成功过了:
dp[i-1][j]==true
这次不选就能成功,置dp[i][j]为true - 就差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];
}
}