当前位置: 首页 > article >正文

算法 | 递归与递推

递归与递推(上)

1.递归与递推的基本概念

在数学与计算机科学中,递归递推是两种非常重要的概念,它们常用于定义序列、解决问题和设计算法。虽然两者看起来相似,但它们的本质和应用有所不同。

1.1 递归(Recursion)

递归是指一个过程(函数、公式等)直接或间接地调用自身。在递归的定义中,通常需要有基本情况(base case)来终止递归过程,否则递归会无限进行下去。

递归过程一般包含两个关键要素:

  • 基本情况:递归终止的条件,避免了无限递归。
  • 递归步骤:问题的一个子问题,通过调用自身来进行求解。

递归示意图

递归通常通过一个问题分解为相同类型的子问题来实现。下面的示意图可以帮助理解递归的过程:

问题 => 子问题1 => 子问题1的解
      => 子问题2 => 子问题2的解
      => 子问题3 => 子问题3的解

比如计算阶乘的递归方式: n!=n×(n−1)!,当 n=1 时返回 1。

阶乘递归例子:
5! = 5 × 4!  
4! = 4 × 3!  
3! = 3 × 2!  
2! = 2 × 1!  
1! = 1  (基本情况)

递归调用树示意图:

               5!
             /    \
          5 * 4!   4!
                  /    \
               4 * 3!   3!
                       /    \
                    3 * 2!   2!
                            /    \
                         2 * 1!   1

每个递归调用都进入更小的子问题,直到达到基本情况 1!=1,然后逐步返回计算结果。

1.2 递推(Recurrence)

递推是指通过前一个或前几个数值来逐步推算后续的数值。递推是一种通过已知信息进行推导和计算的过程,通常用公式来表示。

递推关系式包含:

  • 初始条件:给定序列的前几个元素。
  • 递推公式:使用前一个或前几个元素来计算下一个元素。

递推示意图

递推通常通过已知初值和递推公式,逐步推算出结果。下面的示意图描述了递推过程:

已知初值 -> 递推公式 -> 计算下一个数 -> 重复直到所有数值都被求出

例如,斐波那契数列是一个经典的递推关系: F(n)=F(n−1)+F(n−2),初始条件:F(0)=0,F(1)=1。

斐波那契递推例子:
F(2) = F(1) + F(0) = 1 + 0 = 1
F(3) = F(2) + F(1) = 1 + 1 = 2
F(4) = F(3) + F(2) = 2 + 1 = 3
...

斐波那契数列递推示意图:

               F(5)
             /    \
       F(4) + F(3)
      /    \    /    \
  F(3)+F(2)  F(2)+F(1) 
  /   \    /    \   /  \
F(2)+F(1) F(1)+F(0) F(1) F(0)

每一层递推都依赖于前面计算的数值,通过递推公式计算每个数值,直到初始条件 F(0)=0 和 F(1)=1 被找到。

1.3 递归与递推的区别

|特点|递归|递推| |-|-|-| |定义|问题通过调用自身来求解|使用已知的前几个元素来推算后续值| |过程|通过子问题的求解逐步深入,直到基本情况|通过递推公式,逐步计算后续数值| |终止条件|需要基本情况来避免无限递归|需要初始条件来开始递推| |实现方式|通常采用函数调用自己来实现|通常通过公式或循环来实现|

2.经典的算法题

2.1递归实现指数型枚举

题目链接:递归实现指数型枚举

从 1∼n 这 n 个整数中随机选取任意多个,输出所有可能的选择方案。

输入格式

输入一个整数 n。

输出格式

每行输出一种方案。

同一行内的数必须升序排列,相邻两个数用恰好 1 个空格隔开。

对于没有选任何数的方案,输出空行。

本题有自定义校验器(SPJ),各行(不同方案)之间的顺序任意。

数据范围

1≤n≤15

输入样例:
3

2.1.1解题思路

n个数每个数可选或不选

因为n≤15所以最多有2^15次方种状态

在这里插入图片描述

解题步骤

递归树的结构

  • 选择(选):每个数都可以选择加入方案。
  • 不选:每个数也可以选择不加入方案。
  • 未考虑:对于每个数,我们有两个选择:选或者不选,递归的每个层级考虑这些选择。

递归回溯

  • 使用递归的方式从第一个数字开始,进行两种决策:
    • :将当前数字加入当前的方案,并进入下一层递归。
    • 不选:跳过当前数字,进入下一层递归。
  • 递归会一直进行,直到达到所需的末端状态。

输出所有方案

  • 每一层递归的选择决定了最终生成的一个方案,所有可能的选择路径最终都会遍历并输出。
  • 空集也应当作为一种结果输出,递归终止时可以返回一个空集合。
