Skip to content

喵哈喽~!各位算法大师们,你们好呀!咱是乃爱算法的小猫娘 Nyaatra~!今天咱要和大家一起玩一个有趣的数字游戏,就是 Codeforces 上的 1673C 题,叫做“回文基数”喵!(ฅ'ω'ฅ)

这个问题就像是把一堆闪闪发光的宝石(回文数)放进一个盒子里,让它们的总重量恰好等于 n,我们要数一数总共有多少种放法,听起来是不是很有趣呀?那么,就让咱带大家一起来看看怎么解决这个难题吧!

题目大意

首先,我们来理解一下题目的要求是什么喵~

题目会给我们一个正整数 n。我们需要找到,有多少种不同的方法可以将 n 表示成一堆正回文数的和。

  • 什么是回文数呢? 就是一个正着读和反着读都一样的数字,比如 5, 121, 3443 都是回文数,但是 10, 12 就不是啦。
  • 什么是不同的方法呢? 题目里说,只要每种回文数使用的数量不一样,就算作不同的方法。比如,对于 n=5
    • 5 = 3 + 1 + 15 = 1 + 3 + 1 被认为是同一种方法,因为它们都用了 1 个 3 和 2 个 1
    • 5 = 4 + 15 = 3 + 1 + 1 就是不同的方法,因为第一种用了 41,第二种用了 31,使用的回文数种类和数量都不同。

简单来说,就是把 n 分解成若干个回文数的和,求分解方案数。因为答案可能会很大,所以需要对 10^9 + 7 取模。

解题思路 (ฅ'ω'ฅ)

看到这个问题,咱的猫猫直觉告诉咱,这很可能是一个动态规划(DP)问题!为什么呢?因为它要求“方案数”,而且一个大问题的解可以由小问题的解推导出来。

这个问题其实是一个经典的组合问题,可以转化成我们非常熟悉的完全背包问题

  • 背包的容量 (Knapsack Capacity): 就是题目给定的目标和 n
  • 物品 (Items): 就是所有小于等于 n 的正回文数。比如 1, 2, 3, ..., 9, 11, 22, ... 等等。
  • 物品的重量 (Item Weight): 每个回文数 p 的“重量”就是它本身的值 p
  • 物品的价值 (Item Value): 在这里我们不关心价值,我们只关心方案数。可以认为每个物品的价值都是 1。
  • 问题转化: 求用这些可以无限次使用的“物品”(回文数),恰好装满容量为 n 的“背包”的方案总数是多少?

这不就是完全背包问题的模板了嘛!

于是,我们的解题步骤就清晰了喵:

  1. 预处理: 先把所有 140000 之间的回文数都找出来,存起来当我们的“物品列表”。因为题目中 n 的最大值是 40000,所以我们只需要处理到这个范围就行啦。
  2. 动态规划:
    • 我们创建一个 DP 数组,dp[i] 表示凑成数字 i 的方案数。
    • 状态定义: dp[i] 代表将 i 表示成回文数之和的方案数。
    • 初始化: dp[0] = 1。这是个很重要的基础!因为凑成 0 的方案只有一种,就是什么都不选。
    • 状态转移: 我们遍历每一个回文数 p(我们的物品),然后用它来更新 DP 数组。对于从 pn 的每一个 j,我们都可以通过在凑成 j-p 的方案上再加一个 p 来得到凑成 j 的新方案。所以状态转移方程就是: dp[j] = (dp[j] + dp[j - p]) % MOD

因为题目有多组测试数据,而 n 的范围是固定的,所以我们可以先把 140000 的所有 dp 值都计算出来。这样,对于每个查询,我们就可以直接输出 dp[n] 的值,非常快,喵~!

代码题解

下面就是这份思路的 C++ 实现啦,咱在代码里加了一些注释,方便大家理解每一部分在做什么哦!

cpp
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

// 定义常量喵
const int MAX_N = 40000;
const int MOD = 1000000007;

// 全局的 DP 表和回文数列表
// precompute() 函数会把它们填满
std::vector<int> dp(MAX_N + 1, 0);
std::vector<int> palindromes;

// 检查一个数是不是回文数的函数
// 一个数是回文数,如果它的字符串表示正反都一样,nya~
bool is_palindrome(int n) {
    std::string s = std::to_string(n);
    std::string reversed_s = s;
    std::reverse(reversed_s.begin(), reversed_s.end());
    return s == reversed_s;
}

