动态规划

动态规划

1. 打印字符串的所有子序列

如题,比如有字符串abc,则它的子序列有" ""a","b","c","ac","ab","bc","abc"

public class Solution_PrintAllSub {

    public static void printAllSub(char[] str, int i, String res){
        if(i == str.length){
            System.out.println(res);
            return;
        }
        //结果字符串初始化为空
        //遍历数组,每遇到一个元素,有两个选择:
        //1.结果字符串中不加入该元素
        //2.结果字符串中加入该元素
        //数组遍历完毕,分别打印所得到的结果字符串。
        printAllSub(str, i + 1, res);
        printAllSub(str, i + 1, res + str[i]);
    }

    public static void main(String[] args) {
        String str = "abc";
        printAllSub(str.toCharArray(), 0, "");
    }
}

2. 子集

lletcode-子集

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]

import java.util.ArrayList;
import java.util.List;

class Solution {
    public static List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> lists = new ArrayList<>();
        List<Integer> list = new ArrayList<>();
        process(nums, 0, list, lists);
        return lists;
    }

    public static void process(int[] nums, int i, List<Integer> list, List<List<Integer>> lists){
        if(i == nums.length){
            lists.add(list);
        }else{
            process(nums, i + 1, list, lists);
            List<Integer> temp = new ArrayList<>(list);
            temp.add(nums[i]);
            process(nums, i + 1, temp, lists);
        }
    }

    public static void main(String[] args) {
        int[] nums = new int[]{1,2,3};
        List<List<Integer>> lists = subsets(nums);
    }
}

注:list.add(nums[i])返回的是一个布尔类型,不能直接写process(nums, i + 1, list.add(nums[i]), lists);
如果先添加数据,再进入递归

process(nums, i + 1, list, lists);
list.add(nums[i]);
process(nums, i + 1, list, lists);

会导致递归栈中list的值发生异常改变,list的值不应该改变,那么怎么向下传递呢?
可以复制list到一个辅助链表,但 List对象如果直接使用“=”进行赋值,如List<Integer> temp = list,如果修改temp的话,还是会修改list,因为这种方式是将temp的地址指向了list。那怎么复制呢?

List<Integer> temp = new ArrayList<>(list);//得到一个tmep,地址不同,内容相同。

所以process函数最终代码为

public static void process(int[] nums, int i, List<Integer> list, List<List<Integer>> lists){
    if(i == nums.length){
        lists.add(list);
    }else{
        process(nums, i + 1, list, lists);
        List<Integer> temp = new ArrayList<>(list);
        temp.add(nums[i]);
        process(nums, i + 1, temp, lists);
    }
}

3. 打印一个字符串的全排列

字符串全排列

思路:将字符串转换成char类型的数组,先将数组排序,然后使用process函数,对[begin, end]区间的元素进行排列,每次交换arr[begin - 1]与arr[i] (begin <= i <= end)元素,不同的交换构成不同的前缀。前缀固定后,在对后续元素递归即可。

import java.util.ArrayList;

import static java.util.Arrays.sort;

class Solution {

    //每次增加固定前缀的个数,对后面的进行全排列
    public static ArrayList<String> Permutation(String str) {
        ArrayList<String> lists = new ArrayList<String>();
        char[] arr = str.toCharArray();
        sort(arr);//使用工具类java.util.Arrays进行排序
        process(lists, arr, 0, arr.length - 1);
        return lists;
    }

    public static void process(ArrayList<String> lists, char[] arr, int begin, int end){
        if(begin == end){
            String temp = String.valueOf(arr);
            if(!lists.contains(temp)){
                lists.add(temp);
            }
            return;
        }else{
            if(arr[begin] == arr[begin + 1]){//数组已排好序, 有重复字符的话,直接将其计入前缀
                process(lists, arr, begin + 1, end);
            }else{
                process(lists, arr, begin + 1, end);
                for(int i = begin + 1; i <= end; i++){
                    swap(arr, begin, i);
                    process(lists, arr, begin + 1, end);
                    swap(arr, begin, i);
                }
            }
        }
    }

    public static void swap(char[] arr, int m, int n){
        char temp = arr[m];
        arr[m] = arr[n];
        arr[n] = temp;
    }

    public static void main(String[] args){
        String str = "bba";
        ArrayList<String> lists = Permutation(str);
        for(String s: lists){
            System.out.println(s);
        }
    }
}

4. 母牛的故事

母牛的故事

f(n) = f(n - 1) + f(n - 3):今年母牛的数量=去年牛的数量+三年前牛的数量

5. 暴力递归转动态规划

5.1 最小路径和

题目:给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小

