【动态规划】LeetCode-91.解码方法

算法那些事专栏说明:这是一个记录刷题日常的专栏,每个文章标题前都会写明这道题使用的算法。专栏每日计划至少更新1道题目,在这立下Flag
个人主页:Jammingpro
专栏链接:算法那些事
每日学习一点点,技术累计看得见

题目

题目描述

一条包含字母 A-Z 的消息通过以下映射进行了 编码 :
‘A’ -> “1”
‘B’ -> “2”

‘Z’ -> “26”
要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,“11106” 可以映射为:
“AAJF” ,将消息分组为 (1 1 10 6)
“KJF” ,将消息分组为 (11 10 6)
注意,消息不能分组为 (1 11 06) ,因为 “06” 不能映射为 “F” ,这是由于 “6” 和 “06” 在映射中并不等价。
给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。
题目数据保证答案肯定是一个 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” 并不等价)。

提示

1 <= s.length <= 100
s 只包含数字,并且可能包含前导零。

题解

首先,我们通过上面的解码规则,对"123"进行解码。“1”、“2”、“3"均能够单独解码,这是一种解码方法;将"1"和"2"组合解码为"12”,其范围在1到26之间,故解码成功,因此"12"、“3"是一种解码方法;将"1"单独解码,“2"和"3"组合解码为"23”,其范围在1到26之间,故也解码成功,因此"1”、"23"也是一种解码方法。所以"123"共有3种解码方法。
【动态规划】LeetCode-91.解码方法_第1张图片
再看一个示例:对"1032"进行解码。“1”、“0”、“3”、"2"无法分开单独解码,因为"0"不在1到26范围内,因此,"0"需要与"1"或"3"组合解码。“1"和"0"组合解码为"10”,在1到26之间;“0"和"3"组合解码为"03”,因为其包含前导0,故不满足题目要求。因此,"0"必须与"1"组合解码。“3"和"2"可以分开解码,但无法组合解码,因为其组合为"32”,超出26,故解码失败。这个字符串只有一种解码方法,即1032

由上面分析可知,遇到①组合数有前导0、②构成数不在1到26之间,将会导致解码失败。除此之外,出现连续两个0,即"00",也会解码失败。

我们可以s[i]和s[i-1]是否为0做分类分析:
若s[i]==‘0’,则我们再判断s[i-1]是否为’0’,如果s[i]==0&&s[i-1]==0,则出现连续两个0,这种排列方式无法解码,直接返回0;如果s[i]==0&&s[i-1]!=0,即s[i]无法单独解码,需要与s[i-1]组合在一起才能解码,如果s[i]与s[i-1]组合数<=26,则dp[i]=dp[i-2],否则return 0(即两个字符无法分开解码,也无法合在一起解码)。
ps:为什么这里dp[i]=dp[i-2]呢?dp[i-2]中保存的是从0号字符到i-2号字符的解码方法。因为上述情况只能组合解码,因此s[i]与s[i-1]需组合解码到一起,所以dp[i]的解码方法与dp[i-2]的解码方法相同。

若s[i]!=‘0’,则我们再判断s[i-1]是否为’0’,如果s[i]!=0&&s[i-1]==0,则两个字符无法一起解码,需要s[i-1]与s[i-2]各自解码,因此dp[i]=dp[i-1](此时出现前导0的情况,前导0无法和s[i]组合,但可以与s[i-2]组合;如果无法与s[i-2]组合,则上一次迭代已经return 0了);如果s[i]!=‘0’&&s[i-1]!=‘0’,若两个数字组合数<=26,则dp[i]=dp[i-1]+dp[i-2],若组合数>26,则dp[i]=dp[i-1]。
ps:这里怎么有dp[i]=dp[i-1]+dp[i-2]?因为s[i]能和s[i-1]组合解码,其组合解码后,s[0-i]的解码方法数与dp[i-2]相同;s[i]和s[i-1]分开解码,则s[0-i]阶解码方法数与dp[i-1]相同,故得到上述式子。
【动态规划】LeetCode-91.解码方法_第2张图片
经过上面的分析,我们可以得到如下代码↓↓↓

class Solution {
public:
    int numDecodings(string s) {
        if(s[0] == '0') return 0;
        int n = s.size();
        if(n == 1) return 1;
        vector<int>dp(n);
        dp[0] = 1;
        if(s[1] == '0')
        {
            if((s[0] - '0') * 10 + (s[1] - '0') <= 26)
                dp[1] = 1;
            else
                return 0;
        }
        else
        {
            if((s[0] - '0') * 10 + (s[1] - '0') <= 26)
                dp[1] = 2;
            else
                dp[1] = 1;
        }

        for(int i = 2; i < n; i++)
        {
            if(s[i] == '0' && s[i - 1] == '0') return 0;
            if(s[i] == '0')
            {
                if((s[i - 1] - '0') * 10 + (s[i] - '0') <= 26)
                {
                    if(s[i - 1] == '0')
                        dp[i] = dp[i - 1];
                    else
                        dp[i] = dp[i - 2];
                }
                else
                    return 0;
            }
            else
            {
                if((s[i - 1] - '0') * 10 + (s[i] - '0') <= 26)
                {
                    if(s[i - 1] == '0')
                        dp[i] = dp[i - 1];
                    else
                        dp[i] = dp[i - 1] + dp[i - 2];
                }
                else
                    dp[i] = dp[i - 1];
            }
        }
        return dp[n - 1];
    }
};

这个代码看起来非常繁琐,嵌套了需要分支语句,可阅读性较差。下面我们对这个代码做一些优化↓↓↓

class Solution {
public:
    int numDecodings(string s) {
        int n = s.size();
        vector<int>dp(n + 1);
        dp[0] = 1;//这里没有实质性含义,因为s[0]与s[1]组合数若满足10-26,需要+dp[0],故初始化为1
        dp[1] = s[0] != '0';
        for(int i = 2; i <= n; i++)
        {
            int t = (s[i - 2] - '0') * 10 + (s[i - 1] - '0');//计算组合数
            dp[i] = s[i - 1] != '0' ? dp[i - 1] : 0;//当前字符不为0,则可以单独解码,则dp[i]=dp[i-1]
            dp[i] += t >=10 && t <= 26 ? dp[i - 2] : 0;//组合数在10-26之内,则+dp[i-2]
        }
        return dp[n];
    }
};

优化代码中的dp下标与s下标差1,代码思路与前一代码类似,但更为巧妙。

本文存在不足,欢迎留言或私信批评、指正。希望我的解决方法能够对你有所帮助~~
今日打卡完成,点亮小星星☆→★

你可能感兴趣的:(算法那些事,动态规划,leetcode,算法)