Leetcode 91 Decode Ways

原题地址:https://leetcode.com/problems/decode-ways/

题目描述

A message containing letters from A-Z is being encoded to numbers using the following mapping:
一段仅包含英文A-Z字符的信息被按如下映射编码成了一个数字串:

'A' -> 1
'B' -> 2
...
'Z' -> 26

Given an encoded message containing digits, determine the total number of ways to decode it.
给出一个编码后的数字串,求总共有几种解码方式。

For example,
示例,

Given encoded message “12”, it could be decoded as “AB” (1 2) or “L” (12).
编码后的数字串为”12”,他可以以AB(1 2)或L(12)的方式解码。

The number of ways decoding “12” is 2.
因此”12”的解码方式共有2种。

解题思路

我们可以很容易的想到这个题应该用动态规划来解这个题,在每一步种考虑当前是取下一个数字来解码还是取下两个数字来解码,简单说来就是f(x)=f(x+1)或者f(x)=f(x+1)+f(x+2)的问题。

对于动态规划的问题,我们常用的一种方法是递归,然而对于这个题,如果我们用递归的话很容易就超时了。事实上,当我第一次在leetcode上提交时,就是用递归来做的,而且也毫无意外的超时了。因为会有多次重复的递归操作。这时候我们还可以采用的一种以空间换时间的方式来存储已经求过一次的值,然而这样的话依然不是很好。我们应该想一种不用递归的方法。

对于动态规划,除了递归之外还常用的就是循环了。例如,求阶乘时,我们不用f(x)=x*f(x-1),而是递归从1开始求积(1*2*3*…*x);求斐波那契数列时,我们不用f(x)=f(x-1)+f(x-2),而是从1开始求和(1+2,2+3,3+5….)。对于这个题,我们同样可以用循环来解。

我们不难注意到,对于上述两例,有如下规律:
阶乘:常用的方法是从大往小调用,f(x)=x*f(x-1);而循环的方式是从小往大求积。
斐波那契数列:常用的方法是从大往小调用f(x)=f(x-1)+f(x-2);而循环的方式是从小往大求和。

循环的方式总是跟递归的方式按相反的方向前进

声明:以上仅仅是个人发现的小规律,其正确性有待验证,请不要轻信。如有错误,还望大家指正。

我们按上面的规律来观察这个题,如果按递归的方式的话,当然是(大概思路)求第i位的值时调用第i+1和i+2位的值。所以,用循环的话我们应该反过来,先求后面的,再求前面的。

我们用x表示第i+1之后的所有字符的解码方式数目,y表示第i+2位开始往后的所有字符的解码方式数目,然后根据第i位值即可使用x和y来求得第i位之后所有字符的解码方式数目。

算法描述

  1. 设最后一位之后的解码方式为y=1
  2. 如果最后一位为0,则x=0(即单独一个0无法解码),否则x=1
  3. 往前移动,如果当前数字为1,其后面跟什么字符都是可以的,因此x+y是其解码方式数目,更新x和y
  4. 如果当前数字为2,则其后面只能是0,1,2,3,4,5,6,如果其后一位是这些数字,则x+y是其解码方式数目,更新x和y;否则,x是其解码方式数目,更新y=x。
  5. 如果当前数字为0,以0开头的字符串不可能正确解码,因此更新y=x,x=0.
  6. 如果当前数字为3-9中的一个数字,则以其开头的字符串一定只能以其为单独字符进行解码,所以x就是其解码方式数目,更新x不变,y=x
  7. 重复以上3-6,直至第1个字符,最后返回x的值就是我们要求的值。

简析

算法时间复杂度为O(n),使用常数空间,时间特性和空间特性都很优秀。

为了正确的返回结果,我们还需要处理几组特殊值。

  • 空串,返回0
  • 数字串长度为1,如果其值为”0”,则返回0,否则返回1

其余情况均可以按照算法来进行。

代码

