• Home
  • Archives
  • 随笔
所有文章 友链 关于我

  • Home
  • Archives
  • 随笔

动态规划从套路开始

发布于: 2021-07-25
更新于: 2023-07-09

动态规划

动态规划题目一般都是求最 xxx,最大的,最长的 xxxx,通常解法就是穷举所有的答案找到最大的,为了避免暴力的低效率情况,就需要优化下,通过加入动态规划表优化计算效率,避免浪费。
另外穷举也要求有一个方程来进行穷举,本质上是在于写出这个方程进行穷举的过程

PS: 下文中的代码可以在这个 github 项目找到源码以及相关的内容,欢迎关注提意见 star,谢谢!

斐波那契数列

结题方案有两种思路,一个是备忘录 memorandum 记录每次算过的值,从上往下,又称自顶向下。以及自底向上的 dp^1数组解法。

自顶向下

graph TD;
    root(fib__5)-->|拆分为两个数相加的方程| fib__4
    root(fib__5)-->|拆分为两个数相加的方程| fib__3
    fib__4 -->  memorandum__3
    memorandum__3 -->|备忘录记录着fib__3的计算结果| fib__3
    fib__4 -->  fib__2
    fib__3 --> memorandum__2
    memorandum__2--> |备忘录记录着fib__2的计算结果|fib__2
    fib__3 --> fib__1
    title[自顶向下解法]

体现在代码的话是

package dynamic.programming;

import java.util.Arrays;

/**
 * Fibonacci 斐波那契数列问题$
 * <p>
 * 创建时间: 2021/7/25 20:46
 * 博客地址: <a href="https://tyrantqiao.com">详情戳我(╯‵□′)╯︵┻━┻)</a>
 * <p>
 * 问题:斐波那契数列黄金分割数列,1,1,2,3,5   之后的数是由前两数的累加而成
 *
 * @author tyrantqiao
 * @version 1.0
 **/
public class Fibonacci {
    /**
     * 暴力穷举
     * <p>
     * 问题: 比如说3,会在fib(5)计算一次,然后又在fib(4)中又算了一次,就造成了浪费
     *
     * @param whichNumber 第几位数
     * @return 第几位数的值是啥
     */
    public int fib(int whichNumber) {
        if (whichNumber == 1 || whichNumber == 2) {
            return 1;
        }
        return fib(whichNumber - 1) + fib(whichNumber - 2);
    }

    /**
     * fib 备忘录版本,记录算过的值
     *
     * @param whichNumber 哪个数字
     * @return 数字对应的值
     */
    public int fibMemorandum(int whichNumber) {
        if (whichNumber == 1) {
            return 1;
        }

        int[] memorandumArray = new int[whichNumber + 1];
        Arrays.fill(memorandumArray, 0);
        return memorandumCalculate(memorandumArray, whichNumber);
    }

    /**
     * 计算
     * <p>
     * 其实就是拿个数组记录下,也没有什么很突出的地方,这里数组记下,偷懒没有按照0开始操作的,所以数组记得多加一位定义
     *
     * @param memorandumArray 备忘录数组
     * @param whichNumber     第几位数
     * @return 返回结果
     */
    private int memorandumCalculate(int[] memorandumArray, int whichNumber) {
        if (whichNumber == 1 || whichNumber == 2) {
            return 1;
        }

        if (memorandumArray[whichNumber] != 0) {
            return memorandumArray[whichNumber];
        }

        return memorandumArray[whichNumber] =
                memorandumCalculate(memorandumArray, whichNumber - 1)
                        + memorandumCalculate(memorandumArray, whichNumber - 2);
    }

    public static void main(String[] args) {
        System.out.println(new Fibonacci().fib(5));
        System.out.println(new Fibonacci().fibMemorandum(5));
    }
}

自底向上

