要解决这个问题,即给定一个长度不超过50的数字字符串,计算所有非空子序列(子序列定义为从原序列中删除零个或多个字符后形成的序列,顺序保持不变)中,其构成的数字能被3整除的个数,答案需对 (10^9 + 7) 取模。
解题思路
一个数字可以被3整除当且仅当其各位数字之和能被3整除。因此,问题转化为寻找所有非空子序列,使得子序列的数字之和模3等于0。
使用动态规划(DP)来解决:
- 状态定义:
dp[j]
表示考虑当前字符时,子序列数字和模3余j
的个数(包括空序列),其中j
取值为0、1、2。 - 初始化:初始时(空序列),
dp[0] = 1
(空序列和为0),dp[1] = 0
,dp[2] = 0
。 - 状态转移:对于字符串中的每个字符,其数字值为
d
,计算d_mod = d % 3
(值为0、1或2)。更新DP状态:- 对于每个余数
j
(0、1、2),计算新余数k = (j - d_mod + 3) % 3
(确保k
非负)。 - 更新公式:
new_dp[j] = (dp[j] + dp[k]) % mod
,其中dp[j]
表示不选当前字符,dp[k]
表示选当前字符(因为选当前字符时,新余数为(k + d_mod) % 3 = j
)。
- 对于每个余数
- 更新DP:处理完每个字符后,用
new_dp
更新dp
。 - 结果:最终
dp[0]
包含空序列,因此非空子序列中数字和模3余0的个数为(dp[0] - 1) % mod
。
代码实现
#include <iostream>
#include <string>
using namespace std;
const int mod = 1000000007;
int main() {
string s;
cin >> s;
int n = s.size();
// dp[j] 表示子序列和模3余j的个数(包括空序列)
long long dp[3] = {1, 0, 0}; // 初始化:空序列和为0
for (int i = 0; i < n; i++) {
int d = s[i] - '0'; // 字符转数字
int d_mod = d % 3; // 取模3,值为0、1或2
long long new_dp[3] = {0, 0, 0}; // 临时数组存储新状态
// 更新每个余数状态
for (int j = 0; j < 3; j++) {
int k = (j - d_mod + 3) % 3; // 计算旧余数k,确保非负
new_dp[j] = (dp[j] + dp[k]) % mod;
}
// 复制回dp数组
for (int j = 0; j < 3; j++) {
dp[j] = new_dp[j];
}
}
// 结果:非空子序列个数(排除空序列)
long long ans = (dp[0] - 1) % mod;
if (ans < 0) ans += mod; // 确保非负
cout << ans << endl;
return 0;
}
代码解释
- 输入处理:读取字符串
s
。 - 初始化DP数组:
dp[0] = 1
(空序列),dp[1] = 0
,dp[2] = 0
。 - 遍历字符串:对每个字符:
- 计算其数字值
d
和模3值d_mod
。 - 初始化
new_dp
为{0, 0, 0}
。 - 对于每个余数
j
(0、1、2):- 计算旧余数
k = (j - d_mod + 3) % 3
(加3确保非负)。 - 更新
new_dp[j] = (dp[j] + dp[k]) % mod
(不选和选当前字符的贡献)。
- 计算旧余数
- 将
new_dp
复制到dp
。
- 计算其数字值
- 输出结果:
dp[0] - 1
即为非空子序列中数字和模3余0的个数,取模并确保非负后输出。
此方法时间复杂度为 (O(n))((n) 为字符串长度),空间复杂度为 (O(1)),高效适用于长度不超过50的字符串。