OI Wiki—递归 分治

news2024/11/17 19:33:00

//新生训练,搬运整理

递归

定义

递归(英语:Recursion),在数学和计算机科学中是指在函数的定义中使用函数自身的方法,在计算机科学中还额外指一种通过重复将问题分解为同类的子问题而解决问题的方法。

引入

要理解递归,就得先理解什么是递归。

递归的基本思想是某个函数直接或者间接地调用自身,这样原问题的求解就转换为了许多性质相同但是规模更小的子问题。求解时只需要关注如何把原问题划分成符合条件的子问题,而不需要过分关注这个子问题是如何被解决的。

以下是一些有助于理解递归的例子:

  1. 什么是递归?
  2. 如何给一堆数字排序?答:分成两半,先排左半边再排右半边,最后合并就行了,至于怎么排左边和右边,请重新阅读这句话。
  3. 你今年几岁?答:去年的岁数加一岁,1999 年我出生。
  4. 一个用于理解递归的例子

递归在数学中非常常见。例如,集合论对自然数的正式定义是:1 是一个自然数,每个自然数都有一个后继,这一个后继也是自然数。

递归代码最重要的两个特征:结束条件和自我调用。自我调用是在解决子问题,而结束条件定义了最简子问题的答案。

int func(传入数值) {
  if (终止条件) return 最小子问题解;
  return func(缩小规模);
}

为什么要写递归

  1. 结构清晰,可读性强。例如,分别用不同的方法实现 归并排序:

    // 不使用递归的归并排序算法
    template <typename T>
    void merge_sort(vector<T> a) {
      int n = a.size();
      for (int seg = 1; seg < n; seg = seg + seg)
        for (int start = 0; start < n - seg; start += seg + seg)
          merge(a, start, start + seg - 1, std::min(start + seg + seg - 1, n - 1));
    }
    
    // 使用递归的归并排序算法
    template <typename T>
    void merge_sort(vector<T> a, int front, int end) {
      if (front >= end) return;
      int mid = front + (end - front) / 2;
      merge_sort(a, front, mid);
      merge_sort(a, mid + 1, end);
      merge(a, front, mid, end);
    }

    显然,递归版本比非递归版本更易理解。递归版本的做法一目了然:把左半边排序,把右半边排序,最后合并两边。而非递归版本看起来不知所云,充斥着各种难以理解的边界计算细节,特别容易出 bug,且难以调试。

  2. 练习分析问题的结构。当发现问题可以被分解成相同结构的小问题时,递归写多了就能敏锐发现这个特点,进而高效解决问题。

        

递归的缺点

在程序执行中,递归是利用堆栈来实现的。每当进入一个函数调用,栈就会增加一层栈帧,每次函数返回,栈就会减少一层栈帧。而栈不是无限大的,当递归层数过多时,就会造成 栈溢出 的后果。

显然有时候递归处理是高效的,比如归并排序;有时候是低效的,比如数孙悟空身上的毛,因为堆栈会消耗额外空间,而简单的递推不会消耗空间。比如这个例子,给一个链表头,计算它的长度:

// 典型的递推遍历框架
int size(Node *head) {
  int size = 0;
  for (Node *p = head; p != nullptr; p = p->next) size++;
  return size;
}

// 我就是要写递归,递归天下第一
int size_recursion(Node *head) {
  if (head == nullptr) return 0;
  return size_recursion(head->next) + 1;
}

