Java算法之动态规划

Java算法之动态规划

前言

最近这一段时间一直在刷算法题,基本上一有时间就会做一两道,这两天做了几道动态规划的问题,动态规划之前一直是我比较头疼的一个问题,感觉好复杂,一遇到这样的问题就想跳过,昨天耐着性子做了一道动态规划的题,感觉没有我想象的那么难,无非就是先定义dp数组,然后找到初始值,再写出状态转移方程,一步一步来,难点就是如何确定一个正确的状态,这是一个一直困扰我的问题,而且在写状态方程时要细心一点,不要出现错误,这篇文章就是记录一下自己的学习体会和心得。

动态规划的基本概念

动态规划(Dynamic Programming,简称DP)是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式来求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构特性的问题。

动态规划的基本思想是,将待求解的问题分解成若干个相互联系的子问题,先求解子问题,然后从这些子问题的解得到原问题的解。动态规划的关键在于正确地定义子问题,以及子问题的解如何推导出原问题的解。

动态规划通常用于求解具有最优子结构特性的问题,即问题的最优解可以由其子问题的最优解有效地构造出来。此外,动态规划还要求子问题空间必须足够小,即子问题的数量随着问题规模的增加不会增长得太快,以便能够用有限的内存和时间来解决。

贪心算法

在这里我想提一下贪心算法,为什么要提一下贪心算法呢,因为我觉得这两个算法之间存在一些共同点。贪心算法(Greedy Algorithm)是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法。这和动态规划的将问题分解为若干个子问题求解有些类似,都是从每一个小问题出发,然后慢慢扩展到最后的原问题,这两个算法在最优子结构特性的问题解决上都尤为有效,贪心算法的优点是简单、直观且运行速度快,因为每一步只关注当前状态下的最优选择,而不考虑未来可能的变化。但是,如果问题不满足贪心选择性质,贪心算法就无法保证得到全局最优解。在这种情况下,可能需要使用其他方法,如动态规划。

先举一个贪心算法的小例子,比如找零问题就是一个典型的贪心算法应用。假设有面值为1元、2元、5元、10元、20元、50元、100元的纸币,目标是找出一个给定金额的最少纸币数。贪心策略是每次尽可能选择面值最大的纸币,因为这样可以减少纸币的数量。

再举一个不满足贪心选择的性质,比如贪心算法的一个经典案例,背包问题,背包问题有一个背包,背包容量是M=150。有7个物品,物品可以分割成任意大小。要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。那么运用贪心算法的思想,先求出每种物品的单位价值,再从最值钱的开始装,直到背包装满为止。但如果对这道题修改一下,不能分割物品,那此时贪心算法就会失效,这就是0-1背包问题。此时,需要用动态规划来解决。

几道例题

1.0-1背包问题

QQ截图20240306200546.png
public class Main {
 public static void main(String[] args) {
 Scanner scan = new Scanner(System.in);
 //输入商场物品的数量
 int N = scan.nextInt();
 //输入小明的背包容量
 int V = scan.nextInt();
 //定义两个数组分别记录商品的重量和价格
 int[] weight = new int[N];
 int[] value = new int[N];
 for (int i = 0; i < N; i++) {
 //重量
 weight[i] = scan.nextInt();
 //价值
 value[i] = scan.nextInt();
 }
 //设置dp表
 int[][] dp = new int[N+1][V+1];
 //循环遍历每一个商品,每次循环都要得出在背包容量为1-->V的每种情况下,他所含的价值
 for (int j = 1;j<=N;j++){
 //从背包容量为1时开始遍历
 for(int k = 1;k<=V;k++){
 //如果商品容量大于背包容量,那么就装不进去,那么总价值就为前一个商品在这个容量的总价值
 if(weight[j-1]>k){
 dp[j][k] = dp[j-1][k];
 }
 //否则,比较放入当前物品和不放入当前物品哪种情况价值更高
 else{
 //dp[j-1][k-weight[j-1]]这个代码是为了求减去当前商品的容量后,背包的总价值,再加上此时加入这个商品后的价值,得到一个新的总价值,如果这个总价值大于相同容量下前一个商品的价值,那么就值得放入,否则不值得放入
 dp[j][k] = Math.max(dp[j-1][k-weight[j-1]]+value[j-1],dp[j-1][k]);
 }
 }
 }
 System.out.println(dp[N][V]);
 scan.close();
 }
}

