【动态规划: 线性dp】解码方法

文章目录

题目链接:91. 解码方法


题目解析

动态规划 (Dynamic programming,简称 DP)是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法,在查找 有很多重叠子问题 的情况的最优解时有效。它将问题重新组合成子问题。为了避免多次解决这些子问题,它们的结果都逐渐被计算并被保存,从简单的问题直到整个问题都被解决。因此,动态规划保存递归时的结果,因而不会在解决同样的问题时花费不必要的时间。

通常的解决步骤有五步:

  1. 确定状态表示/dp表的含义
  2. 确定状态转移方程/递推公式
  3. 初始化
  4. 确定填表顺序
  5. 确定返回值

一条包含字母 A-Z 的消息通过以下映射进行了 编码 :

  • "1" -> 'A'

  • "2" -> 'B'

  • ...

  • "25" -> 'Y'

  • "26" -> 'Z'

然而,在 解码 已编码的消息时,你意识到有许多不同的方式来解码,因为有些编码被包含在其它编码当中("2" 和 "5" 与 "25")。

例如,"11106" 可以映射为:

  • "AAJF" ,将消息分组为 (1, 1, 10, 6)
  • "KJF" ,将消息分组为 (11, 10, 6)
  • 消息不能分组为 (1, 11, 06) ,因为 "06" 不是一个合法编码(只有 "6" 是合法的)。

注意,可能存在无法解码的字符串。

给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。如果没有合法的方式解码整个字符串,返回 0。

题目数据保证答案肯定是一个 32 位 的整数。

示例 1:

  • 输入:s = "12"
  • 输出:2
  • 解释:它可以解码为 "AB"(1 2)或者 "L"(12)。

示例 2:

  • 输入:s = "226"
  • 输出:3
  • 解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。

示例 3:

  • 输入:s = "06"
  • 输出:0
  • 解释:"06" 无法映射到 "F" ,因为存在前导零("6" 和 "06" 并不等价)

算法原理

确定状态表示

状态表示/dp表含义可以从题目要求找出,也可以从重复子问题中找出。

通常可以确定 以 i 位置为起始 或者 以 i 位置为结尾。

本题采用后者,因此状态表示:dpi 表示解码到 i 位置时,解码方法的总数。

确定状态转移方程

对于 s[i],解码的思路:

  1. 若 s[i] 可以单独解码,则有 1 <= s[i] <= 9,此时解码方法数和前一个字符的解码方法数一样,即 dp[i] = dp[i - 1]
  2. 若 s[i] 与前一个字符 s[i-1] 合并解码,则当满足条件 10 <= b*10 + a <= 26(其中 b 为 si-1 的 ASCII 码转为整数类型的值,a 同理) 时,此时由于和前一个字符组合了,解码方法数就与前两个字符的解码方法一样,即 dp[i] = dp[i-2]
  3. 综合以上两种情况,可以得出状态转移方程:dp[i] = dp[i-1] + dp[i-2]

初始化

  • 对于第一个字符的初始化,我们只需要考虑是否满足 1 <= ch <= 9 即可
  • 对于第二个字符的初始化,则需要分两种情况考虑:先考虑能否单独解码(条件是第一个字符和自己都必须在 [1,9] 区间内),若可以单独解码,方法数就加一;再考虑与前一个字符组合解码,组合后的字符需要满足 10 <= ch <= 26 方法数才能加一
  • 可将第二个字符的初始化优化:dp表整体向右移动一个单位,使得第二个字符的 dp 值在数组中是第三个位置,这样就可以放到填表中操作

确定填表顺序

从字符串的左边开始向右解码,因此填表顺序是 从左往右。

确定返回值

我们需要返回解码的方案总数,也就是将整个字符串解码后得到的解码方法数,即 dp[n-1]。

细节问题

  • 考虑边界情况:当题目所给的字符串长度只有 1 怎么处理?

代码实现

普通动态规划

java 复制代码
class Solution {
    public int numDecodings(String s) {
        // 预处理
        char[] ss = s.toCharArray();
        int n = ss.length;

        // 创建dp表(dp[i]表示到位置i时的解码方法数)
        int[] dp = new int[n];

        // 初始化第一个字符(若为0------即有前导零,解码方法数是0)
        dp[0] = ss[0] == '0' ? 0 : 1;

        // 边界情况(消息长度只有1)
        if (n == 1) {
            return dp[0];
        }

        // 初始化第二个字符
        // 考虑第一种情况:独自解码(必须满足1<=ch<=9);同时也必须保证ss[0]非0
        if (ss[0] != '0' && ss[1] != '0') {
            dp[1] += 1;
        }
        // 考虑第二种情况:和前一个字符组合解码(组合后的字符必须满足10<=char<=26)
        int temp = (ss[0]-'0')*10 + ss[1]-'0';
        if (temp >= 10 && temp <= 26) {
            dp[1] += 1;
        }

        // 从左往右填dp表
        for (int i = 2; i < n; i++) {
            // 先考虑ss[i]是否为0,若不为0就表示可以独自解码
            if (ss[i] != '0') {
                dp[i] += dp[i-1];
            }
            // 再考虑ss[i]与ss[i-1]组合解码,判断组合后的字符是否满足条件
            temp = (ss[i-1]-'0')*10 + ss[i]-'0';
            if (temp >= 10 && temp <= 26) {
                dp[i] += dp[i-2];
            }
        }

        // 返回结果(将整个字符串解码,总方法数为dp[n-1])
        return dp[n-1];
    }
}

