递推算法:长江后浪推前浪

递推概述

递推是按照一定的规律来计算序列中的每个项,通常是通过计算前面的一些项来得出序列中的指定项的值。其思想是把一个复杂的庞大的计算过程转化为简单过程的多次重复,以初始(起点)值为基础,用相同的运算规律,逐次重复运算,直至运算结束。简单来说就是找规律,根据规律总结出递推公式,然后计算递归公式的第n项。作为题目的答案。

在算法题目中,我们经常遇到此类问题,解决方案包括构造等差等比数列、奇偶项或分情况讨论、利用Catalan数和第二类Stirling数的结论等。

一般数列构造

来看一个简单的场景:一天,有五位同学参加了植树活动,他们完成植树的棵树都不相同。问第一位同学植了多少棵时,他指着旁边的第二位同学说比他多植了两棵;追问第二位同学,他又说比第三位同学多植了两棵;... 如此,都说比另一位同学多植两棵。最后问到第五位同学时,他说自己植了10棵。问:到底第一位同学植了多少棵树?

那么很显然这就是个数列,设第一位同学植树的棵树为a1,欲求a1,需从第五位同学植树的棵数a5入手,根据“多两棵”这个规律,按照一定顺序逐步进行推算,即:a5=10;a4=a5+2=12;a3=a4+2=14;a2=a3+2=16;a1=a2+2=18计算,很显然后一项等于前一项加2,编写程序如下:

#include <iostream>  
using namespace std;  
int main() 
{  
    int trees_last = 10; 
    for (int i = 4; i > 0; i--) 
        trees_last += 2;
    cout << trees_last;  
    return 0;  
}

一维线性递推

上楼梯是一个经典的递推问题,楼梯有 N(N \leq 92) 阶,上楼可以一步上一阶,也可以一步上二阶。要求编一个程序,计算到N阶共有多少种不同的走法。初看这是个十分复杂的问题,要从N阶计算N-1阶和N-2阶,对于N-1阶,再计算N-2N-3阶;对于对于N-2阶,再计算N-3N-4阶,如此往复...

不妨转换一下思路,对于递推问题,我们只需要关心某一项与相邻项的关系。记a_i为上到第i阶的可能种数,那么如何上到第i阶呢,只有两种可能,从第i-1阶或者第i-2阶上来。

台阶

也就是说,a_i = a_{i-1}+a_{i-2}。上到第N阶的可能性种数也就是这个数列的第N项。我们得到了数列的递推公式,就可以直接使用程序计算了。由于a_i = a_{i-1}+a_{i-2},那么我们需要从第3项开始递推,并预先求出a_1和a_2,显然只有1种办法上到第1阶,而有2种办法上到第2阶(一步从地面到第2阶或从第1阶上一阶到到第2阶),那么a_i=1,a_2=2,计算即可:

#include<iostream>

using namespace std;

int main()
{
    long long n, lay[93];
    cin >> n;

    lay[1] = 1, lay[2] = 2;
    for (int i = 3; i <= n; ++i)
    {
        lay[i] = lay[i - 1] + lay[i - 2];
    }

    cout << lay[n] << endl;
    return 0;
}

其实你可能已经发现了,这就是个斐波那契数列{Fib},在日后我们可能还会遇到很多于此相关的问题。

线性状态递推

