【NBUOJ刷题笔记】递推_递归+分治策略1

0. 前言

PS:本人并不是集训队的成员,因此代码写的烂轻点喷。。。本专题一方面是巩固自己的算法知识,另一方面是给NBU学弟学妹们参考解题思路(切勿直接搬运抄袭提交作业!!!)最后,该系列博客AC代码均以Java语言提交,C/C++的可以参考思路编写代码

1. 题目详情

1.1 题目一:王老师爬楼梯

1.1.1 题目信息

题目描述

王老师爬楼梯,他可以每次走1级或者2级或者3级楼梯,输入楼梯的级数,求不同的走法数。(要求递推求解)如果N很大,需要高精度计算。
输入要求

一个整数N,N<=1000。
输出要求

共有多少种走法。

输入样例:

10
输出样例

274
来源

NBUOJ

1.1.2 算法思路(递推=>动态规划)

本题是动态规划的入门题,可以让同学较好的从递推的思维转向动态规划思维
动态规划五步走(重要)

  1. 状态定义 :这一步是相当重要的,定义一个合适的状态表示是解题的关键,我们假设dp[i]为走到第i级台阶的走法总数
  2. 状态转移方程 :假设我们使用上一步骤的状态表示,那么我们需要思考 dp[i] 可以由哪些状态转移得到,根据题意王老师最后可以跨1步,也可以跨2步,也可以跨3步到达第i级台阶,因此可以得出状态转移方程为:dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3](其中i >= 4)
  3. 状态初始化:由于我们在i<4时直接套用该公式会造成数组越界,因此我们需要初始化dp[1]、dp[2]、dp[3]的值
  4. 填表顺序:根据我们的状态转移方程,我们发现dp[i]的值可以由前三项推出,因此填表顺序是从左往右填写的
  5. 返回值:根据题目含义我们需要输出第n级台阶的走法个数,因此我们填表完毕后需要返回的就是dp[n]

示例 :我们假设输入为4进行举例:

即通俗来说,本题的递推思路就是将王老师走到第i级台阶的最后一步的步数进行分类,一共有三种情况:

  • case1(最后走了一步):dp[i - 1]
  • case2(最后走了两步):dp[i - 2]
  • case3(最后走了三步):dp[i - 3]

1.1.3 AC代码(Java实现)

NBUOJ上面Java带中文注释会报错!因此这里放了两个版本的代码,前面不带注释的可以直接跑OJ通过,后面的方便读者阅读

1.1.3.1 温馨提示

由于本题的n值最大为1000,因此当n值过大时会溢出整数的表示范围(无论是Java的long类型还是C++的long long都不行),因此本题需要借助 大数相加 的模板,不过咱们Java程序员可以使用标准库提供的 BigInteger ,下面简单介绍相关常用的API

API 返回值 说明
new BigInteger(String str) BigInteger实例 使用字符串表示的数值构造实例
BigInteger.valueOf(long num) BigInteger实例 使用long类型表示的数值构造实例
x.add(BigInteger y) BigInteger实例 返回x和y所表示的大数相加的结果
x.multiply(BigInteger y) BigInteger实例 返回x和y所表示的大数相乘的结果
1.1.3.2 不带注释版本
java 复制代码
import java.math.BigInteger;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        BigInteger[] dp = new BigInteger[n + 1];
        dp[1] = BigInteger.valueOf(1);
        dp[2] = BigInteger.valueOf(2);
        dp[3] = BigInteger.valueOf(4);
        for (int i = 4; i <= n; i++) {
            dp[i] = dp[i - 1].add(dp[i - 2]).add(dp[i - 3]);
        }
        System.out.println(dp[n]);
    }
}
1.1.3.3 带注释版本
java 复制代码
import java.math.BigInteger;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        // 1. 状态定义:dp[i]表示王老师走到第i级台阶的走法数
        BigInteger[] dp = new BigInteger[n + 1];
        // 2. 状态初始化
        dp[1] = BigInteger.valueOf(1);
        dp[2] = BigInteger.valueOf(2);
        dp[3] = BigInteger.valueOf(4);
        // 3. 状态转移方程
        for (int i = 4; i <= n; i++) {
            dp[i] = dp[i - 1].add(dp[i - 2]).add(dp[i - 3]);
        }
        // 4. 返回值
        System.out.println(dp[n]);
    }
}

1.1.4 扩展题

这里放一些跟本题类似的OJ题(读者可自行尝试解题):

  1. LeetCode70 爬楼梯:https://leetcode.cn/problems/climbing-stairs/
  2. LeetCode118 斐波那契数列:https://leetcode.cn/problems/pascals-triangle/

1.2 题目二:铺砖

1.2.1 题目信息

题目描述

对于一个2行N列的走道。现在用12或2 2的砖去铺满。问有多少种不同的方式(请用递推方式求解)。如果N很大,需要高精度计算。下图是一个2行17列的走道的某种铺法:

输入要求

一个整数N,N<=1000。
输出要求

共有多少种铺法。
输入样例

30
输出样例