**说明:**每次只能向下或者向右移动一步。

示例:

输入:
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
暴力递归解法

思路

  1. 从矩阵的左上角开始走,每到达一个位置,当前路径和加上此矩阵元素arr[i][j]
  2. 然后有两条路径可走:即向右向下。(和第一个问题很像,每一步都有两种选择,0-1背包
  3. 当到达最后一行时只能向右走,到达最后一列时只能 向下走。到达右下角,加上该元素,返回总结果。
class Solution {

    public static int minPathSum(int[][] grid) {
        return process(grid, 0, 0, 0);
    }

    public static int process(int[][]grid, int i, int j, int count){//从[0,0]开始时,路径和count为0

        if(i == grid.length - 1 && j == grid[0].length - 1){
            return count + grid[i][j];
        }
        if(i == grid.length - 1){//到达最后一行,只能向右移动
            return process(grid, i, j + 1, count + grid[i][j]);
        }
        if(j == grid[0].length - 1){//到达最后一列,只能向下移动
            return process(grid, i + 1, j, count + grid[i][j]);
        }
        int down = process(grid, i, j + 1, count + grid[i][j]);
        int right = process(grid, i + 1, j, count + grid[i][j]);
        return  Math.min(down, right);
    }

    public static void main(String[] args){
        int[][] grid = {
            {1,2,3,0},
            {2,4,4,0},
            {3,1,1,0}
        };
        System.out.println(minPathSum(grid));
    }
}

这种解法很慢,因为有很多重复的操作。假设从arr[i][j]到右下角用函数f(i,j)表示。则f(0,1)的后续操作有两种,分别是f(1, 1)f(0, 2);而f(1,0)的后续操作有两种,分别是f(1, 1)f(2,0)。这样的话f(1, 1)的操作就被执行了两次。

动态规划解法

==!!!==之所以该暴力递归能转化为动态规划,是因为每一步的操作都是一样的,而且对于一个固定的位置arr[i,j],从左上角到该位置的最短路径和也是固定的。从而可以创建一个同大小的矩阵arr,将该数据记录下来。其中arrd[i][j]表示原矩阵中从左上角到grid[i][j]的最短路径。

arr[i][j] = grid[i][j] + Math.min(arr[i - 1][j], arr[i][j - 1]);

在这里插入图片描述

public class Solution_MinPathSum {
    public static int minPathSum(int[][] grid){
        if(grid == null || grid.length == 0 || (grid.length == 1 && grid[0].length == 0)){
            return 0;
        }
        int row = grid.length;
        int col = grid[0].length;
        int[][] arr = new int[row][col];
        arr[0][0] = grid[0][0];
        //矩阵第一行和第一列比较特殊
        for(int i = 1; i < row; i++){//对第一列进行赋值
            arr[i][0] = grid[i][0] + arr[i - 1][0];
        }
        for(int j = 1; j < col; j++){//对第一行进行赋值
            arr[0][j] = grid[0][j] + arr[0][j - 1];
        }
        for(int i = 1; i < row; i++){
            for(int j = 1; j < col; j++){
                arr[i][j] = grid[i][j] + Math.min(arr[i][j - 1], arr[i - 1][j]);
            }
        }
        return arr[row - 1][col - 1];
    }
    public static void main(String[] args){
        int[][] grid = {
                {1,2,3,0},
                {2,0,4,2},
                {3,1,3,0}
        };
        System.out.println(minPathSum(grid));
    }
}

5.2 数组中能否使累加和等于aim

题目:给定一个全为正数的数组arr,和一个正数aim。如果可以任意选择arr中的数字,能不能累加到aim,返回true或false。

暴力递归解法

思路:从i = 0开始,遍历数组,每遇到一个元素,有两个选择:加上这个数或者不加。遍历完整个数组,如果数组长度为n,则总共有 2 n 2^ n 2n种结果。暴力递归解法:把数组遍历完成,判断最终这 2 n 2^ n 2n种结果中是否含有aim

public static boolean function(int[] arr, int aim, int i, int sum){
    if(i == arr.length){
        return sum == aim;
    }else{
        return function(arr, aim, i + 1, sum) || function(arr, aim, i + 1, sum + arr[i]);
    }
}
动态规划解法

函数f(i, sum)表示:遍历到第i位元素,后续操作能否将sum累加到aim。毋庸置疑,f(arr.length - 1, aim)的值为true
而且由暴力递归加法可知,f(i, sum)的值由f(i + 1, sum)f(i + 1, sum + arr[i])确定。
因此可以构造一个布尔类型的矩阵,列数为aim + 1,行数为arr.length + 1。假设f(arr.length, aim)为true,则可以从该矩阵的右下角倒推出f(0,0)的值。
在这里插入图片描述

class Solution {

    public static boolean function(int[] arr, int aim){
        int row = arr.length + 1;
        int col = aim + 1;
        boolean[][] grid = new boolean[row][col];
        //将最后一行,除了最右下角,都设为false
        grid[row - 1][col - 1] = true;
        for(int i = 0; i < col - 1; i++){
            grid[row - 1][i] = false;
        }
        //然后由倒数第二行开始,逐层向上,将grid矩阵填充完整
        for(int i = row - 2; i >= 0; i--){
            for(int j = 0; j < col; j++){
                if(j + arr[i] <= aim){
                    grid[i][j] = grid[i + 1][j] || grid[i + 1][j + arr[i]];
                }else{
                    grid[i][j] = grid[i + 1][j];
                }
            }
        }
        //打印grid数组
//        for(int i = 0; i < row; i++){
//            for(int j = 0; j < col; j++){
//                System.out.print(grid[i][j] + " ");
//            }
//            System.out.println();
//        }
        
        //grid数组填充完毕,grid[0][0]的值即为结果
        return grid[0][0];
    }

    public static void main(String[] args){
        int[] arr = {1,3,0,4,2};
        System.out.println(function(arr, 5));
    }
}

5.3 换钱的方法数

在这里插入图片描述

暴力递归解法

arr=[100,50,20,10,5,1],aim=1000。

  • 假设使用一张100元,转换成arr=[50,20,10,5,1],aim=900,计算此结果res1。

  • 假设使用两张100元,转换成arr=[50,20,10,5,1],aim=800,计算此结果res2。

  • 假设使用三张100元,转换成arr=[50,20,10,5,1],aim=700,计算此结果res3。

  • 直到aim=0。此时使用10张100元,转换成arr=[50,20,10,5,1],aim=0,此结果res_n=1。

最终结果res = res1 + res2 + res3 + resn

public static int coins1(int[] arr, int aim) {
    if (arr == null || arr.length == 0 || aim < 0) {
        return 0;
    }
    return process1(arr, 0, aim);
}

public static int process1(int[] arr, int index, int aim) {
    int res = 0;
    if (index == arr.length) {
        res = aim == 0 ? 1 : 0;
    } else {
        for (int i = 0; arr[index] * i <= aim; i++) {
            res += process1(arr, index + 1, aim - arr[index] * i);
        }
    }
    return res;
}
动态规划解法

暴力递归解法太过暴力,有很多重复的操作,如下

arr=[100,50,20,10,5,1],aim=1000。

  • 假设使用2张100元,0张50元,转换成arr=[50,20,10,5,1],aim=800

  • 假设使用1张100元,2张50元,转换成arr=[50,20,10,5,1],aim=800

  • 假设使用0张100元,4张50元,转换成arr=[50,20,10,5,1],aim=800

如上所示,没出现一次arr=[50,20,10,5,1],aim=800,对要对其进行计算。

我们把已经计算的结果存入一个哈希表里。哈希对的key为Index_aim,value为process1(arr, index, aim)的值。这将省去大量重复操作

public static int coins2(int[] arr, int aim) {
    if (arr == null || arr.length == 0 || aim < 0) {
        return 0;
    }
    return process2(arr, 0, aim);
}

public static HashMap<String, Integer> map = new HashMap<>();

public static int process2(int[] arr, int index, int aim) {
    int res = 0;
    if (index == arr.length) {
        res = aim == 0 ? 1 : 0;
    } else {
        for (int i = 0; arr[index] * i <= aim; i++) {
            nextAim = aim - arr[index] * i;
            nextKey = String.valueOf(index + 1) + "_" + String.valueOf(nextAim);
            if(map.contains(nextKey)){
                res += map.get(nextKey);
            }else{
                res += process2(arr, index + 1, nextAim);
            }
        }
    }
    map.put(String.valueOf(index) + "_" + String.valueOf(aim), res);
    return res;
}
动态规划解法进阶版

上述动态规划版本已经很优秀了,我们可以将其推广,构建一个二维的动态规划数组。

假设有arr=[5,3,2],aim=10。数组元素dp[index][aim]表示,使用arr[index],arr[index + 1],..., arr[arr.length -1],能够凑出aim的方案有多少种。dp[0][aim]即为最终结果。

dp[0][10]表示使用5,3,2能凑出10元的方案有多少种。

对于dp[arr.length][aim]表示,把所有钱都用上进行组合,剩余需要凑的钱为aim

  • 如果aim=0,表示刚好能凑出来,即dp[arr.length][0] = 1
  • 如果aim!=0,表示钱无论怎么凑,剩下的目标钱数都不为0,即凑不出来,结果为0。

arr=[5,3,2],aim=10,dp数组如下
在这里插入图片描述
dp[index][aim] = dp[index+1][aim-arr[index]] + dp[index+1][aim-2*arr[index]] + ...

dp[1][8]表示使用3元和2元能凑出8元的方案,

  • 使用1张3元,则只需再凑出5元就行,此时的方案为dp[2][5]
  • 使用2张3元,则只需再凑出2元就行,此时的方案为dp[2][2]

dp[1][8] = dp[2][8] + dp[2][5] + dp[2][2] = 1 + 0 + 1 = 2
在这里插入图片描述
dp[1][5]表示使用3元和2元能凑出5元的方案,

  • 使用1张3元,则只需再凑出5元就行,此时的方案为dp[2][5]
  • 使用2张3元,则只需再凑出2元就行,此时的方案为dp[2][2]

可以看出,dp[1][8] = dp[2][8] + (dp[2][5] + dp[2][2]) =dp[2][8] + dp[1][5]= 1 + 1 = 2

因此可以有递推关系dp[index][j] = dp[index+1][j] + dp[index][j - arr[index]]
在这里插入图片描述
因此,我们可以使用如上的地推关系,快速地把二维表填满,计算到dp[0][10]即为结果

代码如下

public static int coins(int[] arr, int aim) {
    if (arr == null || arr.length == 0 || aim < 0) {
        return 0;
    }
    //将二维表最左一列都赋值为1
    int[][] dp = new int[arr.length + 1][aim + 1];
    for (int i = 0; i <= arr.length; i++) {
        dp[i][0] = 1;
    }
    //将二维表最后一行赋值
    for(int j = 1; j <= aim; j++){
        dp[arr.length][j] = 0;
    }
    for (int i = arr.length; i >= 0; i--) {
        for (int j = 1; j <= aim; j++) {
            //dp[index][j] = dp[index+1][j] + dp[index][j - arr[index]]
            dp[i][j] = dp[i + 1][j];
            dp[i][j] += j - arr[i] >= 0 ? dp[i][j - arr[i]] : 0;
        }
    }
    return dp[0][aim];
}

5.4 排成一条线的纸牌博弈等问题

题目:有一个整型数组A,代表数值不同的纸牌排成一条线。玩家a和玩家b依次拿走每张纸牌,规定玩家a先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家a和玩家b都绝顶聪明,他们总会采用最优策略。请返回最后获胜者的分数。

给定纸牌序列A及序列的大小n,请返回最后分数较高者得分数(相同则返回任意一个分数)。保证A中的元素均小于等于1000。且A的大小小于等于300。

测试样例:

[1,2,100,4], 4

返回:101

暴力递归解法

定义递归函数f(i,j),表示如果arr[i…j]这个排列上的纸牌被绝顶聪明的人拿走,最终能够获得什么分数。

定义递归函数s(i,j),表示如果a[i…j]这个排列上的纸牌被绝顶聪明的人后拿,最终能获得什么分数。

首先来分析,具体过程如下:

  1. 如果i==j(只有一张纸牌),会被先拿纸牌的人拿走,所以返回arr[i];

  2. 如果i!=j,先拿纸牌的人有两种选择,要么拿走arr[i],要么拿走arr[j];

如果拿走arr[i],剩下arr[i+1,j]。对于arr[i+1,j]中的纸牌,当前玩家成了后拿的人,因此他后续能获得的分数为。s(i+1, j)。如果拿走arr[j],那么剩下arr[i,j-1],当前玩家后续能获得的分数为s[i,j-1]

作为绝顶聪明的人,必然会在两种决策中选择最优的。所以返回max{arr[i]+s(i+1, j),arr[j]+s(i, j-1)}

然后来分析s(i,j)

  1. 如果i == j,后拿纸牌的人什么也拿不到,返回0

  2. 如果i != j,玩家的对手会先拿纸牌。对手要么先拿走a[i],要么先拿走arr[j]。如果对手拿走arr[i],那么排列剩下arr[i+1,j];如果对手拿走arr[j],剩下arr[i,j-1]。对手也是绝顶聪明的人,所以也会把最差的情况留给玩家因此返回min{f(i+1,j),f(i,j-1)}

public static int win1(int[] arr) {
    if (arr == null || arr.length == 0) {
        return 0;
    }
    //先拿的人不一定最后一定赢,比如1,100,2
    return Math.max(f(arr, 0, arr.length - 1), s(arr, 0, arr.length - 1));
}

//先拿
public static int f(int[] arr, int i, int j) {
    if (i == j) {
        return arr[i];
    }
    return Math.max(arr[i] + s(arr, i + 1, j), arr[j] + s(arr, i, j - 1));
}

//后拿
public static int s(int[] arr, int i, int j) {
    if (i == j) {
        return 0;
    }
    return Math.min(f(arr, i + 1, j), f(arr, i, j - 1));
}
动态规划解法

假设有数组arr={1,100,2,3}

由上面的代码可以看出,f(i, j)依赖s(i+1, j)s(i, j-1)s(i, j)依赖f(i+1, j)和f(i, j-1)

而最后的结果又由f(i, j)s(i, j)同时决定。所以可以构造出两个表,f表和s表。

先根据暴力代码填充不需要依赖其他信息的值,即对角线上的值。
在这里插入图片描述
s(i, j) = Math.min(f(arr, i + 1, j), f(arr, i, j - 1))可得

  • s(0,1) = min(f(1,1), f(0, 0)) = 1
  • s(1,2) = min(f(2,2), f(1, 1)) = 2
  • s(2,3) = min(f(3,3), f(3, 3)) = 2

故可得S表
在这里插入图片描述
f(i,j) = Math.max(arr[i] + s(arr, i + 1, j), arr[j] + s(arr, i, j - 1))

  • f(0, 1) = max(arr[0] + s(1,1), arr[1] + s(0,0)) = max(1, 100) = 100
  • f(1, 2) = max(arr[1] + s(2,2), arr[2] + s(1,1)) = max(100, 2) = 100
  • f(2, 3) = max(arr[2] + s(3,3), arr[3] + s(2,2)) = max(2, 3) = 3

故可得S表如下
在这里插入图片描述
两个表按照依赖关系循环利用,就可得
在这里插入图片描述
比较f(0,3)s(0,3),故返回最大值为103

代码实现

package class06;

import java.util.Scanner;

class Solution_MaxCardSum{

    public static int maxSum(int[] arr){
        int length = arr.length;
        int[][] f = new int[length][length];
        int[][] s = new int[length][length];
        //对角线赋值
        for(int i = 0; i < length; i++){
            f[i][i] = arr[i];
            s[i][i] = 0;
        }
        //循环利用两者的关系赋值
        for(int j = 1; j < length; j++){
            int i = 0;
            int k = j;
            while(k < length){
                s[i][k] = Math.min(f[i+1][k], f[i][k-1]);
                f[i][k] = Math.max(arr[i] + s[i+1][k], arr[k] + s[i][k-1]);
                i++;
                k++;
            }
        }
        return Math.max(f[0][length - 1], s[0][length - 1]);
    }

    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        int n = 0;
        System.out.println("请输入数组的长度");
        n = sc.nextInt();

        System.out.println("请输入数组中的元素");
        int[] arr = new int[n];
        for(int i = 0; i < n; i++){
            if(sc.hasNext()){
                arr[i] = sc.nextInt();
            }
        }
        System.out.print("最后的结果为:");
        System.out.println(maxSum(arr));
    }
}

