文章目录
对递归的理解
递归是一种非常简单、直观且优雅的程序设计思维。Wikipedia 对递归给出的定义是:递归(英语:Recursion),又译为递回,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。这样说可能不大好理解,我们举个形象的例子,因为递归包括两个步骤:递和归,所以可以考虑一个下楼梯拿外卖再上楼回寝的过程。
- 下楼的每一步都和上一步动作上没什么区别
- 每下一次台阶我们都离外卖更近一点
- 我们拿到外卖后会继续上楼
把这个过程写成伪代码就是:
拿外卖 (剩余台阶数) {
if (剩余台阶数 == 0) {
拿到外卖;
return 外卖;
}
else {
剩余台阶数 = 剩余台阶数 - 1;
拿外卖 (剩余台阶数);
}
}
// 假设有 100 级台阶
拿外卖(100);
- 每下一次台阶我们都离外卖更近一点(母问题规模缩小为子问题)
- 下楼的每一步都和上一步动作上没什么区别(子问题和母问题可以采用同一个解决方案)
- 我们拿到外卖后会继续上楼(解决子问题后将答案带回给母问题)
❗用递归解决问题的步骤
- 确定问题是否能用或者是否适合用递归来解决
- 确定对递归的每一步都有效的解决方案
- 确定递归的终点以及返回值
❗注意
- 递归只是一种程序设计的思维或技巧,不是具体的算法
- 递归非常优雅,主要要是指递归的代码通常结构清晰且可读性强
- 递归也有明显的缺点,在程序执行中,递归是利用堆栈来实现的,由于计算机内存限制,递归层数过多时容易造成堆栈溢出
- 利用了递归思维的算法主要有分治、 dfs(深度优先搜索) 和回溯,理解了递归之后再去看这些算法会变得容易许多
题单
- Problem A 1160 猜数字
- Problem B 1186 Tourist 2
- Problem C 1418 消星星
- Problem D 1439 骰子
- Problem E 1445 中位数
- Problem F 1457 图像
- Problem G 1488 同构字符串
- Problem H 1490 通配符
参考资料
A 1160 猜数字
回溯
- 最开始我的思路是根据给定的 x x x 和 y y y 正向去推可能的值,但是这样只能得到结果的部分位,字典序的话输出还得去枚举其他位所有的可能值
- 后来采取的做法是直接搜索 1 ~ N 中所有不同的 4 位数组合的全排列,再来判断这个序列是否符合要求
核心代码
// 检查得到的序列是否满足已知的 x(数字和位置都对) 和 y(数字对但位置不对)
static boolean check (int[] val) {
int x = 0, y = 0;
for (int i = 0; i < K; ++i) {
x = 0;
y = 0;
for (int j = 0; j < 4; ++j) {
if (val[j] == table[i][j]) ++x;
else for (int k = 0; k < 4; ++k) if (j != k && val[j] == table[i][k]) ++y;
}
if (x != table[i][4] || y != table[i][5]) return false;
}
return true;
}
static void solve (int[] val, int step) {
if (step == 4) {
if (check(val) == true) res.add(Arrays.copyOf(val, 4));
return ;
}
for (int i = 1; i <= N; ++i) {
if (flag[i] != true) {
val[step] = i;
flag[i] = true;
solve(val, step + 1);
flag[i] = false;
}
}
}
B 1186 Tourist 2
回溯,减枝
- 搜索所有可能的路径,确定每一条路径的花费,更新最小花费
- 减枝:如果对某一条路径,还没到终点就发现其花费已经超过当前已确定的最小花费,可以提前结束对这条路径的搜索
核心代码
static void solve (int[] val, int step, int sum) {
if (step == N) {
if (sum < res) {
path.clear();
path.add(Arrays.copyOf(val, N));
res = sum;
}
else if (sum == res) path.add(Arrays.copyOf(val, N));
return ;
}
for (int i = 1; i <= N; ++i) {
if (flag[i] != true) {
val[step] = i;
int cur_step_fare = 0;
// 确定从上一步到当前步所需的花费
if (step == 0) cur_step_fare = fare[0][val[0]];
else cur_step_fare = fare[val[step - 1]][val[step]];
if (step == N - 1) cur_step_fare += fare[val[N - 1]][0];
// 更新当前花费总和
sum += cur_step_fare;
flag[i] = true;
// 没超过当前已确定的最小花费的话可以继续搜索
if (sum <= res) solve(val, step + 1, sum);
// 回溯,更新当前花费总和
sum -= cur_step_fare;
flag[i] = false;
}
}
}
C 1418 消星星
dfs
- 搜索所有的格子,每到一个格子,就对这个格子进行标记,然后向上下左右四个方向继续搜索
- 注意:如果当前位置已超出边界或已经被搜索过就无需重复搜索
- 每消完一次星星就将所需的能量的加 1
核心代码
static void solve (char ch, int x, int y) {
if (x < 0 || x >= n || y < 0 || y >= m || table[x][y] != ch) return ;
table[x][y] = '\0';
solve(ch, x + 1, y);
solve(ch, x - 1, y);
solve(ch, x, y + 1);
solve(ch, x, y - 1);
}
D 1439 骰子
回溯
- 搜索所有可能的骰子顶面数字组合
- 统计所有数字和出现的总次数,出现的数字和的种数,以及每种数字和出现的次数
- 最后的概率 = 该种数字和出现的次数 / 所有数字和出现的总次数
核心代码
static void solve (int step) {
if (step == n) {
++general_cnt; // general_cnt: 所有数字和出现的总次数
if (cnt[tmp_sum] == 0) ++res; // res: 出现的数字和的种数
++cnt[tmp_sum]; // cnt 数组: 每种数字和出现的次数
return ;
}
for (int i = 0; i < MAX_COLUMN; ++i) {
// flag 是一个用来存储所有骰子各面数字的二维数组
if (flag[step][i] != true) {
flag[step][i] = true;
tmp_sum += table[step][i];
solve(step + 1);
flag[step][i] = false;
tmp_sum -= table[step][i];
}
}
}
E 1445 中位数
dfs,分治
核心代码
// 自定义了一个 tmp 类,包括索引和值两部分
static class tmp implements Comparable<tmp>{
int val;
int index;
public tmp () {}
@Override
public int compareTo (tmp obj) { return this.val - obj.val; }
}
static tmp[] arr = new tmp[MAX_N];
static void solve (int left, int right, int weight) {
if (left >= right) return ;
/* Java 中可以利用优先队列来模拟堆,默认为小根堆,
若想实现大根堆,使用 lambda 表达式修改 Comparator 即可 */
Queue<tmp> heap = new PriorityQueue<>();
int mid = left + (right - left) / 2;
for (int i = left; i <= mid; ++i) heap.offer(arr[i]);
for (int i = mid + 1; i < right; ++i) {
if (arr[i].compareTo(heap.peek()) > 0) {
heap.poll();
heap.offer(arr[i]);
}
}
if ((right - left) % 2 != 1) heap.poll();
res += heap.peek().val * weight;
solve(left, heap.peek().index, weight + 1);
solve(heap.peek().index + 1, right, weight + 1);
}
F 1457 图像
dfs
- 将图像分成四个子图,计算出每个子图的宽度及元素个数
- 确定目标在哪个子图里,更新坐标
- 继续对该子图递归直至子图边长为 1 1 1,得到结果
核心代码
// 确定最初的图的宽度
static long get_w () {
if (k == 1) return 1;
for (long w = 2, size; ; w *= 2) {
size = w * w;
if (k >= size / 4 && k <= size) return w;
}
}
static void solve (long w, long up_left) {
if (w == 1) return ;
long size = w * w / 4; // 计算当前子图的元素个数
long up_right = up_left + size;
long bottom_left = up_right + size;
long bottom_right = bottom_left + size;
// 在第一个子图里
if (k >= up_left && k < up_right) solve(w / 2, up_left);
// 在第二个子图里
if (k >= up_right && k < bottom_left) {
y += w / 2;
solve(w / 2, up_right);
}
// 在第三个子图里
if (k >= bottom_left && k < bottom_right) {
x += w / 2;
solve(w / 2, bottom_left);
}
// 在第四个子图里
if (k >= bottom_right) {
x += w / 2;
y += w / 2;
solve(w / 2, bottom_right);
}
}
G 1488 同构字符串
dfs
- 注意串长必须为偶数
- 先判断两个字符串是否相等,相等就一定为真
- 两个条件任意一个满足即可认为是同构的: A A A 的左(右)半等于 B B B 的左(右)半或 A A A 的左(右)半等于 B B B 的右(左)半
- 不断地递归判断直至字符串长度为 1
核心代码
static boolean solve_Jun0603 (String A, String B) {
// Java 判断 String 类型是否相等需要使用 equals 方法,切勿直接用 ==
if (A.equals(B)) return true;
else {
int length_A = A.length(), length_B = B.length();
if (length_A % 2 != 0 || length_B % 2 != 0) return false;
else {
String A_left = A.substring(0, length_A / 2);
String A_right = A.substring(length_A / 2, length_A);
String B_left = B.substring(0, length_B / 2);
String B_right = B.substring(length_B / 2, length_B);
boolean condition1 = solve_Jun0603(A_left, B_left) && solve_Jun0603(A_right, B_right);
boolean condition2 = solve_Jun0603(A_left, B_right) && solve_Jun0603(A_right, B_left);
return condition1 || condition2;
}
}
}
H 1490 通配符
dfs,记忆化搜索
- 从头开始递归地搜索模式串和目标串
- 如果两个串同时到达终点(即索引等于串长)说明完全匹配
- 如果模式串先到了终点说明目标串中有无法被匹配到的字符
- 如果目标串先到了终点,不能武断地认为模式串有多余字符,要考虑模式串中未用到的字符,如果仅余下一个字符且是 *,因为 * 可以匹配 0 0 0 个字符,所以这种情况仍然是完全匹配的,除此之外的情况则不匹配
- 搜索过程中,如果模式串方的字符与目标串方的索引相同,或模式串方为 _(可以匹配任意一个字符),就可以同时继续搜索当前位置的下一个字符
- 搜索过程中,如果模式串方的字符是 *,那么需要考虑两种情况:一是直接跳过它,也就是让它匹配 0 0 0 个字符,二是递归地让它匹配 n n n 个字符,只要有一种能够匹配上就说明最终结果是可以完全匹配的
- 为了避免递归次数过多导致超时,可以采用记忆化搜索,即记录每一次匹配的结果,如果此后遇到相同情况可以直接查看此前记忆的结果里有无记录,有的话可以直接采用
核心代码
static boolean solve (int i, int j) {
if (i == p.length() && j == s.length()) return true;
else if (i == p.length()) return false;
else if (j == s.length()) {
if (p.charAt(i) == '*' && i == p.length() - 1) return true;
return false;
}
if (cache[i][j] != -1) return cache[i][j] == 1;
boolean flag = false;
if (p.charAt(i) == '_' || p.charAt(i) == s.charAt(j))
flag = solve(i + 1, j + 1);
else if (p.charAt(i) == '*')
flag = solve(i + 1, j) || solve(i, j + 1);
cache[i][j] = flag == true ? 1 : 0;
return flag;
}