XTU OJ(三)递归(分治、dfs、回溯)专题

对递归的理解

递归是一种非常简单、直观且优雅的程序设计思维。Wikipedia 对递归给出的定义是:递归(英语:Recursion),又译为递回,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。这样说可能不大好理解,我们举个形象的例子,因为递归包括两个步骤:,所以可以考虑一个下楼梯拿外卖再上楼回寝的过程。

  • 下楼的每一步都和上一步动作上没什么区别
  • 每下一次台阶我们都离外卖更近一点
  • 我们拿到外卖后会继续上楼

把这个过程写成伪代码就是:

拿外卖 (剩余台阶数) {
	if (剩余台阶数 == 0) {
		拿到外卖;
		return 外卖;
	}
	else {
		剩余台阶数 = 剩余台阶数 - 1;
		拿外卖 (剩余台阶数);
	}
}
// 假设有 100 级台阶
拿外卖(100);
  • 每下一次台阶我们都离外卖更近一点母问题规模缩小为子问题
  • 下楼的每一步都和上一步动作上没什么区别子问题和母问题可以采用同一个解决方案
  • 我们拿到外卖后会继续上楼解决子问题后将答案带回给母问题

❗用递归解决问题的步骤

  1. 确定问题是否能用或者是否适合用递归来解决
  2. 确定对递归的每一步都有效的解决方案
  3. 确定递归的终点以及返回值

❗注意

  • 递归只是一种程序设计的思维或技巧,不是具体的算法
  • 递归非常优雅,主要要是指递归的代码通常结构清晰且可读性强
  • 递归也有明显的缺点,在程序执行中,递归是利用堆栈来实现的,由于计算机内存限制,递归层数过多时容易造成堆栈溢出
  • 利用了递归思维的算法主要有分治、 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,分治


  • 求中位数可以理解为一个 Top-k 问题,利用的性质很容易解决,参见 Top-k 问题
  • 求出当前序列的带权中位数后,可以递归地处理左半部分和右半部分

核心代码

// 自定义了一个 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;
}
  • 12
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值