当前位置: 首页 > news >正文

java算法学习索引之动态规划

一 斐波那契数列问题的递归和动态规划

【题目】给定整数N,返回斐波那契数列的第N项。

补充问题 1:给定整数 N,代表台阶数,一次可以跨 2个或者 1个台阶,返回有多少种走法。

【举例】N=3,可以三次都跨1个台阶;也可以先跨2个台阶,再跨1个台阶;还可以先跨1个台阶,再跨2个台阶。所以有三种走法,返回3。

补充问题 2:假设农场中成熟的母牛每年只会生 1 头小母牛,并且永远不会死。第一年农场有1只成熟的母牛,从第二年开始,母牛开始生小母牛。每只小母牛3年之后成熟又可以生小母牛。给定整数N,求出N年后牛的数量。

【举例】N=6,第1年1头成熟母牛记为a;第2年a生了新的小母牛,记为b,总牛数为2;第3年a生了新的小母牛,记为c,总牛数为3;第4年a生了新的小母牛,记为d,总牛数为4。第5年b成熟了,a和b分别生了新的小母牛,总牛数为6;第6年c也成熟了,a、b和c分别生了新的小母牛,总牛数为9,返回9。

【要求】对以上所有的问题,请实现时间复杂度为O(logN)的解法。

斐波那契数列问题

奶牛问题

private int[][] multiMatrix(int[][] m1, int[][] m2) {//矩阵乘法// TODO Auto-generated method stubint[][] res=new int[m1.length][m2[0].length];for (int i = 0; i < m1.length; i++) {for (int j = 0; j < m2[0].length; j++) {for (int k = 0; k < m2.length; k++) {res[i][j]+=m1[i][k]*m2[k][j];}}}return res;
}public int f3(int n)
{if (n<1) {return 0;}if (n==1||n==2) {return 1;}int [][] base= {{1,1},{1,0}};int[][] res=matrixPower(base, n-2);return res[0][0]+res[1][0];
}public int c3(int n)
{if (n<1) {return 0;}if (n==1||n==2||n==3) {return 3;}int [][] base= {{1,0,1},{0,0,1},{1,0,0}};int[][] res=matrixPower(base, n-3);return 3*res[0][0]+2*res[1][0]+res[2][0];
}

二 矩阵的最小路径和

给定一个矩阵 m,从左上角开始每次只能向右或者向下走,最后到达右下角的位置,路径上所有的数字累加起来就是路径和,返回所有的路径中最小的路径和。

经典动态规划方法。假设矩阵 m的大小为 M×N,行数为 M,列数为 N。先生成大小和 m一样的矩阵dp,dp[i][j]的值表示从左上角(即(0,0))位置走到(i,j)位置的最小路径和。对m的第一行的所有位置来说,即(0,j)(0≤j<N),从(0,0)位置走到(0,j)位置只能向右走,所以(0,0)位置到(0,j)位置的路径和就是 m[0][0..j]这些值的累加结果。同理,对 m 的第一列的所有位置来说,即(i,0) (0≤i<M),从(0,0)位置走到(i,0)位置只能向下走,所以(0,0)位置到(i,0)位置的路径和就是m[0..i][0]这些值的累加结果。

除第一行和第一列的其他位置(i,j)外,都有左边位置(i-1,j)和上边位置(i,j-1)。从(0,0)到(i,j)的路径必然经过位置(i-1,j)或位置(i,j-1),所以,dp[i][j]=min{dp[i-1][j],dp[i][j-1]}+m[i][j],含义是比较从(0,0)位置开始,经过(i-1,j)位置最终到达(i,j)的最小路径和经过(i,j-1)位置最终到达(i,j)的最小路径之间,哪条路径的路径和更小。那么更小的路径和就是 dp[i][j]的值。

public int minPathSum1(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 = 0; j < col; j++) {dp[0][j]=dp[0][j-1]+m[0][j];}for (int i = 1; i < row; i++) {for (int j = 0; 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];}

矩阵中一共有 M×N 个位置,每个位置都计算一次从(0,0)位置达到自己的最小路径和,计算的时候只是比较上边位置的最小路径和与左边位置的最小路径和哪个更小,所以时间复杂度为O(M×N),dp矩阵的大小为M×N,所以额外空间复杂度为O(M×N)。动态规划经过空间压缩后的方法。这道题的经典动态规划方法在经过空间压缩之后,时间复杂度依然是O(M×N),但是额外空间复杂度可以从O(M×N)减小至O(min{M,N}),也就是不使用大小为M×N的dp矩阵,而仅仅使用大小为min{M,N}的arr数组。具体过程如下

