牛客网:NC21302被3整除的子序列
问题描述
本题来自牛客网(NC21302),要求计算一个数字串中有多少个子序列构成的数字能被3整除,答案需对10^9+7取模。
输入是一个由数字构成的字符串,长度不超过50;输出是一个整数,表示能被3整除的子序列数量。
解题思路
这是一个典型的动态规划问题,我们可以通过分析数论性质进行高效求解。
数学基础
根据数论,一个数能被3整除的充要条件是该数各位数字之和能被3整除。因此,我们只需关注子序列各位数字之和模3的余数。
动态规划状态设计
我们用状态数组a[i]
表示当前已处理字符后,模3余数为i的子序列个数:
a[0]
: 模3余数为0的子序列数(能被3整除)a[1]
: 模3余数为1的子序列数a[2]
: 模3余数为2的子序列数
状态转移过程
对于每个新数字t
,我们有两种选择:
- 不选该数字,原有状态保持不变
- 选该数字,需要将原有状态的余数与新数字的余数组合
状态转移方程:
b[(i+t)%3] = a[i] + a[(i+t)%3]
(表示选择当前数字的情况)- 同时对于单独选择当前数字的情况:
b[t%3]++
代码解析
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int a[3],b[3]; // a[i]表示余数为i的子序列数量,b为临时数组
char ch;
int main(){
while(cin>>ch){
int t=ch-'0'; // 将字符转为数字
for(int i=0;i<3;i++)
b[(i+t)%3]=a[i]+a[(i+t)%3]; // 状态转移
b[t%3]++; // 单独选择当前数字的情况
for(int i=0;i<3;i++)
a[i%3]=b[i%3]%mod; // 更新状态并取模
}
cout<<a[0]<<endl; // 输出能被3整除的子序列数量
}
算法分析
时间复杂度
- O(n),其中n为字符串长度,每个字符只需处理一次
空间复杂度
- O(1),只使用了常数级别的额外空间
示例解析:举例输入132:
a= [0, 0, 0] (余数0/1/2的组合数初始化为0)
处理第一个字符’1’(t=1)
步骤1:计算b数组转移
for(int i=0; i<3; i++)
b[(i+1)%3] = a[i] + a[(i+1)%3];
初始b全为0,计算后:
b[1] = a[0] + a[1] = 0+0=0
b[2] = a[1] + a[2] = 0+0=0
b[0] = a[2] + a[0] = 0+0=0
步骤2:新增当前数字
b[1%3]++; // b[1]++
b数组变为:[0, 1, 0]
步骤3:更新a数组
a = b → [0, 1, 0]
处理第二个字符’3’(t=3)
步骤1:计算b数组转移
for(int i=0; i<3; i++)
b[(i+3)%3] = a[i] + a[(i+3)%3];
由于(i+3)%3 = i,计算后:
b[0] = a[0] + a[0] = 0+0=0
b[1] = a[1] + a[1] = 1+1=2
b[2] = a[2] + a[2] = 0+0=0
步骤2:新增当前数字
b[3%3]++; // b[0]++
b数组变为:[1, 2, 0]
步骤3:更新a数组
a= b → [1, 2, 0]
处理第三个字符’2’(t=2)
步骤1:计算b数组转移
for(int i=0; i<3; i++)
b[(i+2)%3] = a[i] + a[(i+2)%3];
初始b全为0,计算后:
b[2] = a[0] + a[2] = 1+0=1
b[0] = a[1] + a[0] = 2+1=3
b[1] = a[2] + a[1] = 0+2=2
步骤2:新增当前数字
b[2%3]++; // b[2]++
b数组变为:[3, 2, 2]
步骤3:更新a数组
a= b → [3, 2, 2]
最终输出结果
a[0] = 3,表示存在3个余0的子序列,具体包括:
单独字符"3"(余0)
子序列"12"(1+2=3,余0)
子序列"132"(1+3+2=6,余0)
关键点总结
状态转移逻辑:每个新字符会基于历史状态生成新余数组合(如B[(i+t)%3] += A[i])
单独字符计数:通过B[t%3]++将当前字符单独视为一个子序列
动态更新:通过模运算mod=1e9+7防止溢出,但此示例未涉及大数问题
总结
这道题巧妙地利用了数论性质和动态规划思想,通过维护模3余数的状态来高效计算满足条件的子序列数量。核心在于理解状态转移的过程和模运算的性质。