5.5 机器人行走

题目:给定一个从1到N的有序序列,序列中的元素每次递增1。机器人在M位置上,机器人可以走P步,每次只能往走一步,求机器人走到K位置上的方式有几种。

暴力递归解法
//m:当前机器人的位置
//n:序列为(1,2,3,...n-1,n)
//机器人可以走p步
//机器人最终在k位置上
public static int getWays(int m, int n, int p, int k){
    if(m < 1 || m > n || n < 2 || p < 1 || k < 1 || k > n){
        return 0;
    }
    int res = 0;
    if(p == 0){
        return m == k ? 1 : 0;
    }
    if(m == 1){
        res =  getWays(m + 1, n, p - 1, k);
    }else if(m == n){
        res = getWays(m - 1, n, p - 1, k);
    }else{
        res = getWays(m + 1, n, p - 1, k) + getWays(m - 1, n, p - 1, k);
    }

    return res;
}
动态规划解法

在暴力递归解法中,变化的参数为m和p,可以根据这两个变量做动态规划

假设:k=7,n=10。可做如下动态规划表
在这里插入图片描述
先将矩阵第一行赋值,即还剩余0步时,机器人刚好到k位置,所以记为1。

p=1时,根据res = getWays(m + 1, n, p - 1, k) + getWays(m - 1, n, p - 1, k),由第一行可推第二行。其实该关系就是杨辉三角

最后的结果从最后一行看,如果机器人初始位置为4,可以走5步,那么走到7的方式有5种。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值