什么是动态规划
已经走过的分支,用一张表记着,下次遇到相同的分支,直接从表中拿值,不走重复的支路。
比如斐波那契数列,求f(n),则要计算f(n-1),f(n-2),而计算f(n-1)时,则要计算f(n-2),f(n-3),当计算完f(n-1),回到f(n)的计算,去计算f(n-2),其实前面已经算过,如果用一张表记着,所以这里就不用重复算。
1、假设有排成一行的N个位置,记为1~N,N 一定大于或等于 2,开始时机器人在其中的M位置上(M 一定是 1~N 中的一个)。如果机器人来到1位置,那么下一步只能往右来到2位置;如果机器人来到N位置,那么下一步只能往左来到 N-1 位置;如果机器人来到中间位置,那么下一步可以往左走或者往右走;规定机器人必须走 K 步,最终能来到P位置(P也是1~N中的一个)的方法有多少种?
/**
* 假设有排成一行的N个位置,记为1~N,N 一定大于或等于 2,
* 开始时机器人在其中的M位置上(M 一定是 1~N 中的一个)。
* 如果机器人来到1位置,那么下一步只能往右来到2位置;
* 如果机器人来到N位置,那么下一步只能往左来到 N-1 位置;
* 如果机器人来到中间位置,那么下一步可以往左走或者往右走;
* 规定机器人必须走 K 步,最终能来到P位置(P也是1~N中的一个)的方法有多少种?
* 给定四个参数 N、M、K、P,返回方法数。
* Created by huangjunyi on 2022/9/3.
*/
public class DynamicProgramming01 {
/**
* 暴力递归求解
* @param n 总位置数
* @param m 初始化位置
* @param k 可走步数
* @param p 目标位置
* @return
*/
public static int method01(int n, int m, int k, int p) {
// 不合法参数判断
if (n < 2 || k < 1 || m < 1 || m > n || p < 1 || p > n) return 0;
return processo1(n, m, k, p);
}
/**
* 暴力递归求解
* @param n 总位置数
* @param curr 当前位置
* @param remain 剩余可走步数
* @param p 目标位置
* @return
*/
private static int processo1(int n, int curr, int remain, int p) {
// base case:还剩0步要走,如果当前位置curr在目标位置p,则发现一种方法
if (remain == 0) return curr == p ? 1 : 0;
// 1位置,只能往右走
if (curr == 1) return processo1(n, 2, remain - 1, p);
// n位置,只能往左走
if (curr == n) return processo1(n, n - 1, remain - 1, p);
// 两个递归 往左走,往右走 返回的方法数累加
return processo1(n, n - 1, remain - 1, p) + processo1(n, n + 1, remain - 1, p);
}
/**
* 粗糙的动态规划,记忆化搜索
* @param n 总位置数
* @param m 初始化位置
* @param k 可走步数
* @param p 目标位置
* @return
*/
public static int method02(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[n + 1][k + 1];
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= k; k++) {
dp[i][j] = -1;
}
}
return processo2(n, m, k, p, dp);
}
/**
* 粗糙的动态规划,记忆化搜索
* @param n 总位置数
* @param curr 当前位置
* @param remain 剩余可走步数
* @param p 目标位置
* @param dp 记忆表
* @return
*/
private static int processo2(int n, int curr, int remain, int p, int[][] dp) {
// 如果缓存中有值,直接拿值,返回
if (dp[curr][remain] != -1) return dp[curr][remain];
if (remain == 0) {
dp[curr][remain] = curr == p ? 1 : 0;
return dp[curr][remain];
}
if (curr == 1) {
// 返回得到的值前,先存缓存表
dp[curr][remain] = processo2(n, 2, remain - 1, p, dp);
return dp[curr][remain];
}
if (curr == n) {
// 返回得到的值前,先存缓存表
dp[curr][remain] = processo2(n, n - 1, remain - 1, p, dp);
return dp[curr][remain];
}
// 返回得到的值前,先存缓存表
dp[curr][remain] = processo2(n, curr - 1, remain - 1, p, dp) + processo2(n, curr + 1, remain - 1, p, dp);
return dp[curr][remain];
}
/**
* 动态规划求解
* @param n 总位置数
* @param m 初始化位置
* @param k 可走步数
* @param p 目标位置
* @return
*/
public static int method03(int n, int m, int k, int p) {
// 不合法参数判断
if (n < 2 || k < 1 || m < 1 || m > n || p < 1 || p > n) return 0;
/*
dp表
dp[i][j]
表示当前在i位置
表示还剩j步要走
*/
int[][] dp = new int[n + 1][k + 1];
/*for (int i = 0; i <= n; i++) {
dp[i][0] = 0;
}
for (int j = 0; j <= k; k++) {
dp[0][j] = 0;
}*/
// 根据暴力递归的base case,初始化
dp[p][0] = 1;
/*
根据暴力递归的依赖关系,填表
dp[1][j]依赖dp[2][j-1]
dp[n][j]依赖dp[n-1][j-1]
dp[i][j]依赖dp[i-1][j-1]和dp[i+1][j-1]
*/
for (int remain = 1; remain <= k; remain++) {
for (int curr = 1; curr <= n; curr++) {
if (curr == 1) {
dp[curr][remain] = dp[2][remain - 1];
} else if (curr == n) {
dp[curr][remain] = dp[n - 1][remain - 1];
} else {
dp[curr][remain] = dp[curr + 1][remain - 1] + dp[curr - 1][remain - 1];
}
}
}
return dp[m][k];
}
}
2、背包问题(动态规划求解)
/**
* 背包问题(动态规划求解)
* Created by huangjunyi on 2022/9/3.
*/
public class DynamicProgramming02 {
public static int processByRecursive(int[] weights, int[] values, int bag) {
return byRecursive(weights, values, 0, bag);
}
/**
* 暴力递归
* @param weights 物品总量
* @param values 物品价值
* @param index 当前物品下标
* @param space 背包剩余空间
* @return
*/
private static int byRecursive(int[] weights, int[] values, int index, int space) {
if (space < 0 || index == weights.length) return 0;
// 当前位置物品不要,直接下一轮递归
int p1 = byRecursive(weights, values, index + 1, space);
int p2 = Integer.MIN_VALUE;
if (space >= weights[index]) {
//如果背包还有空间放得下当前物品,参数装入当前物品,进行下一轮递归
p2 = values[index] + byRecursive(weights, values, index + 1, space - weights[index]);
}
//递归返回,总p1,p2中取最大值
return Math.max(p1, p2);
}
/**
* 动态规划
* @param weights 物品总量
* @param values 物品价值
* @param bag 背包能承载的重量
* @return
*/
public static int processByDynamicProgramming(int[] weights, int[] values, int bag) {
int[][] dp = new int[values.length + 1][bag + 1];
for (int index = values.length - 1; index >= 0; index--) {
for (int remain = 0; remain <= bag; remain--) {
// 当前位置物品不要,直接下一轮递归
int p1 = dp[index + 1][remain];
int p2 = Integer.MIN_VALUE;
if (remain >= weights[index]) {
//如果背包还有空间放得下当前物品,参数装入当前物品,进行下一轮递归
p2 = values[index] + dp[index + 1][remain - weights[index]];
}
dp[index][remain] = Math.max(p1, p2);
}
}
return dp[0][bag];
}
}
3、给定一个只由数字组成的字符串,1对应A,2对应B…,将字符串的数据转化为字母,如11,可以转化为AA,也可以转化为K,问:有多少种转化结果
/**
* 给定一个只由数字组成的字符串,1对应A,2对应B...
* 将字符串的数据转化为字母,如11,可以转化为AA,也可以转化为K
* 问:有多少种转化结果
*/
public class DynamicProgramming03 {
public static int getThransferCountByRecursive(String str) {
char[] chars = str.toCharArray();
return process(chars, 0);
}
/**
* 暴力递归
* @param chars 数字字符数组
* @param i 当前位置
* @return
*/
private static int process(char[] chars, int i) {
if (i == chars.length) return 1;
int res = 0;
// 如当前位置字符是0,代表之前做的决定有问题
if (chars[i] == '0') return res;
res += process(chars, i + 1);
//当前数字位1,则有两个分支:1.只把当前数字转换为字母,2.把当前数字和下一位合在一起转换为字母
if (chars[i] == '1') {
if (i + 1 < chars.length) {
res += process(chars, i + 2);
}
}
//当前数字位2,只有在下一位数字是0~6之间,才有两个分支
if (chars[i] == '2') {
if (i + 1 < chars.length && chars[i + 1] >='0' && chars[i + 1] <= '6') {
res += process(chars, i + 2);
}
}
//返回累加结果
return res;
}
/**
* 动态规划
* @param str
* @return
*/
private static int getThransferCountByDp(String str) {
char[] chars = str.toCharArray();
// 因为暴力递归只有一个变化参数,所以改成动态规划后就以一位dp表
int[] dp = new int[chars.length + 1];
dp[chars.length] = 1;
for (int i = chars.length - 1; i >= 0; i--) {
int res = 0;
// 如当前位置字符是0,代表之前做的决定有问题
if (chars[i] == '0') {
dp[i] = 0;
continue;
}
//当前数字位1,则有两个分支:1.只把当前数字转换为字母,2.把当前数字和下一位合在一起转换为字母
res += dp[i + 1];
if (chars[i] == '1') {
if (i + 1 < chars.length) {
res += dp[i + 2];
}
}
//当前数字位2,只有在下一位数字是0~6之间,才有两个分支
if (chars[i] == '2') {
if (i + 1 < chars.length && chars[i + 1] >='0' && chars[i + 1] <= '6') {
res += dp[i + 2];
}
}
dp[i] = res;
}
return dp[0];
}
public static void main(String[] args) {
System.out.println(getThransferCountByRecursive("11111"));
System.out.println(getThransferCountByDp("11111"));
}
}
4、给定一个整形数组,代表一串纸牌,有A、B两个玩家,从中抽取纸牌,但是每次只能拿最左或者最右,假设两个玩家绝顶聪明,求最后获胜方的结果值
比如有一串纸牌50、100、20、10
因为两个玩家都绝顶聪明
先手玩家如果拿了50,后手玩家就能拿100,那么他就输了,所以他不会拿50,他会先拿10
然后剩下50、100、20
然后后手玩家看,无论拿最左还是最右的牌,他都会输,所以他只能去拿较大的牌,
然后先手玩家拿走100
所以先手玩家是必赢的
因此返回获胜方是先手玩家
假设这一串纸牌非常长,返回最后获胜的是谁
/**
* 给定一个整形数组,代表一串纸牌
* 有A、B两个玩家,从中抽取纸牌,但是每次只能拿最左或者最右
* 假设两个玩家绝顶聪明(博弈)
* 求最后获胜方的结果值
* Created by huangjunyi on 2022/9/3.
*/
public class DynamicProgramming04 {
/**
* 先手拿牌
* @param values
* @param left
* @param right
* @return
*/
public static int first(int[] values, int left, int right) {
// 只剩下一张牌,先手拿走
if (left == right) return values[left];
// 因为先手决定聪明,会拿走最大的分数
return Math.max(
// 拿走最左侧牌,然后先手就转为后手了,调后手函数,获得剩下的分数
values[left] + second(values, left + 1, right),
// 拿走最右侧牌,然后先手就转为后手了,调后手函数,获得剩下的分数
values[right] + second(values, left, right - 1)
);
}
/**
* 后手拿牌
* @param values
* @param left
* @param right
* @return
*/
public static int second(int[] values, int left, int right) {
// 只剩下一张牌,会被先手拿走,后手拿不到
if (left == right) return 0;
// 因为先手决定聪明,会让后手拿到最小的分数
return Math.min(
// 最左侧牌被先手拿走,后手转为先手,在left+1~right上调先手函数做决定
first(values, left + 1, right),
// 最右侧牌被先手拿走,后手转为先手,在left~right-1上调先手函数做决定
first(values, left, right - 1)
);
}
/**
* 暴力递归
* @param values
* @return
*/
public static int getWinValue(int[] values) {
return Math.max(
// 先手玩家获得的分数
first(values, 0, values.length - 1),
// 后手玩家获得的分数
second(values, 0, values.length - 1)
);
}
/**
* 先手拿牌
* @param values
* @param left
* @param right
* @param firstMap
*@param secondMap @return
*/
public static int firstByCacheMap(int[] values, int left, int right, int[][] firstMap, int[][] secondMap) {
// 如果缓存中有值,直接返回缓存中的值
if (firstMap[left][right] != -1) return firstMap[left][right];
int res = 0;
// 只剩下一张牌,先手拿走
if (left == right) res = values[left];
// 因为先手决定聪明,会拿走最大的分数
else res = Math.max(
// 拿走最左侧牌,然后先手就转为后手了,调后手函数,获得剩下的分数
values[left] + secondByCacheMap(values, left + 1, right, firstMap, secondMap),
// 拿走最右侧牌,然后先手就转为后手了,调后手函数,获得剩下的分数
values[right] + secondByCacheMap(values, left, right - 1, firstMap, secondMap)
);
// 记录缓存表
firstMap[left][right] = res;
return res;
}
/**
* 后手拿牌
* @param values
* @param left
* @param right
* @param firstMap
*@param secondMap @return
*/
public static int secondByCacheMap(int[] values, int left, int right, int[][] firstMap, int[][] secondMap) {
// 如果缓存中有值,直接返回缓存中的值
if (secondMap[left][right] != -1) return secondMap[left][right];
int res = 0;
// 只剩下一张牌,会被先手拿走,后手拿不到
if (left == right) res = 0;
// 因为先手决定聪明,会让后手拿到最小的分数
else res = Math.min(
// 最左侧牌被先手拿走,后手转为先手,在left+1~right上调先手函数做决定
firstByCacheMap(values, left + 1, right, firstMap, secondMap),
// 最右侧牌被先手拿走,后手转为先手,在left~right-1上调先手函数做决定
firstByCacheMap(values, left, right - 1, firstMap, secondMap)
);
// 记录缓存表
secondMap[left][right] = res;
return res;
}
/**
* 暴力递归改成记忆化搜索
* @param values
* @return
*/
public static int getWinValueByCacheMap(int[] values) {
// 因为暴力递归中,有两个函数,先后,后手,所以搞两个缓存表
int[][] firstMap = new int[values.length][values.length];
int[][] secondMap = new int[values.length][values.length];
// 初始化缓存表
for (int i = 0; i < values.length; i++) {
for (int j = 0; j < values.length; j++) {
firstMap[i][j] = -1;
secondMap[i][j] = -1;
}
}
return Math.max(
// 先手玩家获得的分数
firstByCacheMap(values, 0, values.length - 1, firstMap, secondMap),
// 后手玩家获得的分数
secondByCacheMap(values, 0, values.length - 1, firstMap, secondMap)
);
}
/**
* 动态规划
* @param values
* @return
*/
public static int getWinValueByDp(int[] values) {
int n = values.length;
// 因为暴力递归中,有两个函数,先后,后手,所以搞两个dp表
int[][] first = new int[n][n];
int[][] second = new int[n][n];
// 根据暴力递归先手函数的base case(if (left == right) return values[left];),初始化first
for (int i = 0; i < n; i++) {
first[i][i] = values[i];
}
/*
根据暴力递归推断dp表的依赖关系
first[i][j]依赖second[i+1][j]和second[i][j-1]
second[i][j]依赖first[i+1][j]和first[i][j-1]
所以依着对角线从上往下填,从左往右填完所有对角线就ok
因为left <= right,所以,左下半部分不需要填
因为0对角线已经初始化了,从1对角线开始
*/
for (int i = 1; i < n; i++) {
int left = 0; // 行
int right = i; // 列
while (left < n - 1 && right < n) {
first[left][right] = Math.max(values[left] + second[left + 1][right], values[right] + second[left][right - 1]);
second[left][right] = Math.min(first[left + 1][right], first[left][right - 1]);
// 依着对角线填,所以行++,列++
left++;
right++;
}
}
return Math.max(first[0][n - 1], second[0][n - 1]);
}
public static void main(String[] args) {
int[] values = {
1,2,5,23,8,3,122,8};
System.out.println(getWinValue(values));
System.out.println(getWinValueByCacheMap(values));
System.out.println(getWinValueByDp(values));
}
}
5、货币组合问题
- 给定一个整形数组arr表示一组不同面值的货币
- 给定一个整形aim,表示需要通过不同的货币拼凑一起到总额aim
- 从arr中取货币,每种货币都可以取若干张
- 求有多少种组合方法
/**
* 货币组合问题
* 给定一个整形数组arr表示一组不同面值的货币
* 给定一个整形aim,表示需要通过不同的货币拼凑一起到总额aim
* 从arr中取货币,每种货币都可以取若干张
* 求有多少种组合方法
* Created by huangjunyi on 2022/9/3.
*/
public class DynamicProgramming05 {
public static int method01(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) return 0;
return process01(arr, 0, aim);
}
/**
* 暴力递归
* @param arr 货币数组
* @param index 当前货币
* @param remain 剩余要组出的总额
* @return
*/
private static int process01(int[] arr, int index, int remain) {
if (index == arr.length) return remain == 0 ? 1 : 0;
int res = 0;
// index位置的货币,用0张、用1张、用2张......
for (int i = 0; i * arr[index] <= remain; i++)
res += process01(arr, index + 1, remain - i * arr[index]);
return res;
}
public static int method02(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) return 0;
// 定义并初始化缓存表
int[][] dp = new int[arr.length + 1][aim + 1];
for (int i = 0; i <= arr.length; i++) for (int j = 0; j <= aim; j++) dp[i][j] = -1;
return process02(arr, 0, aim, dp);
}
/**
* 记忆化搜索
* @param arr 货币数组
* @param index 当前货币
* @param remain 剩余要组出的总额
* @return
*/
private static int process02(int[] arr, int index, int remain, int[][] dp) {
// 命中缓存,返回
if (dp[index][remain] != -1) return dp[index][remain];
if (index == arr.length) return dp[index][remain] = remain == 0 ? 1 : 0;
int res = 0;
// index位置的货币,用0张、用1张、用2张......
for (int i = 0; i * arr[index] <= remain; i++)
res += process02(arr, index + 1, remain - i * arr[index], dp);
// 记录到缓存,然后返回
return dp[index][remain] = res;
}
/**
* 动态规划
* @param arr
* @param aim
* @return
*/
public static int method03(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) return 0;
/*
dp[index][remain]
index往后的货币任意选择,凑够remain元,有几种方法
观察暴力递归可知
dp[index][remain]依赖于index+1行的格子
*/
int[][] dp = new int[arr.length + 1][aim + 1];
// base case:if (index == arr.length) return remain == 0 ? 1 : 0;
dp[arr.length][0] = 1;
for (int index = arr.length - 1; index >= 0; index--)
for (int remain = 0; remain <= aim; remain++)
// index位置的货币,用0张、用1张、用2张......
for (int i = 0; i * arr[index] <= remain; i++)
dp[index][remain] += dp[index + 1][remain- i * arr[index]];
return dp[0][aim];
}
/**
* 动态规划,枚举优化
* @param arr
* @param aim
* @return
*/
public static int method04(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) return 0;
int[][] dp = new int[arr.length + 1][aim + 1];
dp[arr.length][0] = 1;
for (int index = arr.length - 1; index >= 0; index--)
for (int remain = 0; remain <= aim; remain++)
/*
观察:
dp[index][remain] = dp[index + 1][remain] + dp[index + 1][remain - 1 * arr[index]] - dp[index + 1][remain - 2 * arr[index]] - ......
dp[index][remain - 1 * arr[index]] = dp[index + 1][remain - 1 * arr[index]] - dp[index + 1][remain - 2 * arr[index]] - ......
所以:
dp[index][remain] = dp[index + 1][remain] + dp[index][remain - 1 * arr[index]]
因此省去了for循环的枚举行为
*/
dp[index][remain] = remain >= arr[index] ?
dp[index + 1][remain] + dp[index][remain - arr[index]] :
dp[index + 1][remain];
return dp[0][aim];
}
public static void main(String[] args) {
int[] arr = {
5, 10, 50, 100};
int aim = 1000;
System.out.println(method01(arr, aim));
System.out.println(method02(arr, aim));
System.out.println(method03(arr, aim));
System.out.println(method04(arr, aim));
}
}
6、给定一个字符串str,给定一个字符串类型的数组arr,出现的字符都是小写英文。arr每一个字符串,代表一张贴纸,你可以把单个字符剪开使用,目的是拼出str来。返回需要至少多少张贴纸可以完成这个任务。例子:str= “babac”,arr = {“ba”,“c”,“abcd”}。ba + ba + c 3张, abcd + abcd 2张, abcd + ba 2张。所以返回2。
这一题也是先写出暴力递归,就很好改成动态规划,但是只需改出记忆化搜索就可以了,因为它的暴力递归的可变参数,是一个字符串,无法确定范围
/**
* 给定一个字符串str,给定一个字符串类型的数组arr,出现的字符都是小写英文。
* arr每一个字符串,代表一张贴纸,你可以把单个字符剪开使用,目的是拼出str来。返回需要至少多少张贴纸可以完成这个任务。
* 例子:str= "babac",arr = {"ba","c","abcd"}。ba + ba + c 3张, abcd + abcd 2张, abcd + ba 2张。所以返回2。
* Created by huangjunyi on 2022/9/3.
*/
public class DynamicProgramming06 {
/**
* 暴力递归
* @param str 目标字符串
* @param arr 贴纸数组
* @return
*/
public static int getMin01(String str, String[] arr) {
int res = process01(str, arr);
return res == Integer.MAX_VALUE ? -1 : res;
}
/**
* 暴力递归
* @param str 剩余字符串
* @param arr 贴纸数组
* @return
*/
private static int process01(String str, String[] arr) {
// 字符串已经拼好,还需0张贴纸
if (str.length() == 0) return 0;
int min = Integer.MAX_VALUE;
// 尝试每一张贴纸
for (int i = 0; i < arr.length; i++) {
// 减去选择的贴纸的字符,返回新的剩余要拼的字符串
String newStr = getNewStr(str, arr[i]);
// 选择的贴纸没有削去任何一个字符,无效,跳过
if (newStr.length() == str.length()) continue;
// 剩余字符串newStr,往下继续跑递归
min = Math.min(min, process01(newStr, arr));
}
// 算上自己使用的一张贴纸
if (min != Integer.MAX_VALUE) min += 1;
return min;
}
/**
* 原字符串,减去贴纸中的字符,返回新的字符串
* @param s1 原字符串
* @param s2 贴纸
* @return
*/
private static String getNewStr(String s1, String s2) {
char[] chs1 = s1.toCharArray();
char[] chs2 = s2.toCharArray();
int[] count = new int[26];
// 统计原字符串的各种字符数
for (int i = 0; i < chs1.length; i++) {
count[chs1[i] - 'a']++;
}
// 减去贴纸含有的各种字符的字符数
for (int i = 0; i < chs2.length; i++) {
count[chs2[i] - 'a']--;
}
// 拼接处新的剩余字符串
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count.length; i++) {
if (count[i] > 0) {
for (int j = 0; j < count[i]; j++) {
sb.append((char)(i + 'a'));
}
}
}
return sb.toString();
}
public static int getMin(String str, String[] arr) {
/*
map记录每种贴纸的字符出现的个数
例如map[i][0] 表示第i张贴纸,a字符出现的次数(a - a == 0)
初始化号map表
*/
int[][] map = new int[arr.length][26];
for (int i = 0; i < arr.length; i++) {
char[] chars = arr[i].toCharArray();
for (char aChar : chars) {
map[i][aChar - 'a'] += 1;
}
}
// 缓存表,key剩余字符串,value需要的贴纸数,一个一维表,因为暴力递归中只有一个可变参数(剩余字符串)
Map<String, Integer> dp = new HashMap<>();
dp.put("", 0);
return process(str, map, dp);
}
private static int process(String remain, int[][] map, Map<String, Integer> dp) {
// 如果缓存表有值,直接返回
if (dp.containsKey(remain)) return dp.get(remain);
// 统计原字符串的各种字符数
char[] chars = remain.toCharArray();
int[] temp = new int[26];
for (char aChar : chars) {
temp[aChar - 'a'] += 1;
}
int res = Integer.MAX_VALUE;
// 尝试每一张贴纸
for (int i = 0; i < map.length; i++) {
StringBuilder sb = new StringBuilder();
// 优化,只选择能消除掉当前字符串第一个字符的贴纸(因为第一个字符串总要消掉的,先消后消不会影响结果)
if (map[i][chars[0] - 'a'] == 0) continue;
// 减去贴纸含有的各种字符的字符数
for (int j = 0; j < 26; j++) {
for (int k = 0; k < Math.max(0, temp[j] - map[i][j]); k++) {
sb.append((char) (j + 'a'));
}
}
// 剩余字符串newStr,往下继续跑递归
int next = process(sb.toString(), map, dp);
// next + 1 算上自己使用的一张贴纸
if (next != -1) res = Math.min(res, next + 1);
}
// 结果返回前,先放入缓存表
dp.put(remain, res == Integer.MAX_VALUE ? -1 : res);
return dp.get(remain);
}
public static void main(String[] args) {
String str = "babac";
String[] arr = {
"ba","c","abcd"};
System.out.println(getMin01(str, arr));
System.out.println(getMin(str, arr));
}
}
7、求两个字符串的最长公共子序列长度
/**
* 求两个字符串的最长公共子序列长度
* Created by huangjunyi on 2022/9/3.
*/
public class DynamicProgramming07 {
/**
* 暴力递归
* @param str1
* @param str2
* @return
*/
public static int getMaxCommon01(String str1, String str2) {
char[] chars1 = str1.toCharArray();
char[] chars2 = str2.toCharArray();
return process01(chars1, chars2, chars1.length - 1, chars2.length - 1);
}
/**
* 求chars1从0到r1与chars2从0到r2的最长公共子序列长度
* @param chars1
* @param chars2
* @param r1
* @param r2
* @return
*/
private static int process01(char[] chars1, char[] chars2, int r1, int r2) {
if (r1 == 0 && r2 == 0) {
// 只剩一个字符,相等时1,不等是0
return chars1[r1] == chars2[r2] ? 1 : 0;
} else if (r1 == 0) {
// chars1只剩一个字符,如果和chars2[r2]相等,返回1,不等看chars2前面的字符
return chars1[r1] == chars2[r2] ? 1 : process01(chars1, chars2, r1, r2 - 1);
} else if (r2 == 0) {
// chars2只剩一个字符,如果和chars1[r1]相等,返回1,不等看chars1前面的字符
return chars1[r1] == chars2[r2] ? 1 : process01(chars1, chars2, r1 - 1, r2);
} else {
// 不要r1,看公共子序列多长
int p1 = process01(chars1, chars2, r1 - 1, r2);
// 不要r2,看公共子序列多长
int p2 = process01(chars1, chars2, r1, r2 - 1);
// r1 == r2,看公共子序列多长
int p3 = chars1[r1] == chars2[r2] ? process01(chars1, chars2, r1 - 1, r2 - 1) : 0;
// 三个结果PK
return Math.max(Math.max(p1, p2), p3);
}
}
/**
* 改成动态规划
* @param str1
* @param str2
* @return
*/
public static int getMaxCommon(String str1, String str2) {
char[] chars1 = str1.toCharArray();
char[] chars2 = str2.toCharArray();
/*
dp[r1][r2]:
chars1从0到r1与chars2从0到r2的最长公共子序列长度
*/
int[][] dp = new int[chars1.length][chars2.length];
// if (r1 == 0 && r2 == 0)
dp[0][0] = chars1[0] == chars2[0] ? 1 : 0;
// else if (r1 == 0)
for (int i = 1; i < chars2.length; i++) {
dp[0][i] = Math.max(dp[0][i-1], chars2[i] == chars1[0] ? 1 : 0);
}
// else if (r2 == 0)
for (int i = 1; i < chars1.length; i++) {
dp[i][0] = Math.max(dp[i-1][0], chars1[i] == chars2[0] ? 1 : 0);
}
// else
for (int i = 1; i < chars1.length; i++) {
for (int j = 1; j < chars2.length; j++) {
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
if (chars1[i] == chars2[j]) dp[i][j] = Math.max(dp[i][j], dp[i-1][j-1] + 1);
}
}
return dp[chars1.length - 1][chars2.length - 1];
}
}
8、给定一个数组,代表每个人喝完咖啡准备刷杯子的时间,只有一台咖啡机,一次只能洗一个杯子,时间耗费a,洗完才能洗下一杯,每个咖啡杯也可以自己挥发干净,时间耗费b,咖啡杯可以并行挥发,返回让所有咖啡杯变干净的最早完成时间。三个参数: int[] arr、int a、 int b
/**
* 给定一个数组,代表每个人喝完咖啡准备刷杯子的时间
* 只有一台洗咖啡机,一次只能洗一个杯子,时间耗费a,洗完才能洗下一杯
* 每个咖啡杯也可以自己挥发干净,时间耗费b,咖啡杯可以并行挥发
* 返回让所有咖啡杯变干净的最早完成时间
* 三个参数: int[] arr、int a、 int b
* Created by huangjunyi on 2022/9/4.
*/
public class DynamicProgramming08 {
public static int processByRecursive(int[] arr, int a, int b) {
return processByRecursive(arr, a, b, 0, 0);
}
/**
* 暴力递归
* @param arr 每个人喝完咖啡准备刷杯子的时间
* @param a 洗杯子花费的时间
* @param b 杯子挥发干净的时间
* @param index 当前的杯子
* @param washLine 咖啡机可以使用的时间
* @return
*/
private static int processByRecursive(int[] arr, int a, int b, int index, int washLine) {
//已经到了最后一个杯子,手洗和挥发,选结束时间早的
//但是如果选手洗,要看喝完咖啡和洗咖啡机能用的时间,哪个比较晚
if (index == arr.length - 1) return Math.min(Math.max(arr[index], washLine) + a, arr[index] + b);
//当前杯子选择使用洗咖啡机,洗完的时间
int time1 = Math.max(arr[index], washLine) + a;
//其他杯子也都干净的时间,因为选择了使用洗咖啡机,所以洗咖啡机能用的时间要更新
int time2 = processByRecursive(arr, a, b, index + 1, time1);
//两个时间PK
int p1 = Math.max(time1, time2);
//当前杯子选择挥发,挥发干净的时间
int time3 = arr[index] + b;
//其他杯子也都干净的时间
int time4 = processByRecursive(arr, a, b, index + 1, washLine);
//两个时间PK
int p2 = Math.max(time3, time4);
//两个时间取最小
return Math.min(p1, p2);
}
/**
* 动态规划
* @param arr 每个人喝完咖啡准备刷杯子的时间
* @param a 洗杯子花费的时间
* @param b 杯子挥发干净的时间
* @return
*/
private static int processByDp(int[] arr, int a, int b) {
/*
应为暴力递归中是两个可变参数,所以改成动态规划是一张二维表
但是第二个参数washLine有点难估计,属于业务限制模型
所以就看所有杯子都选择用洗咖啡机洗,最大能冲到什么时间
*/
int washLineLimit = 0;
for (int i = 0; i < arr.length; i++) {
washLineLimit = Math.max(washLineLimit, arr[i]) + a;
}
// dp[i][j] 从i号杯子开始,洗咖啡机j时间点可以用,所有杯子干净需要的时间
int[][] dp = new int[arr.length][washLineLimit + 1];
// 根据base case初始化dp表
// 已经到了最后一个杯子,手洗和挥发,选结束时间早的
// 但是如果选手洗,要看喝完咖啡和洗咖啡机能用的时间,哪个比较晚
// if (index == arr.length - 1) return Math.min(Math.max(arr[index], washLine) + a, arr[index] + b);
for (int i = 0; i <= washLineLimit; i++) {
dp[arr.length - 1][i] = Math.min(Math.max(arr[arr.length - 1], i) + a, arr[arr.length - 1] + b);
}
for (int i = arr.length - 2; i >= 0; i--) {
for (int j = 0; j <= washLineLimit; j++) {
// 当前杯子选择用洗咖啡机洗,洗完的时间
int p1 = Integer.MAX_VALUE;
int time1 = Math.max(arr[i], j) + a;
// 判断越界,如果当前杯子选择用洗咖啡机洗,洗完时间超过washLineLimit,是无效情况,跳过
if (time1 > washLineLimit) continue;
//其他杯子也都干净的时间,因为选择了使用洗咖啡机,所以洗咖啡机能用的时间要更新
int time2 = dp[i + 1][time1];
//两个时间PK
p1 = Math.max(time1, time2);
//当前杯子选择挥发,挥发干净的时间
int time3 = arr[i] + b;
//其他杯子也都干净的时间
int time4 = dp[i + 1][j];
//两个时间PK
int p2 = Math.max(time3, time4);
//两个时间取最小
dp[i][j] = Math.min(p1, p2);
}
}
// return processByRecursive(arr, a, b, 0, 0);
return dp[0][0];
}
public static void main(String[] args) {
int[] arr = {
1,1,5,5,7,10,12,12,12,12,12,12,15};
int a=3;
int b=10;
System.out.println(processByRecursive(arr, a, b));
System.out.println(processByDp(arr, a, b));
}
}
9、马踏棋盘问题
- 规定马从0,0位置出发,棋盘是10*9大小的棋盘
- 给定三个整形变量x,y,k,表示马走k步到达x,y位置
- 问有几种走法
/**
* 马踏棋盘问题
* 规定马从0,0位置出发,棋盘是10*9大小的棋盘
* 给定三个整形变量x,y,k,表示马走k步到达x,y位置
* 问有几种走法
* Created by huangjunyi on 2022/9/4.
*/
public class DynamicProgramming09 {
public static int processByRecursive(int x, int y, int k) {
// 越界返回0
if (x < 0 || x >= 10 || y < 0 || y >= 9) return 0;
// 剩余0步要走,如果到达目标位置,返回1,代表一种走法,否则返回0
if (k == 0) return x == 0 && y == 0 ? 1 : 0;
// 从(x,y)位置,反方向走,往8个方向跳,累加跳回到起点的方法数,就等于从起点跳到(x,y)的方法数
return processByRecursive(x + 2, y + 1, k - 1) +
processByRecursive(x + 1, y + 2, k - 1) +
processByRecursive(x - 1, y + 2, k - 1) +
processByRecursive(x - 2, y + 1, k - 1) +
processByRecursive(x - 2, y - 1, k - 1) +
processByRecursive(x - 1, y - 2, k - 1) +
processByRecursive(x + 1, y - 2, k - 1) +
processByRecursive(x + 2, y - 1, k - 1);
}
public static int processByDp(int x, int y, int k) {
// 三个变化参数,所以准备一张3维表
int[][][] dp = new int[10][9][k + 1];
// base case:if (k == 0) return x == 0 && y == 0 ? 1 : 0;
dp[0][0][0] = 1;
/*
观察暴力递归
剩k步时的答案,依赖剩k-1步时的答案
所以上层答案依赖下一层,不依赖本层
已就是dp[i][j][k]依赖dp[x][y][k-1]
*/
for (int level = 1; level <= k; level++) {
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 9; j++) {
dp[i][j][level] =
// 从(x,y)位置,反方向走,往8个方向跳,累加跳回到起点的方法数,就等于从起点跳到(x,y)的方法数
getFromDp(dp, i + 2, j + 1, level - 1) +
getFromDp(dp, i + 1, j + 2, level - 1) +
getFromDp(dp, i - 1, j + 2, level - 1) +
getFromDp(dp, i - 2, j + 1, level - 1) +
getFromDp(dp, i - 2, j - 1, level - 1) +
getFromDp(dp, i - 1, j - 2, level - 1) +
getFromDp(dp, i + 1, j - 2, level - 1) +
getFromDp(dp, i + 2, j - 1, level - 1);
}
}
}
return dp[x][y][k];
}
public static int getFromDp(int[][][] dp, int x, int y, int k) {
if (x < 0 || x >= 10 || y < 0 || y >= 9) return 0;
return dp[x][y][k];
}
public static void main(String[] args) {
System.out.println(processByRecursive(6, 8, 10));
System.out.println(processByDp(6, 8, 10));
}
}
10、最长回文子序列
力扣516题
https://leetcode.cn/problems/longest-palindromic-subsequence/description/
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
/**
* https://leetcode.cn/problems/longest-palindromic-subsequence/
* 给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
* 子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
* Created by huangjunyi on 2022/11/27.
*/
public class DynamicProgramming10 {
/**
* 动态规划
*/
class Solution {
public int longestPalindromeSubseq(String s) {
char[] chs = s.toCharArray();
int N = chs.length;
// l和r两个可变参数,l和r的变化返回都是0~N-1
int[][] dp = new int[N][N];
/*
初始化dp表的两种情况
剩一个字符
剩两个字符
if (l == r) return 1;
if (l == r - 1) return chs[l] == chs[r] ? 2 : 0;
*/
dp[N-1][N-1] = 1;
for (int i = 0; i < N - 1; i++) {
dp[i][i] = 1;
dp[i][i + 1] = chs[i] == chs[i + 1] ? 2 : 1;
}
for (int l = N - 2; l >= 0; l--) {
for (int r = l + 2; r < N; r++) {
// 不要l字符和r字符
int p1 = dp[l + 1][r - 1];
// 不要l字符
int p2 = dp[l + 1][r];
// 不要r字符
int p3 = dp[l][r - 1];
// l字符和r字符相等,要
int p4 = chs[l] == chs[r] ? 2 + dp[l + 1][r - 1] : 0;
// 4种情况PK一下
dp[l][r] = Math.max(Math.max(p1, p2), Math.max(p3, p4));
}
}
// return process(chs, 0, chs.length - 1);
return dp[0][N - 1];
}
}
/**
* 暴力递归版本
*/
/*class Solution {
public int longestPalindromeSubseq(String s) {
char[] chs = s.toCharArray();
return process(chs, 0, chs.length - 1);
}
private int process(char[] chs, int l, int r) {
// 剩一个字符
if (l == r) return 1;
// 剩两个字符
if (l == r - 1) return chs[l] == chs[r] ? 2 : 0;
// 不要l字符和r字符
int p1 = process(chs, l + 1, r - 1);
// 不要l字符
int p2 = process(chs, l + 1, r);
// 不要r字符
int p3 = process(chs, l, r - 1);
// l字符和r字符相等,要
int p4 = chs[l] == chs[r] ? 2 + process(chs, l + 1, r - 1) : 0;
// 4种情况PK一下
return Math.max(Math.max(p1, p2), Math.max(p3, p4));
}
}*/
}
11、最小距离累加和
给定一个二维数组matrix,一个人必须从左上角出发,最后达到右下角
沿途只可以向下或者向右走,沿途的数字都累加就是距离累加和
返回最小距离累加和
/**
* 给定一个二维数组matrix,一个人必须从左上角出发,最后达到右下角
* 沿途只可以向下或者向右走,沿途的数字都累加就是距离累加和
* 返回最小距离累加和
* Created by huangjunyi on 2022/11/27.
*/
public class DynamicProgramming11 {
/**
* 动态规划
* @param matrix
* @return
*/
public static int getMinPathSum01(int[][] matrix) {
if (matrix == null || matrix.length == 0 || matrix[0] == null || matrix[0].length == 0) return 0;
int N = matrix.length;
int M = matrix[0].length;
// dp[i][j] 走左上角出发,走到i行j列的格子,最小距离累加和是多少
int[][] dp = new int[N][M];
// 初始化第0行
dp[0][0] = matrix[0][0];
for (int i = 1; i < M; i++) {
dp[0][i] = dp[0][i - 1] + matrix[0][i];
}
// dp[i][j] 从左边走过来(dp[i][j-1]),从上边走过来(dp[i-1][j]),两个方向,选最路径和最短
for (int i = 1; i < N; i++) {
dp[i][0] = dp[i - 1][0] + matrix[i][0];
for (int j = 1; j < M; j++) {
dp[i][j] = Math.min(dp[i][j-1], dp[i-1][j]) + matrix[i][j];
}
}
return dp[N - 1][M - 1];
}
/**
* 动态规划,空间压缩
* @param matrix
* @return
*/
public static int getMinPathSum02(int[][] matrix) {
if (matrix == null || matrix.length == 0 || matrix[0] == null || matrix[0].length == 0) return 0;
int N = matrix.length;
int M = matrix[0].length;
// 因为dp[i][j]只依赖左边和上边一个格子,所以不需要二维dp表,一位表滚动更新即可
int[] dp = new int[M];
// 初始化第0行
dp[0] = matrix[0][0];
for (int i = 1; i < M; i++) {
dp[i] = dp[i - 1] + matrix[0][i];
}
/*
假设滚动更新到i行,现在要填dp[j],此时还没更新dp[j],那么:
dp[j-1] 对应 dp[i][j-1]
dp[j] 对应 dp[i-1][j]
dp[j] = Math.min(dp[j-1], dp[j]) + matrix[i][j];
*/
for (int i = 1; i < N; i++) {
dp[0] = dp[0] + matrix[i][0];
for (int j = 1; j < M; j++) {
dp[j] = Math.min(dp[j-1], dp[j]) + matrix[i][j];
}
}
return dp[M - 1];
}
}
12、货币组合问题2
货币组合问题2
给定一个整形数组arr,表示一组货币
数组每个值都是一张货币
但是值相同的货币没有任何不同
给定一个整形aim,表示需要通过不同的货币拼凑一起到总额aim
从arr中取货币
求有多少种组合方法
例如:arr = [1,2,1,1,2,1,2],aim = 4
方法:1+1+1+1、1+1+2、2+2
/**
* 货币组合问题2
* 给定一个整形数组arr,表示一组货币
* 数组每个值都是一张货币
* 但是值相同的货币没有任何不同
* 给定一个整形aim,表示需要通过不同的货币拼凑一起到总额aim
* 从arr中取货币
* 求有多少种组合方法
*
* 例如:arr = [1,2,1,1,2,1,2],aim = 4
* 方法:1+1+1+1 1+1+2 2+2
* Created by huangjunyi on 2022/9/3.
*/
public class DynamicProgramming12 {
/**
* 暴力递归
* @param arr 货币数组
* @param aim 目标总额
* @return 方法数
*/
public static int method01(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) return 0;
// key:货币面值,value:货币张数
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < arr.length; i++) {
if (map.containsKey(arr[i])) map.put(arr[i], map.get(arr[i]) + 1);
else map.put(arr[i], 1);
}
// 货币面值数组
int[] money = new int[map.size()];
// 货币张数数组
int[] count = new int[map.size()];
int index = 0;
// 根据map初始化money和count数组
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
money[index] = entry.getKey();
count[index] = entry.getValue();
index++;
}
return process01(money, count, 0, aim);
}
/**
* 暴力递归
* @param money 货币面值数组
* @param count 货币张数数组
* @param index 第index号货币开始,往后的货币,自由选择
* @param rest 剩余要凑够的钱
* @return 方法数
*/
private static int process01(int[] money, int[] count, int index, int rest) {
if (index == money.length) return rest == 0 ? 1 : 0;
int res = 0;
// index号货币,使用0张,使用1张,使用2张,......,使用count[index]张
for (int i = 0; rest - money[index] * i >= 0 && i <= count[index]; i++) {
res += process01(money, count, index + 1, rest - money[index] * i);
}
return res;
}
/**
* 动态规划
* @param arr 货币数组
* @param aim 目标总额
* @return 方法数
*/
public static int method02(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) return 0;
// key:货币面值,value:货币张数
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < arr.length; i++) {
if (map.containsKey(arr[i])) map.put(arr[i], map.get(arr[i]) + 1);
else map.put(arr[i], 1);
}
// 货币面值数组
int[] money = new int[map.size()];
// 货币张数数组
int[] count = new int[map.size()];
int index = 0;
// 根据map初始化money和count数组
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
money[index] = entry.getKey();
count[index] = entry.getValue();
index++;
}
/*
暴力递归2个可变参数,二维表,观察可变参数范围,确定dp表的长度
dp[index][rest] index号货币开始往后自由选择,凑够rest元,的方法数
*/
int[][] dp = new int[money.length + 1][aim + 1];
// 根据 base case 初始化dp表:if (index == money.length) return rest == 0 ? 1 : 0;
dp[money.length][0] = 1;
/*
index号货币,使用0张,使用1张,使用2张,......,使用count[index]张
for (int i = 0; rest - money[index] * i >= 0 && i <= count[index]; i++) {
res += process01(money, count, index + 1, rest - money[index] * i);
}
*/
for (index = money.length - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
int res = 0;
for (int i = 0; rest - money[index] * i >= 0 && i <= count[index]; i++) {
res += dp[index + 1][rest - money[index] * i];
}
dp[index][rest] = res;
}
}
// return process01(money, count, 0, aim);
return dp[0][aim];
}
/**
* 动态规划 枚举优化
* @param arr 货币数组
* @param aim 目标总额
* @return 方法数
*/
public static int method03(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) return 0;
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < arr.length; i++) {
if (map.containsKey(arr[i])) map.put(arr[i], map.get(arr[i]) + 1);
else map.put(arr[i], 1);
}
int[] money = new int[map.size()];
int[] count = new int[map.size()];
int index = 0;
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
money[index] = entry.getKey();
count[index] = entry.getValue();
index++;
}
int[][] dp = new int[money.length + 1][aim + 1];
dp[money.length][0] = 1;
for (index = money.length - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
/*
观察:
假设count[index] = 3,index号货币有3张
dp[index][rest] = dp[index + 1][rest] + dp[index + 1][rest - 1 * money[index]] + dp[index + 1][rest - 2 * money[index]] + dp[index + 1][rest - 3 * money[index]]
dp[index][rest - 1 * money[index]] = dp[index + 1][rest - 1 * money[index]] + dp[index + 1][rest - 2 * money[index]] + dp[index + 1][rest - 3 * money[index]] + dp[index + 1][rest - 4 * money[index]]
所以:
dp[index][rest] = dp[index + 1][rest] + dp[index][rest - 1 * money[index]] - dp[index + 1][rest - 4 * money[index]];
dp[index][rest] = dp[index + 1][rest] + dp[index][rest - 1 * money[index]] - dp[index + 1][rest - (count[index] + 1) * money[index]];
*/
dp[index][rest] = dp[index + 1][rest];
if (rest - money[index] >= 0) dp[index][rest] += dp[index][rest - money[index]];
if (rest - (count[index] + 1) * money[index] >= 0) dp[index][rest] -= dp[index + 1][rest - (count[index] + 1) * money[index]];
}
}
return dp[0][aim];
}
public static void main(String[] args) {
int[] arr = {
1,2,1,1,2,1,2};
int aim = 4;
System.out.println(method01(arr, aim));
System.out.println(method02(arr, aim));
System.out.println(method03(arr, aim));
}
}
13、英雄打怪兽
给定三个参数,N、M、K
怪兽有N滴血,等着英雄来砍自己
英雄每一次打击,都会让怪兽流失[0-M]的血量
到底流失多少?每一次在[0-M]上等概率的获得一个值
求K次打击之后,英雄把怪兽砍死的概率
/**
* 给定三个参数,N、M、K
* 怪兽有N滴血,等着英雄来砍自己
* 英雄每一次打击,都会让怪兽流失[0-M]的血量
* 到底流失多少?每一次在[0-M]上等概率的获得一个值
* 求K次打击之后,英雄把怪兽砍死的概率
* Created by huangjunyi on 2022/11/28.
*/
public class DynamicProgramming13 {
/**
* 暴力递归
* @param N 怪兽有N滴血
* @param M 怪兽流失血量范围
* @param K 英雄打击K次
* @return
*/
public static double killMonster01(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 = process01(N, M, K);
return ((double) kill) / ((double) all);
}
/**
* 暴力递归
* @param hp 怪兽剩余血量
* @param M 怪兽流失血量范围
* @param rest 英雄剩K次要砍
* @return
*/
private static long process01(int hp, int M, int rest) {
// base case:砍完了,怪兽血量减到0以下,返回1中有效方法数
if (rest == 0) return hp <= 0 ? 1 : 0;
// 怪兽已经死了,也要继续砍,因为往后的都是算入有效方法数的
if (hp <= 0) return (long) Math.pow(M + 1, rest);
// 枚举砍出的伤害值,累加方法数
long ways = 0;
for (int i = 0; i <= M; i++) {
ways += process01(hp - i, M, rest - 1);
}
return ways;
}
/**
* 动态规划
* @param N 怪兽有N滴血
* @param M 怪兽流失血量范围
* @param K 英雄打击K次
* @return
*/
public static double killMonster02(int N, int M, int K) {
if (N < 1 || M < 1 || K < 1) return 0;
// 所有情况数
long all = (long) Math.pow(M + 1, K);
/*
暴力递归两个可变参数,剩余要砍次数、怪兽剩余血量,所以是一张二维dp表
dp[rest][hp]
英雄剩余要看rest次,怪兽剩余血量hp,砍死怪兽的方法数
观察暴力递归的参数范围,可得dp表长度
*/
long[][] dp = new long[K + 1][N + 1];
/*
根据base case,初始化dp表:
if (rest == 0) return hp <= 0 ? 1 : 0;
if (hp <= 0) return (long) Math.pow(M + 1, rest);
*/
dp[0][0] = 1;
for (int rest = 1; rest <= K; rest++) {
dp[rest][0] = (long) Math.pow(M + 1, rest);
}
/*
观察暴力递归中,外层递归与里层递归的依赖关系
确定dp表的填表顺序
long ways = 0;
for (int i = 0; i < M; i++) {
ways += process01(hp - i, M, rest - 1);
}
dp[rest][...] 依赖 dp[rest - 1][...]
所以是从上往下填
*/
for (int rest = 1; rest <= K; rest++) {
for (int hp = 1; hp <= N; hp++) {
long ways = 0;
for (int i = 0; i <= M; i++) {
// 这里要注意hp减完有可能越界,越界了就通过公式算
ways += hp - i >= 0 ? dp[rest - 1][hp - i] : (long) Math.pow(M + 1, rest - 1);
}
dp[rest][hp] = ways;
}
}
/*
根据暴力递归的最外层递归,确定返回值
long kill = process01(N, M, K);
return ((double) kill) / ((double) all);
*/
return ((double) dp[K][N]) / ((double) all);
}
/**
* 动态规划 枚举行为优化
* @param N 怪兽有N滴血
* @param M 怪兽流失血量范围
* @param K 英雄打击K次
* @return
*/
public static double killMonster03(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 rest = 1; rest <= K; rest++) {
dp[rest][0] = (long) Math.pow(M + 1, rest);
}
for (int rest = 1; rest <= K; rest++) {
for (int hp = 1; hp <= N; hp++) {
// long ways = 0;
// for (int i = 0; i < M; i++) {
// // 这里要注意hp减完有可能越界,越界了就通过公式算
// ways += hp - i >= 0 ? dp[hp - i][rest - 1] : (long) Math.pow(M + 1, rest - 1);
// }
// dp[rest][hp] = ways;
/*
有枚举行为,就随便算一个格子看依赖关系就行了
观察dp[5][10],M=5,英雄剩5次要砍,怪兽剩10点血
dp[5][10] = dp[4][5...10]
dp[5][11] = dp[4][6...11]
所以:
dp[5][11] = dp[4][11] + dp[5][10] - dp[4][5]
推断出:
dp[rest][hp] = dp[rest - 1][hp] + dp[rest][hp - 1] - dp[rest - 1][hp - M - 1]
但是那是hp - M - 1不越界的情况,如果越界:
dp[rest][hp] = dp[rest - 1][hp] + dp[rest][hp - 1] - (long) Math.pow(M + 1, rest - 1)
*/
dp[rest][hp] = dp[rest][hp - 1] + dp[rest - 1][hp];
if (hp - 1 - M >= 0) {
dp[rest][hp] -= dp[rest - 1][hp - 1 - M];
} else {
dp[rest][hp] -= Math.pow(M + 1, rest - 1);
}
}
}
return ((double) dp[K][N]) / ((double) all);
}
public static void main(String[] args) {
System.out.println("begin");
for (int i = 0; i < 20000; i++) {
int N = (int) (Math.random() * 10);
int M = (int) (Math.random() * 10);
int K = (int) (Math.random() * 10);
double res1 = killMonster01(N, M, K);
double res2 = killMonster02(N, M, K);
double res3 = killMonster03(N, M, K);
if (res1 != res2 || res1 != res3) {
System.out.println("error: " + " N=" + N + " M=" + M + " K=" + K);
System.out.println("res1=" + res1 + " res2=" + res2 + " res3=" + res3);
break;
}
}
System.out.println("finished");
}
}
14、货币组合问题3
arr是面值数组,其中的值都是正数且没有重复。
再给定一个正数aim。
每个值都认为是一种面值,且认为张数是无限的。
返回组成aim的最少货币数。
/**
* 货币组合问题3
* arr是面值数组,其中的值都是正数且没有重复。
* 再给定一个正数aim。
* 每个值都认为是一种面值,且认为张数是无限的。
* 返回组成aim的最少货币数。
*
* Created by huangjunyi on 2022/11/30.
*/
public class DynamicProgramming14 {
/**
* 暴力递归
*/
public static int minCoins01(int[] arr, int aim) {
return process(arr, 0, aim);
}
/**
* 暴力递归
* @param arr 货币数组
* @param index 当前货币
* @param rest 剩余要凑够的钱数
* @return
*/
private static int process(int[] arr, int index, int rest) {
// base case 货币都决定完了,剩余要凑够的钱数为0,返回0(代表凑够0元需要0张货币),否则返回MAX,代表无效
if (index == arr.length) return rest == 0 ? 0 : Integer.MAX_VALUE;
int min = Integer.MAX_VALUE;
// index号货币要0张,要1张,要2张......拿最少值
for (int i = 0; i * arr[index] <= rest; i++) {
int next = process(arr, index + 1, rest - i * arr[index]);
if (next != Integer.MAX_VALUE) min = Math.min(min, next + i);
}
return min;
}
/**
* 动态规划
* @param arr 货币数组
* @param aim 要凑够的目标货币数
* @return
*/
public static int minCoins02(int[] arr, int aim) {
/*
根据暴力递归,可变参数的个数,范围,推测dp表的结构
可变参数:index rest => 二维dp表
范围:index:0~arr.length rest: 0~aim
dp[index][rest] 从index号货币开始往后自由选择,凑出rest元的最少货币数
*/
int[][] dp = new int[arr.length + 1][aim + 1];
// 根据暴力递归 base case 初始化dp表
// if (index == arr.length) return rest == 0 ? 1 : Integer.MAX_VALUE;
dp[arr.length][0] = 0;
for (int i = 1; i <= aim; i++) {
dp[arr.length][i] = Integer.MAX_VALUE;
}
/*
观察暴力递归外层与内层的依赖关系,确定填表的方向
int min = Integer.MAX_VALUE;
// index号货币要0张,要1张,要2张......拿最少值
for (int i = 0; i * arr[index] <= rest; i++) {
int next = process(arr, index + 1, rest - i * arr[index]);
if (next != Integer.MAX_VALUE) min = Math.min(min, next + i);
}
index 依赖 index + 1
所以从下往上填
*/
for (int index = arr.length - 1; index >= 0; index--) {
for (int rest = 1; rest <= aim; rest++) {
int min = Integer.MAX_VALUE;
// index号货币要0张,要1张,要2张......拿最少值
for (int i = 0; i * arr[index] <= rest; i++) {
int next = dp[index + 1][rest - i * arr[index]];
if (next != Integer.MAX_VALUE) min = Math.min(min, next + i);
}
dp[index][rest] = min;
}
}
// 根据暴力递归最外层递归的参数,确定返回值
// return process(arr, 0, aim);
return dp[0][aim];
}
/**
* 动态规划 枚举行为优化
* @param arr 货币数组
* @param aim 要凑够的目标货币数
* @return
*/
public static int minCoins03(int[] arr, int aim) {
int[][] dp = new int[arr.length + 1][aim + 1];
dp[arr.length][0] = 0;
for (int i = 1; i <= aim; i++) {
dp[arr.length][i] = Integer.MAX_VALUE;
}
for (int index = arr.length - 1; index >= 0; index--) {
for (int rest = 1; rest <= aim; rest++) {
/*int min = Integer.MAX_VALUE;
for (int i = 0; i * arr[index] <= rest; i++) {
int next = dp[index + 1][rest - i * arr[index]];
if (next != Integer.MAX_VALUE) min = Math.min(min, next + i);
}
dp[index][rest] = min;*/
/*
观察dp[5][10] 假设arr[5]=3:
dp[5][10] = Math.min(dp[6][10], dp[6][7] + 1, dp[6][4] + 2, dp[6][1] + 3)
dp[5][7] = Math.min( dp[6][7], dp[6][4] + 1, dp[6][1] + 2)
所以:
dp[5][10] = Math.min(dp[6][10], dp[5][7] + 1)
dp[index][rest] = Math.min(dp[index + 1][rest], dp[index][rest- arr[index]] + 1)
*/
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], dp[index][rest- arr[index]] + 1);
}
}
}
return dp[0][aim];
}
}
15、数字切分
给你一个数字num,将它切开
比如给你7,你可以切成2/2/3,也可以切成3/4,但是不能切成3/2/2或4/3,也就是前面的数不能大于后面的数
返回一个数一共有多少中切分方法
/**
* 数字切分
* 给你一个数字num,将它切开
* 比如给你7,你可以切成2/2/3,也可以切成3/4,但是不能切成3/2/2或4/3,也就是前面的数不能大于后面的数
* 返回一个数一共有多少中切分方法
* Created by huangjunyi on 2022/11/30.
*/
public class DynamicProgramming15 {
/**
* 暴力递归
* @param n 要切分的数字
*/
public static int splitNumber01(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return process(1, n);
}
/**
* 暴力递归
* 前一个数字是preNum,剩下要切分的数字是restNum,切分restNum的切分方法数
* @param preNum 前一个数字
* @param restNum 剩下要切分的数字
* @return
*/
private static int process(int preNum, int restNum) {
// restNum切成了0,有效切分方法数1
if (restNum == 0)return 1;
// 前一个数比restNum,无效切分
if (preNum > restNum) return 0;
// 当前切出preNum,切出preNum+1,切出preNum+2......,方法数累加
int ways = 0;
for (int i = preNum; i <= restNum; i++) {
ways += process(i, restNum - i);
}
return ways;
}
/**
* 动态规划
* @param n 要切分的数字
* @return
*/
private static int splitNumber02(int n) {
/*
观察暴力递归的可变参数个数,已经可变参数范围
preNum:1~n
restNum:0~n
dp[preNum][restNum]
前一个数字是preNum,剩下要切分的数字是restNum,切分restNum的切分方法数
*/
int[][] dp = new int[n + 1][n + 1];
/*
根据暴力递归的base case初始化dp表
if (restNum == 0)return 1;
if (preNum > restNum) return 0;
*/
for (int preNum = 1; preNum <= n; preNum++) {
dp[preNum][0] = 1;
}
/*
观察暴力递归的外层递归和内层递归的依赖关系,确定dp表的填写顺序
int ways = 0;
for (int i = preNum; i <= restNum; i++) {
ways += process(i, restNum - i);
}
return ways;
preNum依赖preNum+1、preNum+2......
所以从下往上填
因为preNum > restNum 都是无效的,所以只需要填上半部分
*/
for (int preNum = n; preNum >= 1; preNum--) {
for (int restNum = preNum; restNum <= n; restNum++) {
int ways = 0;
for (int i = preNum; i <= restNum; i++) {
ways += dp[i][restNum - i];
}
dp[preNum][restNum] = ways;
}
}
// 根据暴力递归的最外层递归确定返回值
// return process(1, n);
return dp[1][n];
}
/**
* 动态规划 枚举行为优化
* @param n 要切分的数字
* @return
*/
private static int splitNumber03(int n) {
int[][] dp = new int[n + 1][n + 1];
for (int preNum = 1; preNum <= n; preNum++) {
dp[preNum][0] = 1;
}
for (int preNum = n; preNum >= 1; preNum--) {
for (int restNum = preNum; restNum <= n; restNum++) {
/*int ways = 0;
for (int i = preNum; i <= restNum; i++) {
ways += dp[i][restNum - i];
}
dp[preNum][restNum] = ways;*/
/*
观察dp[2][10]
dp[2][10] = dp[2][8] + dp[3][7] + dp[4][6] + dp[5][5]
dp[3][10] = dp[3][7] + dp[4][6] + dp[5][5]
所以:
dp[2][10] = dp[2][8] + dp[3][10]
dp[preNum][restNum] = dp[preNum][restNum - preNum] + dp[preNum + 1][restNum]
*/
dp[preNum][restNum] = dp[preNum][restNum - preNum];
if (preNum != n) dp[preNum][restNum] += dp[preNum + 1][restNum];
}
}
return dp[1][n];
}
public static void main(String[] args) {
int n = 68;
System.out.println(splitNumber01(n));
System.out.println(splitNumber02(n));
System.out.println(splitNumber03(n));
}
}
16、较小集合的累加和
给定一个正数数组arr
请把arr中所有的数分成两个集合,尽量让两个集合的累加和接近
返回:
最接近的情况下,较小集合的累加和
/**
* 较小集合的累加和
* 给定一个正数数组arr
* 请把arr中所有的数分成两个集合,尽量让两个集合的累加和接近
* 返回:
* 最接近的情况下,较小集合的累加和
* Created by huangjunyi on 2022/11/30.
*/
public class DynamicProgramming16 {
/**
* 暴力递归
*/
public static int smallSum01(int[] arr) {
if (arr == null || arr.length <= 1) return 0;
int sum = 0;
for (int num : arr) {
sum += num;
}
// arr数组累加和除以2,把题意转换为从arr数组自由选择,
// 返回最接近但是不超过arr累加和一半的集合累加和
return process(arr, 0 , sum / 2);
}
/**
* 暴力递归,把题意转换为从index位置,arr后面的数只有做选择,返回最近但是不超过rest的数
*/
private static int process(int[] arr, int index, int rest) {
// base case 没有数需要选择了,返回0
if (index == arr.length) return 0;
// 不要index位置的数
int p1 = process(arr, index + 1, rest);
// 要index位置的数,但是要判断不能超过rest
int p2 = 0;
if (arr[index] <= rest) {
p2 = arr[index] + process(arr, index + 1, rest - arr[index]);
}
return Math.max(p1, p2);
}
public static int smallSum02(int[] arr) {
if (arr == null || arr.length <= 1) return 0;
/*
可变参数index,rest
index:0 ~ arr.length
rest: 0 ~ sum/2
*/
int sum = 0;
for (int num : arr) sum += num;
int[][] dp = new int[arr.length + 1][(sum / 2) + 1];
// base case: if (index == arr.length) return 0;
// 数组初始值本身就是0,不用初始化
/*
暴力递归中外层与内层的依赖关系
// 不要index位置的数
int p1 = process(arr, index + 1, rest);
// 要index位置的数,但是要判断不能超过rest
int p2 = 0;
if (arr[index] <= rest) {
p2 = arr[index] + process(arr, index + 1, rest - arr[index]);
}
return Math.max(p1, p2);
所以index依赖index+1
所以从下往上填
*/
for (int index = arr.length - 1; index >= 0; index--) {
for (int rest = 0; rest <= sum / 2; rest++) {
// 不要index位置的数
int p1 = dp[index + 1][rest];
// 要index位置的数,但是要判断不能超过rest
int p2 = 0;
if (arr[index] <= rest) {
p2 = arr[index] + dp[index + 1][rest - arr[index]];
}
dp[index][rest] = Math.max(p1, p2);
}
}
// 暴力递归最外层:return process(arr, 0 , sum / 2);
return dp[0][sum / 2];
}
}
17、较小集合的累加和2
给定一个正整数数组arr,请把arr中所有的数分成两个集合
如果arr长度为偶数,两个集合包含数的个数要一样多
如果arr长度为奇数,两个集合包含数的个数必须只差一个
请尽量让两个集合的累加和接近
返回:
最接近的情况下,较小结合的累加和
/**
* 给定一个正整数数组arr,请把arr中所有的数分成两个集合
* 如果arr长度为偶数,两个集合包含数的个数要一样多
* 如果arr长度为奇数,两个集合包含数的个数必须只差一个
* 请尽量让两个集合的累加和接近
* 返回:
* 最接近的情况下,较小结合的累加和
* Created by huangjunyi on 2022/11/30.
*/
public class DynamicProgramming17 {
/**
* 暴力递归
*/
public static int smallSum01(int[] arr) {
if (arr == null || arr.length <= 1) 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));
}
}
/**
* 从index位置开始,在数组arr中只有挑选数字,必须挑满picks个,
* 返回最接近rest但是不超过rest的集合累加和
*/
private static int process(int[] arr, int index, int picks, int rest) {
// base case:没数了,看挑完没,没挑完要返回-1,表示无效
if (index == arr.length) return picks == 0 ? 0 : -1;
// index的数不要
int p1 = process(arr, index + 1, picks, rest);
// index的数要
int p2 = -1;
int next = -1;
if (arr[index] <= rest) next = process(arr, index + 1, picks - 1, rest - arr[index]);
if (next != -1) p2 = arr[index] + next;
// 两个累加和PK一下
return Math.max(p1, p2);
}
/**
* 动态规划
*/
public static int smallSum02(int[] arr) {
if (arr == null || arr.length <= 1) return 0;
int sum = 0;
for (int num : arr) {
sum += num;
}
/*
可变参数:index picks rest
index:0 ~ arr.length
picks: 偶数 0 ~ arr.length / 2 奇数 0 ~ arr.length / 2 + 1
rest: 0 ~ sum / 2
dp[index][picks][rest]:从index位置开始,在数组arr中只有挑选数字,必须挑满picks个,最接近rest但是不超过rest的集合累加和
*/
int[][][] dp = new int[arr.length + 1][(arr.length + 1) / 2 + 1][sum / 2 + 1];
/*
根据base case 初始化dp表
// base case:没数了,看挑完没,没挑完要返回-1,表示无效
if (index == arr.length) return picks == 0 ? 0 : -1;
*/
for (int index = 0; index <= arr.length; index++) {
for (int picks = 0; picks <= (arr.length + 1) / 2; picks++) {
for (int rest = 0; rest <= sum / 2; rest++) {
dp[index][picks][rest] = -1;
}
}
}
for (int rest = 0; rest <= sum / 2; rest++) {
dp[arr.length][0][rest] = 0;
}
/*
暴力递归外层与内层的依赖关系:
// index的数不要
int p1 = process(arr, index + 1, picks, rest);
// index的数要
int p2 = -1;
int next = -1;
if (arr[index] <= rest) next = process(arr, index + 1, picks - 1, rest - arr[index]);
if (next != -1) p2 = arr[index] + next;
// 两个累加和PK一下
return Math.max(p1, p2);
index层依赖于index+1层,所以从下往上填
*/
for (int index = arr.length - 1; index >= 0; index--) {
for (int picks = 0; picks <= (arr.length + 1) / 2; picks++) {
for (int rest = 0; rest <= sum / 2; rest++) {
// index的数不要
int p1 = dp[index + 1][picks][rest];
// index的数要
int p2 = -1;
int next = -1;
if (arr[index] <= rest && picks > 0) next = dp[index + 1][picks - 1][rest - arr[index]];
if (next != -1) p2 = arr[index] + next;
// 两个累加和PK一下
dp[index][picks][rest] = Math.max(p1, p2);
}
}
}
// 根据暴力递归的最外层,确定返回值
if ((arr.length & 1) == 0) {
return dp[0][arr.length / 2][sum / 2];
} else {
return Math.max(dp[0][arr.length / 2][sum / 2], dp[0][arr.length / 2 + 1][sum / 2]);
}
}
}
总结
什么题目能用动态规划写
只要一个题目可以使用递归解,而且在递归的过程中,出现了求重复解的情况,就可以优化为动态规划,前提是递归过程的可变参数不会过于复杂(比如数组类型,集合类型)
递归如何优化为动态规划
1、 根据递归的可变参数确定动态规划dp表的维度,一个可变参数,就是一维表,两个可变参数,就是二维表;
2、 根据递归的可变参数的范围,确定dp表的大小;
3、 dp表一个格子代表的含义,就是一层递归的返回值代表的含义;
4、 dp表定好后,根据递归的终止条件(basecase),初始化dp表;
5、 根据外层递归与内层递归的依赖关系,确定dp表的填写顺序;
6、 如果存在枚举行为观察临近格子与当前格子,是否存在重复枚举的部分,如果有,则可以进行枚举行为优化;
7、 根据最外层递归的可变参数的值,确定以dp表中哪个格子作为返回值;
枚举行为优化
存在枚举行为是指递归里面存在for循环嵌套递归来计算本层递归的返回值,或者填dp表某个格子时存在for循环遍历其他格子计算当前格子的值的行为。
如何进行枚举行为的优化?
1、 观察某个格子,计算这个格子的值需要依赖到其他哪些格子;
2、 再观察这个格子的临近的格子(上下左右),计算这个格子的值需要依赖到其他哪些格子;
3、 对比这两个格子,在计算的时候,所依赖的其他格子,是否用重复的部分;
4、 如果有,则可以通过复用临近的这个格子的值(可能要减掉多余的部分),替代掉枚举行为;
空间压缩
比如在填二维dp表的时候,发现填本行时,只依赖到上一行,可以尝试把二维dp表优化为一维表(但不一定可以,要看依赖到哪些格子)
递归的几种尝试模型
1、 从左往右的尝试模型;
比如题目17“较小集合的累加和”,从左往右依次遍历,每一个位置保留和不保留,得出不同结果,这种尝试模型,就是从左往右
2、 一个样本做行,一个样本做列;
比如题目7“求两个字符串的最长公共子序列长度”,两个字符串都是一个样本,要枚举两个字符串不同长度时的情况,得出不同结果,这种尝试模型,就是“一个样本做行,一个样本做列”
3、 范围尝试模型;
比如题目10“最长回文子序列”,在某个范围上进行尝试,通常这种在填dp表时左下半区的dp表是不用填的
4、 业务限制的尝试模型;
什么是参数过于复杂,不能用动态规划?比如N皇后问题
N皇后问题是指在N*N的棋盘上要摆N个皇后,
要求任何两个皇后不同行、不同列,也不在同一条斜线上
给定一个正数n,返回n皇后的摆法有多少种。
n=1,返回1
n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0
n=8,返回92
/**
* N皇后问题
*
* N皇后问题是指在N*N的棋盘上要摆N个皇后,
* 要求任何两个皇后不同行、不同列,也不在同一条斜线上
* 给定一个正数n,返回n皇后的摆法有多少种。
* n=1,返回1
* n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0
* n=8,返回92
*
* 需要用递归,但是不能优化为动态规划的例子
*
* Created by huangjunyi on 2022/11/30.
*/
public class DynamicProgramming18 {
public static int ways(int n) {
if (n < 1) return 0;
// record[i]表示在第i行的皇后摆在第几列
int[] record = new int[n];
return process(n, record, 0);
}
private static int process(int n, int[] record, int i) {
// 来到第n行,表示全都摆好了,返回一种可行方法数
if (i == n) return 1;
int res = 0;
// 放在第1列,放在第2列...,累加方法数
for (int j = 0; j < n; j++) {
// 摆在该列是否违规,违规则跳过,当前行只与往上的行进行比较,不用管下面的行,下面的行交给下层递归
boolean flag = true;
for (int k = 0; k < i; k++) {
if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {
flag = false;
break;
}
}
// 记录:第i行的皇后,摆在j列
if (flag) {
record[i] = j;
res += process(n, record, i + 1);
}
}
return res;
}
}
因为可变参数中存在一个record 数组,是无法改为dp表的,或者强行改成dp表会非常的复杂,因为这个record数组有很多种情况,无法像整形那样很简单的确定范围