奇妙的算法【1】-动态规划算法
1,菲波那契数列
斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波纳契数列以如下被以递推的方法定义:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用
兔子生兔子问题,兔子数列。
1.1 生兔子问题
古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第四个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少
兔子的规律为数列1,1,2,3,5,8,13,21....
package com.cnblogs.mufasa.Fibonacci; public class Answer1_Rabite { //最普通的递归调用,会做出很多次重复计算 public static int solution1(int x){ if(x==1 || x==2){ return 1; }else { return solution1(x-1)+solution1(x-2); } } //动态规划,速度很快【逆推导】 public static int solution2(int x){ if(x==1 || x==2){ return 1; } int a=1,b=1,c=0; for(int i=3;i<=x;i++){ c=a+b; a=b; b=c; } return c; } }
1.2 爬楼梯问题
有 N 阶楼梯,每次可以上一阶或者两阶,求有多少种上楼梯的方法。
package com.cnblogs.mufasa.Fibonacci; public class Answer2_climbStairs { public static int solution1(int x){//递归解法 if(x<=2){ return x; } return solution1(x-1)+solution1(x-2); } public static int solution2(int x){//动态规划 if(x<=2){ return x; } int a=1,b=2,c=0;//与生兔子问题不同,这里起始有变化 for(int i=3;i<=x;i++){ c=a+b; a=b; b=c; } return c; } }
1.3 强盗抢劫
https://leetcode-cn.com/problems/house-robber-ii/
抢劫一排住户,但是不能抢邻近的住户,求最大抢劫量
values={1,2,3,11,12,3,1,12};
package com.cnblogs.mufasa.Fibonacci; public class Answer3_Robber { //最普通的递归调用,会做出很多次重复计算 public static int solution1(int[] values,int index){ int x=values.length; if(x-index==1){ return values[index]; }else if(x-index==2) { return (values[index] > values[index-1] ? values[index] : values[index-1]); } int a=solution1(values,index+1);//不抢这一家 int b=values[index]+solution1(values,index+2);//抢这一家 return (a>b?a:b); } //动态规划,速度很快【逆推导】 public static int solution2(int[] values){ int x=values.length; if(x==1){ return values[0]; }else if(x==2) { return (values[0] > values[1] ? values[0] : values[1]); } int a=values[0],b=(values[0]>values[1]?values[0]:values[1]),c=0; for(int i=2;i<x;i++){ c=(a+values[i]>b?a+values[i]:b); a=b; b=c; } return c; } }
1.4 强盗在环形街区抢劫
https://leetcode.com/problems/house-robber-ii/description/
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
package com.cnblogs.mufasa.Fibonacci; public class Answer4_Robber2 { //最普通的递归调用,会做出很多次重复计算 public static int solution1(int[] values,int index0,int index1){//起始位置、终止 int x=values.length; if(x-index1-index0==1){ return values[index0]; }else if(x-index1-index0==2) { return (values[index0] > values[index0-1] ? values[index0] : values[index0-1]); } int a=solution1(values,index0+1,index1);//不抢这一家 int b=values[index0]+solution1(values,index0+2,index1);//抢这一家 return (a>b?a:b); } //动态规划,速度很快【逆推导】 public static int solution2(int[] values){ if(values==null || values.length==0){ return 0; } int x=values.length; if(x==1){ return values[0]; } int a=singleDeal(values,0,1); int b=singleDeal(values,1,0); return (a>b?a:b); } private static int singleDeal(int[] values,int index0,int index1){// int x=values.length; if(x-index1-index0==1){ return values[index0]; }else if(x-index1-index0==2) { return (values[index0] > values[index0+1] ? values[index0] : values[index0+1]); } int a=values[index0],b=(values[index0]>values[index0+1]?values[index0]:values[index0+1]),c=0; for(int i=index0+2;i<x-index1;i++){ c=(a+values[i]>b?a+values[i]:b); a=b; b=c; } return c; } }
1.5 信件错排
https://www.cnblogs.com/yjxyy/p/11116364.html
有 N 个 信 和 信封,它们被打乱,有多少种情况是所有人都收到了错误的邮件?
当n个编号元素放在n个编号位置,元素编号与位置编号各不对应的方法数用dp[n]表示,那么dp[n-1]就表示n-1个编号元素放在n-1个编号位置,各不对应的方法数,其它类推.
第一步,把第n个元素放在一个位置,比如位置k,一共有n-1种方法;
第二步,放编号为k的元素,这时有两种情况:⑴把它放到位置n,那么,对于剩下的n-1个元素,由于第k个元素放到了位置n,剩下n-2个元素就有dp[n-2]种方法;⑵第k个元素不把它放到位置n,这时,对于这n-1个元素,有dp[n-1]种方法;
package com.cnblogs.mufasa.Fibonacci; public class Answer5_mail { //最普通的递归调用,会做出很多次重复计算 public static int solution1(int x){ if(x<=1){ return 0; }else if(x==2){ return 1; } return (x-1)*(solution1(x-1)+solution1(x-2)); } //动态规划,速度很快【逆推导】 public static int solution2(int x){ if(x<=1){ return 0; }else if(x==2){ return 1; } int a=0,b=1,c=0; for(int i=3;i<=x;i++){ c=(i-1)*(a+b); a=b; b=c; } return c; } }
1.6 母牛生产【与兔子生兔子相似】
题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。
package com.cnblogs.mufasa.Fibonacci; public class Answer6_cow { //最普通的递归调用,会做出很多次重复计算 public static int solution1(int x){ if(x<=4){ return x; }else { return solution1(x-1)+solution1(x-3); } } //动态规划,速度很快【逆推导】 public static int solution2(int x){ if(x<=4){ return x; } int a=2,b=3,c=4,d=0; for(int i=5;i<=x;i++){ d=a+c; a=b; b=c; c=d; } return d; } }
验证程序:
package com.cnblogs.mufasa.Fibonacci; public class Client { public static void main(String[] args) { // System.out.println(Answer1_Rabite.solution1(8));//兔子问题,递归解法 // System.out.println(Answer1_Rabite.solution2(8));//兔子问题,动态规划 // System.out.println(Answer2_climbStairs.solution1(5));//爬楼梯问题,递归解法 // System.out.println(Answer2_climbStairs.solution2(5));//爬楼梯问题,动态规划 // System.out.println(Answer3_Robber.solution1(new int[]{1,2,3,11,12,3,1,12},0));//强盗问题,递归解法 // System.out.println(Answer3_Robber.solution2(new int[]{1,2,3,11,12,3,1,12}));//强盗问题,动态规划 // int a=Answer4_Robber2.solution1(new int[]{1,2,3,11,12,3,1,12},0,1);//环形强盗问题,递归解法 // int b=Answer4_Robber2.solution1(new int[]{1,2,3,11,12,3,1,12},1,0); // System.out.println((a>b?a:b));//环形强盗问题,递归解法 // System.out.println(Answer4_Robber2.solution2(new int[]{1,2,3,11,12,3,1,12}));//环形强盗问题,动态规划 // System.out.println(Answer5_mail.solution1(5));//信件问题,递归解法 // System.out.println(Answer5_mail.solution2(5));//信件问题,动态规划 // System.out.println(Answer6_cow.solution1(8));//母牛问题,递归解法 // System.out.println(Answer6_cow.solution2(8));//母牛问题,动态规划 } }
2,矩阵路径【有之前的一维转变到二维平面】
2.1 最小路径和
https://leetcode-cn.com/problems/minimum-path-sum/
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
package com.cnblogs.mufasa.Matrix_path; public class Answer1_minPath { public static int Solution1(int[][] grid) {//递归调用,算法复杂度O(2^(m+n)),空间复杂度为O(m+n) return calculate(grid, 0, 0); } private static int calculate(int[][] grid, int i, int j){ if (i == grid.length || j == grid[0].length) return Integer.MAX_VALUE;//无法到达此位置 if (i == grid.length - 1 && j == grid[0].length - 1) return grid[i][j];//此位置为目的地 return grid[i][j] + Math.min(calculate(grid, i + 1, j), calculate(grid, i, j + 1));//类似于二叉树分裂 } public static int Solution2(int[][] grid) {//动态规划,二维 if(grid.length==0||grid[0].length==0){ return 0; } int m=grid.length,n=grid[0].length; int[][] dp=new int[m][n]; for (int i = m - 1; i >= 0; i--) { for (int j = n - 1; j >= 0; j--) { if (i == grid.length - 1 && j != grid[0].length-1) { dp[i][j] = grid[i][j] + dp[i][j + 1]; } else if (i != grid.length - 1 && j == grid[0].length-1) { dp[i][j] = grid[i][j] + dp[i + 1][j]; } else if (i != grid.length - 1 && j != grid[0].length-1) { dp[i][j] = grid[i][j] + Math.min(dp[i + 1][j], dp[i][j + 1]); } else { dp[i][j] = grid[i][j]; } } } return dp[0][0]; } public static int Solution3(int[][] grid) {//一维动态规划 if (grid.length == 0 || grid[0].length == 0) { return 0; } int m = grid.length, n = grid[0].length; int[] dp = new int[n]; for(int i = m-1; i >=0; i--){ for(int j = n-1; j >=0; j--){ if(i==m-1&&j!=n-1){//水平移动 dp[j]=grid[i][j]+dp[j+1]; }else if(i!=m-1&&j==n-1){//上下移动 dp[j]=grid[i][j]+dp[j]; }else if(i!=m-1&&j!=n-1){ dp[j]=grid[i][j]+Math.min(dp[j],dp[j+1]); }else { dp[j]=grid[i][j]; } } } return dp[0]; } public static int Solution4(int[][] grid) {//动态规划,本质与上一个一维动态规划一致 if (grid.length == 0 || grid[0].length == 0) { return 0; } int m = grid.length, n = grid[0].length; int[] dp = new int[n]; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (j == 0) { dp[j] = dp[j]; // 只能从上侧走到该位置 } else if (i == 0) { dp[j] = dp[j - 1]; // 只能从左侧走到该位置 } else { dp[j] = Math.min(dp[j - 1], dp[j]); } dp[j] += grid[i][j]; } } return dp[n - 1]; } public static int Solution5(int[][] grid) {//动态规划,不开辟新空间,但是会修改元数组内容,本质还是一样 if (grid.length == 0 || grid[0].length == 0) { return 0; } int m = grid.length, n = grid[0].length; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if(i==0&&j!=0){ grid[i][j]=grid[i][j-1]+grid[i][j]; }else if(i!=0&&j==0){ grid[i][j]=grid[i-1][j]+grid[i][j]; }else if(i!=0&&j!=0){ grid[i][j]=grid[i][j]+Math.min(grid[i-1][j],grid[i][j-1]); } } } return grid[m-1][n-1]; } }
2.2 矩阵的总路径数
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
package com.cnblogs.mufasa.Matrix_path; import java.util.Arrays; public class Answer2_multiple { public static int solution1(int m, int n){//递归调用 if(m==1&&n==1){//原地不动 return 1; } if(m>1&&n>1){ return solution1(m-1, n)+solution1(m, n-1); }else { return 1; } } public static int solution2(int m, int n){//动态规划,一维 int[] dp = new int[n]; Arrays.fill(dp,1); for (int i = 1; i < m; i++) { for (int j = 1; j < n; j++) { dp[j] = dp[j] + dp[j - 1]; } } return dp[n-1]; } }
验证程序:
package com.cnblogs.mufasa.Matrix_path; public class Client { public static void main(String[] args) { // int[][] grid={{1,3,4,8},{3,2,2,4},{5,7,1,9},{2,3,2,3},{2,3,2,3}}; // System.out.println(Answer1_minPath.Solution1(grid));//递归调用 // System.out.println(Answer1_minPath.Solution2(grid));//二维动态规划 // System.out.println(Answer1_minPath.Solution3(grid));//一维动态规划 // System.out.println(Answer1_minPath.Solution4(grid));//一维动态规划 // System.out.println(Answer1_minPath.Solution5(grid));//动态规划,不开辟空间 System.out.println(Answer2_multiple.solution1(3,7)); System.out.println(Answer2_multiple.solution2(3,7)); } }
3,数组区间
4,背包问题
①时间-任务-价值问题:https://blog.csdn.net/houboTech/article/details/79689157
视频讲解:https://www.bilibili.com/video/av16544031?from=search&seid=4083931774448408088
这里使用三种解题思路:①递归算法;②登记表【overlap sub-problem重叠子问题】;③动态规划。
package com.cnblogs.mufasa.demo1; import java.util.HashMap; public class Time_value { private final int num=8; private final int[][] times=new int[][] {{1,4},{3,5},{0,6},{4,7},{3,8},{5,9},{6,10},{8,11}}; private final int[] values={5,1,8,4,6,3,2,4}; private final int[] prevs=preNode(times); //递归法,时间复杂度为O(2^n) public int rec_opt(int i) { if(i<=0) return 0; if(i==1) return values[i-1]; int choice = values[i-1] + rec_opt(prevs[i-1]); int notChoice = rec_opt(i-1); return max(notChoice,choice); } //登记表形式,算法复杂度(n)【本质是将重复子问题进行记录防止重复计算】 HashMap<Integer,Integer> hm=new HashMap<>(50); public int dict_opt(int i){ if(hm.containsKey(i)){ return hm.get(i); } int outNum=0; if(i==1){ outNum= values[i-1]; }else { outNum=max(values[i-1] + rec_opt(prevs[i-1]),rec_opt(i-1)); } hm.put(i,outNum); return outNum; } //动态规划,时间复杂度为O(n),本质上与登记表形式相同,但是代码更加优雅 public int dp_opt() {//求做任务可以得到的最大价值(动态规划法) int[] opt = new int[values.length + 1]; opt[0] = 0; opt[1] = values[0]; for (int i = 1; i < opt.length; i++) { int choice = values[i - 1] + opt[prevs[i - 1]]; int notChoice = opt[i - 1]; opt[i] = max(choice, notChoice); } return opt[opt.length - 1]; } //辅助方法 private int[] preNode(int[][] times){//获取prevs数据【成功】 int[] prevs=new int[times.length]; for(int i=1;i<times.length;i++){ for(int j=i-1;j>=0;j--){ if(times[j][1]<=times[i][0]){ prevs[i]=j+1; break; } } } return prevs; } private static int max(int a,int b){ return (a>=b?a:b); } //算法验证 public static void main(String[] args) { Time_value tv=new Time_value(); int result1=tv.rec_opt(8); int result2=tv.dp_opt(); int result3=tv.dict_opt(8); System.out.println(result1); System.out.println(result2); System.out.println(result3); } }
参考链接: