数据结构与算法笔记七(暴力递归到动态规划)

本文探讨了从暴力递归逐步转化为动态规划的方法,通过机器人运动问题、硬币问题、拿数字游戏、象棋问题、走格子问题以及CoinsWays问题为例,详细解释了记忆化搜索和严格表结构的优化过程,以及如何通过动态规划降低时间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

暴力递归到动态规划

阶段:

尝试,递归 --》 记忆化搜索,DP --》 严格表结构,DP --》 严格表精版

某些问题上,记忆化搜索 和 严格表结构 用同样的时间复杂度。

机器人运动问题

一个整数n代表 你有n个位置,
一个整数s代表 开始的位置, 1到n之间。
一个整数e 代表 代表要去的 目标位置
一个整数k 代表 机器人必须走k步

走的规则: 1位置只能走向2, n位置下一步只能走向n-1

问: 走k步,从s到e有多少种方法?

在这里插入图片描述展开表示:
在这里插入图片描述
暴力递归:

public static int ways1(int N, int M, int K, int P) {
		// 参数无效直接返回0
		if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {
			return 0;
		}
		// 总共N个位置,从M点出发,还剩K步,返回最终能达到P的方法数
		return walk(N, M, K, P);
	}

	// N : 位置为1 ~ N,固定参数
	// cur : 当前在cur位置,可变参数
	// rest : 还剩res步没有走,可变参数
	// P : 最终目标位置是P,固定参数
	// 该函数的含义:只能在1~N这些位置上移动,当前在cur位置,走完rest步之后,停在P位置的方法数作为返回值返回
	public static int walk(int N, int cur, int rest, int P) {
		// 如果没有剩余步数了,当前的cur位置就是最后的位置
		// 如果最后的位置停在P上,那么之前做的移动是有效的
		// 如果最后的位置没在P上,那么之前做的移动是无效的
		if (rest == 0) {
			return cur == P ? 1 : 0;
		}
		// 如果还有rest步要走,而当前的cur位置在1位置上,那么当前这步只能从1走向2
		// 后续的过程就是,来到2位置上,还剩rest-1步要走
		if (cur == 1) {
			return walk(N, 2, rest - 1, P);
		}
		// 如果还有rest步要走,而当前的cur位置在N位置上,那么当前这步只能从N走向N-1
		// 后续的过程就是,来到N-1位置上,还剩rest-1步要走
		if (cur == N) {
			return walk(N, N - 1, rest - 1, P);
		}
		// 如果还有rest步要走,而当前的cur位置在中间位置上,那么当前这步可以走向左,也可以走向右
		// 走向左之后,后续的过程就是,来到cur-1位置上,还剩rest-1步要走
		// 走向右之后,后续的过程就是,来到cur+1位置上,还剩rest-1步要走
		// 走向左、走向右是截然不同的方法,所以总方法数要都算上
		return walk(N, cur + 1, rest - 1, P) + walk(N, cur - 1, rest - 1, P);
	}

第一步优化: 记忆化搜索,就加了缓存

第二步优化: 动态规划,纠结位置依赖顺序
严格表结构的动态规划。
时间复杂度:

在这里插入图片描述

硬币问题

leetcode322
给你一个正数数组,里面每一个值,代表一枚硬币的面值。
一个数代表一枚硬币。

aim = 10.
组成这个10,最少用多少硬币。 7 + 3

在这里插入图片描述

递归:

package com.wanghaha.algorithm;

public class Day8_03_62CoinsMin {

    public static int minCoins1(int[] arr, int aim){
        process(arr, 0, aim);
    }

    // arr[index..]组成出rest这么多钱,最少的硬币数量返回
    public static int process(int[] arr, int index, int rest){
        if(rest < 0){
            return -1;
        }
        if( rest == 0 ){
            return 0;
        }
        // rest > 0
        if(index == arr.length ){
            return -1;
        }
        // rest > 0 and  也有硬币
        int p1 = process(arr, index +1 , rest);
        int p2Next = process(arr, index + 1 , rest - arr[index]);
        if(p1 == -1 && p2Next == -1){
            return -1;
        }else {
            if(p1 == -1){
                return p2Next + 1 ;
            }
            if(p2Next == -1 ){
                return p1;
            }
        }
        return Math.min(p1, p2Next);
    }
}

记忆化搜索

    public static int minCoins2(int[] arr, int aim){
        int[][] dp = new int[arr.length  +1 ][aim + 1];

        for (int i = 0; i < arr.length + 1; i++) {
            for (int j = 0; j < aim + 1; j++) {
                dp[i][j] = -2;
            }
        }
        return process2(arr, 0, aim, dp);
    }

    // arr[index..]组成出rest这么多钱,最少的硬币数量返回
    public static int process2(int[] arr, int index, int rest,int[][] dp){
        if(rest < 0){
            return -1;
        }
        if(dp [index][rest] != -2 ){
            return dp[index][rest];
        }

        if( rest == 0 ){
            dp[index][rest] = 0;
        }else if (index == arr.length ){
            dp[index][rest] = -1;
        }else {
            int p1 = process2(arr, index +1 , rest,dp);
            int p2Next = process2(arr, index + 1 , rest - arr[index],dp);
            if(p1 == -1 && p2Next == -1){
                dp[index][rest] =  -1;
            }else {
                if(p1 == -1){
                    dp[index][rest] = p2Next + 1 ;
                }else if (p2Next == -1 ){
                    dp[index][rest] = p1;
                }else {
                    dp[index][rest] = Math.min(p1, p2Next);
                }
            }
        }

        return dp[index][rest];
    }

严格表结构

    public static int minCoins3(int[] arr, int aim){
        int N = arr.length ;
        int[][] dp  = new int[N + 1][aim + 1];
        for (int row = 0; row < N ; row++) {
            dp[row][0] = 0;
        }
        for (int col = 1; col < aim; col++) {
            dp[N][col] = -1;
        }
        for (int index = N-1; index >= 0 ; index--) {
            for (int rest = 1; rest <= aim; rest++) {
                int p1 = dp[index+1][rest];
                int p2Next = -1;
                if(rest - arr[index] >= 0 ){
                    p2Next = dp[index+1][rest - arr[index]];
                }

                if(p1 == -1 && p2Next == -1){
                    dp[index][rest] =  -1;
                }else {
                    if(p1 == -1){
                        dp[index][rest] =   p2Next + 1 ;
                    }else if (p2Next == -1 ){
                        dp[index][rest] =   p1;
                    }else {
                        dp[index][rest] =   Math.min(p1, p2Next+1);
                    }
                }
            }
            
        }
        return dp[0][aim];
    }

递归:
记忆化搜索
严格表结构:

  1. 分析可变参数的变化范围
  2. 标出你要计算出来的终止位置
  3. 根据basecase,标出不用计算直接出答案的 位置
  4. 推普遍的位置如何依赖其他位置的。
  5. 定出严格表从哪些格子推到哪些格子,然后到终止位置。确定依次计算的顺序。 递归代码拿过来改

拿数字

有两个人玩游戏,能看到一个数组上所有的数字。
每个人依次拿,只能拿最左或者最右的数组。
两个人都很聪明,谁最后会获胜?返回获胜的分数。

先手和后手

在这里插入图片描述

package com.wanghaha.algorithm;

import java.util.List;

public class Day8_04_65GrapNumber {

    public static int grapNumber(int[] arr){
        if(arr == null || arr.length ==0 ){
            return 0;
        }
        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) );
    }

    private 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));
    }

    public static int grapNumber2(int[] arr){
        if(arr == null || arr.length ==0 ){
            return 0;
        }
        int[][] firstDP = new int[arr.length][arr.length];
        int[][] secondDP = new int[arr.length][arr.length];
        for (int i = 0; i < arr.length; i++) {
            firstDP[i][i] = arr[i];
            secondDP[i][i] = 0;
        }
        int j = 1;
        int i = 0;
        int a;
        int b;
        while (j != arr.length){
            System.out.println("j: " + j);
            a = i;
            b = j;
            while (a<arr.length && b< arr.length){
                firstDP[a][b] = Math.max(arr[a] + secondDP[a+1][b], arr[b] + secondDP[a][b-1]);
                secondDP[a][b] = Math.min(firstDP[a+1][b],firstDP[a][b-1]);
                a ++ ;
                b ++ ;
            }
            j++;
        }
        return Math.max(firstDP[0][arr.length-1],secondDP[0][arr.length-1]);

    }




    public static void main(String[] args) {
        int[] arr = {1,9,11};
        System.out.println(grapNumber(arr));
        System.out.println(grapNumber2(arr));
    }
}

象棋

马 ,10乘9的棋盘上,从 (0,0) 要到 (a,b)去
一定要跳K步
在这里插入图片描述

在这里插入图片描述
step张xy二维表
在这里插入图片描述

public static int getWays(int x, int y, int step) {
		return process(x, y, step);
	}
	
	// 潜台词: 从(0,0)位置出发
	//  要去往(x,y)位置, 必须跳 step步
	// 返回方法数 

	public static int process(int x, int y, int step) {
		if (x < 0 || x > 8 || y < 0 || y > 9) {
			return 0;
		}
		if (step == 0) {
			return (x == 0 && y == 0) ? 1 : 0;
		}
		return process(x - 1, y + 2, step - 1)
				+ process(x + 1, y + 2, step - 1)
				+ process(x + 2, y + 1, step - 1)
				+ process(x + 2, y - 1, step - 1)
				+ process(x + 1, y - 2, step - 1)
				+ process(x - 1, y - 2, step - 1)
				+ process(x - 2, y - 1, step - 1)
				+ process(x - 2, y + 1, step - 1);
	}