// 预处理函数,在处理测试用例前只调用一次
void precompute() {
    // 1. 生成所有小于等于 MAX_N 的回文数
    // 我们要把所有能用的“宝石”(回文数)都收集起来!
    for (int i = 1; i <= MAX_N; ++i) {
        if (is_palindrome(i)) {
            palindromes.push_back(i);
        }
    }

    // 2. 动态规划,计算凑成每个数的方案数
    // 这就是完全背包问题的核心部分啦
    // dp[i] 会存储把 i 表示成回文数之和的方案数

    // 基础情况:凑成 0 的方案有 1 种(什么都不选)
    dp[0] = 1;

    // 遍历我们收集到的每一种回文数“宝石”
    for (int p : palindromes) {
        // 更新 DP 表,对于所有能用上 p 的 j
        for (int j = p; j <= MAX_N; ++j) {
            // 凑成 j 的方案数,等于原来的方案数,
            // 加上“在凑成 j-p 的方案上再加一个 p”的方案数
            dp[j] = (dp[j] + dp[j - p]) % MOD;
        }
    }
}

// 解决单个测试用例的函数
void solve() {
    int n;
    std::cin >> n;
    // 答案我们已经提前算好啦,直接从 DP 表里拿出来就行
    std::cout << dp[n] << "\n";
}

int main() {
    // 快速输入输出,让程序跑得更快一点~
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    // 进行一次预计算
    precompute();

    int t;
    std::cin >> t;
    while (t--) {
        solve();
    }

    return 0;
}

知识点介绍:完全背包问题

这道题的核心就是完全背包问题方案数的变种。让咱来详细讲讲这个知识点吧,喵~

什么是完全背包问题?

标准定义是:有 N 种物品和一个容量为 V 的背包,每种物品都有无限件可用。第 i 种物品的重量是 w[i],价值是 v[i]。求解将哪些物品装入背包,可使这些物品的耗费的总重量不超过背包容量,且价值总和最大。

和本题的联系

我们的问题不是求最大价值,而是求恰好装满背包的方案数

  • 物品: 所有回文数 p
  • 物品重量: p 本身。
  • 背包容量: n
  • 目标: 求恰好装满容量为 n 的背包,有多少种不同的装法。

DP 状态与转移

  1. 状态定义:dp[j]:表示装满容量为 j 的背包的方案总数。

  2. 初始化:dp[0] = 1。这是关键!容量为 0 的背包只有一种装法,就是什么都不装。所有后续的计算都基于这个初始状态。其他的 dp[j] (j>0) 初始化为 0。

  3. 状态转移方程: 对于第 i 个物品(重量为 w[i]),当我们考虑如何装满容量为 j 的背包时,可以有两种选择:

    • 不放i 个物品:方案数来自之前的结果,即 dp[j] 的旧值。
    • i 个物品:这意味着我们从容量为 j-w[i] 的背包状态转移而来,方案数为 dp[j-w[i]]

    所以,总的方案数是这两种选择的方案数之和。当我们用循环实现时,这个过程可以简化为: dp[j] = dp[j] (不放第 i 个物品的方案数) + dp[j - w[i]] (放第 i 个物品的方案数)

    写成代码就是:

    cpp
    // 遍历物品
    for (int p : palindromes) { 
        // 遍历背包容量
        for (int j = p; j <= MAX_N; ++j) {
            dp[j] = (dp[j] + dp[j - p]) % MOD;
        }
    }

    这里有一个小细节:为什么内层循环 j从小到大(正序)的?

    • 正序循环(完全背包): 当我们计算 dp[j] 时,dp[j-p] 已经是被当前物品 p 更新过的状态。这意味着 dp[j-p] 可能已经包含了物品 p。所以 dp[j-p] 再加上一个 p 得到 dp[j],就实现了“物品 p 可以使用多次”的效果。
    • 倒序循环(0/1背包): 如果内层循环是从大到小,那么在计算 dp[j] 时,所用到的 dp[j-p] 还是上一轮(处理前一个物品时)的结果,没有被当前物品 p 更新过。这就保证了每个物品最多只被使用一次。

所以,通过这个小小的循环方向的差异,我们就能区分开完全背包和 0/1 背包啦!是不是很奇妙呢?喵~

好啦,今天的题解分享就到这里啦!希望大家都能理解并掌握完全背包这个强大的工具!如果还有什么问题,随时可以再来找咱玩哦!Nya~ (ฅ^•ﻌ•^ฅ)

Released under the MIT License.