从换烟问题到递归实现
https://www.springbot.top/wp-content/uploads/2025/11/4bdaabcae1bd2ab3f51ac219d182a2e5.webp

题目来源:洛谷P1150 Peter 的烟

题目描述

Peter 有 n 根烟,他每吸完一根烟就把烟蒂保存起来,kk>1)个烟蒂可以换一个新的烟,那么 Peter 最终能吸到多少根烟呢?

与某些脑筋急转弯不同的是,Peter 并不能从异次元借到烟蒂,抽完后再还回去。

感觉传统香烟还是不如电子烟吧(电子烟没有烟蒂)

输入格式

每组测试数据一行包括两个整数 n,k(1<n,k≤108)。

输出格式

对于每组测试数据,输出一行包括一个整数表示最终烟的根数。

思路

  • 先以样例2为例,

n = 10, k = 3时

第一次抽,抽了10根, 剩下10个烟蒂可以兑换3根烟,还剩1个

第二次,抽了3根,剩下的3+1个烟蒂可以兑换一根烟,还剩1个

第三次,抽了1根,烟蒂只剩1+1<3,无法兑换,抽烟终止

  • 这样,我们能梳理这一题目的逻辑:

除第一次外,每次先抽完上次兑换的烟,剩下的烟蒂加上之前剩下的烟蒂,尝试兑换

如果能够兑换(yandi >= k),继续循环;反之退出。

int smoking(int n,int k){
    //初始化部分
    int num = n;        // 先吸完初始的 n 根烟
    int yandi = n;      // 产生 n 个烟蒂

    //循环部分
    while(yandi >= k){
        int newSmoke = yandi / k;  // k 个烟蒂换 1 根新烟
        num += newSmoke;            // 吸掉新换来的烟
        yandi = yandi % k + newSmoke;  // 剩余烟蒂 = 换不了的 + 新烟吸完的
    }

    return num;
}

//一个更简单的实现
int smoking(int n,int k){
    static int sum = n;//静态变量,只初始化一次
    do{
        sum = sum + n / k;
        n = n % k + n / k;
    } while (n >= k);
}

用while循环很好实现,那么,我们要想用更少的代码实现,应该怎么办呢?

使用递归。

递归的定义

借着这个问题,我们先了解一下递归。

递归的基本思想是某个函数直接或者间接地调用自身

递归,拆开来看,是”递“和”归“的两个过程。

类比我们搜索信息的过程,递归的过程就是一个不断追问,理解意思(到达终止条件)后回到文中的过程。

举个例子,这是算法组学长授课的一张图

image-20251126142924875

注意右下角,当我们要找”U群常见问题速查“的图片时,我们进入”U群常见问题速查“,又发现了这个问题,再进入”U群常见问题速查“,再发现这个问题……

这就是一种无限递归。

还没太搞懂?我们来看一下下面的实际应用。

递归的实际应用

实际问题中,我们一般采用有限次数的递归。

  • 一个典型的例子就是阶乘的递归实现
int fact(int n)//输入为自然数
{
    return n == 0 ? 1 : n * f(n-1);//这里使用了三目运算符,注意 == 0不能舍去
}

n = 4的情况为例,输入4后函数作出如下操作:

4 == 0 为假,因此返回 4 * f(3)

f(3)返回值为多少?我们进入f(3),3 == 0仍然为假,返回 3 * f(2)

f(2)又返回多少?返回2* f(1)

f(1)呢?返回1 * f(0).

到f(0)时,我们终于得知了返回值为1

这便是”递“。

因此

f(1) = 1 * f(0) = 1;

f(2) = 2 * f(1) = 2;

f(3) = 3 * f(2) = 6;

f(4) = 4 * f(3) = 24;

这便是”归“

《算法竞赛入门经典中》有一个有趣的比喻:

皇帝(拥有main函数的栈帧):大臣,你给我算一下f(3)。

大臣(拥有f(3)的栈帧):知府,你给我算一下f(2)。

知府(拥有f(2)的栈帧):县令,你给我算一下f(1)。

县令(拥有f(1)的栈帧):师爷,你给我算一下f(0)。

师爷(拥有f(0)的栈帧):回老爷,f(0)=1。

县令:(心算f(1)=f(0)*1=1)回知府大人,f(1)=1。

知府:(心算f(2)=f(1)*2=2)回大人,f(2)=2。

大臣:(心算f(3)=f(2)*3=6)回皇上,f(3)=6。

皇帝满意了。

