前言
之所以决定写这个学习笔记是因为自己在备战蓝桥杯的过程中遇到了很多的问题。首先是自己之前写的代码太少了,属于纯小白了,刚开始接触算法属实是有点摸不着头脑,报了acwing蓝桥杯的辅导课跟着y总学了一段时间发现“一看就会,一做就废”,甚至一道题要看好几遍才看懂,更别说自己做出来了。还有一点就是容易忘,学完这个忘那个,其实主要是说明自己对那个知识点没掌握住,做的题还不够。所以我打算在学习新知识点的同时,把之前学的东西做一下笔记,整理一下自己遇到的一些问题以及解题思路。同时建议大家多做题,同一个知识点光辅导课的上的题是不够的,要做多了然后遇到同一类型的题时能够自己独立做出来那才算真正掌握了那个知识点。最后,如果大家觉得我的文章对你有帮助的话记得给我点点关注,点点赞,大家一起加油努力!
知识点
什么是递归?
所谓递归,说简单点就是自己调用自己。
但是递归必须具备两个条件,一是调用自己,二是终止条件。
这两个条件必须同时具备,缺一不可。
在解题的时候我们通常利用递归+dfs枚举全排列,就是把所有可能的情况都列出来,最后再根据题目的要求筛选出我们需要的情况。(注:这个一般仅局限于数据范围较小的情况下使用,否则会超时)
说到超时,那就可以对我们的代码进行优化,当我们可以提前判断出有些情况是不存在或者是没必要进行枚举时就可以加一些判断条件,提前终止递归,即“剪枝”。
递归搜索树
每一个递归问题都可以将其转换成一棵递归搜索树。在做题的时候建议大家可以先思考一下题目的意思,然后试着画一下递归搜索树,这样写起代码来可能更加的清晰,且更容易去理解问题本身。
例题
题目1:820. 递归求斐波那契数列
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner iuput = new Scanner(System.in);
int n = iuput.nextInt();
System.out.println(f(n));
}
public static int f(int n) {
if(n == 1)
return 1;
if(n == 2)
return 2;
return f(n-1) + f(n-2);
}
}
题目2:92. 递归实现指数型枚举
分析:考虑每个位置上的数选还是不选,题目要求每一种方案升序排列,这里我们只需要从1(从小打到)开始枚举的话自然而然就是升序排列了。
时间复杂度:O(n*2^n) n个数,每个数都有选或不选两种情况。
参数:st[] 记录 每个位置的状态 0 :初始状态 1:选 2:不选
import java.util.Scanner;
//时间复杂度n*2^n
public class 递归实现指数型枚举 {
static int n;
static int N = 16;
static int[] st;// 0表示初始,1表示选,2表示不选
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
n = input.nextInt();
st = new int[N];
dfs(0);
}
public static void dfs(int index) {
if (index == n) {//表示枚举到边界了 0 1 2 n-1 当index == n时就枚举完了
for (int i = 0; i < n; i++) {
if (st[i] == 1)
System.out.print(i + 1 + " ");
}
System.out.println();
return;
}
// 选
st[index] = 1;//选它
dfs(index + 1);//递归到下一个位置
st[index] = 0;//恢复现场(每次遍历的时候对于左右孩子都是公平的)
// 不选
st[index] = 2;
dfs(index + 1);
st[index] = 0;
}
}
用数组记录每一种方案
import java.util.Scanner;
public class Main {
static int n;
static int[] st = new int[n];;
static int[][] ways = new int[1 << 15][16]; // << 左位运算相当于乘2 最多有2^15种方案;
static int cnt;// 记录方案的数量
public static void dfs(int index) {
if (index == n) {
for (int i = 0; i < n; i++) {
if (st[i] == 1)
ways[cnt][i] = i + 1;
}
cnt++;
return;
}
st[index] = 1;
dfs(index + 1);
st[index] = 0;
st[index] = 2;
dfs(index + 1);
st[index] = 0;
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
n = input.nextInt();
st = new int[n];
ways = new int[1 << 15][16];
dfs(0);
// 统一进行输出
for (int i = 0; i < cnt; i++) {
for (int j = 0; j < n; j++) {
if (ways[i][j] != 0) {
System.out.print(ways[i][j] + " ");
}
}
System.out.println();
}
}
}
题目3:94. 递归实现排列型枚举
分析:依次枚举每个位置放哪个数。
按字典序大小排列其实我们只要在枚举的时候只需注意数从小到大进行枚举,最后的结果自然是按字典序升序排列。
时间复杂度O(n * n ) n个位置,每个位置可以放n个不同的数。
参数:这里相对于指数型枚举多了一步判断每个位置可以放哪些数,所以需要两个变量。
res[] 记录每一种可能方案
used[] 判断每个位置上的每个数有没有被用过
import java.util.*;
//时间复杂度n*n!
public class Main {
static int n;
static int N = 9;
static int[] res;
static boolean[] used;
public static void dfs(int index) {
if (index == n) {
//输出方案
for (int i = 0; i < n; i++) {
System.out.print(res[i] + " ");
}
System.out.println();
return;
}
//依次枚举每个分支,看每个位置可以放哪些数
for (int i = 0; i < n; i++) {
if (!used[i]) {
res[index] = i + 1;//因为i从0开始 所以值要加1
used[i] = true;
dfs(index + 1);
res[index] = 0;
used[i] = false;
}
}
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
n = input.nextInt();
res = new int[N];//0表示初始状态,没有放数,0~N表示放哪个数
used = new boolean[N];//false表示没用过,true表示用过了
dfs(0);
}
}
习题
题目1:93. 递归实现组合型枚举
分析:组合型枚举就是把指数型符合长度的结果挑出来。
从5个数里选3个,即有5 * 4 * 3 / 3 * 2 * 1 = 10种方案,每一种方案不考虑顺序。(即选3个相同的数,不管它们的排列顺序如何,都看做是一种方案)
3个空白的位置,先看第一个位置可以放1 2 3 4 5,5个数即有五种方案,每种方案再考虑第二个位置放哪几个数(除去第一个位置放的数),再考虑第三个位置放那几个数(除去第一个位置和第二个位置上的数)。为了避免重复枚举,即枚举到123,132,321等的情况,我们可以规定后一个数比前一个数大,即枚举下一个位置的数时,从上一个数+1开始枚举。
参数:way[] 记录3个位置可能的方案
index 当前该枚举哪个位置
start 当前最小从哪个数开始枚举
剪枝优化:
我们可以提前发现4——、5——无解,可以提前退出,提高效率。当前正在选第index个数,此时已经选了index - 1个数,从start选到n有n - start +1 个数,index - 1 + n - start + 1 < m => index + n - start < m 提前退出(如果把后面的数都选上都不够m个,当前分支一定无解)
import java.util.Scanner;
public class Main {
static int n;
static int m;
static int N = 30;
static int []way;
public static void dfs(int index,int start) {
if(index + n - start < m) //剪枝优化 当前正在选第index个数,选了index-1个数,从start开始选,选到n,有n-start+1个数
return;//index-1+n-start+1<m 即index+n-start<m 就提前退出 (如果把后面的数都选上都不够m个,当前分支一定无解)
if(index == m) {
for(int i = 0;i < m;i++) {
System.out.print(way[i] + " ");
}
System.out.println();
return;
}
//枚举的时候应该从start开始
for(int i = start;i < n;i++) {
way[index] = i + 1;
dfs(index + 1,i + 1);//枚举下一个位置,当前数是i应该从i+1开始枚举
way[index] = 0;
}
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
n = input.nextInt();
m = input.nextInt();
way = new int [N];
dfs(0,0);
}
}
题目2:1209. 带分数
暴力做法:(个人觉得这个方法容易理解一些)
分析:n = a + b / c,两边同时乘c,c·n = c·a + b
,n已知,枚举a,b,c即可求解。即我们可以先枚举全排列,再枚举每个数的位数,最后判断等式是否成立。
import java.util.Scanner;
//解题思路
//暴力枚举出9个数的全排列,然后用一个长度为9的数组保存全排列的结果
//从全排列的结果中用两重循环暴力分解出三段,通过 i,j将一个排列分割,每段代表一个数
//验证枚举出来的三个数是否满足题干条件,若满足则计数
public class Main {
static int N = 10;
static boolean[] used;//判断这个数是否用过
static int[] st;//记录全排列的结果
static int target;//题目所给的目标数
static int count;//计数,最后输出的结果
// 生成全排列
// 当全排列生成后进行分段
public static void dfs(int index) {
if (index == 9) {//边界
// 用两层循环分成三段
for (int i = 0; i < 7; i++) {
for (int j = i + 1; j < 8; j++) {
int a = cal(0, i);
int b = cal(i + 1, j);
int c = cal(j + 1, 8);
if (a * c + b == target * c) {
count++;
}
}
}
return;
}
//搜索模板 依次枚举每个分支,看每个位置可以放哪些数
for (int i = 1; i <= 9; i++) {
if (!used[i]) {
used[i] = true;
st[index] = i;
dfs(index + 1);
used[i] = false;
}
}
}
//计算每一个数是多少
public static int cal(int l, int r) {
int res = 0;
for (int i = l; i <= r; i++) {
res = res * 10 + st[i];//要在一个数的个位上加上一个数,只需要将这个数乘10,再加上那一个数
}
return res;
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
target = input.nextInt();
used = new boolean[N];
st = new int[N];
dfs(0);
System.out.println(count);
}
}
y总优化做法:其实我们不用枚举b了,当枚举完a和c后,利用公式变换得到b = c·n - c·a
即得到b的值。
import java.util.Scanner;
/*
* n=a+b/c
* 换成
* cn=ca+b
* 枚举a,c再判断b是否合理
*/
public class Main {
static int target;
static int N = 10;
static boolean[] used;
static boolean[] backup;
static int count;
//a是dfs_a所得的,c是dfs_c所得的
public static boolean check(int a, int c) {
// 由a,c的值算出b
int b = target * c - a * c;
// 提前剪枝(若是出现0则直接结束)
if (a == 0 || b <= 0 || c == 0)//注意这里要判断b为负的情况
return false;
// 复制拷贝是否存在的数组(避免重新恢复现场)
backup = (boolean[]) used.clone();
// 遍历b的所有元素
while (b != 0) {
int x = b % 10;// 取个位
b = b / 10;// 把个位去掉
// 判断b是否有位数与a或c重合或者出现数字0
if (x == 0 || backup[x])
return false;
// 标记一下,相当于x已经被用过了
backup[x] = true;
}
//判断得到的a,b,c三数是否能覆盖1~9的全部数字
for (int i = 1; i <= 9; i++) {
if (!backup[i])
return false;
}
return true;
}
//u表示当前用过的数字数量,a是dfs_a所得到的数,c是dfs_c所得到的的数
public static void dfs_c(int u, int a, int c) {
if (u == 10)
return;
//相当于枚举出来一组a,c
if (check(a, c))
count++;
//向下递归推举出下一个c
for (int i = 1; i <= 9; i++) {
if (!used[i]) {
used[i] = true;
dfs_c(u + 1, a, c * 10 + i);
used[i] = false;
}
}
//return;可以将u全部删掉,当st[]数组全为true的时候,递归自然就结束了。
}
//进行第一次深搜,u表示用了多少数字,a表示当前的值
public static void dfs_a(int u, int a) {
// 若是a的值>=target,此时就直接提前结束
if (a >= target)
return;
// 如果说a是满足情况的,那么我们就枚举一下c,后面那个0表示c的大小
if(a > 0) {
dfs_c(u, a, 0);
}
// 枚举一下当前这个位置a可以用哪些数字
for (int i = 1; i <= 9; i++) {
if (!used[i]) {
used[i] = true;
dfs_a(u + 1, a * 10 + i);// 如果这个数没有被用过,那么我们就加上它,并且dfs下一层
used[i] = false;
}
}
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
target = input.nextInt();
used = new boolean[N];
backup = new boolean[N];
dfs_a(0, 0);
System.out.println(count);
}
}
如果有写的不对的地方,希望大家予以改正。