2.爬楼梯

题目描述:假设你现在正在爬楼梯。需要n阶你才能到达楼顶。每次你可以爬1或者2个台阶,一共有多少种方法可以到达楼顶?

那么对于这题,很明显我们可以使用动态规划来进行求解,状态很好确定,首先确立边界,当爬一层有多少方法,当爬两层有多少种方法,那么设立状态转移方程,当爬第n阶时,有两种状态,要么现在处于第n-1个阶梯,要么现在处于第n-2个阶梯,那爬第n阶的方法总数就是爬n-1个阶梯的方法数加上爬n-2个阶梯的方法数。

class Solution {
 public int climbStairs(int n) {
 //当n为1时直接返回1,这里返回是因为后面dp数组长度为n+1,如果n为1,那后面对dp[2]赋值就会出现溢出
 if(n == 1){
 return 1;
 }    //设置dp数组,表示爬第n阶台阶的方法数 
 int [] dp = new int[n+1];
 //爬第1阶台阶的方法数
 dp[1] = 1;
 //爬第2阶台阶的方法数
 dp[2] = 2;
 //从第三阶开始循环遍历
 for(int i = 3;i < n+1;i++){
 //状态转移方程
 dp[i] = dp[i-2] + dp[i-1];
 }
 //返回
 return dp[n];

 }

}

3.买股票的最佳时机

QQ截图20240307155421.png

在练习数组的相关算法题时,有过一道买卖股票的题,但这两个题不太一样,但都是用动态规划来解决的,这道题要求是只能买卖一次,那根据动态规划的思想,每天都有两种状态,一种是手里没有股票,一种是手里有股票,那可以设置两个边界,一个是第一天手里没有股票的利润,一种是手里有股票的利润,然后递推下一个,根据题目我们知道只能买卖一次,那么如果这一天手里有股票,那可能是前面买的,也可能是今天买的,有这两种情况,比较两种的较大者作为当天有股票的最大利润

如果当天手里没有股票,那可能是前面卖的,也可能是当天卖的,同样,我们比较两者的较大值作为当天的最大利润。

class Solution {
 public int maxProfit(int[] prices) {
 if (prices == null || prices.length == 0)
 return 0;
 int length = prices.length;
 int[][] dp = new int[length][2];
 //边界条件
 dp[0][0]= 0;
 dp[0][1] = -prices[0];
 for (int i = 1; i < length; i++) {
 //递推公式
 dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
 dp[i][1] = Math.max(dp[i - 1][1], -prices[i]);
 }
 //毋庸置疑,最后肯定是手里没持有股票利润才会最大,也就是卖出去了
 return dp[length - 1][0];
}
}

4.最大子序和

QQ截图20240307201014.png

看到这道题后,感觉没有什么思路,后来去问了一下ai,感觉茅塞顿开,基本思路是定义一个dp数组,这个dp数组所记录的就是以当前索引为右边界时,这时候的最大子数组,那么递推公式就是dp[i] = nums[i] + Math.max(dp[i-1],0),为什么要有这个代码呢Math.max(dp[i-1],0),这段代码是比较dp[i-1]是否大于0,如果小于零,那么就不能再加了,因为会越加越小,此时从当前索引重新开始记录,为什么会越加越小呢,我当时的疑问是,如果当前索引是一个正数,那不仍然会变大吗,怎么会是越加越小呢,我思索了一会,想明白了,以当前索引的数据来看,如果加上一个负数,那就会变小,不如直接舍弃,重新开始记录,至于当前索引是正数还是负数,这个不用考虑,因为无论是正数还是负数,加上一个负数都会变小。max = Math.max(dp[i],max);,这里则是记录最大值,每次循环都更新max的值,最后返回max。