与自顶向下一样都是用了数组记录已经算过的值,区别就在于计算的顺序变为了,从 f(1)+f(2) 、f(2)+f(3)的顺序开始,只是顺序不一样

    /**
     * 自底向上,其实跟上面自顶向下步骤是一样的,只是这边是从底部开始算,其实效率上都一样。
     *
     * @param whichNumber 第几位数
     * @return 第几位数的数值是什么
     */
    private int fibonacciFromBottom(int whichNumber) {
        if (whichNumber < 1) {
            return 0;
        }

        if (whichNumber == 1 || whichNumber == 2) {
            return 1;
        }

        int[] dpArray = new int[whichNumber + 1];

        dpArray[1] = dpArray[2] = 1;
        for (int currentIndex = 3; currentIndex <= whichNumber; currentIndex++) {
            dpArray[currentIndex] = dpArray[currentIndex - 1] + dpArray[currentIndex - 2];
        }
        return dpArray[whichNumber];
    }

由上面的程序可以推导出这样的状态转移方程式

$$
f(n)=
\begin{cases}
1,& \text{n=1,2} \[5ex]
f(n-1)+f(n-2) & \text{n>2}
\end{cases}
$$

解开动态规划的核心在于找到这个方程,此外像本题中其实每轮计算都只需要两个状态 f(n-1)和 f(n-2),我们只需要记录两个即可,不需要把整个斐波那契的计算过程记下来,空间密度由 n 降至了 2

    /**
     * 斐波那契压缩版,只记录fib(n-1)和fib(n-2)的状态
     *
     * @param whichNumber 第几位数
     * @return 第几位数的值是什么
     */
    private int fibonacciCompression(int whichNumber) {
        if (whichNumber < 1) {
            return 0;
        }

        if (whichNumber == 1 || whichNumber == 2) {
            return 1;
        }

        int prev = 1, curr = 1;
        int result = 0;
        for (int currentIndex = 3; currentIndex <= whichNumber; currentIndex++) {
            result = prev + curr;
            prev = curr;
            curr = result;
        }
        return result;
    }

爬楼梯找硬币

本质和之前的斐波那契一样,重点是找到状态转移方程。

比如说题目:现在有十一块钱/十一层楼,但你只有1块钱or2块钱/每次走一层or两层楼梯的选择法,怎么样才可以最快到达楼层。

确定基本状态base case

找到常量的那个状态,我们可以很轻松地得知当楼层为0时,那么就不需要走了,楼层为1,那就只能选1,如果说楼层减成了负数,那也不需要再走下去了,直接返回-1代表失败即可。

$$
f(n)=
\begin{cases}
-1,& \text{n<0} \[5ex]
0 & \text{n==0} \[5ex]
1 & \text{n==1}
\end{cases}
$$

确定状态

找到状态转移方程中会不断变化的结果值,这样子才能向我们的base case方程去接近,当楼梯只有1层,那就只能选1,由此也可以看出我们的状态、变量也就是楼层/硬币

找到让状态变动的行为

如何让状态趋向base case,比如自顶向下那就要有减的过程,自底向上就要有加的过程。楼梯/硬币中我们每次有1/2的选择方式,选了1就相当于走了一层楼梯,加了一块钱,那我们的目标金额/楼层也就相应地减1,这样子就能够趋向我们的base case

明确dp函数怎么写

函数的参数变量一般是状态转移方程里面的状态变量值,对应着要到达的阶梯/硬币.

函数的返回值就是题目要求的东西,比如说最小达成目标的计算方式.

$$
f(n)=
\begin{cases}
0, & \text{n=0} \[5ex]
-1 & \text{n<0} \[5ex]
Math.min(dp(n-stair)+1|stair \in stairs), & \text{n>0}
\end{cases}
$$

代码实现

体现在代码的话如下

package dynamic.programming;

import java.util.Arrays;
import java.util.List;

