目录
一、砍死怪兽的概率问题
二、组成aim的最少货币数
三、求N值的裂开方法数
给定3个参数,N,M,K
怪兽有N滴血,等着英雄来砍自己
英雄每一次打击,都会让怪兽流失[0~M]的血量
到底流失多少?每一次在[0~M]上等概率的获得一个值
求K次打击之后,英雄把怪兽砍死的概率
package class22;
/**
* 给定3个参数,N,M,K
* 怪兽有N滴血,等着英雄来砍自己
* 英雄每一次打击,都会让怪兽流失[0~M]的血量
* 到底流失多少?每一次在[0~M]上等概率的获得一个值
* 求K次打击之后,英雄把怪兽砍死的概率
*/
public class KillMonster {
//方法一: 暴力递归
public static double right(int N, int M, int K) {
//边界的判断 小于1都是异常入参 直接返回0
if(N < 1 || M < 1 || K < 1) return 0;
//求得是砍死怪兽得概率,我们可以先分析总共得砍法数
//每次打击时 0-M得等概率一个值 也就是有 M+1种 ,共K次 每次就是M+1种 那么总方法数就是 (M+1)^ k次方
long all = (long) Math.pow(M+1,K);
//递归函数 砍K次 每次砍掉 0-M的等概率血量 血量为N 返回砍死的方法次数
long kill = process(K,M,N);
//返回砍死方法数/ 全部的方法数 就得到了一个砍死概率
return (double)kill/ (double) all;
}
/**
* 递归 剩余rest 次砍, 每次砍0-m血量, 剩余血量hp 返回砍死的方法数
* @param rest
* @param m
* @param hp
* @return
*/
public static long process(int rest, int m, int hp){
//base case: 剩余打击次数为0时 看看当前的血量是否是小于等于0 是则表示已经砍死怪兽 返回1种方法 否则就是0种
if(rest == 0){
return hp <=0 ? 1 : 0;
}
//base case: 剩余打击次数还有剩余,如果当前血量已经是小于等于0了 表示已经砍死怪兽了 后续的都是砍死的方法数
//加入剩余次数rest = 2 那么1次砍 会有0-m的血量流失 有m+1种 第2次砍 也会有0-m的血量流失 有m+1种 所以方法数就是 (m+1)^2 种
if(hp <= 0){
return (long) Math.pow(m+1,rest);
}
//接着开始分析常规情况 定义变量保存结果方法数
long ways = 0;
//剩下rest次打击, 每次都是 0-m的血量流失 遍历m+1种
for(int i = 0; i <= m; i++){
//累加方法数 步数-1 , 流失m的血量固定值 , 血量剩下 hp-i
ways += process(rest-1,m,hp-i);
}
//最后返回全部方法数
return ways;
}
//方法二:动态规划
public static double dp1(int N, int M, int K){
//边界的判断 小于1都是异常入参 直接返回0
if(N < 1 || M < 1 || K < 1) return 0;
//求得是砍死怪兽得概率,我们可以先分析总共得砍法数
//每次打击时 0-M得等概率一个值 也就是有 M+1种 ,共K次 每次就是M+1种 那么总方法数就是 (M+1)^ k次方
long all = (long) Math.pow(M+1,K);
//分析递归的变量 有K 剩余打击数 范围0-K N 剩余血量 范围是 负数 N 负数范围可不考虑 则为0 N
long[][] dp = new long[K+1][N+1]; // 行对应 打击数 列对应 血量
//base case: 剩余打击次数为0时 看看当前的血量是否是小于等于0 是则表示已经砍死怪兽 返回1种方法 否则就是0种
dp[0][0] = 1; //第0行已经处理好
//rest次数 依赖 rest-1 也就是 第1行依赖第0行 第2行依赖第1行.. 下一行依赖上一行 目前已经完善第0行 接着往下填充赋值
//剩下rest次打击, 1-m的血量流失情况
//血量为0 且打击数大于0 的情况 前面base case已经分析完善 所以血量从1开始
for(int rest = 1; rest <= K; rest++){
//base case: 剩余打击次数还有剩余,如果当前血量已经是小于等于0了 表示已经砍死怪兽了 后续的都是砍死的方法数
//加入剩余次数rest = 2 那么1次砍 会有0-m的血量流失 有m+1种 第2次砍 也会有0-m的血量流失 有m+1种 所以方法数就是 (m+1)^2 种
dp[rest][0] = (long)Math.pow(M+1,rest);
for(int hp = 1; hp <= N; hp++){
//从第一行 每行的第一行开始 跳过了第0列 第0行 base case已经处理
long ways = 0;
for(int i = 0; i <= M;i++){
//填充每个格子的方法数 需要枚举循环 0-m的血量流失的情况累加
if((hp - i) >= 0){
//注意这里 列值 也就是剩余血量 可能会存在被流失到负数 而我们dp数组边界是从0 开始 如果是大于等于0 则不越界 那么累加就按依赖进行累加
ways += dp[rest-1][hp-i];
}else{
//如果 hp-i 剩余血量 减到了负数 比如 当前血量是 2 剩余打击数 1 m假设比较大5 那么可能会是5的情况 2-5= -3 就是负数情况
//我们可以转换考虑,如果是负数的血量 那么后续的打击次数 肯定都是符合情况的 砍死情况 那么剩下的打击数 就是rest -1 次 后续的方法数就是 (m+1)^ rest-1 种
ways += (long) Math.pow(M+1,rest-1);
}
}
//枚举情况累加完之后 就把值赋给对应dp的位置
dp[rest][hp] = ways;
}
}
//递归调用是从 K次打击数 N血量开始 所以对应就返回dp位置的方法数 除以 总的方法数得到砍死概率
return (double)dp[K][N]/ (double) all;
}
//方法三:动态规划 优化枚举的时间复杂度 转成成常数时间处理每个单元格的方法数
public static double dp2(int N, int M, int K){
//边界的判断 小于1都是异常入参 直接返回0
if(N < 1 || M < 1 || K < 1) return 0;
//求得是砍死怪兽得概率,我们可以先分析总共得砍法数
//每次打击时 0-M得等概率一个值 也就是有 M+1种 ,共K次 每次就是M+1种 那么总方法数就是 (M+1)^ k次方
long all = (long) Math.pow(M+1,K);
//分析递归的变量 有K 剩余打击数 范围0-K N 剩余血量 范围是 负数 N 负数范围可不考虑 则为0 N
long[][] dp = new long[K+1][N+1]; // 行对应 打击数 列对应 血量
//base case: 剩余打击次数为0时 看看当前的血量是否是小于等于0 是则表示已经砍死怪兽 返回1种方法 否则就是0种
dp[0][0] = 1; //第0行已经处理好
//rest次数 依赖 rest-1 也就是 第1行依赖第0行 第2行依赖第1行.. 下一行依赖上一行 目前已经完善第0行 接着往下填充赋值
//剩下rest次打击, 1-m的血量流失情况
//血量为0 且打击数大于0 的情况 前面base case已经分析完善 所以血量从1开始
for(int rest = 1; rest <= K; rest++){
//base case: 剩余打击次数还有剩余,如果当前血量已经是小于等于0了 表示已经砍死怪兽了 后续的都是砍死的方法数
//加入剩余次数rest = 2 那么1次砍 会有0-m的血量流失 有m+1种 第2次砍 也会有0-m的血量流失 有m+1种 所以方法数就是 (m+1)^2 种
dp[rest][0] = (long) Math.pow(M+1,rest);
for(int hp = 1; hp <= N; hp++){
//从第一行 每行的第一行开始 跳过了第0列 第0行 base case已经处理
//根据前面动态规划的推算。 假设dp[3][4] 剩余3次打击 4血量 M=3
//dp[3][4] 依赖于m+1中丢失血量0,1,2,3 四种情况累加
//dp[3][4] = dp[2][4] + dp[2][3] + dp[2][2] + dp[2][1]
//同理 判断dp[3][5] 依赖于m+1中丢失血量0,1,2,3 四种情况累加
//dp[3][5] =dp[2][5] + dp[2][4] + dp[2][3] + dp[2][2]
//得到dp[3][5] 其实可以通过dp[3][4] 常数个值来转换得到 比dp[3][4]少了 dp[2][1] 多了dp[2][5] 那么少了就- 多了就+
//dp[3][5] = dp[3][4] - dp[2][1] + dp[2][5] 而这里注意了dp[2][1] 是有可能越界的 如果M比较大,那么多的这个值就不能直接减去 否则会越界异常 越界说明剩下小于0的血量了 都是符合砍死的方法数 直接返回(m+1)^剩余次数-1
//我们可以先赋值左位置的依赖以及加上该位置下位置[rest-1][hp]。 目前是不会越界的 rest-1>=0 hp-1>=0
dp[rest][hp] = dp[rest][hp-1] + dp[rest-1][hp];
//而要减去左位置的值dp[rest][hp-1] 多了最后一个值 dp[rest-1][hp-1-M] 可能存在越界
// dp[2][1] 就是等于dp[2][5 - 1- 3 ] = dp[2][1] 这么转换来的
if((hp - 1 - M)>=0){
//不越界 就说明在dp数组中直接减
dp[rest][hp] -= dp[rest-1][hp-1-M];
}else{
//越界了 就说明血量小于0 依赖的是rest-1次打击 每次 0-m血量丢失 m+1种 所以方法数就是(m+1)^rest-1
dp[rest][hp] -= Math.pow(M+1,rest-1);
}
}
}
//递归调用是从 K次打击数 N血量开始 所以对应就返回dp位置的方法数 除以 总的方法数得到砍死概率
return (double)dp[K][N]/ (double) all;
}
public static void main(String[] args) {
int NMax = 10;
int MMax = 10;
int KMax = 10;
int testTime = 200;
System.out.println("测试开始");
for (int i = 0; i < testTime; i++) {
int N = (int) (Math.random() * NMax);
int M = (int) (Math.random() * MMax);
int K = (int) (Math.random() * KMax);
double ans1 = right(N, M, K);
double ans2 = dp1(N, M, K);
double ans3 = dp2(N, M, K);
if (ans1 != ans2 || ans1 != ans3) {
System.out.println("Oops!");
break;
}
}
System.out.println("测试结束");
}
}
arr是面值数组,其中的值都是正数且没有重复。再给定一个正数aim。
每个值都认为是一种面值,且认为张数是无限的。
返回组成aim的最少货币数
package class22;
/**
* arr是面值数组,其中的值都是正数且没有重复。再给定一个正数aim。
* 每个值都认为是一种面值,且认为张数是无限的。
* 返回组成aim的最少货币数
*/
public class MinCoinsNoLimit {
//方法一: 暴力递归
public static int minCoins(int[] arr, int aim) {
//调用递归 arr面值数组 从第一个面值开始 0索引 组成aim 返回最少货币数 如果没有就返回整形最大值
return process(arr, 0, aim);
}
/**
* 返回 当前面值数组 arr[index...] 组成剩余目标值rest的最少货币数
*
* @param arr
* @param index
* @param rest
* @return
*/
public static int process(int[] arr, int index, int rest) {
//base case: 当面值越界 没有面值的时候 如果rest所剩余是0 说明不需要货币数了返回0 否则就是返回最大值
if (index == arr.length) {
return rest == 0 ? 0 : Integer.MAX_VALUE;
}
//定义一个辅助遍历 保存每个面值的各种选择 取较小值 初始化为最大值
int ways = Integer.MAX_VALUE;
//每个面值都是无限张 从0...开始多种尝试 张数*面值要小于等于目标值rest
for (int count = 0; count * arr[index] <= rest; count++) {
//当前arr[i] 取0,1,2...张对应的下个面值arr[i+1]的各种情况的较小组成张数
//下个面值来到index+1位置 剩余目标值是 rest - 张数*当前面值
int next = process(arr, index + 1, rest - count * arr[index]);
if (next != Integer.MAX_VALUE) {
//需要先判断 后面的面值最小张数是否存在 存在的话就不是整形最大值,那么就进行比较
//当前结果是ways 跟 next的最小张数 加上 当前index选择的张数 count 就是当前新的最少张数
ways = Math.min(ways, next + count);
}
}
return ways;
}
//方法二: 动态规划
public static int dp1(int[] arr, int aim) {
//递归分析 可变参数 index 索引 0,arr.length aim目标值 0-aim
int n = arr.length;
int[][] dp = new int[n + 1][aim + 1];
//base case: 当面值越界 没有面值的时候 如果rest所剩余是0 说明不需要货币数了返回0 否则就是返回最大值
for (int rest = 1; rest <= aim; rest++) {
dp[n][rest] = Integer.MAX_VALUE; //越界index等于n 没有面值 如果剩余目标值还大于0 就是返回最大值
}
//分析常规情况 递归依赖 index 依赖于 index+1 最后一行n已经填充好 从n-1行往上填充每一行
for (int index = n - 1; index >= 0; index--) {
//每一行从左往右遍历到aim最后一列
for (int rest = 0; rest <= aim; rest++) {
//定义一个辅助遍历 保存每个面值的各种选择 取较小值 初始化为最大值
int ways = Integer.MAX_VALUE;
//每个面值都是无限张 从0...开始多种尝试 张数*面值要小于等于目标值rest
for (int count = 0; count * arr[index] <= rest; count++) {
//当前arr[i] 取0,1,2...张对应的下个面值arr[i+1]的各种情况的较小组成张数
//下个面值来到index+1位置 剩余目标值是 rest - 张数*当前面值
int next = dp[index + 1][rest - count * arr[index]];
if (next != Integer.MAX_VALUE) {
//需要先判断 后面的面值最小张数是否存在 存在的话就不是整形最大值,那么就进行比较
//当前结果是ways 跟 next的最小张数 加上 当前index选择的张数 count 就是当前新的最少张数
ways = Math.min(ways, next + count);
}
}
dp[index][rest] = ways;
}
}
return dp[0][aim]; //根据递归函数 从第0个面值开始 aim目标值 返回对应位置
}
//方法三: 动态规划 优化其中每个位置枚举行为 用相邻位置替换法 处理成常数位置求值
public static int dp2(int[] arr, int aim) {
//递归分析 可变参数 index 索引 0,arr.length aim目标值 0-aim
int n = arr.length;
int[][] dp = new int[n + 1][aim + 1];
//base case: 当面值越界 没有面值的时候 如果rest所剩余是0 说明不需要货币数了返回0 否则就是返回最大值
for (int rest = 1; rest <= aim; rest++) {
dp[n][rest] = Integer.MAX_VALUE; //越界index等于n 没有面值 如果剩余目标值还大于0 就是返回最大值
}
//分析常规情况 递归依赖 index 依赖于 index+1 最后一行n已经填充好 从n-1行往上填充每一行
for (int index = n - 1; index >= 0; index--) {
//每一行从左往右遍历到aim最后一列
for (int rest = 0; rest <= aim; rest++) {
//画图找位置。 递归中 可以得到是[index][rest] 依赖下一行的多个值。同理[index][rest - arr[index]] 也是依赖下一行多个值
//dp[3][14] arr[3]面值假设是3 目标值14 依赖
//0张 dp[3+1][14 - 0* arr[3]]= dp[4][14]
//1张 dp[4][11]
//2张 dp[4][8]
//3张 dp[4][5]
//4张 dp[4][2] ..再往下就越界了 0-4张 依赖这5个位置的最小值表示最少张数
//对于dp[3][11] 依赖
//0张 dp[4][11]
//1张 dp[4][8]
//2张 dp[4][5]
//3张 dp[4][2] ..再往下就越界了 0-3张 依赖着4个位置的最小值 表示最少张数
//这里注意转换时候张数问题 dp[3][14] 想用到 dp[3][11] 对应 11 8 5 2 位置都一致 但张数都比dp[3][14]少1张 所以需要+1 少了dp[4][14] 要再进行比较大小
//dp[3][14] = 依赖每个位置的最小值 而dp[3][11]的值 需要+1张 就对应上dp[3][14]的公共位置张数 再跟下位置比较最小
//先赋值下位置的值 待会再跟左位置的比较大小
dp[index][rest] = dp[index+1][rest];
if((rest-arr[index])>=0 && dp[index][rest - arr[index]] != Integer.MAX_VALUE){
//左边列rest-arr[index]需要不越界 并且这个左边位置值应该不是整形最小值 才加入与 下位置进行比较大小, 注意要加上1
// 前面的分析结论 就是该位置与当前位置的公共依赖是少了1张的 否则就没必要比较了
dp[index][rest] = Math.min(dp[index][rest],dp[index][rest-arr[index]]+1);
}
}
}
return dp[0][aim]; //根据递归函数 从第0个面值开始 aim目标值 返回对应位置
}
// 为了测试
public static int[] randomArray(int maxLen, int maxValue) {
int N = (int) (Math.random() * maxLen);
int[] arr = new int[N];
boolean[] has = new boolean[maxValue + 1];
for (int i = 0; i < N; i++) {
do {
arr[i] = (int) (Math.random() * maxValue) + 1;
} while (has[arr[i]]);
has[arr[i]] = true;
}
return arr;
}
// 为了测试
public static void printArray(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
// 为了测试
public static void main(String[] args) {
int maxLen = 20;
int maxValue = 30;
int testTime = 300000;
System.out.println("功能测试开始");
for (int i = 0; i < testTime; i++) {
int N = (int) (Math.random() * maxLen);
int[] arr = randomArray(N, maxValue);
int aim = (int) (Math.random() * maxValue);
int ans1 = minCoins(arr, aim);
int ans2 = dp1(arr, aim);
int ans3 = dp2(arr, aim);
if (ans1 != ans2 || ans1 != ans3) {
System.out.println("Oops!");
printArray(arr);
System.out.println(aim);
System.out.println(ans1);
System.out.println(ans2);
break;
}
}
System.out.println("功能测试结束");
}
}
给定一个正数n,求n的裂开方法数,
规定:后面的数不能比前面的数小
比如4的裂开方法有:
1+1+1+1、1+1+2、1+3、2+2、4
5种,所以返回5
package class22;
/**
* 给定一个正数n,求n的裂开方法数,
* 规定:后面的数不能比前面的数小
* 比如4的裂开方法有:
* 1+1+1+1、1+1+2、1+3、2+2、4
* 5种,所以返回5
*/
public class SplitNumber {
//方法一: 暴力递归
// n为正数
public static int ways(int n) {
//边界判断 小于0 返回0 无效
if(n < 0) return 0;
//等于1时 拆分就只能是自己 返回1种
if(n == 1) return 1;
//调用递归函数 根据题意 后面值大于等于前面 所以函数定义一个 前置值 目标值n
return process(1, n);
}
//递归函数 前面已经选择了pre 目前剩下rest需要拆分 返回裂开方法数
public static int process(int pre, int rest){
if(rest == 0) return 1; //剩余需拆分为0 说明前面都已经拆分好了返回1
//base case: 如果前置数 大于 剩余的需要拆分值 那么肯定该方法是无效的 不符合题意 后面大于等于前面
if(pre > rest) return 0;
//分析常规情况 pre < rest时
//rest可以如何拆分: 确保后面大于等于前面 所以rest拆分的最小值就是从pre开始 到 rest
int ways= 0;
for(int i = pre; i <= rest;i++){
ways += process(i,rest-i);
}
return ways;
}
//方法二: 动态规划
public static int dp1(int n){
//边界判断 小于0 返回0 无效
if(n < 0) return 0;
//等于1时 拆分就只能是自己 返回1种
if(n == 1) return 1;
//分析递归可变参数 pre前置值 范围是1-n 剩余后续需拆分值rest 范围0-n
int[][] dp = new int[n+1][n+1]; //pre前置表示行,0行不涉及不需要处理 目标值rest表示列
for(int i = 1; i <= n; i++){
dp[i][0] = 1; //base case 剩余需拆分为0 说明前面都已经拆分好了返回1 第一列为0
//对角线base case dp[i][i]依赖 dp[i][rest-i] 此时rest=i 得到依赖dp[i][0] 也就是前面赋值得第一列得值 所以直接初始化1
dp[i][i] = 1;
}
//base case: 如果前置数 大于 剩余的需要拆分值 那么肯定该方法是无效的 不符合题意 后面大于等于前面
//也就是 dp[pre][rest] pre>rest 数组的下三角区 位置都是0 默认值就是0 无需处理
//分析依赖 rest 依赖 rest-pre 也就是右边列依赖左边列
//比如 dp[3][6]位置 前置拆分3 剩余6 依赖哪些位置: 6拆 1-5 2-4 3-3 4-2 5-1 6-0 其中前置3比1 2 大 所以不符合
// 所以依赖 dp[3][3] dp[4][2] dp[5][1] dp[6][0] 仔细发现这个规律 是dp[3][6]左边 减去前置值3的列数位置dp[3][3]开始往左下位置直到不能拆分的位置
//由于前面的初始化 我们已经填充好了矩阵的下三角区 根据某个位置的依赖情况 依赖于下一行 左边列 那么我们就可以从最底部的位置开始出发 这些依赖都是有填充好值的了
//目前是dp[n-1][n]右下角位置 开始填充。所以从n-1行往上填充 每一行都已经填充到了dp[pre][pre]对角线位置 所以列就是从pre+1开始填充
for(int pre = n-1; pre >=0; pre--){ //从n-1行往上填充
for(int rest = pre+1;rest <= n; rest++){ //列就是从pre+1开始填充
//列 第一列开始
//分析常规情况 pre < rest时
//rest可以如何拆分: 确保后面大于等于前面 所以rest拆分的最小值就是从pre开始 到 rest
int ways= 0;
for(int i = pre; i <= rest;i++){
ways += dp[i][rest-i];
}
dp[pre][rest] = ways;
}
}
return dp[1][n]; //递归主程序调用 是前置1 目标数n 对于返回数组位置
}
//方法三: 动态规划 优化单个位置的枚举行为 通过相邻位置法 找到可以替换的具体有限个数
public static int dp2(int n){
//边界判断 小于0 返回0 无效
if(n < 0) return 0;
//等于1时 拆分就只能是自己 返回1种
if(n == 1) return 1;
//分析递归可变参数 pre前置值 范围是1-n 剩余后续需拆分值rest 范围0-n
int[][] dp = new int[n+1][n+1]; //pre前置表示行,0行不涉及不需要处理 目标值rest表示列
for(int i = 1; i <= n; i++){
dp[i][0] = 1; //base case 剩余需拆分为0 说明前面都已经拆分好了返回1 第一列为0
//对角线base case dp[i][i]依赖 dp[i][rest-i] 此时rest=i 得到依赖dp[i][0] 也就是前面赋值得第一列得值 所以直接初始化1
dp[i][i] = 1;
}
//base case: 如果前置数 大于 剩余的需要拆分值 那么肯定该方法是无效的 不符合题意 后面大于等于前面
//也就是 dp[pre][rest] pre>rest 数组的下三角区 位置都是0 默认值就是0 无需处理
//分析依赖 rest 依赖 rest-pre 也就是右边列依赖左边列
//比如 dp[3][6]位置 前置拆分3 剩余6 依赖哪些位置: 6拆 1-5 2-4 3-3 4-2 5-1 6-0 其中前置3比1 2 大 所以不符合
// 所以依赖 dp[3][3] dp[4][2] dp[5][1] dp[6][0] 仔细发现这个规律 是dp[3][6]左边 减去前置值3的列数位置dp[3][3]开始往左下位置直到不能拆分的位置
//由于前面的初始化 我们已经填充好了矩阵的下三角区 根据某个位置的依赖情况 依赖于下一行 左边列 那么我们就可以从最底部的位置开始出发 这些依赖都是有填充好值的了
//目前是dp[n-1][n]右下角位置 开始填充。所以从n-1行往上填充 每一行都已经填充到了dp[pre][pre]对角线位置 所以列就是从pre+1开始填充
for(int pre = n-1; pre >=0; pre--){ //从n-1行往上填充
for(int rest = pre+1;rest <= n; rest++){ //列就是从pre+1开始填充
//已知前面dp[3][6] 依赖累加的位置是 dp[3][3] dp[4][2] dp[5][1] dp[6][0]
//通过尝试邻近位置发现在下位置 dp[4][6] 依赖值有公共位置 累加依赖位置 dp[4][2] dp[5][1] dp[6][0] 比dp[3][6]少一个dp[3][3]
// 左边 拆分与前置值等大数 dp[3][6] 6拆分下个值是从3 开始 所以就是 6-3 来到的左边该位置 + 下位置的值
dp[pre][rest] = dp[pre][rest-pre] + dp[pre+1][rest];
}
}
return dp[1][n]; //递归主程序调用 是前置1 目标数n 对于返回数组位置
}
public static void main(String[] args) {
int test = 39;
System.out.println(ways(test));
System.out.println(dp1(test));
System.out.println(dp2(test));
}
}