int numDecodings(char* s) {
    int len = strlen(s); // 获取待解码字符串的长度
    char *p = s + len - 1; // 指针指向最后一个字符

    if (len == 0) // 如果是空串,返回0(特殊值)
        return 0;
    else if (len == 1) // 如果只有一个字符,根据该字符是否为0返回
        return *p == '0' ? 0 : 1; // 如果为"0"返回0,否则返回1

    int x, y = 1, tmp; // 初始化y=1,即最后一位之后的解码方式只有一种
    x = *p == '0' ? 0 : 1; // 根据最后一个字符初始化x
    while (--p >= s) { // 从后至前遍历字符串
        if (*p == '0') { // 当前字符为0
            y = x; // y赋值x,以备下次循环使用
            x = 0; // x置零,以0开头的串不可解码
        } 
        else if (*p == '1') { // 当前字符为1,后面是任意字符都可以有两种解码方式
            tmp = x; // 暂存x
            x += y; // x=x+y
            y = tmp; // y赋值x,以备下次循环使用
        } 
        else if (*p == '2') { // 当前字符为2
            if (*(p + 1) < '7') { // 后面是0-6中的字符则有两种解码方式
                tmp = x;
                x += y;
                y = tmp;    
            }
            else // 后面是7-9中点字符,当前字符只有一种解码方式
                y = x;
        } 
        else // 当前为3-9中的字符,当前字符只有一种解码方式
            y = x;
    }

    return x;
}

运行情况

Status:Accept
Time:4ms

优化代码

代码中有些地方是可以优化的,对于上面的代码,不管待解码的字符串长度是多少,都会遍历整个字符串。其实对于某些特殊情况下可以提前结束循环。

提前结束循环的情况仅仅是由于0字符的特殊情况,除0外点其他字符不论如何搭配都至少有一种解码方式,但是如果出现 00/30/40/50/60/70/80/90 之中的任意一个子串,则无法解码;或者如果第一个字符是0的话也无法解码。

判断提前结束的方式有两种:

  • 可以判断x和y是否都为0,因为x的增长方式只有x+=y,如果x和y都为0的话,不论如何增长,x都永远为0;
  • 可以只在当前字符为0的时候判断,判断其是否是第一个字符或者其前面的字符是否是0或者3-9之中的字符,如果是则结束循环。

优化一

int numDecodings(char* s) {
    int len = strlen(s); // 获取待解码字符串的长度
    char *p = s + len - 1; // 指针指向最后一个字符

    if (len == 0) // 如果是空串,返回0(特殊值)
        return 0;
    else if (len == 1) // 如果只有一个字符,根据该字符是否为0返回
        return *p == '0' ? 0 : 1; // 如果为"0"返回0,否则返回1

    int x, y = 1, tmp; // 初始化y=1,即最后一位之后的解码方式只有一种
    x = *p == '0' ? 0 : 1; // 根据最后一个字符初始化x
    while (--p >= s) { // 从后至前遍历字符串
        if (*p == '0') { // 当前字符为0
            if (p == s) // 如果第一个字符是0,返回0
                return 0;
            if (*(p - 1) > '2' || *(p - 1) == '0') // 如果前面的字符不是1或2,则返回0
                return 0;
            else {
                y = x; // y赋值x,以备下次循环使用
                x = 0; // x置零,以0开头的串不可解码
            }
        } 
        else if (*p == '1') { // 当前字符为1,后面是任意字符都可以有两种解码方式
            tmp = x; // 暂存x
            x += y; // x=x+y
            y = tmp; // y赋值x,以备下次循环使用
        } 
        else if (*p == '2') { // 当前字符为2
            if (*(p + 1) < '7') { // 后面是0-6中的字符则有两种解码方式
                tmp = x;
                x += y;
                y = tmp;    
            }
            else // 后面是7-9中点字符,当前字符只有一种解码方式
                y = x;
        } 
        else // 当前为3-9中的字符,当前字符只有一种解码方式
            y = x;
    }

    return x;
}
运行情况

Status:Accept
Time:2ms

优化二