优化初始化版本

优化版本主要针对第二个字符的初始化,将这部分代码放到 "填表" 里。

以下是细节问题:

  • 由于这个版本的 dp表是往右移动了一个单位的,因此在填表时,访问字符串的下标都要 减一
  • 同时也要注意 dp表左边多出一个位置 dp[0] 不再对应 s[0],并且由于状态转移方程 dp[i] = dp[i-1] + dp[i-2],当 i 取 2 时会用到 dp[0],本题将其初始化为 1 即可。
  • 在填表时也要注意循环边界可取 i == n。
java 复制代码
class Solution {
    public int numDecodings(String s) {
        // 预处理
        char[] ss = s.toCharArray();
        int n = ss.length;

        // 创建dp表(dp[i]表示到位置i时的解码方法数),往右移动一个单位
        int[] dp = new int[n+1];

        // 初始化第一个位置的值为1
        dp[0] = 1;

        // 初始化第一个字符(若为0------即有前导零,解码方法数是0)
        dp[1] = ss[0] == '0' ? 0 : 1;

        // 从左往右填dp表
        for (int i = 2; i <= n; i++) {
            // 先考虑ss[i]是否为0,若不为0就表示可以独自解码
            if (ss[i-1] != '0') {
                dp[i] += dp[i-1];
            }
            // 再考虑ss[i]与ss[i-1]组合解码,判断组合后的字符是否满足条件
            int temp = (ss[i-2]-'0')*10 + ss[i-1]-'0';
            if (temp >= 10 && temp <= 26) {
                dp[i] += dp[i-2];
            }
        }

        // 返回结果(将整个字符串解码,总方法数为dp[n])
        return dp[n];
    }
}

空间优化版本

由于 dp[i] 只依赖 dp[i-1] 和 dp[i-2] 这两个值,我们可以不创建整个 dp 数组,而是用有两个变量动态滚动保存即可。

我们这里用 prev1 代表 dp[i-1],用 prev2 代表 dp[i-2],用 cur 代表 dp[i]。

则状态转移方程为:cur = prev1 + prev2。

滚动的过程中,必须严格按照顺序:

  1. prev2 = prev1
  2. prev1 = cur

最终返回值是 prev1,当整个字符串解码结束,prev1 的值就是结果(若 n == 1,直接返回 prev1,此时值为 1,结果也正确)

代码直接在优化初始化版改即可:

java 复制代码
class Solution {
    public int numDecodings(String s) {
        // 预处理
        char[] ss = s.toCharArray();
        int n = ss.length;

        // 通过滚动数组优化空间复杂度:prev1为dp[i-1],prev2为dp[i-2]
        int prev1 = ss[0] == '0' ? 0 : 1;
        int prev2 = 1;

        // 从左往右填dp表
        for (int i = 2; i <= n; i++) {
            int cur = 0;
            // 先考虑ss[i]是否为0,若不为0就表示可以独自解码
            if (ss[i-1] != '0') {
                cur += prev1;
            }
            // 再考虑ss[i]与ss[i-1]组合解码,判断组合后的字符是否满足条件
            int temp = (ss[i-2]-'0')*10 + ss[i-1]-'0';
            if (temp >= 10 && temp <= 26) {
                cur += prev2;
            }
            prev2 = prev1;
            prev1 = cur;
        }

        // 返回结果(将整个字符串解码,当滚动结束时总方法数为prev1)
        return prev1;
    }
}

完

相关推荐
春弦_9 小时前
自用算法整理二
数据结构·算法·图论
缓慢更新9 小时前
审批的加签与会签:多人意见怎么合
android·java·数据库·低代码·rxjava
hetao173383710 小时前
2026-10-22~23 hetao1733837 的刷题记录
c++·算法
yuniko-n10 小时前
【JUC】volatile 非原子性深入解析
java
java_nnnn10 小时前
LeetCode刷题滑动窗口(串联所有单词的子串+最小覆盖的子串)Java
算法·leetcode·职场和发展
sun_weitao10 小时前
极简版 MCP 工具实现方案
java·linux·服务器
Navigator_Z11 小时前
LeetCode //C - 1301. Number of Paths with Max Score
c语言·算法·leetcode
金銀銅鐵12 小时前
[Java] 展示增删元素时 LinkedList 的内部变化
java·后端·ai编程
卡卡~~12 小时前
“101计划”核心课程实践教材《数据实验结构指导-C++语言版》题目集
开发语言·c++·学习·算法