OneCoder Avatar
OneCodercoderli.com · 957 篇博文
四级

C++ 算法考级专栏

真题分析、矩阵探测、递归回溯与基础语法

🎨 视觉封面

【GESP/CSP练习】GESP四级 / CSP-J 题解:luogu-P1028 [NOIP2001 普及组] 数的计算

📅 2026-09-17·✍️ OneCoder·计算中...·⏱️ 11 分钟
#NOIP#洛谷#C++#递推#记忆化#GESP四级#CSP-J

NOIP 2001 普及组第一题,洛谷 P1028。本题是算法竞赛与考级中学习简单递归、记忆化与线性递推模型的标志性启蒙经典试题,被标准收录于 CCF GESP 四级认证考纲(简单递归与递推初步考点)及 CSP-J 普及组核心基础必做题单。题目核心考察如何将数列构造规则抽象为自底向上的子问题状态转移方程,并理解直接纯递归导致的指数级重复计算瓶颈,进而利用数组递推(或记忆化搜索)实现 O(n2)\mathcal{O}(n^2) 乃至 O(n)\mathcal{O}(n) 的高效求解。题目难度⭐☆☆☆☆,洛谷难度等级评定为普及-

luogu-P1028 [NOIP2001 普及组] 数的计算

🔗 洛谷原题传送门luogu-P1028 [NOIP2001 普及组] 数的计算

题目要求

题目描述

给出自然数 nn,要求按如下方式构造数列:

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

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

输入格式

输入只有一行一个整数,表示 nn

输出格式

输出一行一个整数,表示合法的数列个数。

输入输出样例 #1

样例输入 #1
Code 1 行
6
样例输出 #1
Code 1 行
6

说明/提示

样例 1 解释

满足条件的数列为:

  • 66
  • 6,16, 1
  • 6,26, 2
  • 6,36, 3
  • 6,2,16, 2, 1
  • 6,3,16, 3, 1
数据规模与约定

对于全部的测试点,保证 1n1031 \leq n \leq 10^3


题目深度剖析

1. 规则转化与数学建模

题目要求以一个给定的自然数 nn 开头,每次可以在当前数列末尾添加一个正整数 xx,且该正整数必须满足: 1x末尾项21 \le x \le \lfloor \frac{\text{末尾项}}{2} \rfloor

我们需要统计从数字 nn 出发,能够产生的所有合法数列的总数量。

观察构造过程可以发现:后续允许添加的数字集合仅取决于当前数列的末尾数字,而与更早之前添加了哪些数完全无关。这正是典型的“无后效性”特征。

我们定义状态:

  • f(i)f(i) 表示以正整数 ii 为开头的合法数列的总个数。

对于以 ii 开头的任意合法数列,其构成方式分为两类:

  1. 单元素数列:仅由 [i][i] 本身构成的数列,方案数为 11
  2. 多元素数列:在 ii 后面拼接一个合法正整数 jj(满足 1ji/21 \le j \le \lfloor i/2 \rfloor)。一旦确定了第二项为 jj,由于后续的拼接规则完全由 jj 及其后续末尾项决定,因此以 jj 为开头的合法数列共有 f(j)f(j) 种,每一种都可以无缝拼接在 ii 的后面形成以 [i,j,][i, j, \dots] 开头的新合法数列。

综合以上两种情况,我们可以得到严密的数学递推关系式: f(i)=1+j=1i/2f(j)f(i) = 1 + \sum_{j=1}^{\lfloor i / 2 \rfloor} f(j)

边界条件非常直观:

  • i=1i = 1 时,1/2=0\lfloor 1/2 \rfloor = 0,求和项为空,故 f(1)=1f(1) = 1(仅有数列 [1][1]);
  • i=2i = 2 时,2/2=1\lfloor 2/2 \rfloor = 1f(2)=1+f(1)=1+1=2f(2) = 1 + f(1) = 1 + 1 = 2(数列为 [2],[2,1][2], [2, 1]);
  • i=3i = 3 时,3/2=1\lfloor 3/2 \rfloor = 1f(3)=1+f(1)=1+1=2f(3) = 1 + f(1) = 1 + 1 = 2(数列为 [3],[3,1][3], [3, 1]);
  • i=6i = 6 时,6/2=3\lfloor 6/2 \rfloor = 3f(6)=1+f(1)+f(2)+f(3)=1+1+2+2=6f(6) = 1 + f(1) + f(2) + f(3) = 1 + 1 + 2 + 2 = 6(与样例完全一致)。