/**
 * 找到小费的最优组成形式$
 * <p>
 * 创建时间: 2021/7/26 6:35
 * 博客地址: <a href="https://tyrantqiao.com">详情戳我(╯‵□′)╯︵┻━┻)</a>
 *
 * @author tyrantqiao
 * @version 1.0
 **/
public class CoinTip {
    /**
     * 暴力解法,本质在于不断求解子方程,并利用当等于0时,返回1,假如扣成负数,那就返回-1
     *
     * @param canUseCoinList   可以使用的硬币列表
     * @param needToCombineTip 需要凑成的小费
     * @return 最小数量是什么
     */
    public int findMinCoinTipCombination(List<Integer> canUseCoinList, int needToCombineTip) {
        if (needToCombineTip == 0) {
            return 0;
        }
        if (needToCombineTip < 0) {
            return -1;
        }

        int result = Integer.MAX_VALUE;
        for (int eachCombinationCoin : canUseCoinList) {
            int eachResult = findMinCoinTipCombination(canUseCoinList, needToCombineTip - eachCombinationCoin);
            if (eachResult == -1) {
                continue;
            }
            result = Math.min(result, 1 + eachResult);
        }
        return result == Integer.MAX_VALUE ? -1 : result;
    }

    /**
     * 备忘录方法,本质在于不断求解子方程,并利用当等于0时,返回1,假如扣成负数,那就返回-1
     *
     * @param minCoinTipCombinationArray 每次计算的最小结果集
     * @param canUseCoinList             可以使用的硬币列表
     * @param needToCombineTip           需要凑成的小费
     * @return 最小数量是什么
     */
    public int findMinCoinTipCombinationMemorandum(int[] minCoinTipCombinationArray, List<Integer> canUseCoinList, int needToCombineTip) {
        if (needToCombineTip == 0) {
            return 0;
        }

        if (needToCombineTip < 0) {
            return -1;
        }

        if (minCoinTipCombinationArray[needToCombineTip] != 0) {
            return minCoinTipCombinationArray[needToCombineTip];
        }

        int result = Integer.MAX_VALUE;
        for (int eachCombinationCoin : canUseCoinList) {
            int eachResult = findMinCoinTipCombinationMemorandum(minCoinTipCombinationArray, canUseCoinList, needToCombineTip - eachCombinationCoin);
            if (eachResult == -1) {
                continue;
            }
            result = Math.min(result, 1 + eachResult);
        }
        return minCoinTipCombinationArray[needToCombineTip] = result == Integer.MAX_VALUE ? -1 : result;
    }

    public static void main(String[] args) {
        List<Integer> canUseCoinList = Arrays.asList(1, 2, 5, 10);

        int needToCombineTip = 22;
        int[] minCoinTipCombinationArray = new int[needToCombineTip + 1];
        Arrays.fill(minCoinTipCombinationArray, 0);
        System.out.println(new CoinTip().findMinCoinTipCombination(canUseCoinList, needToCombineTip));
        System.out.println(new CoinTip().findMinCoinTipCombinationMemorandum(minCoinTipCombinationArray, canUseCoinList, needToCombineTip));
    }
}
package dynamic.programming;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Stair combination$
 * <p>
 * 创建时间: 2021/7/26 22:27
 * 博客地址: <a href="https://tyrantqiao.com">详情戳我(╯‵□′)╯︵┻━┻)</a>
 *
 * @author tyrantqiao
 * @version 1.0
 **/
public class StairCombination {
    /**
     * 阶梯组合
     *
     * @param allCombinationSize 一共有多少组合
     * @param canUseStairList    可以使用的阶梯列表
     * @param needToReachStair   需要到达的阶梯
     * @return 阶梯组合
     */
    public int findMinStairCombination(AtomicInteger allCombinationSize, List<Integer> canUseStairList, int needToReachStair) {
        if (needToReachStair == 0) {
            allCombinationSize.incrementAndGet();
            return 0;
        }

        if (needToReachStair < 0) {
            return -1;
        }

        int result = Integer.MAX_VALUE;
        for (int canUseStair : canUseStairList) {
            int eachResult = findMinStairCombination(allCombinationSize, canUseStairList, needToReachStair - canUseStair);
            if (eachResult == -1) {
                continue;
            }

            result = Math.min(result, eachResult + 1);
        }

        return result == Integer.MAX_VALUE ? -1 : result;
    }