class Solution {
 public int maxSubArray(int[] nums) {
 int length = nums.length;
 int [] dp = new int [length];
 dp[0] = nums[0];
 int max = dp[0];
 for(int i = 1;i<length;i++){
 dp[i] = nums[i] + Math.max(dp[i-1],0);
 max = Math.max(dp[i],max);
 }
 return max;
 }
}

5.打家劫舍

QQ截图20240307203034.png

这道题也比较简单,好理解,定义一个dp数组,如果不抢这一家,那能获取的利润有两种情况,一种是抢了前一家,另一种是没抢前一家,比较这两种的最大值,如果抢了这一家,那就只有一种情况,没抢前一家的最大利润。

class Solution {
 public int rob(int[] nums) {
 int length = nums.length;
 if(length == 1){
 return nums[0];
 }
 //定义dp数组
 int[][] dp = new int [length][2];
 //不抢第一家的利润
 dp[0][0] = 0;
 //抢了第一家的利润
 dp[0][1] = nums[0];
 int max = 0;
 int mid = 0;
 for(int i = 1;i<length;i++){
 dp[i][0] = Math.max(dp[i-1][1],dp[i-1][0]);
 dp[i][1] = dp[i-1][0]+nums[i];
 //mid用于比较抢和不抢那种利润最大
 mid = Math.max(dp[i][0],dp[i][1]);
 max = Math.max(max,mid);
 }
 return max;
 }
}

6.蜗牛

QQ截图20240307213153.png

这是一道去年蓝桥杯省赛B组的真题,那么解题思路如下

首先,还是定义dp数组,那状态如何确立,确立状态就是看有几种选择,那么由题可知,蜗牛移动有两种方式,一种是直接爬过去,一种是通过传送门传送,那么可以定义dp[i][0]表示到达第i个结点需要的最短时间,dp[i][1]表示到达第i个传送门的最短时间。

然后就定义初始值,最后写出转移方程就行了

public class Main {
 public static void main(String[] args) {
 Scanner scan = new Scanner(System.in);
 // 在此输入您的代码...
 int n = scan.nextInt();
 //定义存储竹竿x轴坐标的数组
 int [] x = new int[n+1];
 //定义记录每个竹竿传送门的纵坐标的数组
 int [] a = new int[n+1];
 //定义记录被传送到下一个竹竿的纵坐标的数组
 int [] b = new int[n+1];
 //输入竹竿纵坐标
 for (int i = 1; i <= n; i++) {
 x[i] = scan.nextInt();
 }
 //输入传送门的位置和被传送到的位置
 for (int i = 1; i < n; i++){
 a[i] = scan.nextInt();
 b[i+1] = scan.nextInt();
 }
 double [][] dp = new double[n+1][2];
 //dp[i][0]表示到达竹竿底部的最短时间
 //dp[i][1]表示到达竹竿传送门的最短时间
 dp[1][0] = x[1];
 dp[1][1] = x[1]+a[1]/0.7;
 for (int i = 2; i <= n; i++) {
 dp[i][0] = Math.min(dp[i-1][0]+x[i]-x[i-1],dp[i-1][1]+b[i]/1.3);
 if(a[i]>b[i]) {
 dp[i][1] = Math.min(dp[i - 1][0] + a[i] / 0.7+x[i]-x[i-1], dp[i - 1][1]+(a[i]-b[i])/0.7);
 }
 else{
 dp[i][1] = Math.min(dp[i - 1][0] + a[i] / 0.7+x[i]-x[i-1], dp[i - 1][1]+(b[i]-a[i])/1.3);
 }
 }

 System.out.printf("%.2f",dp[n][0]);
 }
}

后记

这几天动态规划的题做的比较多,其中有一些也涉及到贪心算法,也了解了一下,下一步我觉得应该就开始学背包问题了,动态规划涉及到了0-1背包问题,感觉是个很经典的问题,背包问题又有很多分支,0-1背包问题只是其中一个小问题,还有很多问题等着我去学习,看了一下去年蓝桥杯的题目,感觉自己还差的有些远,算法掌握的还不够多,接下来就要更加努力去学习了,这一个月就专心准备算法,其他的事情都先放放,等到蓝桥杯结束再说。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342