一、记忆缓存
遇到出现同样的参数多次递归调用时,可以添加记忆缓存,以空间换时间。
原理说明:
1、如下面案例中的递归,调用时每次都从n执行到0(n一次衰减),则可能存在n-1的阶乘次重复参数调用,这里将重复部分都缓存起来,达到了跟动态规划类似的效果。
下面的力扣案例,没加缓存记忆会执行超时,增加后顺利通过。
#案例
给你一个整数 n
和一个二维数组 requirements
,其中 requirements[i] = [endi, cnti]
表示这个要求中的末尾下标和 逆序对 的数目。
整数数组 nums
中一个下标对 (i, j)
如果满足以下条件,那么它们被称为一个 逆序对 :
i < j
且nums[i] > nums[j]
请你返回 [0, 1, 2, ..., n - 1]
的
排列
perm
的数目,满足对 所有 的 requirements[i]
都有 perm[0..endi]
恰好有 cnti
个逆序对。
由于答案可能会很大,将它对 109 + 7
取余 后返回。
示例 1:
输入:n = 3, requirements = [[2,2],[0,0]]
输出:2
解释:
两个排列为:
[2, 0, 1]
- 前缀
[2, 0, 1]
的逆序对为(0, 1)
和(0, 2)
。 - 前缀
[2]
的逆序对数目为 0 个。
- 前缀
[1, 2, 0]
- 前缀
[1, 2, 0]
的逆序对为(0, 2)
和(1, 2)
。 - 前缀
[1]
的逆序对数目为 0 个。
- 前缀
示例 2:
输入:n = 3, requirements = [[2,2],[1,1],[0,0]]
输出:1
解释:
唯一满足要求的排列是 [2, 0, 1]
:
- 前缀
[2, 0, 1]
的逆序对为(0, 1)
和(0, 2)
。 - 前缀
[2, 0]
的逆序对为(0, 1)
。 - 前缀
[2]
的逆序对数目为 0 。
示例 3:
输入:n = 2, requirements = [[0,0],[1,0]]
输出:1
解释:
唯一满足要求的排列为 [0, 1]
:
- 前缀
[0]
的逆序对数目为 0 。 - 前缀
[0, 1]
的逆序对为(0, 1)
。
提示:
2 <= n <= 300
1 <= requirements.length <= n
requirements[i] = [endi, cnti]
0 <= endi <= n - 1
0 <= cnti <= 400
- 输入保证至少有一个
i
满足endi == n - 1
。 - 输入保证所有的
endi
互不相同。
class Solution {
public int numberOfPermutations(int n, int[][] requirements) {
Arrays.sort(requirements,(o1,o2)->{return o1[0]-o2[0];});
int[][] memo = new int[n][requirements[requirements.length-1][1]+1];
for(int[] in:memo){
Arrays.fill(in,-1);
}
return dfs(n-1,requirements[requirements.length-1][1],requirements,requirements.length-1,memo);
}
// 前i个数有k个逆序对的最大排列数量
public int dfs(int i,int k,int[][] req,int reqIndex,int[][] memo){
// 缓存函数处理过的i,k结果,增加递归速度
if(memo[i][k] != -1){
return memo[i][k];
}
if(i==0){
return memo[i][k]=k==0?1:0;
}
if(k<0){
return memo[i][k]=0;
}
// 如果i-1有被限制最大逆序对个数,则dfs(i,k)排列数量与dfs(i-1,end(i-1))相等。
if(reqIndex>0 && i == req[reqIndex-1][0]+1){
// i位置的逆序对数量只会大于或者等于它前面的位置的数量,且只会比它上一个位置最多大i个逆序对(当最小的数字0放最后时就会增加i个逆序对)
if(k<req[reqIndex-1][1] || k>req[reqIndex-1][1]+i){
return memo[i][k]=0;
}
return dfs(i-1,req[reqIndex-1][1],req,reqIndex-1,memo);
}
int res = 0;
// 如果i-1没有有被限制最大逆序对个数,则dfs(i,k)排列数量与dfs(i-1,k-p)相等,p表示前面有几个数大于第i个数(即从i-1到i后会增加的逆序对个数可以有p种)。
for(int p=0;p<=Math.min(i,k);p++){
res=(res+dfs(i-1,k-p,req,reqIndex,memo))%1000000007;
}
return memo[i][k]=res;
}
}
二、状态记录
部分算法题目中,在从顶层到最底层的一次递归中时,数组中前面层使用过的元素不允许再使用。该场景下即可使用数据状态记录的方法。
使用一个相同大小的状态数组boolean[n],数组初始化为false,对于目标数组使用过的元素,对应状态数组下同下标visit[i]赋值true,递归完,再将其赋值回false。
#案例
HJ67 24点游戏算法
描述
给出4个1-10的数字,通过加减乘除运算,得到数字为24就算胜利,除法指实数除法运算,运算符仅允许出现在两个数字之间,本题对数字选取顺序无要求,但每个数字仅允许使用一次,且需考虑括号运算
此题允许数字重复,如3 3 4 4为合法输入,此输入一共有两个3,但是每个数字只允许使用一次,则运算过程中两个3都被选取并进行对应的计算操作。
输入描述:
读入4个[1,10]的整数,数字允许重复,测试用例保证无异常数字。
输出描述:
对于每组案例,输出一行表示能否得到24点,能输出true,不能输出false
示例1
输入:
7 2 1 10
复制输出:
true
import java.io.*;
/*
知识点:递归、深度优先搜索、回溯算法
*/
public class Main {
static int count =0;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String str;
while ((str = br.readLine()) != null) {
String[] numstr = str.split(" ");
int[] nums = new int[4]; // 存放数字
int[] visit = new
int[4]; // 存放对应位置数字的使用状态(1代表已使用)
boolean flag = false;
for (int i = 0; i < 4; i++) {
nums[i] = Integer.parseInt(numstr[i]); // 读取数字
}
for (int i = 0; i < 4; i++) {
visit[i] = 1; // 把当前数字标记为已使用
if (dfs(nums, visit, nums[i])) { // 进入递归
flag = true;
break;
}
}
System.out.println(flag);
}
}
public static boolean dfs(int[] nums, int[] visit, int temp) {
count++;
for (int i = 0; i < nums.length; i++) {
if (visit[i] == 0) { // 如果是未使用的数字
visit[i] = 1; // 标记为已使用
if (dfs(nums, visit, temp + nums[i]) // 递归判断
|| dfs(nums, visit, temp - nums[i])
|| dfs(nums, visit, temp * nums[i])
|| dfs(nums, visit, temp / nums[i])) {
// 如果存在满足条件的,终止循环
return true;
}
// 不存在满足条件的,说明当前的数字顺序不符要求,进行回溯,把标记重置为0
visit[i] = 0;
}
}
// 数字都已使用且结果为24,返回真
if (temp == 24) {
return true;
}
// 不存在24,返回假
return false;
}
}