[二者的对比,compiler 设为 Clang 10.0,优化设为 O1](https://quick-bench.com/q/rZ7jWPmSdltparOO5ndLgmS9BVc)

递归的优化

主页面:搜索优化 和 记忆化搜索

比较初级的递归实现可能递归次数太多,容易超时。这时需要对递归进行优化。1

分治

定义

分治(英语:Divide and Conquer),字面上的解释是「分而治之」,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。

过程

分治算法的核心思想就是「分而治之」。

大概的流程可以分为三步:分解 -> 解决 -> 合并。

  1. 分解原问题为结构相同的子问题。
  2. 分解到某个容易求解的边界之后,进行递归求解。
  3. 将子问题的解合并成原问题的解。

分治法能解决的问题一般有如下特征:

  • 该问题的规模缩小到一定的程度就可以容易地解决。
  • 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质,利用该问题分解出的子问题的解可以合并为该问题的解。
  • 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。

注意

如果各子问题是不独立的,则分治法要重复地解公共的子问题,也就做了许多不必要的工作。此时虽然也可用分治法,但一般用 动态规划 较好。

以归并排序为例。假设实现归并排序的函数名为 merge_sort。明确该函数的职责,即 对传入的一个数组排序。这个问题显然可以分解。给一个数组排序等于给该数组的左右两半分别排序,然后合并成一个数组。

void merge_sort(一个数组) {
  if (可以很容易处理) return;
  merge_sort(左半个数组);
  merge_sort(右半个数组);
  merge(左半个数组, 右半个数组);
}

传给它半个数组,那么处理完后这半个数组就已经被排好了。注意到,merge_sort 与二叉树的后序遍历模板极其相似。因为分治算法的套路是 分解 -> 解决(触底)-> 合并(回溯),先左右分解,再处理合并,回溯就是在退栈,即相当于后序遍历。

merge 函数的实现方式与两个有序链表的合并一致。

要点

写递归的要点

明白一个函数的作用并相信它能完成这个任务,千万不要跳进这个函数里面企图探究更多细节, 否则就会陷入无穷的细节无法自拔,人脑能压几个栈啊。

以遍历二叉树为例。

void traverse(TreeNode* root) {
  if (root == nullptr) return;
  traverse(root->left);
  traverse(root->right);
}

这几行代码就足以遍历任何一棵二叉树了。对于递归函数 traverse(root),只要相信给它一个根节点 root,它就能遍历这棵树。所以只需要把这个节点的左右节点再传给这个函数就行了。

同样扩展到遍历一棵 N 叉树。与二叉树的写法一模一样。不过,对于 N 叉树,显然没有中序遍历。

void traverse(TreeNode* root) {
  if (root == nullptr) return;
  for (auto child : root->children) traverse(child);
}

区别

递归与枚举的区别

递归和枚举的区别在于:枚举是横向地把问题划分,然后依次求解子问题;而递归是把问题逐级分解,是纵向的拆分。

递归与分治的区别

递归是一种编程技巧,一种解决问题的思维方式;分治算法很大程度上是基于递归的,解决更具体问题的算法思想。

例题详解

Q:

给定一个二叉树,它的每个结点都存放着一个整数值。

找出路径和等于给定数值的路径总数。

路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

二叉树不超过 1000 个节点,且节点数值范围是 [-1000000,1000000] 的整数。

示例:

root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8

      10
     /  \
    5   -3
   / \    \
  3   2   11
 / \   \
3  -2   1

返回 3。和等于 8 的路径有:

1.  5 -> 3
2.  5 -> 2 -> 1
3. -3 -> 11
/**
 * 二叉树结点的定义
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */

A:

int pathSum(TreeNode *root, int sum) {
  if (root == nullptr) return 0;
  int pathImLeading = count(root, sum);  // 自己为开头的路径数
  int leftPathSum = pathSum(root->left, sum);  // 左边路径总数(相信它能算出来)
  int rightPathSum =
      pathSum(root->right, sum);  // 右边路径总数(相信它能算出来)
  return leftPathSum + rightPathSum + pathImLeading;
}

int count(TreeNode *node, int sum) {
  if (node == nullptr) return 0;
  // 能不能作为一条单独的路径呢?
  int isMe = (node->val == sum) ? 1 : 0;
  // 左边的,你那边能凑几个 sum - node.val ?
  int leftNode = count(node->left, sum - node->val);
  // 右边的,你那边能凑几个 sum - node.val ?
  int rightNode = count(node->right, sum - node->val);
  return isMe + leftNode + rightNode;  // 我这能凑这么多个
}

P:

题目看起来很复杂,不过代码却极其简洁。

首先明确,递归求解树的问题必然是要遍历整棵树的,所以二叉树的遍历框架(分别对左右子树递归调用函数本身)必然要出现在主函数 pathSum 中。那么对于每个节点,它们应该干什么呢?它们应该看看,自己和它们的子树包含多少条符合条件的路径。好了,这道题就结束了。

按照前面说的技巧,根据刚才的分析来定义清楚每个递归函数应该做的事:

PathSum 函数:给定一个节点和一个目标值,返回以这个节点为根的树中,和为目标值的路径总数。

count 函数:给定一个节点和一个目标值,返回以这个节点为根的树中,能凑出几个以该节点为路径开头,和为目标值的路径总数。

还是那句话,明白每个函数能做的事,并相信它们能够完成。

总结下,PathSum 函数提供了二叉树遍历框架,在遍历中对每个节点调用 count 函数(这里用的是先序遍历,不过中序遍历和后序遍历也可以)。count 函数也是一个二叉树遍历,用于寻找以该节点开头的目标值路径。

OI Wiki原文:递归 & 分治 - OI Wiki

~~~//仅当笔者个人备忘录使用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1637488.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python语言零基础入门——模块

目录 一、模块的导入与使用 1.模块的导入 2.模块的使用 二、包的使用 1.包 2.包的使用 三、常见的标准库 1.random的运用举例 2.random小游戏 &#xff08;1&#xff09;石头剪刀布 &#xff08;2&#xff09;猜大小 3.re 4.time库的使用 5.turtle库的使用 6.so…

Zapier 与生成式 AI 的自动化(一)

原文&#xff1a;zh.annas-archive.org/md5/057fe0c351c5365f1188d1f44806abda 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 前言 当组织处理手动和重复性任务时&#xff0c;生产力会遇到重大问题。Zapier 处于无代码运动的前沿&#xff0c;提供了一种先进的工具&a…

【C++】详解string类

目录 简介 框架 构造 全缺省构造函数 ​编辑 传对象构造函数 拷贝构造 析构函数 容量 size() capacity&#xff08;&#xff09; empty() clear() reserve() ​编辑 resize() 遍历 检引用符号"[ ]"的重载 迭代器 begin() end() rbegin() rend(…

【竞技宝】欧冠:拜仁2比2战平皇马,金玟哉状态低迷

拜仁在欧冠半决赛首回合较量中坐镇主场跟皇马相遇,这场比赛踢得非常激烈好看。拜仁并没有在主场苟着踢,而是跟皇马打对攻,让球迷大呼过瘾。最终,拜仁与皇马激战90分钟后,以2比2比分战平。对于这样的比赛结果,大部分拜仁球迷都觉得不服气。因为,他们认为自家中卫金玟哉太差,如果…

3D建模在游戏行业的演变和影响

多年来&#xff0c;游戏行业经历了显着的转变&#xff0c;这主要是由技术进步推动的。 深刻影响现代游戏的关键创新之一是 3D 建模领域。 从像素化精灵时代到我们今天探索的错综复杂的游戏世界&#xff0c;3D 建模已成为游戏开发不可或缺的基石。 本文讨论 3D 建模在游戏行业中…

智能健康管理子卡(ChMC/IPMC)模块,支持IPMI2.0标准通信协议,0Kbps可配置,可通过IPMI命令控制其他刀片开关电,具备故障上报、开机自检

是一款BMC子卡&#xff0c;该子卡输出1路千兆网络接口与千兆交换芯片相连,对外输出1路百兆调试网络接口&#xff0c;对外输出2路8路&#xff08;可选&#xff09;IPMB&#xff08;I2C&#xff09;接口并做隔离处理&#xff08;I2C BUFFER&#xff09;&#xff0c;支持IPMI2.0标…

【代码随想录——链表】

1.链表 什么是链表&#xff0c;链表是一种通过指针串联在一起的线性结构&#xff0c;每一个节点由两部分组成&#xff0c;一个是数据域一个是指针域&#xff08;存放指向下一个节点的指针&#xff09;&#xff0c;最后一个节点的指针域指向null&#xff08;空指针的意思&#…

rust疑难杂症

rust疑难杂症解决 边碰到边记录&#xff0c;后续可能会逐步增加&#xff0c;备查 cargo build时碰到 Blocking waiting for file lock on package cache 原因是Cargo 无法获取对包缓存的文件锁&#xff0c; 有时vscode中项目比较多&#xff0c;如果其中某些库应用有问题&…

脸爱云一脸通智慧管理平台 SystemMng 管理用户信息泄露漏洞(XVE-2024-9382)

0x01 产品简介 脸爱云一脸通智慧管理平台是一套功能强大,运行稳定,操作简单方便,用户界面美观,轻松统计数据的一脸通系统。无需安装,只需在后台配置即可在浏览器登录。 功能包括:系统管理中心、人员信息管理中心、设备管理中心、消费管理子系统、订餐管理子系统、水控管…

Kafka介绍、安装以及操作

Kafka消息中间件 1.Kafka介绍 1.1 What is Kafka&#xff1f; 官网&#xff1a; https://kafka.apache.org/超过 80% 的财富 100 强公司信任并使用 Kafka &#xff1b;Apache Kafka 是一个开源分布式事件流平台&#xff0c;被数千家公司用于高性能数据管道、流分析、数据集成…

CentOS7安装MySQL8.3(最新版)踩坑教程

安装环境说明 项值系统版本CentOS7 &#xff08;具体是7.9&#xff0c;其他7系列版本均可&#xff09;位数X86_64&#xff0c;64位操作系统MySQL版本mysql-8.3.0-1.el7.x86_64.rpm-bundle.tar 实际操作 官网下载安装包 具体操作不记录&#xff0c;相关教程很多。 mkdir /o…

Mysql-黑马

Mysql-黑马 编写规范&#xff1a;## 一级1. 二级三级 1.Mysql概述 数据库概念mysql数据仓库 cmd启动和停止 net start mysql180 net stop mysql180备注&#xff1a;其中的mysql180是服务名 客户端连接 远程连接数据仓库 -h 主机号 -P端口号 mysql [-h 127.0.0.1] [-P 33…

YOLOv5改进之bifpn

目录 一、原理 二、代码 三、在YOLOv5中的应用 一、原理 论文链接:

Android4.4真机移植过程笔记(二)

5、盘符挂载 先定义overlay机制路径&#xff0c;后面storage_list.xml要用到&#xff1a; 在路径&#xff1a; rk3188_android4.4.1/device/rockchip/OK1000/overlay/frameworks/base/core/res/res/xml/定义好&#xff0c;注意名字要和emmc的代码片段&#xff08;往下面看&am…

大数据信用花了,一般多久能正常?

在当今数字化时代&#xff0c;大数据技术被广泛应用于各个领域&#xff0c;包括金融、电商、社交等。然而&#xff0c;随着大数据技术的普及&#xff0c;个人信用问题也日益凸显&#xff0c;其中“大数据信用花”现象尤为引人关注。那么&#xff0c;大数据信用花究竟是什么?一…

(四)小程序学习笔记——自定义组件

1、组件注册——usingComponents &#xff08;1&#xff09;全局注册&#xff1a;在app.json文件中配置 usingComponents进行注册&#xff0c;注册后可以在任意页面使用。 &#xff08;2&#xff09;局部注册&#xff0c;在页面的json文件中配置suingComponents进行注册&#…

2023 广东省大学生程序设计竞赛(部分题解)

目录 A - Programming Contest B - Base Station Construction C - Trading D - New Houses E - New but Nostalgic Problem I - Path Planning K - Peg Solitaire A - Programming Contest 签到题&#xff1a;直接模拟 直接按照题目意思模拟即可&#xff0c;为了好去…

labview强制转换的一个坑

32位整形强制转换成枚举的结果如何&#xff1f; 你以为的结果是 实际上的结果是 仔细看&#xff0c;枚举的数据类型是U16&#xff0c;"1"的数据类型是U32&#xff0c;所以转换产生了不可预期的结果。所以使用强制转换时一定要保证两个数据类型一致&#xff0c;否则…

04 - 步骤 JSON input

简介 Kettle 的 JSON Input 步骤是用于从 JSON 格式的数据源中读取数据的步骤。它允许用户指定 JSON 格式的输入数据&#xff0c;然后将其转换成 Kettle 中的行流数据&#xff0c;以供后续的数据处理、转换和加载操作使用。 使用 场景 1、拖拽到面板 2、指定JSON input 为 K…

正点原子[第二期]Linux之ARM(MX6U)裸机篇学习笔记-9.1-LED灯(模仿STM32驱动开发实验)

前言&#xff1a; 本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM&#xff08;MX6U&#xff09;裸机篇”视频的学习笔记&#xff0c;在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。…