递归练习解题报告

前言

最近正在深入学习递归,初学递归时,会发现递归算法虽然代码简洁,但是真要理解或者上手编写还是有一定难度的。在此给出课本上的递归练习题的解题报告,并对几类递归问题做一个总结。由于时间有限,暂时并未完成所有题目的报告,但是已完成的题目可以给大家作个参考。

3-1 猴子吃枣

思路

用递归算法求解问题时,难点在于找到递归边界与状态转移方程(或称作递推公式),而此题这两个条件都非常好得到。

设第 n 天吃枣前剩余的枣子为 D(n),则递归边界为 D(10)=1,状态转移方程为 D(n+1)=D(n)−D(n)/2−1(1<=n<=9),推得 D(n)=2D(n+1)+2。

根据这两个条件编程实现即可。

代码

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

int D(int n){
    if(n == 10) return 1;
    return 2*D(n+1)+2;
}
int main(){
    cout << D(1);
}

输出

1534

3-2 三齿轮问题

思路

经过分析可知,实际上就是求 a,b,c 三个数的最小公倍数 x,三个齿轮的次数分别为 x/a,x/b,x/c。

最小公倍数(LCM)与最大公因数(GCD)有关系:LCM(a,b)×GCD(a,b)=a×b,而我们可以用递归实现辗转相除法计算 GCD。

代码

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

int gcd(int m, int n){
    return n ? gcd(n, m % n) : m;
}
int main(){
    int a, b, c;
    cin >> a >> b >> c;
    int x = a*b*c/gcd(a, gcd(b, c));
    cout << x/a << " " << x/b << " " << x/c;
}

3-3 整数因子分解问题

思路

朴素算法:首先给出整数分解的递推公式: factor(n)=∑i=2nfactor(n/i),(i|n)。接着对该公式进行解释,直观地看,整数因子分解式有多少个,就是对它所有因子的因子分解式数求和。

备忘录法:备忘录法实际上就是记忆化。很显然,朴素算法的实现过程会有非常多的重复计算,而在每次计算一个因子有多少分解式时,将其通过 STL 提供的 unordered_map 容器离散化存储,下次可通过复杂度 O(1) 的查找,达到剪枝的目的。

注:输入 2e9 时,朴素算法需要 10 秒左右,而记忆化算法运行速度为毫秒级。实际上当输入多组数据时,除了记忆化之外,还可以筛出 1e9 以内的所有质数,将其的分解式数 1 提前存入容器,达到进一步优化的目的。如果理解备忘录法有难度,可以只掌握朴素算法。

朴素算法

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

int factor(int n){
    int cnt = 1, i;
    for(i = 2; i*i < n; ++i)
        if(n%i == 0)
            cnt += factor(i) + factor(n/i);
    if(i*i == n) cnt += factor(i);
    return cnt;
}

int main(){
    int n;
    cin >> n;
    cout << factor(n);
}

备忘录法

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

unordered_map<int, int> mp;
int factor(int n){
    int cnt = 1, i;
    for(i = 2; i*i < n; ++i)
        if(n%i == 0)
            cnt += (mp[i] ? mp[i] : (mp[i] = factor(i))) + (mp[n/i] ? mp[n/i] : (mp[n/i] = factor(n/i)));
    if(i*i == n) cnt += mp[i] ? mp[i] : (mp[i] = factor(i));
    return cnt;
}

int main(){
    int n;
    cin >> n;
    cout << factor(n);
}

3-4 有重复元素的排列问题

思路

给定一组序列,利用逐位确定元素的方法,为避免排序重复,确定某一位时不得有重复数字。使用递归模拟下图过程即可实现。

 

代码

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

int n, ans = 0;
string s;
void arr(int cnt){
    if(cnt == n-1){
        ans++;
        cout << s << endl;
    }
    bool vis[26]{};
    for(int i = cnt; i < n; ++i){
        if(!vis[s[i] - 'a']){
            vis[s[i] - 'a'] = 1;
            swap(s[cnt], s[i]);
            arr(cnt+1);
            swap(s[cnt], s[i]);
        }
    }

}
int main(){
    cin >> n >> s;
    arr(0);
    cout << ans;
}

3-5 求集合的所有子集问题

思路

采用递归实现求集合子集问题无非在每次递归时求解两个子问题:将该元素放入子集后进行下一个元素的递归;不将该元素放入子集直接进入下一个元素的递归。

采取非递归实现该问题只需将某一元素存在与否存储在一个位中,递推遍历所有状态即可。

递归代码

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

int a[100], b[100], n;
void subset(int p, int cnt){
    if(p == n) return; // 注意递归边界
    b[cnt++] = a[p];
    for(int i = 0; i < cnt; ++i) cout << b[i] << " ";
    cout << endl;
    subset(p+1, cnt);
    subset(p+1, cnt-1);
}

