【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/
相关推荐
丁总学Java5 分钟前
--spring.profiles.active=prod
java·spring
上等猿12 分钟前
集合stream
java
java1234_小锋16 分钟前
MyBatis如何处理延迟加载?
java·开发语言
菠萝咕噜肉i17 分钟前
MyBatis是什么?为什么有全自动ORM框架还是MyBatis比较受欢迎?
java·mybatis·框架·半自动
林的快手31 分钟前
209.长度最小的子数组
java·数据结构·数据库·python·算法·leetcode
千天夜40 分钟前
多源多点路径规划:基于启发式动态生成树算法的实现
算法·机器学习·动态规划
从以前1 小时前
准备考试:解决大学入学考试问题
数据结构·python·算法
stm 学习ing1 小时前
HDLBits训练6
经验分享·笔记·fpga开发·fpga·eda·verilog hdl·vhdl
向阳12181 小时前
mybatis 缓存
java·缓存·mybatis
.Vcoistnt1 小时前
Codeforces Round 994 (Div. 2)(A-D)
数据结构·c++·算法·贪心算法·动态规划