public int minPathSum2(int[][] m)
{if (m==null||m.length==0||m[0]==null||m[0].length==0) {return 0;}int more=Math.max(m.length, m[0].length);int less=Math.min(m.length, m[0].length);boolean rowmore= more==m.length;int[] arr=new int[less];arr[0]=m[0][0];for (int i = 1; i < less; i++) {arr[i]=arr[i-1]+(rowmore? m[0][i]:m[i][0]);//先求出到对角线的值}//数组  arr 中保存的是dp[i][i]中的值,但如果给定的矩阵行数小于列数(M<N),那么就生成长度为M的arr,然后令arr更新成dp矩阵每一列的值,及将arr 中的值保存为 dp[i][N]// 从左向右滚动过去for (int i = 1; i < more; i++) {arr[0]=arr[0]+(rowmore?m[i][0]:m[0][i]);for (int j = 1; j < arr.length; j++) {arr[j]=Math.min(arr[j-1], arr[j])+(rowmore?m[i][j]:m[j][i]);}}return arr[less-1];}

三 换钱的最少货币数

给定数组 arr,arr 中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim,代表要找的钱数,求组成aim的最少货币数。

方法一:暴力递归

public int minCoins1(int[] arr,int aim)
{if (arr==null||arr.length==0||aim<0) {return -1;}return process(arr,0,aim);
}private int process(int[] arr, int i, int rest) {if (i==arr.length) {return rest==0?0:-1;}int res=-1;for (int k = 0; k * arr[i]<=rest; k++) {int next=process(arr, i+1, rest-k*arr[i]);if (next!=-1) {res= res==-1?next+k:Math.min(res, next+k);}}return res;
}

//方法二:动态规划


public int minCoins(int[] arr, int aim) {
    if (arr == null || arr.length == 0 || aim < 0) {
        return -1; // 对于非法输入,返回-1
    }
    int N = arr.length;
    // 创建一个二维数组 dp,dp[i][j] 表示使用前 i 个货币组成金额 j 的最少货币数
    int[][] dp = new int[N + 1][aim + 1];
    
    // 初始化最后一行(i=N)为-1,表示使用最后一个货币无法组成任何金额
    for (int col = 1; col <= aim; col++) {
        dp[N][col] = -1;
    }
    
    // 从倒数第二行开始向第一行递推,计算每个单元格的值
    for (int i = N - 1; i >= 0; i--) {
        for (int rest = 0; rest <= aim; rest++) {
            dp[i][rest] = -1;   // 先将当前位置的值设为-1,表示无法组成目标金额
            
            // 如果使用下一行的值能够组成当前金额,直接继承下一行的值
            if (dp[i + 1][rest] != -1) {
                dp[i][rest] = dp[i + 1][rest];
            }
            
            // 如果当前面值可以被使用,并且使用当前面值可以组成剩余金额,
            // 则更新当前位置的值为使用当前面值和不适用当前面值两种情况中的最小值
            if (rest - arr[i] >= 0 && dp[i][rest - arr[i]] != -1) {
                if (dp[i][rest] == -1) {
                    dp[i][rest] = dp[i][rest - arr[i]] + 1;
                } else {
                    dp[i][rest] = Math.min(dp[i][rest], dp[i][rest - arr[i]] + 1);
                }
            }
        }
    }
    
    // 返回组成目标金额的最少货币数
    return dp[0][aim];
}
 

这段代码使用动态规划思想,通过填表的方式计算出组成目标金额所需的最少货币数。

minCoins2 方法就是填一张 N×aim 的表,而且因为省掉了枚举过程,所以每个位置的值都在O(1)的时间内得到,该方法时间复杂度为O(N×aim)。

四  机器人达到指定位置方法数

假设有排成一行的N个位置,记为1~N,N一定大于或等于2。开始时机器人在其中的M位置上(M一定是1~N中的一个),机器人可以往左走或者往右走,如果机器人来到1位置,那么下一步只能往右来到2位置;如果机器人来到N位置,那么下一步只能往左来到N-1位置。规定机器人必须走K步,最终能来到P位置(P也一定是1~N中的一个)的方法有多少种。给定四个参数N、M、K、P,返回方法数。

(1)
public int ways1(int N, int M, int K, int P) {
    // 检查输入参数的合法性
    if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {
        return 0; // 如果参数不合法,直接返回0
    }
    // 调用 walk 方法计算机器人在K步内走到位置P的方法数
    return walk(N, M, K, P);
}

private int walk(int N, int cur, int rest, int P) {
    if (rest == 0) {
        return cur == P ? 1 : 0; // 如果已经走完K步,检查是否到达位置P,是则返回1,否则返回0
    }
    if (cur == 1) {
        return walk(N, 2, rest - 1, P); // 如果当前位置是1,则只能往右走到位置2
    }
    if (cur == N) {
        return walk(N, N - 1, rest - 1, P); // 如果当前位置是N,则只能往左走到位置N-1
    }
    // 否则,计算能够往左或往右走到下一步位置的方法总数
    return walk(N, cur + 1, rest - 1, P) + walk(N, cur - 1, rest - 1, P);
}
 

这段代码使用递归的方式计算机器人在K步内走到位置P的方法数。walk 方法负责计算具体的步数和位置情况,其中使用了递归的方式进行计算。

(2)动态规划

public int ways2(int N, int M, int K, int P) {if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {return 0;}int[][] dp = new int[K + 1][N + 1];dp[0][P] = 1;for (int i = 1; i <= K; i++) {for (int j = 1; j <= N; j++) {if (j == 1) {dp[i][j] = dp[i - 1][2];} else if (j == N) {dp[i][j] = dp[i - 1][j - 1];} else {dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j + 1];}}}return dp[K][M];
}

(3)动态规划+空间压缩


public int ways3(int N, int M, int K, int P) {
    if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {
        return 0; // 如果参数不合法,直接返回0
    }
    int[] dp = new int[N + 1]; // 创建一个长度为N+1的数组,用于保存每个位置的方法数
    dp[P] = 1; // 初始化目标位置P的方法数为1
    for (int i = 1; i <= K; i++) {
        int leftUp = dp[1]; // 保存上一行的左上角的值,即位置1的值
        for (int j = 1; j <= N; j++) {
            int tmp = dp[j]; // 保存当前位置的值
            if (j == 1) {
                dp[j] = dp[j + 1]; // 如果当前位置是1,只能往右走到位置2
            } else if (j == N) {
                dp[j] = leftUp; // 如果当前位置是N,只能往左走到位置N-1
            } else {
                dp[j] = leftUp + dp[j + 1]; // 否则,计算能够往左或往右走到下一步位置的方法总数
            }
            leftUp = tmp; // 更新左上角的值为上一行的当前位置的值
        }
    }
    return dp[M]; // 返回机器人在K步内走到位置M的方法数
}


这种解法利用动态规划,使用一维数组dp来记录每个位置的方法数。外层循环遍历K步,内层循环遍历每个位置,通过更新数组元素的方式计算每个位置的方法数。最后返回机器人在K步内走到位置M的方法数。

五  换钱的方法数

给定数组 arr,arr 中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim,代表要找的钱数,求换钱有多少种方法

(1)暴力解法

public int coins1(int[] arr, int aim) {if (arr == null || arr.length == 0 || aim < 0) {return 0;}return process1(arr, 0, aim);
}private int process1(int[] arr, int index, int aim) {// TODO Auto-generated method stubint 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;}
(2) 记忆搜索的方法
public int coins2(int[] arr, int aim) {if (arr == null || arr.length == 0 || aim < 0) {return 0; // 如果数组为空或目标钱数小于0,直接返回0}int[][] map = new int[arr.length + 1][aim + 1]; // 创建一个二维数组,用于保存每个索引和钱数对应的换钱方法数return process2(arr, 0, aim, map); // 调用递归函数,并返回结果
}private int process2(int[] arr, int index, int aim, int[][] map) {int res = 0; // 定义结果变量resif (index == arr.length) { // 如果递归到达数组末尾res = aim == 0 ? 1 : 0; // 如果目标钱数为0,说明找到一种换钱方法,将res设为1,否则设为0} else {int mapValue = 0; // 用于存储之前计算过的换钱方法数for (int i = 0; arr[index] * i <= aim; i++) { // 遍历使用当前面值货币的数量mapValue = map[index + 1][aim - arr[index] * i]; // 查看之前计算过的换钱方法数if (mapValue != 0) { // 如果之前计算过res += mapValue == -1 ? 0 : mapValue; // 如果之前计算得到的是-1,说明之前的结果无效,不加入结果res中,否则加入结果res中} else { // 如果之前没有计算过res += process2(arr, index + 1, aim - arr[index] * i, map); // 继续递归计算剩余钱数的换钱方法数}}}map[index][aim] = res == 0 ? -1 : res; // 将计算得到的结果存入map数组中,若结果为0,则设为-1,表示无效结果return res; // 返回结果
}

段代码使用递归的方式解决换钱问题,并利用二维数组map来记录每个索引和钱数对应的换钱方法数。在递归函数process2中,首先判断递归是否到达了数组末尾,如果是,判断目标钱数是否为0,然后返回相应的结果。如果未到达数组末尾,则遍历使用当前面值货币的数量,并查看之前是否计算过对应的换钱方法数。如果计算过,将结果加入结果变量res中(若之前的结果为-1,说明无效,则不加入结果中),如果未计算过,则继续递归计算剩余钱数的换钱方法数。最后将计算得到的结果存入map数组中,并返回结果res。

public int coins3(int[] arr, int aim) {if (arr == null || arr.length == 0 || aim < 0) {return 0; // 如果数组为空或目标钱数小于0,直接返回0}int[][] dp = new int[arr.length][aim + 1]; // 创建一个二维数组dp,用于保存每个索引和钱数对应的换钱方法数for (int i = 0; i < arr.length; i++) {dp[i][0] = 1; // 当钱数为0时,方法数为1}for (int j = 1; arr[0] * j <= aim; j++) {dp[0][arr[0] * j] = 1; // 第一个面值的货币只能用来凑整数倍的自己,所以设置对应的方法数为1}int num = 0; // 用于存储临时的换钱方法数for (int i = 0; i < arr.length; i++) {for (int j = 1; j <= aim; j++) {num = 0; // 初始化临时变量为0for (int k = 0; j - arr[i] * k >= 0; k++) {num += dp[i - 1][j - arr[i] * k]; // 计算当前面值货币使用k张时的换钱方法数,并累加到临时变量中}dp[i][j] = num; // 将计算得到的结果存入dp数组中}}return dp[arr.length - 1][aim]; // 返回结果
}

这段代码使用动态规划解决换钱问题,并利用二维数组dp来记录每个索引和钱数对应的换钱方法数。首先初始化第一列,即钱数为0时的方法数都为1,因为总钱数为0时只有一种换钱方法,即什么都不换。然后初始化第一行,即第一个面值的货币只能用来凑整数倍的自己,所以设置对应的方法数为1。接下来,遍历数组中的每个面值货币和钱数,并根据状态转移方程计算当前面值货币和钱数对应的换钱方法数。最后返回dp数组的最后一个元素,即最终的结果。

可以优化为

public int coins4(int[] arr, int aim) {if (arr == null || arr.length == 0 || aim < 0) {return 0; // 如果数组为空或目标钱数小于0,直接返回0}int[][] dp = new int[arr.length][aim + 1]; // 创建一个二维数组dp,用于保存每个索引和钱数对应的换钱方法数for (int i = 0; i < arr.length; i++) {dp[i][0] = 1; // 当钱数为0时,方法数为1}for (int j = 1; arr[0] * j <= aim; j++) {dp[0][arr[0] * j] = 1; // 第一个面值的货币只能用来凑整数倍的自己,所以设置对应的方法数为1}for (int i = 0; i < arr.length; i++) {for (int j = 1; j <= aim; j++) {dp[i][j] = dp[i - 1][j]; // 初始化为上一个硬币的情况if (j - arr[i] >= 0) { // 判断当前面值的硬币是否可以组成jdp[i][j] += dp[i][j - arr[i]]; // 若可以组成,则加上组成j-arr[i]的情况数}}}return dp[arr.length - 1][aim]; // 返回结果
}

(4)空间压缩

public int coins5(int[] arr, int aim) {if (arr == null || arr.length == 0 || aim < 0) {return 0; // 如果数组为空或目标钱数小于0,直接返回0}int[] dp = new int[aim + 1]; // 创建一个一维数组dp,用于保存每个钱数对应的换钱方法数for (int j = 1; j <= aim; j++) {dp[arr[0] * j] = 1; // 第一个面值的货币只能用来凑整数倍的自己,所以设置对应的方法数为1}for (int i = 0; i < arr.length; i++) {for (int j = 1; j <= aim; j++) {dp[j] += j - arr[i] >= 0 ? dp[j - arr[i]] : 0; // 根据状态转移方程计算当前钱数对应的换钱方法数}}return dp[aim]; // 返回结果
}

这段代码使用动态规划解决换钱问题,并利用一维数组dp来记录每个钱数对应的换钱方法数。首先初始化第一个面值的货币的倍数对应的方法数为1。然后遍历数组中的每个面值货币和钱数,并根据状态转移方程计算当前钱数对应的换钱方法数。最后返回dp数组的最后一个元素,即最终的结果。这种方法优化了空间复杂度,只使用一维数组来保存动态规划的结果。

相关文章:

  • IntelliJ IDEA 安装 GitHub Copilot插件 (最新)
  • HTTP服务器——tomcat的安装和使用
  • 前端案例-css实现ul中对li进行换行
  • 若依侧边栏添加计数标记效果
  • Flink之Java Table API的使用
  • GPU编程
  • 【机器学习】 朴素贝叶斯算法:原理、实例应用(文档分类预测)
  • 使用validator实现枚举类型校验
  • MacOS Ventura 13 优化配置(ARM架构新手向导)
  • VSCode 好用的插件分享
  • 【多线程 - 04、Join 方法与 wait 方法】
  • postman调用接口报{“detail“:“Method \“DELETE\“ not allowed.“}错误, 解决记录
  • Spark3.0中的AOE、DPP和Hint增强
  • C++知识点梳理:移动语意、右值
  • js实现对象数组去重
  • Apache Spark Streaming 使用实例
  • CSS实用技巧
  • JavaScript学习总结——原型
  • Linux快速复制或删除大量小文件
  • Promise面试题,控制异步流程
  • Python连接Oracle
  • tab.js分享及浏览器兼容性问题汇总
  • vue-loader 源码解析系列之 selector
  • zookeeper系列(七)实战分布式命名服务
  • 大主子表关联的性能优化方法
  • 事件委托的小应用
  • 项目实战-Api的解决方案
  •  一套莫尔斯电报听写、翻译系统
  • 原生Ajax
  • ​ 无限可能性的探索:Amazon Lightsail轻量应用服务器引领数字化时代创新发展
  • ​水经微图Web1.5.0版即将上线
  • # Panda3d 碰撞检测系统介绍
  • #14vue3生成表单并跳转到外部地址的方式
  • #Js篇:单线程模式同步任务异步任务任务队列事件循环setTimeout() setInterval()
  • #NOIP 2014#Day.2 T3 解方程
  • #QT(串口助手-界面)
  • $().each和$.each的区别
  • $.extend({},旧的,新的);合并对象,后面的覆盖前面的
  • ( )的作用是将计算机中的信息传送给用户,计算机应用基础 吉大15春学期《计算机应用基础》在线作业二及答案...
  • (03)光刻——半导体电路的绘制
  • (1/2) 为了理解 UWP 的启动流程,我从零开始创建了一个 UWP 程序
  • (9)STL算法之逆转旋转
  • (C#)Windows Shell 外壳编程系列4 - 上下文菜单(iContextMenu)(二)嵌入菜单和执行命令...
  • (Redis使用系列) SpirngBoot中关于Redis的值的各种方式的存储与取出 三
  • (二)【Jmeter】专栏实战项目靶场drupal部署
  • (紀錄)[ASP.NET MVC][jQuery]-2 純手工打造屬於自己的 jQuery GridView (含完整程式碼下載)...
  • (欧拉)openEuler系统添加网卡文件配置流程、(欧拉)openEuler系统手动配置ipv6地址流程、(欧拉)openEuler系统网络管理说明
  • (淘宝无限适配)手机端rem布局详解(转载非原创)
  • (转载)PyTorch代码规范最佳实践和样式指南
  • ... 是什么 ?... 有什么用处?
  • .NET 4.0中使用内存映射文件实现进程通讯
  • .NET Framework杂记
  • .NET MAUI学习笔记——2.构建第一个程序_初级篇
  • .NET 反射 Reflect
  • .NET 将多个程序集合并成单一程序集的 4+3 种方法