top1编程
← 返回题目
题解

数的计数

1 条题解

  • 0
    @ 2026-8-5 23:37:13

    P4741 数的计数(基础)

    解题思路

    题目说:有一个自然数 n,可以"不做任何处理",也可以在左边加一个自然数,但加的数不能超过原数的一半;加完之后,还可以继续加。问一共能得到多少种数(包括 n 自己)。

    例如 n=6:能得到的数是 6、16、26、126、36、136,一共 6 个。我们可以总结出规律:设 f(n) 表示以 n 开头的数的个数(包括 n 本身)。那么 f(n)=1+f(1)+f(2)+…+f(⌊n/2⌋)。为什么?因为 n 自己算 1 种;如果在左边加上 i(i 从 1 到 n/2),得到的新数开头就变成了 i,后面还能继续加,所以再加上 f(i)。

    例如 f(6)=1+f(1)+f(2)+f(3)=1+1+2+2=6,和例子一致。注意这里的 f 有点特别:f(1)=1(只有"1"),f(2)=1+f(1)=2(2 和 12),f(3)=1+f(1)=2(3 和 13)。

    注意 n≤1000,如果用普通递归,f(1000) 会重复计算很多次 f(小值),非常浪费。所以用"记忆化":用一个数组 f[1005] 把算过的结果存起来,如果 f[n] 已经算过了,直接返回,不用再算。这样每个数最多算一次,速度非常快。

    边界情况:n=1 时,f(1)=1,只有"1"本身一种。输出的是满足条件的数的个数,不是把每个数列出来。

    参考代码

    // 数的计数:记忆化递归统计满足条件的数的个数
    #include <iostream>
    
    int f[1005];
    
    int cnt(int n) {
        if (f[n]) return f[n];
        int s = 1;               // 先算上自己本身
        for (int i = 1; i <= n / 2; i++) s += cnt(i);  // 左边加不超过一半的数
        return f[n] = s;
    }
    
    int main() {
        int n;
        std::cin >> n;
        std::cout << cnt(n) << "\n";
        return 0;
    }
    

    复杂度分析

    因为用了记忆化数组,每个 f(n) 只会被真正计算一次,计算 f(n) 时要循环 n/2 次。总共对 n 个不同值计算,所以时间复杂度约为 O(n²),当 n=1000 时大约 50 万次加法,一秒内轻松完成。空间上只需要一个大小 1005 的数组,是 O(n) 的,非常小。如果不做记忆化,普通递归会重复计算导致变慢,所以记忆化是这道题的关键。

    • 1