题解:用动态规划解决替换字符串使成为倍数的问题
这是一道动态规划与字符串处理结合的题目,目标是替换字符串中的 ?,使其表示的整数是正整数 p 的倍数,并计算方案数。
问题拆解
输入理解
s是由数字字符和?组成的字符串。- 每个
?需要替换成一个数字字符(0-9)。 - 最终生成的数字需要是
p的倍数。 - 输出所有满足条件的方案数,结果需要取模10^9 + 7。
核心挑战
?的替换有多种可能,需要逐一穷举。- 对于较长的字符串,穷举所有可能性会导致指数级复杂度,需要用动态规划优化。
- 字符串的前导零问题需要特殊处理。
解决方案
1. 动态规划定义
设 dp[i][j] 表示字符串前 i 个字符替换后,模 p 余数为 j 的方案数。
i是字符串的索引。j是余数,取值范围为[0, p-1]。
2. 状态转移
对于字符串中的每一位:
- 如果是数字字符:可以直接计算当前的余数。
- 如果是
?:可以枚举0-9,尝试每种可能的替换。
公式如下:
其中:
- k是上一位的余数。
- d是当前替换的数字。
3. 初始化
- dp[0][0] = 1:空串表示 0,有一种方法。
4. 最终结果
- dp[n][0] 即为所有替换方案数,其中 (n) 是字符串的长度。
代码实现
import java.util.Arrays;
public class Main {
private static final int MOD = 1000000007;
public static int solution(String s, int p) {
int n = s.length();
int[][] dp = new int[n + 1][p];
// 初始化 dp 数组
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
char current = s.charAt(i - 1);
Arrays.fill(dp[i], 0); // 重置当前行
if (current == '?') {
for (int k = 0; k < p; k++) { // 遍历所有余数
for (int d = 0; d <= 9; d++) { // 遍历数字 0-9
dp[i][(10 * k + d) % p] = (dp[i][(10 * k + d) % p] + dp[i - 1][k]) % MOD;
}
}
} else {
int digit = current - '0';
for (int k = 0; k < p; k++) {
dp[i][(10 * k + digit) % p] = (dp[i][(10 * k + digit) % p] + dp[i - 1][k]) % MOD;
}
}
}
return dp[n][0];
}
public static void main(String[] args) {
System.out.println(solution("??", 1) == 100);
System.out.println(solution("????1", 12) == 0);
System.out.println(solution("1??2", 3) == 34);
}
}
代码逻辑与测试
关键逻辑
- 动态规划状态设计:
dp[i][j]的二维数组存储每一位替换的可能状态。- 用模运算连接当前状态和之前状态。
- 优化与取模:
- 在更新
dp时,每一步都取模 10^9+7 防止溢出。
测试结果
测试用例说明:
- 用例 1:
s="??", p=1
所有可能替换的结果都能整除1,因此总方案数为 (10 \times 10 = 100)。 - 用例 2:
s="????1", p=12
由于末尾固定为1,无法通过替换其他位使其为12的倍数,因此答案为0。 - 用例 3:
s="1??2", p=3
枚举所有可能替换后计算,最终方案数为34。
感想
-
对动态规划的再认识 动态规划是一种解决多阶段决策问题的重要方法,通过分解问题并保存中间状态,避免了重复计算。在这道题中,状态
dp[i][j]记录了当前的部分解,通过状态转移公式完成整合,极大地降低了时间复杂度。这使我更深刻地体会到 "状态的定义" 和 "转移方程的设计" 是动态规划的核心。 -
解决问题时的逐步抽象 初看题目时,感觉“穷举所有可能性”似乎是唯一方法,但仔细思考发现,穷举的复杂度过高,不可能直接实现。通过将问题逐步转化为余数的计算、分阶段累积结果的过程,将原本复杂的全局问题拆解为一个个小问题,这种抽象问题能力在算法设计中非常关键。
-
对取模的认识加深 在涉及大数的情况下,模运算的使用不仅可以防止溢出,还能帮助我们简化计算和状态转移。在动态规划中,实时取模操作是非常重要的一环,否则中间状态可能会因为累积数值过大而影响结果的正确性。
-
实践中的挑战 这道题最大的挑战在于构建合适的动态规划模型。需要考虑如何优雅地处理
?的替换,并用枚举来设计状态转移。而且末尾的特殊字符也可能会让替换出现偏差,例如确定性的数字与不确定性混合后仍需准确计算模值。这一过程考验了对问题细节的分析能力。
基础知识点总结
- 动态规划基础
- 状态设计: 将问题拆解成子问题。这里的
dp[i][j]就是子问题的描述,表示替换到第i位时,模p余数为j的方案数。 - 状态转移方程: 通过递推关系将上一阶段的结果转移到当前阶段。公式:
- 字符串处理
- 对字符串中的
?逐位替换,意味着可能会有多个分支,这里通过枚举0-9的值来实现可能性扩展。 - 数字字符的直接处理相对简单,只需参与余数的递推计算。
- 取模运算
- 动态规划中,常用的取模技巧有两种:
- 递推中取模: 防止中间结果溢出;
- 最终取模: 确保返回结果在题目要求范围内。
- 本题模数 10^9 + 7 是常见的模值,防止大数运算带来的溢出问题。
- 时间复杂度分析
- 枚举
?的可能性,结合动态规划的转移次数,总复杂度约为 (O(n \cdot p \cdot 10)),其中n为字符串长度,p为模数。这在大部分输入情况下是可接受的。
总结与收获
-
解决复杂问题的分步法则 通过分解问题,将未知的复杂性降维到已知的简单问题。本题中,我们从 字符串替换 和 整除判定 出发,将问题简化为动态规划状态的累积。
-
对动态规划的应用理解 动态规划的强大之处在于可以高效地保存中间状态,而不是盲目地通过递归或暴力枚举来解决问题。掌握这一思想后,我在面对类似问题时更加自信。
-
在细节中寻找优化方向
- 用模运算优化大数处理。
- 用一维数组压缩动态规划表,节约空间。
这道题让我更清楚地意识到,"如何转化问题" 是算法学习的重要核心之一。算法解题不是为了简单的答案,而是通过不断优化和思考,提升自己的逻辑能力和问题解决能力。