[啃书] 第6篇

前言

接着第四章

本篇总结自《算法笔记》4.3 递归

正文

知识点1:递归

递归的简单定义:函数反复调用自身,每次调用都把问题范围缩小,一直缩小到可以直接得到边界数据,然后再在返回的路上求出对应的解。

递归的逻辑中两个重要概念

1 递归边界

2 递归式(递归调用)

其中的递归式是将原问题分成若干子问题的手段,递归边界则是分解的尽头。(如果没有尽头就会无限分解)

经典例子1️⃣:使用递归求解n的阶乘

n! = 1*2*3*4*5……(n-1)*n

写成递推形式:n! = (n-1)! * n 【规模从n变成了n-1】

用F(n)表示n!的递推形式:F(n) = F(n-1) * n 

找边界:把规模一直减小到F(0)即0!,而0!=1,所以可以把F(0)=1作为递归的边界。

经典例子2️⃣:求Fibonacci数列的第n项

斐波那契数列满足F(0)=1,F(1)=1,F(n)=F(n-1)+F(n-2) (n≥2)

经典例子3️⃣:全排列(Full Permutation)

一个数列所有的排列方式的集合称为全排列。

【举例】如1,2,3的全排列:(1,2,3),(1,3,2),(2,1,3),(2,3,1),(3,1,2),(3,2,1);(按字典序)

【目的】按字典序从小到大输出全排列:其中(a1,a2,...an)的字典序小于(b1,b2,...bn),是指存在一个i,使得a1=b1,a2=b2,……,ai<bi成立;

#include<cstdio>
const int maxn = 11; // max number
int n, p[maxn], hashTable[maxn] = {false}; 
// p: Now permutation, hashTable: Record whether x has been placed in p

void generateP(int index)
{
    if(index == n + 1) // recursion boundary
    {
        for(int i = 1; i <= n; i++) // print result
        {
            printf("%d", p[i]);
        }
        printf("
");
        return;
    }
    for(int x = 1; x <= n; x++)
    {
        if(hashTable[x] == false) // x hasn't been used
        {
            p[index] = x; // use x (place x in p)
            hashTable[x] = true; // x has been used
            generateP(index + 1); // recursion (resolve the sub-problems when p[index] = x)
            hashTable[x] = false; // restore the x (resolved)
        }
    }
}

int main()
{
    n = 3; // want to print 1~3 full permutation
    generateP(1); // recursion start from 1
    return 0;
}

经典例子4️⃣:n皇后问题

#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;

const int maxn = 11;
int n, P[maxn], hashTable[maxn] = {false};
// p:
当前排列, hashTable: 记录x是否已经放入p
int counter = 0;

void generateP1(int index){
    /* 递归边界 生成一个排列 */ 
    if(index == n + 1){
        bool flag = true; // 当前排列合法 
        /* 遍历任意两个皇后 */ 
        for(int i = 1; i <= n; i++){
            for(int j = i + 1; j <= n; j++){
                for(int j = i + 1; j <= n; j++){
                    if(abs(i - j) == abs(P[i] - P[j])){ // 在同一对角线上 
                        flag = false; // 冲突 
                    }
                }
            }
        }
        if(flag) counter++;
        return; 
    }
    for(int x = 1; x <= n; x++){
        if(hashTable[x] == false){
            P[index] = x;
            hashTable[x] = true;
            generateP1(index + 1);
            hashTable[x] = false;
        }
    }
}
void generateP2(int index){
    /* 递归边界 */ 
    if(index == n + 1){
        counter++;
        return;
    }
    /* 遍历行 */ 
    for(int x = 1; x <= n; x++){
        /* 若第x行还没皇后 */ 
        if(hashTable[x] == false){
            bool flag = true; // flag为true表示不会与之前的皇后冲突 
            /* 遍历之前的皇后 */ 
            for(int pre = 1; pre < index; pre++){ 
                /* 判断是否冲突 */ 
                if(abs(index - pre) == abs(x - P[pre])){
                    flag = false; // 冲突(在同一对角线)
                    break; // 跳出循环 
                }
            }
            /* 无冲突则放置皇后 */ 
            if(flag) {
                P[index] = x;
                hashTable[x] = true; //占用第x行 
                generateP2(index + 1); // 处理下一行(子问题) 
                hashTable[x] = false; // 子问题都处理完了,还原第x行状态 
            }
        }
    }
}

int main(){
    n = 8; // 棋盘大小 n*n
    generateP2(1); //从P[1]开始填 
    printf("%d
", counter);
    return 0;
}

【注】n皇后问题方法总结

普通枚举(暴力):随意选取空位摆放。

全排列(暴力):按列/行逐个摆下去。把所有的子问题都遍历处理到底,最后检测是否符合要求(见generateP1方法)

全排列(回溯):按列/行逐个摆下去。过程中增加检测功能,即若继续放已经不再可能达成目标,则不再遍历处理子问题,立即返回(见generateP2方法)

普通枚举要处理行冲突、列冲突、对角线冲突,全排列只需处理对角线冲突

“递归”总结到此为止;

“贪心”见下一篇;

加油

博主ChaosBlog原创,搬运请注明。
个人博客地址 cnblogs.com/cc1997;
个人网站地址 Ccoding.cn
原文地址:https://www.cnblogs.com/cc1997/p/13261578.html