2. 算法演进:从指数爆炸到线性递推

在等级考试和信奥入门阶段,许多同学初次接触本题时容易直接写出无记忆化的纯递归函数:

C++ 8 行
// 错误示范:未经记忆化的朴素递归
int dfs(int x) {
    int ans = 1;
    for (int j = 1; j <= x / 2; ++j) {
        ans += dfs(j);
    }
    return ans;
}
为什么朴素递归会严重超时?

如果对 n=1000n=1000 运行上述纯递归代码,计算 dfs(1000) 需要调用 dfs(500)dfs(250) 等,而计算 dfs(999) 同样会重复调用 dfs(499)dfs(249)……子问题被重复展开了天文数字般的次数,其调用树的时间复杂度呈指数级爆炸(O(2n)\mathcal{O}(2^n)),程序将直接卡死并导致 TLE(Time Limit Exceeded)。

自底向上的递推 DP 方案

因为计算 f(i)f(i) 只需要依赖已知比它小的子问题答案 f(1),f(2),,f[i/2]f(1), f(2), \dots, f[\lfloor i/2 \rfloor],我们完全可以采用**自底向上(Bottom-Up)**的双层循环递推:

  • 外层循环遍历 ii11nn
  • 内层循环遍历 jj11i/2i / 2,累加已算出的 f[j]f[j]
  • 状态计算完毕后直接保存在数组 f[i] 中。
方案 核心思想 时间复杂度 空间复杂度 洛谷评测结果
朴素无记忆化递归 自顶向下暴力递归展开 O(2n)\mathcal{O}(2^n) 指数级 O(n)\mathcal{O}(n) 栈空间 严重超时 (TLE)
自底向上线性递推 (DP) 循环自小到大推导,打表保存状态 O(n2)2.5×105\mathcal{O}(n^2) \approx 2.5 \times 10^5 次运算 O(n)\mathcal{O}(n) 数组空间 最优满分 (AC),耗时 < 5ms

n1000n \le 1000 的数据规模下,内层总计算次数仅为 i=11000i21000×10004=2.5×105\sum_{i=1}^{1000} \frac{i}{2} \approx \frac{1000 \times 1000}{4} = 2.5 \times 10^5 次加法,现代计算机可在不到 11 毫秒内瞬时算出!

3. 数据边界与整型溢出防范

在编写算法题解时,必须对数值上限保持高度敏锐:

  • n=1000n = 1000 时,经过递推计算得出的 f(1000)=1981471878f(1000) = 1981471878
  • 标准 32 位有符号整数 int 的上限为 2311=21474836472^{31} - 1 = 2147483647
  • 可以看出,19814718781981471878 已经达到了约 1.98×1091.98 \times 10^9,与 int 最大上限仅差不到 8%8\%

如果在累加过程中稍有微小改动或拓展,极易触发 32 位整型上溢。因此,遵循 CCF GESP 及 CSP 优良规范,本题推荐使用 64 位整型 long long 存储状态数组,彻底消除边界溢出隐患。

4. 规范避坑与实现要点

  1. 严禁局部变长数组(VLA)
    • 严禁在 main() 函数内部声明形如 long long f[n + 1]; 的局部数组;
    • 规范做法是在全局数据区声明固定常量与数组:const int MAXN = 1005; long long f[MAXN];。全局数组位于静态存储区,自动初始化为全 0。
  2. 标准竞赛输入输出
    • 保持主函数输入逻辑干净清晰,直接 cin >> n;,不引入冗余的防御性判断代码;
    • 无需复杂的快读模板,基础 cin/cout 即可轻量级秒杀本题。

