喵哈喽~!各位算法大师们,你们好呀!咱是乃爱算法的小猫娘 Nyaatra~!今天咱要和大家一起玩一个有趣的数字游戏,就是 Codeforces 上的 1673C 题,叫做“回文基数”喵!(ฅ'ω'ฅ)
这个问题就像是把一堆闪闪发光的宝石(回文数)放进一个盒子里,让它们的总重量恰好等于 n
,我们要数一数总共有多少种放法,听起来是不是很有趣呀?那么,就让咱带大家一起来看看怎么解决这个难题吧!
题目大意
首先,我们来理解一下题目的要求是什么喵~
题目会给我们一个正整数 n
。我们需要找到,有多少种不同的方法可以将 n
表示成一堆正回文数的和。
- 什么是回文数呢? 就是一个正着读和反着读都一样的数字,比如
5
,121
,3443
都是回文数,但是10
,12
就不是啦。 - 什么是不同的方法呢? 题目里说,只要每种回文数使用的数量不一样,就算作不同的方法。比如,对于
n=5
:5 = 3 + 1 + 1
和5 = 1 + 3 + 1
被认为是同一种方法,因为它们都用了 1 个3
和 2 个1
。5 = 4 + 1
和5 = 3 + 1 + 1
就是不同的方法,因为第一种用了4
和1
,第二种用了3
和1
,使用的回文数种类和数量都不同。
简单来说,就是把 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
到40000
之间的回文数都找出来,存起来当我们的“物品列表”。因为题目中n
的最大值是40000
,所以我们只需要处理到这个范围就行啦。 - 动态规划:
- 我们创建一个 DP 数组,
dp[i]
表示凑成数字i
的方案数。 - 状态定义:
dp[i]
代表将i
表示成回文数之和的方案数。 - 初始化:
dp[0] = 1
。这是个很重要的基础!因为凑成 0 的方案只有一种,就是什么都不选。 - 状态转移: 我们遍历每一个回文数
p
(我们的物品),然后用它来更新 DP 数组。对于从p
到n
的每一个j
,我们都可以通过在凑成j-p
的方案上再加一个p
来得到凑成j
的新方案。所以状态转移方程就是:dp[j] = (dp[j] + dp[j - p]) % MOD
- 我们创建一个 DP 数组,
因为题目有多组测试数据,而 n
的范围是固定的,所以我们可以先把 1
到 40000
的所有 dp
值都计算出来。这样,对于每个查询,我们就可以直接输出 dp[n]
的值,非常快,喵~!
代码题解
下面就是这份思路的 C++ 实现啦,咱在代码里加了一些注释,方便大家理解每一部分在做什么哦!
#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 状态与转移
状态定义:
dp[j]
:表示装满容量为j
的背包的方案总数。初始化:
dp[0] = 1
。这是关键!容量为 0 的背包只有一种装法,就是什么都不装。所有后续的计算都基于这个初始状态。其他的dp[j]
(j>0) 初始化为 0。状态转移方程: 对于第
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~ (ฅ^•ﻌ•^ฅ)