算法具体步骤
  • 递归函数:设定递归函数来生成所有选择方案。
  • 回溯:每次递归返回时,如果当前数字被选,则继续进入下一层递归;如果不选,则跳过当前数字。
  • 结果输出:所有递归结束时,存储并输出每个有效的结果。
时间复杂度
  • 对于每个数字来说,我们有两种选择(选或不选),所以总共有 2^n 种可能的选择方案。因此,时间复杂度为 O(2^n)。

2.1.2代码实现

头文件(最常用,后文的头文件也是这四个不再赘述

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;

定义全局变量,注意是为了在dfs函数和主函数中都能用

const int N=16;//最多有15个数,遍历时从1开始所以N=16
int n;
int st[N];//记录每个数状态的数组 1选 -1不选

dfs函数

void dfs(int u)
{
  if(u>n)//u从1开始,如果u>n递归结束
  {
    for(int i=1;i<=n;i++)//遍历记录状态的数组
    {
      if(st[i]==1)//被选的数 输出
      {
        printf("%d ",i);
      }
    }
    puts("");//puts是输出字符串+回车,现在字符串为空相当于回车
    return;
  }
  //递归
  st[u]=1;
  dfs(u+1);

  st[u]=-1;
  dfs(u+1);
}

主函数

int main()
{
  cin>>n;
  dfs(1);//从1开始考虑
  return 0;
}

如果我们想要在dfs()函数中记录全部状态

定义全局变量时应该添加一个二维数组记录状态

const int N=16;//最多有15个数,遍历时从1开始所以N=16
int n;
int st[N];//记录每个数状态的数组 1选 -1不选
int ways[1<<15][16],cnt=0;//cnt是计数器

在 C++ 中,1 << 15 是一种位操作表达式,它表示将整数 1 左移 15 位。

1 << 15 等价于 2^15,即 32768。左移运算相当于将数字乘以 2 的幂。

详细解释:

  • <<左移运算符,它会将一个数的二进制位向左移动指定的位数。
  • 1 << 15 中,1 是一个整数,它的二进制表示为 0000...0001(假设为 32 位整数)。
  • 通过左移 15 位,意味着把 1 的二进制表示向左移动 15 个位置。移动后,1 变成 1000...0000(总共有 15 个零在 1 后面)。

举例:

假设我们使用 32 位的整数表示:

  • 1 的二进制表示:00000000000000000000000000000001
  • 1 << 15 的结果将是:10000000000000000000000000000000,即 32768。

修改dfs函数

void dfs(int u)
{
  if(u>n)//u从1开始,如果u>n递归结束
  {
    /*for(int i=1;i<=n;i++)//遍历记录状态的数组
    {
      if(st[i]==1)//被选的数 输出
      {
        printf("%d ",i);
      }
    }
    puts("");//puts是输出字符串+回车,现在字符串为空相当于回车*/
    for(int i=1;i<=n;i++)
    {
      if(st[i]==1)//被选的数记录在数组中
      {
        ways[cnt][i]=[i];
      }
    }
    cnt++;
    return;
  }
  //递归
  st[u]=1;
  dfs(u+1);

  st[u]=-1;
  dfs(u+1);
}

在主函数中输出

int main()
{
  cin>>n;
  dfs(1);//从1开始考虑
  for(int i=0;i<cnt;i++)
  {
    for(int j=1;j<=n;j++)
    {
      printf("%d ",ways[i][j]);
    }
    puts("");
  }
  return 0;
}

如果用vector来记录,代码如下

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=16;//最多有15个数,遍历时从1开始所以N=16
int n;
int st[N];//记录每个数状态的数组 1选 -1不选
vector<vector<int>> ways;//可变长的二维数组
void dfs(int u)
{
  if(u>n)//u从1开始,如果u>n递归结束
  {
    vector<int> way;
    for(int i=1;i<=n;i++)
    {
      if(st[i]==1)//被选的数记录在数组中
      {
        way.push_back(i);
      }
    }
    ways.push_back(way);
    return;
  }
  //递归
  st[u]=1;
  dfs(u+1);

  st[u]=-1;
  dfs(u+1);
}


int main()
{
  cin>>n;
  dfs(1);//从1开始考虑
  for(int i=0;i<ways.size();i++)
  {
    for(int j=0;j<ways[i].size();j++)
    {
      printf("%d ",ways[i][j]);
    }
    puts("");
  }

  return 0;
}

2.2递归实现排列型枚举

题目链接:递归实现排列型枚举

把 1∼n 这 n 个整数排成一行后随机打乱顺序,输出所有可能的次序。

输入格式

一个整数 n。

输出格式

按照从小到大的顺序输出所有方案,每行 1 个。

首先,同一行相邻两个数用一个空格隔开。

其次,对于两个不同的行,对应下标的数一一比较,字典序较小的排在前面。

数据范围

1≤n≤9

输入样例:
3
输出样例:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

2.2.1题目思路

在这里插入图片描述

解题思路

这个问题要求我们将 1∼nn 个整数排成一行并输出所有可能的排列,按照字典序从小到大的顺序输出所有排列。

1. 排列的基本概念

排列是指从一组元素中选择并按顺序排列这些元素。对于 n 个整数 [1, 2, ..., n],其所有可能的排列可以通过 n! (n的阶乘) 种方式排列出来。

在字典序中,我们要求按照每一位的数字进行逐一比较。可以理解为按升序排列每一组数字。

2. 如何生成所有排列

可以使用回溯算法(DFS)来生成所有排列。通过递归,我们可以逐步选择未使用的数字,并将这些数字填入排列中的每一个位置。递归深度达到 n 时,表示一个完整的排列生成,我们就可以打印它。

3. 回溯方法
  • 使用一个数组(例如 used 数组)来记录每个数字是否已经被选择。
  • 在每层递归中,遍历所有可能的未选择的数字,选择一个数字并递归进入下一层。
  • 当递归完成并回到上层时,需要恢复状态,允许该数字在其他分支中被选中。
4. 如何保证字典序

由于我们从小到大地遍历每一个数字,保证了排列的生成顺序是字典序的。换句话说,如果我们按顺序遍历数字并递归,那么生成的排列会按照数字的顺序依次排列,保证字典序。

2.2.2代码实现

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=10;
int n;
int st[N];//记录 输出方案 0表示未考虑
bool used[N];//记录数是否被选的状态 true 已选 false未选
void dfs(int u){
    if(u>n)//边界
    {
        for(int i=1;i<=n;i++)
        {
            printf("%d ",st[i]);//依次输出方案中的数
        }
        puts("");
    }
    for(int i=1;i<=n;i++)
    {
        if(!used[i])
        {
            st[u]=i;//选中的数 记录下了
            used[i]=true;
            dfs(u+1);

            used[i]=false;//回溯

        }
    }
}
int main()
{
    cin>>n;
    dfs(1);
    return 0;
}
[N];//记录数是否被选的状态 true 已选 false未选
void dfs(int u){
    if(u>n)//边界
    {
        for(int i=1;i<=n;i++)
        {
            printf("%d ",st[i]);//依次输出方案中的数
        }
        puts("");
    }
    for(int i=1;i<=n;i++)
    {
        if(!used[i])
        {
            st[u]=i;//选中的数 记录下了
            used[i]=true;
            dfs(u+1);

            used[i]=false;//回溯

        }
    }
}
int main()
{
    cin>>n;
    dfs(1);
    return 0;
}

http://www.kler.cn/a/514897.html

相关文章:

  • C# 网络协议第三方库Protobuf的使用
  • KNN的调参方法
  • solidity基础 -- 存储类型
  • 基于微信小程序的健身管理系统设计与实现(LW+源码+讲解)
  • 客户案例:电商平台对帐-账单管理(亚马逊amazon)
  • 数据结构与算法之递归: LeetCode 131. 分割回文串 (Ts 版)
  • 大语言模型LMM学习路线—从入门到进阶
  • [OpenGL]实现屏幕空间环境光遮蔽(Screen-Space Ambient Occlusion, SSAO)
  • 大一计算机的自学总结:随机快速排序及随机快速算法
  • 学习一下强化学习
  • C语言之整数转换英文表示
  • 机器学习(6):K 近邻算法
  • VirtualBox can‘t enable the AMD-V extension
  • 扬帆数据结构算法之雅舟航程,漫步C++幽谷——LeetCode刷题之移除链表元素、反转链表、找中间节点、合并有序链表、链表的回文结构
  • 剑指Offer|LCR 040.最大矩形
  • Solidity06 Solidity变量数据存储和作用域
  • 安装centos7之后问题解决
  • 根除埃博拉病毒(2015MCM美赛A)
  • 嵌入式入门(一)-STM32CubeMX
  • c++中的链表list
  • 【Android】创建基类BaseActivity和BaseFragment
  • Spring注解篇:@RestController详解
  • AI大模型-提示工程学习笔记11-思维树
  • 【线性代数】列主元法求矩阵的逆
  • 云原生架构下的AI智能编排:ScriptEcho赋能前端开发
  • 2025_1_22_进程替换