完整参考代码 (C++11)

C++ 40 行
/**
 * Problem: luogu-P1028 [NOIP2001 普及组] 数的计算
 * Algorithm: 基础递推 (Dynamic Programming / Recurrence)
 * Standard: C++11 (CCF GESP 官方大纲规范)
 * Author: OneCoder
 */

#include <iostream>

using namespace std;

// 数据规模保证 1 <= n <= 1000
// f[i] 表示以正整数 i 为起点的所有合法数列总数
// 经推导 f(1000) = 1981471878,极接近 32 位有符号整型上限 (2147483647)
// 在全局数据区开辟静态数组,杜绝局部变长数组 (VLA),使用 long long 确保安全防溢出
const int MAXN = 1005;
long long f[MAXN];

int main() {
    int n;
    // 直接读入目标正整数 n
    cin >> n;

    // 自底向上递推计算每一个子问题的解 (1 到 n)
    for (int i = 1; i <= n; ++i) {
        // 每个数本身作为一个单元素数列 [i],即为 1 种合法方案
        f[i] = 1;

        // 在 i 后面可以追加的正整数 j 必须满足 1 <= j <= i / 2
        // 追加 j 之后,后续所有以 j 为开头的合法数列均可接在其后,故累加 f[j]
        for (int j = 1; j <= i / 2; ++j) {
            f[i] += f[j];
        }
    }

    // 输出以 n 开头的合法数列总数量
    cout << f[n] << endl;

    return 0;
}
💡 OneCoder 资源指引

所有代码开源上传至 GitHub:yummy-code 仓库 · GESP 专题站:GESP WIKI

🤝 技术交流与答疑

欢迎加入:C++ GESP/CSP 考级答疑群(688906745)Java/Python交流群(982860385),点击可直接加群。

📚

猜你想读 · 相关文章推荐

GESP 编程与算法 · 四级⏱️ 14 分钟

【GESP/CSP练习】GESP四级 / CSP-J 题解:luogu-P1093 [NOIP2007 普及组] 奖学金

NOIP 2007 普及组第一题,洛谷 P1093。本题是青少年信息学奥赛与编程等级考试中学习结构体(struct)与多关键字排序(Multi-key Sorting)的标志性入门经典试题,被标准收录于 CCF GESP 四级认证考纲(结构体与排序考点)及 CSP-J 普及组核心必做题单。题目重点考察结构体定义、数据复...

阅读全文 →
GESP 编程与算法 · 四级⏱️ 5 分钟

【GESP真题】GESP四级 / CSP-J 题解:luogu-B4579 [GESP202609 四级] 新汉诺塔

CCF GESP 2026年9月认证(第十五次认证)C++ 四级试题,洛谷 B4579。本题严格遵循 CCF GESP 官方大纲规范,重点考察顺时针单向递归与双状态递推。题目逻辑严密,模型典型,是深入理解与掌握信奥核心考点的经典范例。

阅读全文 →
GESP 编程与算法 · 四级⏱️ 10 分钟

【GESP/CSP练习】GESP四级 / CSP-J 题解:luogu-P1271 【深基9.例1】选举学生会

洛谷 P1271【深基9.例1】选举学生会。本题出自《深入浅出程序设计竞赛 - 基础篇》,是学习计数排序(Counting Sort / 桶排序思想)与非比较排序算法的标志性入门经典试题,被标准收录于 CCF GESP 四级认证考纲(初级排序考点)及 CSP-J 普及组核心必做题单。题目核心考点在于突破传统基于比较的...

阅读全文 →
OneCoder

OneCoder (lihongzheshuai)

一个中年人的自留地,记录学习 C++、GESP/NOI、Java、Python 与算法架构的心得体会。本站唯一网址:coderli.com

读者讨论与留言

0 条讨论
✨ 支持点击留言直接回复 · Markdown 引用格式
💬 还没有读者留言,快来成为第一个讨论者吧!