int main(){
    cin >> n;
    for(int i = 0; i < n; ++i) cin >> a[i];
    cout << endl; //表示空集
    subset(0, 0);
}

非递归代码

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

int a[100];
int main(){
    int n;
    cin >> n;
    for(int i = 0; i < n; ++i) cin >> a[i];
    for(int i = 0; i < (1 << n); ++i){
        int t = i;
        for(int j = 0; j < n; ++j){
            if(t&1) cout << a[j] << " ";
            t >>= 1;
        }
        cout << endl;
    }
}

对比总结

就代码量而言,二者并无太大区别,但是递推代码更加直观,而且二者输出顺序不同,由此引出递归与递推求解问题的顺序不同。递推是由子问题出发,正向到达目标问题的过程;而递归分解目标问题向子问题转移。

3-6 求 n 个数中 r 个数的全部组合问题

思路

对 3-5 求递归子集的代码稍作修改即可实现。既然已经找到所有子集,那么 n 个数中的 r 个数的全部组合问题不过就是当子集长度为 r 时进行输出即可。

代码

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

int a[100], b[100], n, m;
void combine(int p, int cnt){
    if(p == n) return;
    b[cnt++] = a[p];
    if(cnt == m) { // 加上判断条件,当子集长度为m时输出
        for(int i = 0; i < cnt; ++i) cout << b[i] << " ";
        cout << endl;
    }
    combine(p+1, cnt);
    combine(p+1, cnt-1);
}

int main(){
    cin >> n >> m;
    for(int i = 0; i < n; ++i) cin >> a[i];
    combine(0, 0);
}

3-7 二叉树的三种遍历的推导关系

思路