public static int dpWays(int x, int y, int step) {
		if (x < 0 || x > 8 || y < 0 || y > 9 || step < 0) {
			return 0;
		}
		int[][][] dp = new int[9][10][step + 1];
		dp[0][0][0] = 1;
		for (int h = 1; h <= step; h++) {
			for (int r = 0; r < 9; r++) {
				for (int c = 0; c < 10; c++) {
					dp[r][c][h] += getValue(dp, r - 1, c + 2, h - 1);
					dp[r][c][h] += getValue(dp, r + 1, c + 2, h - 1);
					dp[r][c][h] += getValue(dp, r + 2, c + 1, h - 1);
					dp[r][c][h] += getValue(dp, r + 2, c - 1, h - 1);
					dp[r][c][h] += getValue(dp, r + 1, c - 2, h - 1);
					dp[r][c][h] += getValue(dp, r - 1, c - 2, h - 1);
					dp[r][c][h] += getValue(dp, r - 2, c - 1, h - 1);
					dp[r][c][h] += getValue(dp, r - 2, c + 1, h - 1);
				}
			}
		}
		return dp[x][y][step];
	}

	public static int getValue(int[][][] dp, int row, int col, int step) {
		if (row < 0 || row > 8 || col < 0 || col > 9) {
			return 0;
		}
		return dp[row][col][step];
	}

走格子

leetcode668
给行数和列数,表示这个格子有多大。

行数为5,列数为6。
Bob所在格子是(a,b)
Bob所在任何一个点,都等概率随机的向上下左右走。走K步。
Bob能活下来的概率是多少?

public static String bob1(int N, int M, int i, int j, int K) {
		long all = (long) Math.pow(4, K);
		long live = process(N, M, i, j, K);
		long gcd = gcd(all, live); // 求一个最大公约数
		return String.valueOf((live / gcd) + "/" + (all / gcd));
	}

	// N * M 的区域,Bob在(row,col)的位置, 走test步之后,获得的生存方法数
	public static long process(int N, int M, int row, int col, int rest) {
		if (row < 0 || row == N || col < 0 || col == M) {
			return 0;
		}
		// row, col 没越界
		if (rest == 0) {
			return 1;
		}
		// 还没走完, row , col 也没越界。
		long live = process(N, M, row - 1, col, rest - 1);
		live += process(N, M, row + 1, col, rest - 1);
		live += process(N, M, row, col - 1, rest - 1);
		live += process(N, M, row, col + 1, rest - 1);
		return live;
	}

	public static long gcd(long m, long n) {
		return n == 0 ? m : gcd(n, m % n);
	}

CoinsWays

leetcode 518

一个数组整数,无重复值
一个位置上的值,代表一个面值
每一面值的货币无限多。
组出1000的货币,方法数都多少种。

递归尝试

public static int way1(int[] arr, int aim)  {
        return process(arr, 0, aim);
    }

    private static int process(int[] arr, int index, int rest) {
        if (index == arr.length) {
            return rest == 0 ? 1 : 0;
        }
        // arr[index] 0张  1张 。。 不要超过rest的钱数
        int ways = 0;
        for (int zhang = 0; arr[index] * zhang <= rest; zhang++) {
            ways += process(arr, index + 1, rest-arr[index] * zhang);
        }
        return ways;
    }

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
不优化的严格表结构

时间复杂度 N*(aim^2)

 public static int way2(int[] arr, int aim){
        if(arr == null || arr.length ==0){
            return 0;
        }
        int N = arr.length;
        int[][] dp = new int[N+1][aim  + 1];

        dp[N][0] = 1;

        for (int index = N - 1; index >= 0 ; index--) {
            for (int rest = 0; rest <= aim; rest++) {

                int ways = 0;
                for (int zhang = 0; arr[index] * zhang <= rest; zhang++) {
                    ways += dp[index + 1 ] [rest-arr[index] * zhang];
                }
                dp[index][rest] = ways;

            }
        }

        return dp[0][aim]   ;
    }

在这里插入图片描述
斜率优化
临近的位置能不能替代 枚举行为。

public static int way3(int[] arr, int aim){
        if(arr == null || arr.length ==0){
            return 0;
        }
        int N = arr.length;
        int[][] dp = new int[N+1][aim  + 1];

        dp[N][0] = 1;

        for (int index = N - 1; index >= 0 ; index--) {
            for (int rest = 0; rest <= aim; rest++) {

                dp[index][rest] = dp[index+1][rest] ;
                if(rest - arr[index] >= 0 ){
                    dp[index][rest] += dp[index][rest - arr[index]];
                }

            }
        }

        return dp[0][aim]   ;
    }


    // for test
    public static int[] generateRandomArray(int len, int max) {
        int[] arr = new int[(int) (Math.random() * len) + 1];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = (int) (Math.random() * max) + 1;
        }
        return arr;
    }

    public static void main(String[] args) {
        int len = 10;
        int max = 10;
        int testTime = 10000;
        for (int i = 0; i < testTime; i++) {
            int[] arr = generateRandomArray(len, max);
            int aim = (int) (Math.random() * 3 * max) + max;
            if ( way1(arr, aim)  != way2(arr,aim)
                 || way1(arr, aim)  != way3(arr,aim)) {
                System.out.println("ooops!");
                break;
            }
        }
    }

尝试模型 : 从左往右,范围 7成以上
记忆化搜索的方式:
分析空间格子,严格表结构动态规划。
可以进一步优化,优化枚举行为。
在这里插入图片描述
尝试时,两件事

  1. 尝试的时候每一个可变参数自己维度
  2. 可变参数的个数,最好是整数,最少原则。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值