715827883
来源

NBUOJ

1.2.2 算法思路(递归=>动态规划)

动态规划五步走(重要)

  1. 状态定义 :这一步是相当重要的,定义一个合适的状态表示是解题的关键,我们假设dp[i]表示为2行i列的砖的铺法总数
  2. 状态转移方程 :假设我们使用上一步骤的状态表示,那么我们需要思考 dp[i] 可以由哪些状态转移得到,根据题意我们可以根据最后一块砖的长度为1还是为2进行区分,因此可以得出状态转移方程为:dp[i] = dp[i - 1] + 2 * dp[i - 2](其中i >= 3)
  3. 状态初始化:由于我们在i<3时直接套用该公式会造成数组越界,因此我们需要初始化dp[1]、dp[2]的值
  4. 填表顺序:根据我们的状态转移方程,我们发现dp[i]的值可以由前两项推出,因此填表顺序是从左往右填写的
  5. 返回值:根据题目含义我们需要输出长度为n的铺砖方法数,因此我们填表完毕后需要返回的就是dp[n]

示例 :也许上面的文字描述有点抽象,还是以画图进行举例:

相信此图一出,大家瞬间就明白公式:dp[i] = dp[i - 1] + 2 * dp[i - 2](其中i >= 3)的由来了,其实就是根据最后一块砖的摆法划分出不同的方案

1.2.3 AC代码(Java实现)

NBUOJ上面Java带中文注释会报错!因此这里放了两个版本的代码,前面不带注释的可以直接跑OJ通过,后面的方便读者阅读

1.2.3.1 温馨提示

由于本题的n值最大为1000,因此当n值过大时会溢出整数的表示范围(无论是Java的long类型还是C++的long long都不行),因此本题需要借助 大数相加 的模板,不过咱们Java程序员可以使用标准库提供的 BigInteger ,下面简单介绍相关常用的API

API 返回值 说明
new BigInteger(String str) BigInteger实例 使用字符串表示的数值构造实例
BigInteger.valueOf(long num) BigInteger实例 使用long类型表示的数值构造实例
x.add(BigInteger y) BigInteger实例 返回x和y所表示的大数相加的结果
x.multiply(BigInteger y) BigInteger实例 返回x和y所表示的大数相乘的结果
1.2.3.2 不带注释版本
java 复制代码
import java.math.BigInteger;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        BigInteger[] dp = new BigInteger[n + 1];
        dp[1] = new BigInteger("1");
        dp[2] = new BigInteger("3");
        for (int i = 3; i <= n; i++) {
            dp[i] = dp[i - 1].add(dp[i - 2].multiply(new BigInteger("2")));
        }
        System.out.println(dp[n]);
    }
}
1.2.3.3 带注释版本
java 复制代码
import java.math.BigInteger;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        // 1. 状态定义:dp[i]表示2行i列的铺法数
        BigInteger[] dp = new BigInteger[n + 1];
        // 2. 状态初始化
        dp[1] = new BigInteger("1");
        dp[2] = new BigInteger("3");
        // 3. 状态转移方程
        for (int i = 3; i <= n; i++) {
            dp[i] = dp[i - 1].add(dp[i - 2].multiply(new BigInteger("2")));
        }
        // 4. 返回值
        System.out.println(dp[n]);
    }
}

1.3 题目三:整数分割(1)

1.3.1 题目信息

题目描述

N年M月O日CX和ZJS同学在刚学玩整数加减后,就在那里比试谁厉害。比试内容是:将一个正整数N拆成若干个正整数的和。看谁拆的多谁就赢。为了赢得比赛,CX就向你求助一个整数的所有拆分方法。
输入要求

输入N( 1 <= N <= 50 )。
输出要求

对于N的拆分方法,请以字典序排列(数字越大越排前面)。每一种拆分之间以一个空格分开。
输入样例

7
输出样例:

来源

NBUOJ

1.3.2 算法思路(暴力DFS深搜)

这里直接给大家介绍我的算法思路了:观察输出用例我们可以发现,数字的排列顺序就是从大到小的,因此我们可以从大到小不断添加元素(如果>=target就回溯,其中=target的时候就进行输出)这样我们就可以不重不漏的找到所有的拆分情况
算法步骤(重要)

  1. 我们设计一个递归函数dfs(int[] numArr, int curLen, int curSum, int target, int curNum)用于查找所有的拆分情况,各个参数含义如下所示:
  • numArr:保存已经被拆分的数字
  • curLen:记录numArr的元素个数
  • curSum:当前numArr元素之和(即已经拆分元素之和)
  • target:拆分目标和
  • curNum:当前需要进行匹配的元素
  1. 然后从[curNum ------ 1]不断从大到小尝试添加元素到拆分数组,直到所有情况都搜索完毕

1.3.3 AC代码(Java实现)

NBUOJ上面Java带中文注释会报错!因此这里放了两个版本的代码,前面不带注释的可以直接跑OJ通过,后面的方便读者阅读

1.3.3.1 优化点