注:在二叉树中,遍历顺序表示父结点与其左右两子结点的访问顺序,前序表示父结点早于左右子结点,中序表示在二者之间,后序表示在二者之后,而对于三种遍历方式,左结点的访问总是早于右结点。(以下说明来自 OI-wiki

preorder

inorder Postorder

已知中序遍历序列和另外一个序列可以求第三个序列。

reverse

  1. 前序的第一个是 root,后序的最后一个是 root。
  2. 先确定根节点,然后根据中序遍历,在根左边的为左子树,根右边的为右子树。
  3. 对于每一个子树可以看成一个全新的树,仍然遵循上面的规律。

上述描述显然是一段递归的描述,我们可以根据该描述利用递归实现后序序列的推导。后序序列的访问顺序是先访问所有的左结点,再访问所有的右结点,最后访问父结点,前序的父结点优先与左右结点。

算法思路:

  1. 前序序列的首元素为子树的根节点,在中序序列中找到根结点并记录下标。
  2. 通过下标划分左右子树,如果左右子树结点数大于 1,则回到第 1 步。
  3. 输出子树根节点。

代码

#include <bits/stdc++.h>
using namespace std;
string preorder, inorder;

void printPost(int inindex, int preindex, int len){
    if(!len) return;
    int offset = 0;
    while(offset < len && preorder[preindex] != inorder[inindex + offset]) offset++; // 在中序序列中找到结点
    printPost(inindex, preindex + 1, offset); // 划分左子树
    printPost(inindex + offset + 1, preindex + offset + 1, len - offset - 1); // 划分右子树
    cout << preorder[preindex]; // 打印结点
}

int main(){
    cin >> preorder >> inorder;
    printPost(0, 0, inorder.length());
}

3-8 阶梯问题

思路

模拟上楼梯的过程,从步长 1 到 3 尝试上楼,走一次增加一次步数,如果满足所上台阶数等于 n 则输出上楼方法,若大于则跳出,若小于则继续递推。

代码

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

int a[10], n;

void step(int cnt, int t){ // cnt 为上台阶数, t为步数
    if(cnt > n) return;
    if(cnt == n) {
        for(int i = 0; i < t; ++i) cout << a[i] << " ";
        cout << endl;
        return;
    }
    for(int i = 1; i <= 3; ++i){
        a[t] = i;
        step(cnt + i, t+1);
    }
}
int main(){
    cin >> n;
    step(0, 0);
}

3-9 集合划分问题

思路

首先给出状态转移方程:s(n,m)=m×s(n−1,m)+s(n−1,m−1)

接着解释该状态转移方程:问题 s(n,m) 可由两个子问题 m×s(n−1,m) 与 s(n−1,m−1) 构成。其中前者表示 n−1 个元素被划分进 m 个集合中,我们将第 n 个元素放入 m 个集合中的任何一个集合;后者表示 n−1 个元素被划分成 m−1 个集合,我们只能将第 n 个元素放入一个新的集合,这样才会划分出 m 个集合。反过来思考,我们对 s(n,m)移除一个元素,只可能向 s(n−1,m) 和 s(n-1,m-1)两种状态空间转移,所以状态转移方程成立。

代码

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

int s(int n, int m){
    if(m == 0) return 0;
    if(m == n) return 1;
    return m*s(n-1, m) + s(n-1, m-1); // 保证无重复?
}
int main(){
    int n, m;
    cin >> n >> m;
    cout << s(n, m);
}

3-10 整数划分问题

思路

与 3-8 是一类问题,不过根据输出可知,划分数字按字典序排列,各数字出现数量相同而顺序不同视为同种划分。划分数 S(n) 满足递推方程:S(n)=∑i=1n/2S(i)×S(n−i)。而递归可模仿这一操作,例如:输入 6,因为 6>1 故 6=1+5 输出 1+5,其中 5>1,继续划分 5=1+4,输出 1+1+4·······依此类推,递归边界为不可再划分的 1。

代码

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

int ans = 0, a[100] = {1};
void dfs(int n, int cnt){
    if(n == 1) return;
    for(int i = a[cnt-1]; i <= n/2; ++i){
        a[cnt] = i;
        ans++;
        for(int j = 1; j <= cnt; ++j) cout << a[j] << "+";
        cout << n-i << endl;
        dfs(n-i, cnt+1);
    }

}
int main(){
    int n;
    cin >> n;
    dfs(n, 1);
    cout << ans;
}

3-11 n 皇后问题

思路

n 皇后问题是一个经典的问题,其解法无非就是遍历所有状态空间,搜索符合条件的解。建立两个数组 bool 类型 viss[],visc[],分别存储对角线和列是否存在一名皇后。对角线的存储用到平面直角坐标系下直线方程 x+y=c 与 x−y=c,3n+r−i 与 r+i 中的 +3n 是为了防止出现下标负数且避免与r+i重复的情况。

代码

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

bool viss[100]{}, visc[100]{};
int a[100]{}, n, ans = 0;
void dfs(int r){
    if(r == n+1){
        ans++;
        for(int i = 1; i <= n; ++i) cout << a[i] << " ";
        cout << endl;
        return;
    }
    for(int i = 1; i <= n; ++i){
        if(!viss[r+i] && !viss[3*n+r-i]&& !visc[i]){
            viss[r+i] = viss[3*n+r-i] = visc[i] = 1; // 标记该列及对应两条斜边
            a[r] = i;
            dfs(r+1);
            viss[r+i] = viss[3*n+r-i] = visc[i] = 0; // 注意清除标记
        }
    }
}
int main(){
    cin >> n;
    dfs(1);
    cout << ans;
}

3-12 分书问题

思路

思路同 n 皇后问题,不再赘述。

代码

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

bool like[5][5] = {{0, 0, 1, 1, 0}, {1, 1, 0, 0, 1}, {0, 1, 1, 0, 1}, {0, 0, 0, 1, 0}, {0, 1, 0, 0, 1}}, vis[5];
int a[5], ans = 0;

void dfs(int n){
    if(n == 5){
        for(int i = 0; i < 5; ++i)
            cout << a[i] + 1 << " ";
        cout << endl;
        ans++;
        return;
    }
    for(int i = 0; i < 5; ++i){
        if(like[n][i] && !vis[i]){
            vis[i] = 1;
            a[n] = i;
            dfs(n+1);
            vis[i] = 0;
        }
    }
}
int main(){
    dfs(0);
    cout << ans;
}

3-13 迷宫问题

思路

简单的递归实现 dfs 的过程。但是有几处细节需要注意。首先该问题计算可行路径,即只要路径中有至少一点不一样,则视为两条路径。为避免走回头路同时存储路径途径结点,我们只需在访问到某点时将其打上标记并存入路径 s 中,在结束访问时清除标记并从路径 s 中删除。

代码

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

int n, m, a[100][100], ans = 0;
int dr[] = {0, 0, 1, -1};
int dc[] = {1, -1, 0, 0};
vector<pair<int, int> > s;
void dfs(int r, int c){
    s.push_back({r, c});
    a[r][c] = 1; // 若已访问则标记
    if(r == n-1 && c == m-1){
        ans++;
        for(int i = 0; i < s.size(); ++i) printf("(%d, %d)\n", s[i].first, s[i].second);
        cout << "------\n";
        a[r][c] = 0; // 返回前清除标记
        return;
    }
    for(int i = 0; i < 4; ++i){
        int tr = r+dr[i], tc = c+dc[i];
        if(tr >= 0 && tr < n && tc >= 0 && tc < m && a[tr][tc] == 0)
            dfs(tr, tc);
    }
    s.pop_back();
    a[r][c] = 0; // 返回前清除标记
}

int main(){
    cin >> n >> m;
    for(int i = 0; i < n; ++i)
        for(int j = 0; j < m; ++j)
            cin >> a[i][j];
    dfs(0, 0);
    cout << ans;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值