暴力递归到动态规划(四)

news/2024/12/2 13:57:46/

请添加图片描述
⭐️前言⭐️

本篇文章是从暴力递归到动态规划篇目的最后一篇文章,包含了几道题目还有最终的大总结,相信这篇文章能让各位读者对动态规划有更深一步的了解。

🍉欢迎点赞 👍 收藏留言评论 📝私信必回哟😁

🍉博主将持续更新学习记录收获,友友们有任何问题可以在评论区留言

🍉博客中涉及源码及博主日常练习代码均已上传GitHub


请添加图片描述

📍内容导读📍

  • 🍅砍怪兽
  • 🍅最少货币数
  • 🍅拆分数字
  • 🍅数组拆分
  • 🍅数组拆分2
  • 🍅贴纸拼词
  • 🍅总结

🍅砍怪兽

题目:
给定3个参数,N,M,K
怪兽有N滴血,等着英雄来砍自己
英雄每一次打击,都会让怪兽流失[0~M]的血量
到底流失多少?每一次在[0~M]上等概率的获得一个值
求K次打击之后,英雄把怪兽砍死的概率

题解思路1:
总的情况数为(M+1)^k,通过暴力递归求出砍死怪兽的情况数,即可得到把怪兽砍死的概率。
递归终止的条件:
当次数为0时,看剩余血量,如果血量小于等于0,则返回1(说明是一种情况)
当血量为0时,剩余的次数都是能够砍死怪兽的情况,直接返回(M+1)^ times即为该情况下的所有情况数。
其余情况递归,根据0~M 血量的变化,进入不同的递归,记录不同的递归的情况数,求得最后的结果。

代码实现:

public class KillMonster {public static double right(int N,int M,int K) {if(N<1 || M<1 || K<1) {return 0;}long all=(long) Math.pow(M+1,K);long kill=process(K,M,N);return (double) ((double)kill/(double) all);}// 怪兽还剩hp点血// 每次的伤害范围为0~M// 还有times可以砍// 返回砍死的情况数public static long process(int times,int M,int hp) {if (times == 0) {return hp <= 0 ? 1 : 0;}if(hp<=0) {return (long) Math.pow(M+1,times);}long ways=0;for (int i = 0; i <=M ; i++) {ways+=process(times-1,M,hp-i);}return ways;}
}

题解思路2:
根据暴力递归来转化为动态规划解法,由于有两个可变参数,所以可以通过构建一个二维DP表来存储不同剩余次数、不同血量时的情况数,最后返回表中需要位置的结果即可。
(注意:当血量为负的时候,把怪兽砍死的结果数就是(M+1)^(times-1))

代码实现:

public class KillMonster {public static double dp1(int N,int M,int K) {if(N<1 || M<1 || K<1) {return 0;}long all=(long) Math.pow(M+1,K);long[][] dp=new long[K+1][N+1];dp[0][0]=1;for (int times = 1; times <=K ; times++) {dp[times][0]=(long) Math.pow(M+1,times);for (int hp = 1; hp <=N ; hp++) {long ways=0;for (int i = 0; i <=M ; i++) {if(hp-i>=0) {ways+=dp[times-1][hp-i];}else {  // 当血量为负的时候,情况数直接就是下边的公式ways+=(long) Math.pow(M+1,times-1);}}dp[times][hp]=ways;}}long kill=dp[K][N];return (double) ((double)kill/(double) all);}
}

题解思路3:
由于在求每个位置的dp结果时,存在for循环的枚举依赖,所以可以通过严格依赖的表结构关系进行优化,不使用枚举依赖。

代码实现:

public class KillMonster {// 枚举优化public static double dp2(int N,int M,int K) {if(N<1 || M<1 ||K<1) {return 0;}long all=(long) Math.pow(M+1,K);long[][] dp=new long[K+1][N+1];dp[0][0]=1;for (int times = 1; times <=K ; times++) {dp[times][0]=(long) Math.pow(M+1,times);for (int hp = 1; hp <=N ; hp++) {dp[times][hp]=dp[times][hp-1]+dp[times-1][hp];if(hp-1-M>=0) {// 没有越界dp[times][hp]-=dp[times-1][hp-1-M];}else {// 如果越界了说明血量为负,也得减去情况数dp[times][hp]-=Math.pow(M+1,times-1);}}}long kill=dp[K][N];return (double) ((double)kill/(double) all);}
}

🍅最少货币数

题目:
arr是面值数组,其中的值都是正数且没有重复。再给定一个正数aim。
每个值都认为是一种面值,且认为张数是无限的。
返回组成aim的最少货币数

题解思路:
从左往右的尝试模型,依次考虑每个面值对应不同数的情况,记录能够组成aim的最小张数返回即可。

代码实现:

public class MinCoinsNoLimit {public static int minCoins(int[] arr,int aim) {return process(arr,0,aim);}// arr[index...]面值,每种面值张数自由选择// 凑出正好rest的钱数,返回最小张数// 拿Integer.MAX_VALUE标记怎样也凑不出public static int process(int[] arr,int index,int rest) {if(index==arr.length) {return rest==0?0:Integer.MAX_VALUE;}else {int ans=Integer.MAX_VALUE;for (int pages = 0; pages*arr[index] <=rest ; pages++) {int next=process(arr,index+1,rest-pages*arr[index]);if(next!=Integer.MAX_VALUE) {ans=Math.min(ans,next+pages);}}return ans;}}
}

题解思路2:
根据暴力递归转化为动态规划,由于有两个可变参数,所以可以依此构建一个二维dp表,记录不同面值,不同rest,再根据index+1位置面值的不同张数返回的情况数,取最小结果填入。

代码实现:

public class MinCoinsNoLimit {public static int dp1(int[] arr,int aim) {if(aim==0) {return 0;}int N=arr.length;int[][] dp=new int[N+1][aim+1];dp[N][0]=0;for (int j = 1; j <=aim ; j++) {dp[N][j]=Integer.MAX_VALUE;}for (int index = N-1; index >=0 ; index--) {for (int rest = 0; rest <=aim ; rest++) {int ans=Integer.MAX_VALUE;for (int pages = 0; pages*arr[index] <=rest ; pages++) {int next=dp[index+1][rest-pages*arr[index]];if(next!=Integer.MAX_VALUE) {ans=Math.min(ans,pages+next);}}dp[index][rest]=ans;}}return dp[0][aim];}
}

题解思路3:
由于每个位置的填写,都依赖于枚举得到的结果,所以可以根据严格依赖的表结构位置关系,优化枚举行为。
在这里插入图片描述

红色方格的值依赖于Math.min(a+0,b+1,c+2,d+3) 【最后加几取决于rest,只要不越过左边界即可】
黄色方格的值依赖于Math.min(b+0,c+1,d+2)

所以dp[index][rest]=Math.min(dp[index+1][rest],dp[index][rest-arr[index]]+1)

代码实现:

public class MinCoinsNoLimit {public static int dp2(int[] arr,int aim) {if(aim==0) {return 0;}int N=arr.length;int[][] dp=new int[N+1][aim+1];dp[N][0]=0;for (int j = 1; j <=aim ; j++) {dp[N][j]=Integer.MAX_VALUE;}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-arr[index]]!=Integer.MAX_VALUE) {dp[index][rest]=Math.min(dp[index][rest-arr[index]]+1,dp[index][rest]);}}}return dp[0][aim];}
}

🍅拆分数字

题目:
给定一个正数n,求n的裂开方法数,
规定:后面的数不能比前面的数小
比如4的裂开方法有:
1+1+1+1、1+1+2、1+3、2+2、4
5种,所以返回5

题解思路1:
如果想要拆分5,就假设拆分6,先拆成了1和5,看5剩下多少种拆分办法。
base case:当rest剩余0时,说明完全拆分完成,则返回一种情况,如果pre>rest,则不符合条件,返回0。

代码实现:

public class SplitNumber {// n为正数public static int ways(int n) {if(n<0) {return 0;}if(n==1) {return 1;}return process(1,n);}// 上一个拆出来的数是pre// 还剩rest需要去拆// 返回拆解的方法数public static int process(int pre, int rest) {if(rest==0) {return 1;}if(pre>rest) {return 0;}int ways=0;for (int first = pre; first <=rest ; first++) {ways+=process(first,rest-first);}return ways;}
}

题解思路2:
由暴力递归转化为动态规划,有两个可变参数pre和rest,先跟base case完成部分位置的填写,再根据暴力递归的依赖关系,来推断出dp表中其他位置该如何填写。
在这里插入图片描述

代码实现:

public class SplitNumber {public static int dp1(int n) {if(n<0) {return 0;}if(n==1) {return 1;}int[][] dp=new int[n+1][n+1];for (int pre = 1; pre <=n ; pre++) {dp[pre][0]=1;dp[pre][pre]=1;}for (int pre = n-1; pre >=0 ; pre--) {for (int rest = pre+1; rest <=n ; rest++) {int ways=0;for (int first = pre; first <=rest ; first++) {ways+=dp[first][rest-first];}dp[pre][rest]=ways;}}return dp[1][n];}
}

题解思路3:
由于每个位置的填写有枚举依赖,所以可以根据严格依赖的位置关系再做优化,对枚举行为进行优化。
dp[pre][rest]=dp[pre+1][rest]+dp[pre][rest-pre]
在这里插入图片描述

代码实现:

public class SplitNumber {public static int dp2(int n) {if(n<0) {return 0;}if(n==1) {return 1;}int[][] dp=new int[n+1][n+1];for (int pre = 1; pre <=n ; pre++) {dp[pre][0]=1;dp[pre][pre]=1;}for (int pre = n-1; pre >=0 ; pre--) {for (int rest = pre+1; rest <=n ; rest++) {dp[pre][rest]=dp[pre+1][rest];dp[pre][rest]+=dp[pre][rest-pre];}}return dp[1][n];}
}

🍅数组拆分

题目:
给定一个正数数组arr,
请把arr中所有的数分成两个集合,尽量让两个集合的累加和接近
返回最接近的情况下,较小集合的累加和

题解思路1:
从左往右的尝试模型,每个位置都有要或者不要两种情况,递归就是返回接近于rest但不大于rest的最大累加和。
代码实现:

public class SplitSumClosed {public static int right(int[] arr) {if(arr==null||arr.length<2) {return 0;}int sum=0;for(int num:arr) {sum+=num;}return process(arr,0,sum/2);}// arr[i...]可以自由选择,返回累加和尽量接近rest,但不能超过rest的情况下,最接近的累加和是多少public static int process(int[] arr,int i,int rest) {if(i==arr.length) {return 0;}else { // 还有数// 可能性1,不使用arr[i]int p1=process(arr,i+1,rest);// 可能性2,使用arr[i]int p2=0;if(arr[i]<=rest) {p2=arr[i]+process(arr,i+1,rest-arr[i]);}return Math.max(p1,p2);// 返回两种可能性中更接近的}}
}

题解思路2:
暴力递归中有两个可变参数,所以可以优化为一个二维dp表,来进行存储不同状态下的结果,根据位置依赖关系来填写dp表,最后返回所需要状态的结果。
代码实现:

public class SplitSumClosed {public static int dp(int[] arr) {if(arr==null||arr.length<2) {return 0;}int sum=0;for (int num:arr) {sum+=num;}sum/=2;int N=arr.length;int[][] dp=new int[N+1][sum+1];for (int i = N-1; i >=0 ; i--) {for (int rest = 0; rest <=N ; rest++) {// 可能性1,不使用arr[i]int p1=dp[i+1][rest];// 可能性2,使用arr[i]int p2=0;if(arr[i]<=rest) {p2=arr[i]+dp[i+1][rest-arr[i]];}dp[i][rest]= Math.max(p1,p2);}}return dp[0][sum];}
}

🍅数组拆分2

题目:
给定一个正数数组arr,请把arr中所有的数分成两个集合
如果arr长度为偶数,两个集合包含数的个数要一样多
如果arr长度为奇数,两个集合包含数的个数必须只差一个
请尽量让两个集合的累加和接近
返回最接近的情况下,较小集合的累加和

题解思路1:
与数组拆分1类似,只不过加上了个数限制,要根据arr长度来决定集合中只能选几个数。
代码实现:

public class SplitSumClosedSizeHalf {public static int right(int[] arr) {if(arr==null||arr.length<2) {return 0;}int sum=0;for (int num:arr) {sum+=num;}if((arr.length&1)==0) {return process(arr,0,arr.length/2,sum/2);}else {return Math.max(process(arr,0,arr.length/2,sum/2),process(arr,0,arr.length/2+1,sum/2));}}// arr[i...]自由选择,挑选的个数一定是picks个,累加和<=rest,离rest最接近的返回public static int process(int[] arr, int i, int picks, int rest) {if(i==arr.length) {return picks==0?0:-1;  // 不合法用-1标记}else {// 不用arr[i]int p1=process(arr, i+1, picks, rest);// 用arr[i]int p2=-1;int next=-1;if(arr[i]<=rest) {next=process(arr,i+1,picks-1,rest-arr[i]);}// 如果下一层有效if(next!=-1) {p2=arr[i]+next;}return Math.max(p1,p2);}}
}

题解思路2:
根据暴力递归的三个可变参数,建立一个三维的dp表,根据位置依赖关系填写dp表,最后返回所要位置的dp结果。
代码实现:

public class SplitSumClosedSizeHalf {public static int dp(int[] arr) {if(arr==null||arr.length<2) {return 0;}int sum=0;for (int num:arr) {sum+=num;}sum/=2;int N=arr.length;int M=(N+1)/2;// 向上取整int[][][] dp=new int[N+1][M+1][sum+1];for (int i = 0; i <= N; i++) {for (int j = 0; j <= M; j++) {for (int k = 0; k <= sum; k++) {dp[i][j][k] = -1;}}}for (int rest = 0; rest <=sum ; rest++) {dp[N][0][rest]=0;}for (int i = N-1; i >=0 ; i--) {for (int picks = 0; picks <=M ; picks++) {for (int rest = 0; rest <=sum ; rest++) {// 不用arr[i]int p1=dp[i+1][picks][rest];// 用arr[i]int p2=-1;int next=-1;if(arr[i]<=rest) {next=dp[i+1][picks-1][rest-arr[i]];}// 如果下一层有效if(next!=-1) {p2=arr[i]+next;}dp[i][picks][rest]= Math.max(p1,p2);}}}if((arr.length&1)==0) {return dp[0][arr.length/2][sum];}else {return Math.max(dp[0][arr.length/2][sum],dp[0][arr.length/2+1][sum]);}}
}

🍅贴纸拼词

题目:
给定一个字符串str,给定一个字符串类型的数组arr。
arr里的每一个字符串,代表一张贴纸,你可以把单个字符剪开使用,目的是拼出str来。
返回需要至少多少张贴纸可以完成这个任务。
例子:str= “babac”,arr = {“ba”,“c”,“abcd”}
至少需要两张贴纸"ba"和"abcd",因为使用这两张贴纸,把每一个字符单独剪开,含有2个a、2个b、1个c。是可以拼出str的。所以返回2。
https://leetcode-cn.com/problems/stickers-to-spell-word/

题解思路:
每张贴纸都可以作为第一张,如果该贴纸有效(即可以减掉部分字符),则递归进入下一层(传入的参数变为减后的字符串);如果该贴纸无效,则直接就是默认为系统最大值,最后返回记录的最小结果。

代码实现:

public class StickersToSpellWord {public static int minSticker1(String[] stickers,String target) {int ans=process(stickers,target);return ans==Integer.MAX_VALUE?-1:ans;}// 所有贴纸stickers,每一种贴纸都有无穷张// target// 最少张数public static int process(String[] stickers,String target) {if(target.length()==0) {return 0;}int min=Integer.MAX_VALUE;for (String first:stickers) {String rest=minus(target,first);if(rest.length()!=target.length()) {min=Math.min(min,process(stickers,rest));}}return min+(min==Integer.MAX_VALUE?0:1);}public static String minus(String s1,String s2) {char[] str1=s1.toCharArray();char[] str2=s2.toCharArray();int[] count=new int[26];for (char ch:str1) {count[ch-'a']++;}for (char ch:str2) {count[ch-'a']--;}StringBuilder builder=new StringBuilder();for (int i = 0; i < 26; i++) {if(count[i]>0) {for (int j = 0; j < count[i]; j++) {builder.append((char) (i+'a'));}}}return builder.toString();}
}

题解思路2:
可以将词频表和贴纸都转换为数组,在相减时可以直接数组相减。

代码实现:

public class StickersToSpellWord {public static int minStickers2(String[] stickers,String target) {int N=stickers.length;// 关键优化(用词频表代替贴纸数组)int[][] counts=new int[N][26];for (int i = 0; i < N; i++) {char[] str=stickers[i].toCharArray();for (char ch:str) {counts[i][ch-'a']++;}}int ans=process2(counts,target);return ans==Integer.MAX_VALUE?-1:ans;}// stickers[i]数组// 每种贴纸都有无穷张// 返回搞定target的最少张数public static int process2(int[][] stickers,String t) {if(t.length()==0) {return 0;}// target做出词频统计char[] target=t.toCharArray();int[] tcounts=new int[26];for (char ch:target) {tcounts[ch-'a']++;}int N=stickers.length;int min=Integer.MAX_VALUE;for (int i = 0; i < N; i++) {// 尝试第一张贴纸是谁int[] sticker=stickers[i];// 剪枝优化if(sticker[target[0]-'a']>0) {  // 如果目标target中的字符在该贴纸中存在StringBuilder builder=new StringBuilder();for (int j = 0; j < 26; j++) {if(tcounts[j]>0) {int nums=tcounts[j]-sticker[j];for (int k = 0; k < nums; k++) {builder.append((char) j+'a');}}}String rest=builder.toString();min=Math.min(min,process2(stickers,rest));}}return min+(min==Integer.MAX_VALUE?0:1);}
}

🍅总结

在这里插入图片描述
什么暴力递归可以继续优化?
有重复调用同一个子问题的解,这种递归可以优化;
如果每个子问题都是不同的解,无法优化也不用优化。

暴力递归和动态规划的关系
某一个暴力递归,有解的重复调用,就可以把这个暴力递归优化成动态规划
任何动态规划问题,都一定对应着某一个有解的重复调用的暴力递归
但不是所有的暴力递归,都一定对应着动态规划

常见的四种模型
1、从左往右的尝试模型
2、范围尝试模型
3、样本对应模型
4、业务限制模型


⭐️最后的话⭐️
总结不易,希望uu们不要吝啬你们的👍哟(^U^)ノ~YO!!如有问题,欢迎评论区批评指正😁

请添加图片描述


http://www.ppmy.cn/news/347623.html

相关文章

KW 新闻 | KaiwuDB 发布智慧矿山解决方案

5月21日&#xff0c;天津第七届世界智能大会&#xff08;WIC&#xff09;圆满落幕。作为智能领域的国家级盛会&#xff0c;WIC 汇聚了全球知名院士、顶级学者、产业领袖分享先进技术和实践经验&#xff0c;推进智能技术创新合作。KaiwuDB 受邀出席大会并正式发布智慧矿山解决方…

使用 Docker 高效搭建本地开发环境(详细教程)

Docker本地开发环境的好处 试错 对开发者而言&#xff0c;每天会催生出的各式各样的新技术都需要尝试&#xff0c;然而开发者却不太可能为他们一一搭建好环境并进行测试。时间非常宝贵&#xff0c;正是得益于 Docker&#xff0c;让我们有可能在一条或者几条命令内就搭建完环境…

plus.runtime.version总是13.8.4

引言 最近在uniapp中使用到了plus.runtime.version&#xff0c;但是在开发环境下一直无法获取到真正的版本号&#xff0c;他的值一直都是13.8.4&#xff0c;在全局进行搜索也没有发现哪里设置了13.8.4&#xff0c;后来查阅了相关资料才知道这并不是自己写错了。 场景复现&…

redhat 7及以上版本crsctl start crs启动失败问题

因为rac老版本启动依赖的是init.d&#xff0c;而redhat 7及以上版本默认为systemd&#xff0c;两者的差异较大。导致redhat 7及以上版本启动crs的ohasd服务时会卡住一段时间且最后无法启动成功&#xff0c;strace日志则会显示无法找到/var/tmp/.oracle/npohasd文件。 手动的方…

AppTask.moveToFront()源码分析

ActivityManager.AppTask.moveToFront()执行后&#xff0c;导致其他AppTask退到了后台&#xff0c;点击返回直接回到了桌面&#xff08;HomeScreen&#xff09;&#xff0c;没有回到上一个AppTask。 下面分析一下源码看看为什么其他AppTask退到了后台&#xff0c;如何解决该问题…

提升网络安全的关键利器:EventLog Analyzer

导语&#xff1a; 随着网络攻击和数据泄露事件的不断增加&#xff0c;企业对于网络安全的关注度也日益提高。在这样的背景下&#xff0c;安全信息与事件管理系统&#xff08;SIEM&#xff09;成为了提升网络安全的关键利器之一。本文将重点介绍一款强大的SIEM工具——EventLog…

MySQL 日期与时间函数

一、获取日期、时间 函数用法CURDATE()&#xff0c;CURRENT_DATE()返回当前日期&#xff0c;只包含年、月、日CURTIME() &#xff0c; CURRENT_TIME()返回当前时间&#xff0c;只包含时、分、秒NOW() &#xff0c; SYSDATE()&#xff0c;CURRENT_TIMESTAMP() &#xff0c; LOC…

漂白剂哪种好:

市面上的漂白剂主要分为「液体状」&#xff08;漂白水&#xff09;及「粉末状」&#xff08;漂白粉或漂白素&#xff09;两大类&#xff0c;那么这两大类漂白剂漂白剂哪种好呢&#xff1f;其实不同品牌之间的配方又各有特色&#xff0c;必须配合想清洁的污垢种类选择适当的配方…