本题如果使用Java语言是比较不容易过的,我的代码做优化的地方有如下几点:

  • 输入输出优化:输入流将Scanner换成了BufferReader、输出流将System.out换成了PrintWriter
  • 数据结构优化:使用List集合保存拆分元素,频繁调用add和remove方法,开销太大,因此我换用数组保存拆分元素;并且由于输入组数很多,打印频率非常高,于是我使用StringBuildler保存全部的结果,最后统一打印
  • 函数参数优化:此时终于将时间变成了 2016ms!最后一步优化就是将StringBuilder和numArr数组从函数参数中抽离到成员变量,省去了函数调用过程中传参的开销,至此程序优化到1703ms!
1.3.3.2 不带注释版本
java 复制代码
package week1.blog_code.exer1002;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;

public class Main {
    private static int[] numArr = new int[51];
    private static StringBuilder stringBuilder = new StringBuilder();
    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        PrintStream printStream = new PrintStream(System.out);
        String line = null;
        while ((line = reader.readLine()) != null) {
            int target = Integer.parseInt(line);
            numArr = new int[51];
            stringBuilder = new StringBuilder();
            dfs(numArr, 0, 0, target, target);
            printStream.print(stringBuilder.toString());
        }

    }

    public static void dfs(int[] numArr, int curLen, int curSum, int target, int curNum) {
        if (curSum == target) {
            for (int i = 0; i < curLen - 1; i++) {
                stringBuilder.append(numArr[i] + " ");
            }
            stringBuilder.append(numArr[curLen - 1] + "\n");
            return;
        }
        for (int i = curNum; i >= 1; i--) {
            if (curSum + i <= target) {
                numArr[curLen] = i;
                dfs(numArr,curLen + 1, curSum + i, target, i);
            }
        }
    }
}
1.3.3.3 带注释版本
java 复制代码
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;

public class Main {
    private static int[] numArr = new int[51];
    private static StringBuilder stringBuilder = new StringBuilder();
    public static void main(String[] args) throws IOException {
        // 缓冲输入流(高级版平替scanner)
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        // 打印流(高级版平替System.out.print)
        PrintStream printStream = new PrintStream(System.out);
        String line = null;
        while ((line = reader.readLine()) != null) {
            int target = Integer.parseInt(line);
            numArr = new int[51];
            stringBuilder = new StringBuilder();
            dfs(numArr, 0, 0, target, target);
            printStream.print(stringBuilder.toString());
        }

    }

    /**
     * @param numArr 当前拆分元素构成的数组
     * @param curLen 当前拆分数组长度
     * @param curSum 当前拆分方法之和
     * @param target 目标和
     * @param curNum 当前遍历到的数
     */
    public static void dfs(int[] numArr, int curLen, int curSum, int target, int curNum) {
        // 出口:当前拆分和==目标和就不用继续搜索
        if (curSum == target) {
            for (int i = 0; i < curLen - 1; i++) {
                stringBuilder.append(numArr[i] + " ");
            }
            stringBuilder.append(numArr[curLen - 1] + "\n");
            return;
        }
        for (int i = curNum; i >= 1; i--) {
            if (curSum + i <= target) {
                // 将i值加入拆分数组中
                numArr[curLen] = i;
                // 继续深搜
                dfs(numArr,curLen + 1, curSum + i, target, i);
            }
        }
    }
}

1.3.4 扩展题

这里放一些跟本题类似的OJ题(读者可自行尝试解题):

  1. LeetCode46 全排列:https://leetcode.cn/problems/permutations/description/
  2. LeetCOde78 子集:https://leetcode.cn/problems/subsets/description/
相关推荐
wfeqhfxz25887825 小时前
YOLO13-C3k2-GhostDynamicConv烟雾检测算法实现与优化
人工智能·算法·计算机视觉
毕设源码-朱学姐5 小时前
【开题答辩全过程】以 基于JavaWeb的网上家具商城设计与实现为例,包含答辩的问题和答案
java
Aaron15885 小时前
基于RFSOC的数字射频存储技术应用分析
c语言·人工智能·驱动开发·算法·fpga开发·硬件工程·信号处理
C雨后彩虹7 小时前
CAS与其他并发方案的对比及面试常见问题
java·面试·cas·同步·异步·
_不会dp不改名_7 小时前
leetcode_3010 将数组分成最小总代价的子数组 I
算法·leetcode·职场和发展
童话名剑7 小时前
序列模型与集束搜索(吴恩达深度学习笔记)
人工智能·笔记·深度学习·机器翻译·seq2seq·集束搜索·编码-解码模型
java1234_小锋8 小时前
Java高频面试题:SpringBoot为什么要禁止循环依赖?
java·开发语言·面试
2501_944525548 小时前
Flutter for OpenHarmony 个人理财管理App实战 - 账户详情页面
android·java·开发语言·前端·javascript·flutter
计算机学姐8 小时前
基于SpringBoot的电影点评交流平台【协同过滤推荐算法+数据可视化统计】
java·vue.js·spring boot·spring·信息可视化·echarts·推荐算法