    public int findMinStairCombinationMemorandum(int[] minCombinationMemorandum, List<Integer> canUseStairList, int needToReachStair) {
        if (needToReachStair == 0) {
            return 0;
        }

        if (needToReachStair < 0) {
            return -1;
        }

        if (minCombinationMemorandum[needToReachStair] != 0) {
            return minCombinationMemorandum[needToReachStair];
        }

        int result = Integer.MAX_VALUE;
        for (int canUseStair : canUseStairList) {
            int eachResult = findMinStairCombinationMemorandum(minCombinationMemorandum, canUseStairList, needToReachStair - canUseStair);
            if (eachResult == -1) {
                continue;
            }

            result = Math.min(result, eachResult + 1);
        }

        minCombinationMemorandum[needToReachStair] = result == Integer.MAX_VALUE ? -1 : result;
        return minCombinationMemorandum[needToReachStair];
    }

    public static void main(String[] args) {
        AtomicInteger allCombinationSize = new AtomicInteger(0);
        int needToReachStair = 9;
        System.out.println(new StairCombination().findMinStairCombination(allCombinationSize, Arrays.asList(1, 2), needToReachStair));
        System.out.println("all: " + allCombinationSize);

        int[] memorandumCombination = new int[needToReachStair + 1];
        System.out.println(new StairCombination().findMinStairCombinationMemorandum(memorandumCombination, Arrays.asList(1, 2), needToReachStair));
    }
}
动态规划从套路开始
/archives/f53aa71b/
作者
tyrantqiao
发布于
2021-07-25
更新于
2023-07-09
许可协议
CC BY-NC-SA 4.0
赏

蟹蟹大佬的打赏,大家一起进步

支付宝
微信
  • 算法

扫一扫,分享到微信

微信分享二维码
Hadoop架构原理知识
springCloud从0到丧心病狂
© 2024 tyrantqiao 本站总访问量次 本站访客数人次 载入天数...载入时分秒...
  • 所有文章
  • 友链
  • 关于我

tag:

  • 复盘
  • 我
  • 规划
  • java
  • 面试
  • 源码
  • 架构
  • Hadoop
  • HTTP
  • TCP
  • 学习笔记
  • IDEA
  • maven
  • idea
  • Java
  • jdk
  • 面经
  • linux
  • 爱情
  • mysql
  • 性能
  • sql
  • Mysql
  • JAVA
  • 技术
  • Redis
  • MQ
  • Spring
  • 数据库
  • TIDB
  • spring
  • unity
  • chatgpt
  • 经验分享
  • 前端
  • redis
  • vue
  • git
  • shadowsocks
  • hexo
  • blog
  • bug
  • 开发
  • 业务
  • jvm
  • 算法
  • MySQL
  • nginx
  • Linux
  • mq
  • db
  • springCloud
  • ssh
  • python
  • 爬虫
  • test
  • vim
  • 影视剧
  • 中间件
  • 事务
  • 性格
  • 音乐
  • 程序员
  • 随笔
  • mybatis
  • 演讲
  • 域名
  • 猫咪
  • 她
  • github
  • 计划
  • 旅游
  • 软件
  • 心理
  • 情商
  • 幽默
  • 才艺
  • 穿搭
  • 编程
  • 排序
  • 查找
  • 缓存
  • 网络
  • 设计模式
  • c
  • 课程设计
  • centos
  • 数学
  • 本网站主题yilia设计者的主页
如果有问题或者想讨论的可以联系[email protected]或者[email protected]