1. 题目解析与解题思路
小红删数字这道题目看似简单,但蕴含着动态规划中状态压缩的经典思想。题目要求我们对一个数字序列的相邻数字之间选择加法或乘法运算,最终统计所有可能运算结果(0-9)的方案数。
1.1 题目核心理解
题目有几个关键点需要注意:
- 运算顺序是从左到右依次进行
- 每一步运算后都要对10取模(即只保留个位数)
- 需要统计所有可能的运算符组合下,最终结果为0-9的方案数
举个例子,对于数字序列[2,3],可能的运算方式有:
- 2+3=5
- 2×3=6
所以结果为5和6的方案数各为1,其他结果为0。
1.2 动态规划思路选择
这道题最自然的解法是动态规划,因为:
- 问题具有最优子结构:最终结果取决于前面的运算结果
- 存在重叠子问题:不同的运算符组合可能产生相同的中间结果
但直接暴力枚举所有可能的运算符组合显然不可行,因为时间复杂度会达到O(2^n)。我们需要找到状态压缩的方法。
2. 动态规划状态设计
2.1 状态定义
定义cnt[i][v]表示从第i个数字到最后一个数字的子序列,经过运算后结果为v的方案数。这里v的取值范围是0-9。
这种状态定义的精妙之处在于:
- 将结果范围压缩到0-9,大大减少了状态空间
- 从右向左计算,可以方便地利用已经计算出的子问题结果
2.2 状态转移方程
对于每个位置i和可能的结果v(0-9),状态转移有两种情况:
- 选择加法:
cnt[i-1][(a[i-1]+v)%10] += cnt[i][v] - 选择乘法:
cnt[i-1][(a[i-1]*v)%10] += cnt[i][v]
每次转移都要对1e9+7取模,防止数值溢出。
3. 算法实现细节
3.1 初始化处理
初始化时,最后一个数字的结果就是它本身:
cpp复制cnt[n][a[n]] = 1;
这里需要注意一个边界情况:当n=1时,直接输出该数字对应的结果为1,其他为0。
3.2 递推过程
从倒数第二个数字开始向前递推:
cpp复制for(int i=n; i>1; --i) {
for(int j=0; j<10; ++j) {
// 加法转移
cnt[i-1][(a[i-1]+j)%10] =
(cnt[i-1][(a[i-1]+j)%10] + cnt[i][j]) % MOD;
// 乘法转移
cnt[i-1][(a[i-1]*j)%10] =
(cnt[i-1][(a[i-1]*j)%10] + cnt[i][j]) % MOD;
}
}
3.3 结果输出
最终,第一个数字位置的所有可能结果就是答案:
cpp复制for(int i=0; i<10; ++i) {
cout << cnt[1][i] << ' ';
}
4. 算法复杂度分析
4.1 时间复杂度
算法有两层循环:
- 外层循环遍历n-1个数字位置
- 内层循环遍历10种可能的结果状态
因此时间复杂度为O(10n),对于n=2e5的数据规模完全可接受。
4.2 空间复杂度
使用了一个n×10的二维数组存储状态,空间复杂度为O(10n)。可以进一步优化为O(10)的空间,因为每次只需要前一个位置的状态。
5. 优化与扩展
5.1 空间优化
实际上我们只需要保存前一个位置的状态即可:
cpp复制int curr[10], prev[10];
prev[a[n]] = 1;
for(int i=n; i>1; --i) {
memset(curr, 0, sizeof(curr));
for(int j=0; j<10; ++j) {
if(prev[j]) {
curr[(a[i-1]+j)%10] = (curr[(a[i-1]+j)%10] + prev[j]) % MOD;
curr[(a[i-1]*j)%10] = (curr[(a[i-1]*j)%10] + prev[j]) % MOD;
}
}
memcpy(prev, curr, sizeof(curr));
}
5.2 类似题目扩展
这种状态压缩的动态规划思想可以应用于许多类似问题:
- 给定数字序列和运算符,求特定结果的方案数
- 运算符优先级不同的情况
- 多个运算符组合的情况
6. 常见错误与调试技巧
6.1 边界条件处理
常见错误包括:
- 忘记处理n=1的特殊情况
- 初始化时没有对数字取模
- 模数写错(如写成1e9+9)
调试时可以先用小样例验证:
- [2,3]应该输出0 0 0 0 0 1 1 0 0 0
- [1,2,3]应该输出0 0 1 1 1 0 1 0 1 1
6.2 运算顺序问题
题目明确要求从左到右计算,不能先算乘法后算加法。如果误解这一点,会导致完全错误的结果。
7. 实际编码中的注意事项
- 使用long long防止中间结果溢出
- 数组大小要足够(比最大n多5左右)
- 初始化时要清零
- 取模运算要彻底,包括初始化和每次更新
8. 算法思想总结
这道题展示了动态规划中状态压缩的经典技巧:
- 识别问题中的有限状态(这里是个位数0-9)
- 设计高效的状态转移方程
- 选择合适的计算顺序(这里从右向左更直观)
这种思想在数位DP、概率DP等问题中都有广泛应用。掌握这种状态压缩技巧,可以解决许多看似复杂的问题。