介绍递归和动态规划
暴力递归:
1.把问题转化为规模缩小了的同类问题的子问题
2.有明确的不需要继续进行递归的条件(base case)
3.有当得到了子问题的结果之后的决策过程
4.不记录每个子过程的解
动态规划
1.从暴力递归中来
2.将每一个子问题的解记录下来,避免重复计算
3.把暴力递归的过程,抽象成了状态表达
4.并且存在化简状态表达,使其更加简洁的可能
求n!
package com.zuogod.java;
/**
* @author quanquan
* @create 2020-05-02-21:04
*/
public class Factorial {
public static int factorial(int n){
if (n == 1){
return 1;
}
return n*factorial(n-1);
}
public static void main(String[] args) {
System.out.println(factorial(4));
}
}
汉诺塔问题
打印n层汉诺塔从最左边到最右边的全部过程
package com.zuogod.java;
/**
* @author quanquan
* @create 2020-05-02-21:29
*/
public class Hanoi {
public static void process(int N, String from, String to, String help){
if(N == 1){
System.out.println("move 1 from " + from + " to " +to);
}else {
process(N-1, from,help,to);
System.out.println("move " + N + " from " + from + " to " + to);
process(N-1,help,to,from);
}
}
public static void main(String[] args) {
process(3,"左","右","中");
}
}
package com.zuogod.java;
/**
* @author quanquan
* @create 2020-05-02-22:10
*/
public class Print_All_Subsequences {
public static void printAllSub(char[] str,int i,String res){
if (i == str.length){
System.out.println(res);
return;
}
printAllSub(str,i+1,res);
printAllSub(str,i+1,res+String.valueOf(str[i]));
}
public static void main(String[] args) {
String test = "abc";
printAllSub(test.toCharArray(),0,"");
}
}
先不考虑是否出现重读字符,要对一个字符进行全排列,可以把第一个字符和后面的字符看成两部分,而第一个字符后面的字符又可看成第一个字符与后面两部分,这显然是一个递归的过程,只要第一个字符的位置没有到达字符串的末尾就分别将第一个字符与后面的字符进行交换。这里有一点需要注意:那就是比如第一个字符与后面的某个位置的字符发生交换后,需要再次发生交换,不然顺序就会被打乱。举个例子,在字符串abc中,在把第一个字符看成是a,后面的字符b、c看成一个整体的时候,abc这个相对顺序不能改变,所以当b与c发生交换变成了acb之后,需要再次交换两个字符,重新回到abc
package com.zuogod.java;
import java.util.HashSet;
/**
* @author quanquan
* @create 2020-05-02-22:44
*/
public class Print_All_Permutations {
public static void printAllPermutations1(String str) {
char[] chs = str.toCharArray();
process1(chs, 0);
}
public static void process1(char[] chs, int i) {
if (i == chs.length) {
System.out.println(String.valueOf(chs));
}
for (int j = i; j < chs.length; j++) {
swap(chs, i, j);
process1(chs, i + 1);
swap(chs, i, j);
}
}
public static void swap(char[] chs, int i, int j) {
char tmp = chs[i];
chs[i] = chs[j];
chs[j] = tmp;
}
//不打印重复字符串
public static void printAllPermutations2(String str) {
char[] chs = str.toCharArray();
process2(chs, 0);
}
public static void process2(char[] chs, int i) {
if (i == chs.length) {
System.out.println(String.valueOf(chs));
}
HashSet<Character> set = new HashSet<>();
for (int j = i; j < chs.length; j++) {
if (!set.contains(chs[j])) {
set.add(chs[j]);
swap(chs, i, j);
process2(chs, i + 1);
swap(chs, i, j);
}
}
}
public static void main(String[] args) {
String test = "abc";
printAllPermutations1(test);
System.out.println();
printAllPermutations2(test);
}
}
母牛每年生一只母牛,新出生的母牛成长三年后也能每年生出一只母牛,假设不会死。求N年后,母牛的数量
【答】f(n) = f(n-1) + f(n-3),n >=4(f(n-1)表示去年的牛,f(n-3)表示三年前的牛所生的小牛)
package com.zuogod.java;
/**
* @author quanquan
* @create 2020-05-03-14:10
*/
public class Cow {
public static int cowNumber(int N){
if (N < 1) {
return 0;
}
int res = 0;
if (N < 4){
return N;
}
res = cowNumber(N-1) + cowNumber(N-3);
return res;
}
public static int cowNumber2(int n) {
if (n < 1) {
return 0;
}
if (n == 1 || n == 2 || n == 3) {
return n;
}
int res = 3;
int pre = 2;
int prepre = 1;
int tmp1 = 0;
int tmp2 = 0;
for (int i = 4; i <= n; i++) {
tmp1 = res;
tmp2 = pre;
res = res + prepre;
pre = tmp1;
prepre = tmp2;
}
return res;
}
public static void main(String[] args) {
System.out.println(cowNumber(20));
System.out.println(cowNumber2(20));
}
}
当暴力递归展开发现有重复状态,且重复状态与到达它的路径无关(无后效性问题),则这种问题一定可以改为动态规划
给你一个二维数组,二维数组中的每一个数都是正数,要求从左上角走到右下角,每一步只能向右或者向下。沿途经过的数字要累加起来。返回最小的路径和。
package com.zuogod.java;
/**
* @author quanquan
* @create 2020-05-03-14:38
*/
public class MinPath {
//暴力递归
public static int minPath(int[][] matrix,int i,int j){
if (i == matrix.length-1 && j == matrix[0].length-1){
return matrix[i][j];
}
if (i == matrix.length-1){
return matrix[i][j] + minPath(matrix,i,j+1);
}
if (j == matrix[0].length-1){
return matrix[i][j] + minPath(matrix,i+1,j);
}
int right = minPath(matrix,i,j+1); // 右边位置到右下角的最短路径和
int down = minPath(matrix,i+1,j); // 下边位置到右下角的最短路径和
return matrix[i][j] + Math.min(right,down);
}
//动态规划
public static int minPath2(int[][] m){
if (m == null || m.length == 0 || m[0] == null || m[0].length == 0){
return 0;
}
int row = m.length;
int col = m[0].length;
int[][] dp = new int[row][col];
dp[0][0] = m[0][0];
for (int i=1; i<row; i++){
dp[i][0] = dp[i-1][0] + m[i][0];
}
for (int j = 1; j < col; j++) {
dp[0][j] = dp[0][j - 1] + m[0][j];
}
for (int i = 1; i < row; i++) {
for (int j = 1; j < col; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + m[i][j];
}
}
return dp[row - 1][col - 1];
}
public static void main(String[] args) {
int[][] m = { { 1, 3, 5, 9 }, { 8, 1, 3, 4 }, { 5, 0, 6, 1 }, { 8, 8, 4, 0 } };
System.out.println(minPath(m, 0, 0));
System.out.println(minPath2(m));
}
}
package com.zuogod.java;
/**
* @author quanquan
* @create 2020-05-03-15:32
*/
public class Money_Problem {
//暴力递归
public static boolean isSum(int[] arr, int i,int sum,int aim){
if (i == arr.length){
return sum == aim;
}
return isSum(arr,i+1,sum,aim) || isSum(arr,i+1,sum+arr[i],aim);
}
//动态规划
public static boolean isSumByDP(int target,int[] arrs){
int sum = 0;
for(int i = 0; i < arrs.length; i++)
{
sum += arrs[i];
}
if(target > sum)return false;//所有值加起来都没有目标值大,直接返回false
boolean[][] dp = new boolean[arrs.length+1][sum+1];
for (int j = 0; j <= sum; j++) {//先将最后一行已经知道结果的填充进去
dp[arrs.length][j] = j==target;
}
for(int i = arrs.length - 1; i >= 0; i--)
for(int j = 0; j <= sum; j++)
{
// dp[i][j] = dp[i + 1][j];
if(j + arrs[i] <= sum){//不超出部分,j+arrs[i]表示当前叠加值加上该位置值
dp[i][j] = dp[i+1][j] || dp[i+1][j+arrs[i]];//选中和不选中方案
}
}
return dp[0][0];
}
public static void main(String[] args) {
int[] arr = { 1, 4, 8 };
int aim = 12;
System.out.println(isSum(arr, 0, 0, aim));
System.out.println(isSumByDP( 12,arr));
}
}
参考1
参考2