小U的问号替换问题| 豆包MarsCode AI刷题

89 阅读6分钟

image.png

题解:用动态规划解决替换字符串使成为倍数的问题

这是一道动态规划与字符串处理结合的题目,目标是替换字符串中的 ?,使其表示的整数是正整数 p 的倍数,并计算方案数。

问题拆解

输入理解

  • s 是由数字字符和 ? 组成的字符串。
  • 每个 ? 需要替换成一个数字字符(0-9)。
  • 最终生成的数字需要是 p 的倍数。
  • 输出所有满足条件的方案数,结果需要取模10^9 + 7。

核心挑战

  1. ? 的替换有多种可能,需要逐一穷举。
  2. 对于较长的字符串,穷举所有可能性会导致指数级复杂度,需要用动态规划优化。
  3. 字符串的前导零问题需要特殊处理。

解决方案

1. 动态规划定义

dp[i][j] 表示字符串前 i 个字符替换后,模 p 余数为 j 的方案数。

  • i 是字符串的索引。
  • j 是余数,取值范围为 [0, p-1]

2. 状态转移

对于字符串中的每一位:

  • 如果是数字字符:可以直接计算当前的余数。
  • 如果是 ?:可以枚举 0-9,尝试每种可能的替换。

公式如下:

image.png

其中:

  • 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);
    }
}

代码逻辑与测试

关键逻辑

  1. 动态规划状态设计:
  • dp[i][j] 的二维数组存储每一位替换的可能状态。
  • 用模运算连接当前状态和之前状态。
  1. 优化与取模:
  • 在更新 dp 时,每一步都取模 10^9+7 防止溢出。

测试结果

测试用例说明:

  • 用例 1s="??", p=1
    所有可能替换的结果都能整除 1,因此总方案数为 (10 \times 10 = 100)。
  • 用例 2s="????1", p=12
    由于末尾固定为 1,无法通过替换其他位使其为 12 的倍数,因此答案为 0
  • 用例 3s="1??2", p=3
    枚举所有可能替换后计算,最终方案数为 34

感想

  1. 对动态规划的再认识 动态规划是一种解决多阶段决策问题的重要方法,通过分解问题并保存中间状态,避免了重复计算。在这道题中,状态 dp[i][j] 记录了当前的部分解,通过状态转移公式完成整合,极大地降低了时间复杂度。这使我更深刻地体会到 "状态的定义""转移方程的设计" 是动态规划的核心。

  2. 解决问题时的逐步抽象 初看题目时,感觉“穷举所有可能性”似乎是唯一方法,但仔细思考发现,穷举的复杂度过高,不可能直接实现。通过将问题逐步转化为余数的计算、分阶段累积结果的过程,将原本复杂的全局问题拆解为一个个小问题,这种抽象问题能力在算法设计中非常关键。

  3. 对取模的认识加深 在涉及大数的情况下,模运算的使用不仅可以防止溢出,还能帮助我们简化计算和状态转移。在动态规划中,实时取模操作是非常重要的一环,否则中间状态可能会因为累积数值过大而影响结果的正确性。

  4. 实践中的挑战 这道题最大的挑战在于构建合适的动态规划模型。需要考虑如何优雅地处理 ? 的替换,并用枚举来设计状态转移。而且末尾的特殊字符也可能会让替换出现偏差,例如确定性的数字与不确定性混合后仍需准确计算模值。这一过程考验了对问题细节的分析能力。

基础知识点总结

  1. 动态规划基础
  • 状态设计: 将问题拆解成子问题。这里的 dp[i][j] 就是子问题的描述,表示替换到第 i 位时,模 p 余数为 j 的方案数。
  • 状态转移方程: 通过递推关系将上一阶段的结果转移到当前阶段。公式:

image.png

  1. 字符串处理
  • 对字符串中的 ? 逐位替换,意味着可能会有多个分支,这里通过枚举 0-9 的值来实现可能性扩展。
  • 数字字符的直接处理相对简单,只需参与余数的递推计算。
  1. 取模运算
  • 动态规划中,常用的取模技巧有两种:
    • 递推中取模: 防止中间结果溢出;
    • 最终取模: 确保返回结果在题目要求范围内。
  • 本题模数 10^9 + 7 是常见的模值,防止大数运算带来的溢出问题。
  1. 时间复杂度分析
  • 枚举 ? 的可能性,结合动态规划的转移次数,总复杂度约为 (O(n \cdot p \cdot 10)),其中 n 为字符串长度,p 为模数。这在大部分输入情况下是可接受的。

总结与收获

  1. 解决复杂问题的分步法则 通过分解问题,将未知的复杂性降维到已知的简单问题。本题中,我们从 字符串替换整除判定 出发,将问题简化为动态规划状态的累积。

  2. 对动态规划的应用理解 动态规划的强大之处在于可以高效地保存中间状态,而不是盲目地通过递归或暴力枚举来解决问题。掌握这一思想后,我在面对类似问题时更加自信。

  3. 在细节中寻找优化方向

  • 用模运算优化大数处理。
  • 用一维数组压缩动态规划表,节约空间。

这道题让我更清楚地意识到,"如何转化问题" 是算法学习的重要核心之一。算法解题不是为了简单的答案,而是通过不断优化和思考,提升自己的逻辑能力和问题解决能力。