这或许能部分说明递归的本质。

  • 另一个例子是最大公因数(gcd)的计算
int gcd(int a,int b){
    return b ? a : gcd(b,a % b);
}

这里采用的是欧几里得算法(辗转相除法)

  • 先介绍一下实现原理:gcd(a,b) = gcd(b,a % b)

举个例子,18和12的最大公因数显然是6,6即为18和12的余数。所以gcd(18,12)=gcd(12,6)

数学原理:设 a % b = r1,那么a = n1 * b + r1;

下一轮,输入的值是b和a % b(r1);

设b % r1 = r2,则 b = n2 * r1 + r2;

下一轮,输入的值是a % b(r1)和b % r1(r2);

设r1 % r2 = r3,则 r1 = n3 * r2 + r3;

(此时a = (n1 * n2 + 1) * r1 + n1 * r2 = (n1 * n2 + 1) * (n3 * r2 + r3) + n1 * r2)

……

通过数学归纳法可以知道,之后输入的数值都可以转换成rn和r(n+1)的形式表示,这样当rn和r(n+1)存在倍数关系时,就会到达终止条件。

直观地看,实现过程如下图:

image-20251126142007709
  • 有读者可能会担心递归的时间复杂度问题,实际上,递归的时间复杂度一般为O(n)或O(logn),如果实现正确不会出现明显的运行时间延长

回到本题,如何用递归实现?

让我们回到本体的变量间核心关系上。

对上面的代码等价替换变量可以发现这个表达式:n = n / k + n % k

  • 如何解释这个式子?

按照之前的思路,我们每次计算好的应该是本轮抽的烟数目,将烟蒂(n/k+n%k)交给下一轮处理,直到最后一轮烟蒂不够换烟

但是,要想实现代码的简洁性,我们应该只有两个变量,k值固定,那么只能每次传输一个参数

但是这一次需要换一种思路:由于剩下的烟蒂无法存储(“n”只能用于传输烟蒂数量),我们每轮只能抽能兑换烟部分(能被k整除)的烟数,剩下的放到下一回合再抽。

因此n / k是烟蒂兑换的烟数量,而 n % k则是剩下的烟(留着换完抽)的数量,n就表示烟的数量,这个表达式就是烟数量的变化过程

这样一来,传入和返回的n的定义就相同了(都可以看成上一轮留下的烟数量)

同时,根据题目意思,函数内部要进行的就是这一轮抽的烟数,可以用n / k * kn - n % k表示

int smoking(int n,int k)
{
    if(n < k)
        return n;
      return n / k * k + smoking(n / k + n % k, k);//return已经退出函数,无需else
}

简化成一行就是

int smoking(int n,int k){
    return n < k ? n : (n / k) * k + smoking(n / k + n % k, k);
}

在这个程序中,当传入的n >= k时,我们会抽掉能被k整除的部分,并把剩余的烟和兑换的烟给下一个递归进程处理,当最后n < k时,程序会返回n(抽掉所有烟)。这个过程中,所有被抽掉的烟都在每一个进程中被加上,最后返回主进程的就是抽过的烟数。

完整程序如下:

#include <bits/stdc++.h>
using namespace std;

int smoking(int n,int k){
    return n < k ? n : (n / k) * k + smoking(n / k + n % k, k);
}
int main(){
    int n,k;
    cin >> n >> k;
    cout << smoking(n,k) << endl;
    return 0;
}

最后对比一下直觉和递归的不同之处:

直觉递归
抽烟数量每次全抽只抽k的整数倍根
参数数量多个每次只传递一个
烟蒂保存用参数保存(可保存多个回合)只保存到下一回合

所以说:

简洁而严密,这就是递归定义的优点。

当然,递归肯定有它本身的缺陷,它的特性导致它难以阅读且在大型项目中容易出错

递归深度过大会导致栈溢出(Stack Overflow),应谨慎使用。

如图,在计算阶乘时,如果输入f(100),程序直接返回了错误结果0

  • 在搜索时,还发现了一种时间复杂度为O(1)的数论解法,贴在这里,想了解原理的可以去原文章
#include <iostream>
using namespace std;

int main() {
    int n, k;
    cin >> n >> k;
    cout << n + (n - 1) / (k - 1) << endl;

    return 0;
}

结语

递归是一种简洁高效的小规模程序实现方式,适合学习算法时锻炼脑力,但不适合大规模的项目开发。

此外,在本章中笔者未涉及到递归的原理,有兴趣的读者可以自行查询

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