int numDecodings(char* s) {
    int len = strlen(s); // 获取待解码字符串的长度
    char *p = s + len - 1; // 指针指向最后一个字符

    if (len == 0) // 如果是空串,返回0(特殊值)
        return 0;
    else if (len == 1) // 如果只有一个字符,根据该字符是否为0返回
        return *p == '0' ? 0 : 1; // 如果为"0"返回0,否则返回1

    int x, y = 1, tmp; // 初始化y=1,即最后一位之后的解码方式只有一种
    x = *p == '0' ? 0 : 1; // 根据最后一个字符初始化x
    while (--p >= s) { // 从后至前遍历字符串
        if (*p == '0') { // 当前字符为0
            y = x; // y赋值x,以备下次循环使用
            x = 0; // x置零,以0开头的串不可解码
        } 
        else if (*p == '1') { // 当前字符为1,后面是任意字符都可以有两种解码方式
            tmp = x; // 暂存x
            x += y; // x=x+y
            y = tmp; // y赋值x,以备下次循环使用
        } 
        else if (*p == '2') { // 当前字符为2
            if (*(p + 1) < '7') { // 后面是0-6中的字符则有两种解码方式
                tmp = x;
                x += y;
                y = tmp;    
            }
            else // 后面是7-9中点字符,当前字符只有一种解码方式
                y = x;
        } 
        else // 当前为3-9中的字符,当前字符只有一种解码方式
            y = x;

        // 如果xy都是0,返回0
        if (x == 0 && y == 0)
            return 0;
    }

    return x;
}
运行情况

Status:Accept
Time:1ms

总结

使用递归求值时超时,使用循环方式仅用几毫秒。时间复杂度O(n),常数级空间。

对于代码优化,第一种方式中可以提前结束,减少遍历次数,但是其中有多层分支判断;第二种方式可以提前结束,减少遍历次数,而且分支判断相对来说简单,因此性能最好。

测试数据

"" : 0
"0" : 0
"00" : 0
"100" : 0
"11" : 2
"10" : 1
"01" : 0
"1010" : 1
"12345678910" : 3
"22316258210" : 12

// 个人学习记录,若有错误请指正,大神勿喷
// sfg1991@163.com
// 2015-05-13

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LeetCode 91题是一道非常经典的动态规划问题,题目名为"解码方法"(Decode Ways)。给定一个只包数字的非空,求解有多少种解码方式具体来说给定的字符串由数字字符组成,可以解码成字母。规定字母'A'对应数字1,字母'B对应数字2,以此类推,字母'Z'对应数字26。现在要求计算出给定字符串的所有可能的解码方式数量。 解决这个问题的一种常见方法是使用动态规划。我们可以定义一个长度为n+1的数组dp,其中dp[i]表示字符串的前i个字符的解码方式数量。根据题目要求,我们需要考虑以下几种情况: 1. 如果当前字符为0,那么它不能单独解码,必须与前一个字符组合起来解码。如果前一个字符为1或2,那么可以将当前字符与前一个字符组合起来解码,即dp[i] = dp[i-2];否则,无法解码,返回0。 2. 如果当前字符不为0,那么它可以单独解码成一个字母,即dp[i] = dp[i-1]。同时,如果前一个字符和当前字符组合起来可以解码成一个字母(范围在1到26之间),那么也可以将前两个字符一起解码,即dp[i] += dp[i-2]。 最终,我们可以通过遍历字符串的每个字符,更新dp数组的值,最后返回dp[n]即可得到解码方式的数量。 下面是Java语言的示例代码: ```java public int numDecodings(String s) { int n = s.length(); int[] dp = new int[n + 1]; dp[0] = 1; dp[1] = s.charAt(0) == '0' ? 0 : 1; for (int i = 2; i <= n; i++) { int oneDigit = Integer.parseInt(s.substring(i - 1, i)); int twoDigits = Integer.parseInt(s.substring(i - 2, i)); if (oneDigit >= 1 && oneDigit <= 9) { dp[i] += dp[i - 1]; } if (twoDigits >= 10 && twoDigits <= 26) { dp[i] += dp[i - 2]; } } return dp[n]; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值