我们在数学试卷上也可能见到类似的问题(,一只蜜蜂在下图所示的数字蜂房上爬动,已知它只能从标号小的蜂房爬到标号大的相邻蜂房,现在问你:蜜蜂从蜂房 m 开始爬到蜂房 nm<n,且n-m \leq 92,则有多少种爬行路线?

蜂房

输入输入 m,n 的值,输出爬行有多少种路线。对于这个问题,似乎并没有跟上一个问题一样简明的思路。这里我们先做一个简单的转换,蜜蜂从蜂房 m 开始爬到蜂房 n就相当于从蜂房1爬到蜂房n-m。考虑相邻状态吗,由于它只能从标号小的蜂房爬到标号大的相邻蜂房,那么到达第n个蜂房只有两种可行的办法,一种是从上面也就是第n-1个蜂房爬过去;另一种就是从左边也就是第n-2个蜂房爬过去,我们以n=3时做一个简单的演示。
n=3时的状态转移过程

a_i为爬到第i个蜂房的可能种数,那么a_i = a_{i-1}+a_{i-2}。又是熟悉的味道。

#include<iostream>

using namespace std;

int main()
{
    long long m,n, room[93];
    cin >> m >> n;

    room[1] = 1, room[2] = 2;
    for (int i = 3; i <= n-m; ++i)
    {
        room[i] = room[i - 1] + room[i - 2];
    }

    cout << room[n-m] << endl;
    return 0;
}

许多递推问题很像模拟问题,比如“数的计算”问题:给出正整数 n,按如下方式构造数列:

  1. 只有一个数 n 的数列是一个合法的数列。
  2. 在一个合法的数列的末尾加入一个正整数,但是这个正整数不能超过该数列最后一项的一半,可以得到一个新的合法数列。

请你求出,一共有多少个合法的数列。两个合法数列 a, b 不同当且仅当两数列长度不同或存在一个正整数 i \leq |a|,使得 a_i \neq b_i

拿到一道递归题,我们就像做上一个题一样,先手撕一个小样例找找规律。记a_in=i时的可能种数,考察a_5a_6

n=5和n=6的情况

可以看出,对于a_6而言,a_6=a_3+a_5,进一步地,我们可以猜想,当n为偶数时,存在递推式a_{2n}=a_{2n/2} + a_{2n-1}。下作简要证明:当n为偶数时,n/2为正整数,可以成为新的合法数列的一项,a_{2n}=\sum\limits_{i=1}^{2n/2}a_i=a_{2n/2} + \sum\limits_{i=1}^{2n/2-1}a_in为奇数时,n/2不为正整数,所以得(n-1)/2项为新的合法数列的一项,即a_{2n-1}=\sum\limits_{i=1}^{(2n-1)/2-1}a_i=\sum\limits_{i=1}^{2n/2-1}a_i,代回即得a_{2n}=a_{2n-1} +a_{2n/2},故命题得证。偶数项的情况得出了,下面我们看奇数项。

n=4和n=5的情况

显然a_5=a_4,进一步地,我们可以猜想,当n为奇数时,存在递推式a_{2n-1}=a_{2n-2}。下面证明:a_{2n-1}=\sum\limits_{i=1}^{(2n-1)/2-1}a_i=\sum\limits_{i=1}^{2n/2-1}a_ia_{2n-2}=\sum\limits_{i=1}^{(2n-2)/2}a_i=\sum\limits_{i=1}^{2n/2-1}a_i,那么显然a_{2n-1}=a_{2n-2}成立。其实我们也完全可以感性的理解这个递推式,当n为奇数时,下一个数字必然是它的一半再向下取整,这与前一个数字结果相同。

有了这些推断,那么这就是个奇偶数列求项问题。

#include<iostream>

using namespace std;

int main()
{
    int n, a[1001];
    cin >> n;

    a[1] = 1, a[2] = 2;
    for (int i = 3; i <= n; ++i)
    {
        if (i % 2 == 0)
            a[i] = a[i - 1] + a[i / 2];
        else
            a[i] = a[i - 1];
    }

    cout << a[n] << endl;
    return 0;
}

双重数列递推

考察“覆盖墙壁”问题。你有一个长为 N 宽为 2 的墙壁,给你两种砖头:一个长 21,另一个是 L 型覆盖 3 个单元的砖头。

砖头

砖头可以旋转,两种砖头可以无限制提供。你的任务是计算用这两种来覆盖 N\times 2 的墙壁的覆盖方法。例如一个 2\times3 的墙可以有 5 种覆盖方法,如下:
覆盖方法

注意可以使用两种砖头混合起来覆盖,给定 N,要求计算 2\times N 的墙壁的覆盖方法。由于结果很大,所以只要求输出最后 4 位。例如 2\times 13 的覆盖方法为 13465,只需输出 3465 即可。如果答案少于 4 位,就直接输出就可以,不加前导 0,如 N=3 时输出 5输入一个整数 N,表示墙壁的长。输出覆盖方法的最后 4 位,不足 4 位就输出整个答案。

这题的难点在于“基本状况”的寻找,记a_i为仅考虑(a),n=i时的可能种数,则a_N就是题给2×N的墙壁被全部覆盖的情况。有两种情况可以达到a_N,如下:

两种情况

那么麻烦的是还有"L"形砖头,怎么办?不妨记b_i为空白区域的较短一块的长度为i时,仅考虑(b)的可能种数。我们又可以找到两条通往a_n的道路。
另两种情况

所以a_n=a_{n-1} + a_{n-2}+2b_{n-1},对a_n的递推推导完毕。接下来维护b_n。上文设b_n时说过,b_n就是短边为n的缺角矩形。一方面,b_n可以由a_{n-2}的右边摞一个L形状砖块来达到;另一方面,b_n可以由b_{n-1}的右边摞一个长条来达到,分别对应以下左图和右图。
bn的两种情况

那么有b_n=b_{n-1}+a_{n-2}。两个数列,两个递推式,交替求解这两个数列的每一项即可。

#include<iostream>
using namespace std;

const int mod = 10000;
int a[1000002], b[1000002];

int main()
{
    int n;
    cin >> n;

    a[0] = 1;
    a[1] = b[1] = 1;
    for (int i = 2; i <= n; i++)
    {
        a[i] = ((a[i - 1] + a[i - 2]) % mod + 2 * b[i - 2] % mod) % mod;
        b[i] = (b[i - 1] + a[i - 1]) % mod;
    }

    cout << a[n];

    return 0;
}

组合数学应用

在递推问题中,有许多较难、在短时间内解决的问题。但幸运的是,我们有几个成熟的模型可以应用于这些问题。

卡特兰数(Catalan Number)是组合数学的计数问题中较为常见的一个数列。传统的定义为在平面直角坐标系中,从原点出发,每次只能向上走或者向右走,且不能越过y=x这条直线(可以碰到),求走到(n,n)的不同路径的数量。通过推导得到的卡特兰数的通项公式为:C_n = \frac{1}{n+1} \binom{2n}{n} = \frac{(2n)!}{(n+1)!n!}。这个推导过程比较复杂,我们不做要求。

证明如下:由定义知每一步可以是向上走一个单位(记为U)或向右走一个单位(记为R),因此到达 (n, n) 需要恰好 n 步向右和 n 步向上。

  1. 总的路径数:首先,如果不考虑任何限制条件,从 (0, 0)(n, n) 的总路径数是通过选择 n 次向右走的位置来决定的,即从 2n 步中选择 n 步作为向右走的步数,剩下的 n 步自然就是向上走的步数。这可以通过组合数表示为 \binom{2n}{n}
  2. 排除非法路径:接下来,我们需要从这些总路径中去除那些越过了 y = x 直线的路径。对于任意一条非法路径,它必定会在某个点第一次越过 y = x 直线。设这个点为 (i, i+1),那么在这之前的路径必须是从 (0, 0)(i, i+1) 的一条路径,之后的路径则是从 (i, i+1)(n, n) 的一条路径。注意到,如果我们对这条非法路径进行变换,将从起点到首次超过 y = x 之前的路径中所有的 U 和 R 互换(即将每个 U 变成 R,每个 R 变成 U),则会得到一条从 (-1, 1)(n-1, n+1) 的路径。由于这种变换是一一对应的,所以非法路径的数量就等于从 (-1, 1)(n-1, n+1) 的路径数量。
  3. 计算非法路径数:从 (-1, 1)(n-1, n+1) 的路径同样需要选择 n 次向右走的位置(因为总共需要向右走 n+1 步,其中一步已经在初始位置 (-1, 1) 上了),因此非法路径的数量是 \binom{2n}{n+1}
  4. 合法路径数:最后,合法路径的数量就是总路径数减去非法路径数,即
    C_n = \binom{2n}{n} - \binom{2n}{n+1}
  5. 简化公式:利用组合数的性质 \binom{n}{k} = \frac{n!}{k!(n-k)!},我们可以进一步简化上述表达式。注意到 \binom{2n}{n+1} = \frac{2n!}{(n+1)!(2n-(n+1))!} = \frac{2n!}{(n+1)!n!},我们有
    C_n = \frac{(2n)!}{n!n!} - \frac{(2n)!}{(n+1)!n!} = \frac{(2n)!}{n!n!} \left(1 - \frac{1}{n+1}\right) = \frac{(2n)!}{n!n!} \cdot \frac{n}{n+1} = \frac{(2n)!}{(n+1)!n!}
    这与卡特兰数的标准形式一致。至此定理得证。

卡特兰数的数列特征十分明显,当看到答案具有1, 1, 2, 5, 14,...这样的规律时,要敏锐的察觉到这就是卡特兰数。我们将通过一个实例来直观的推出卡特兰数的形式:过n个不同点可以构造多少颗二叉树?

为了找规律,我们开始逐级考虑。

  1. n=1时,显然有且只有1棵树。
  2. n=2时,第1个点可以向左扩展,使第2个点成为其左子树;同理,第1个点可以向右扩展,使第2个点成为其右子树。
    n=1和n=2的情况
  3. n=3时,n=2的情况可以分别作为第3个点的左子树和右子树,有2×2=4种情况,另外可以让其它2两点分别作为第3个点的左和右子树,为1种情况,共计5种。
    n=3的情况
  4. 依次继续递推即可,画图可知n=4时有13种情况。

那么这就是个妥妥的卡特兰数列了,套公式计算即可,这里需要注意的是,这里的每一项需要比原始卡特兰数列延后一项。

#include <iostream>

using namespace std;

long long factorial(int n)
{
    if (n == 0 || n == 1) 
        return 1;
    long long result = 1;
    for (int i = 2; i <= n; ++i) {
        result *= i;
    }
    return result;
}

long long catalanNumber(int n)
{
    return factorial(2 * n) / (factorial(n + 1) * factorial(n));
}

int main() {
    int n;
    cin >> n;

    cout << catalanNumber(n + 1) << endl;
    return 0;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,192评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,858评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,517评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,148评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,162评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,905评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,537评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,439评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,956评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,083评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,218评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,899评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,565评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,093评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,201评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,539评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,215评论 2 358

推荐阅读更多精彩内容