繁体   English   中英

查看答案 - 解码方式

[英]Review an answer - Decode Ways

我正在尝试解决一个问题,我的问题是为什么我的解决方案不起作用? . 这是问题,下面是答案。

问题取自 leetcode: http ://oj.leetcode.com/problems/decode-ways/

使用以下映射将包含来自 AZ 的字母的消息编码为数字:

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

给定一个包含数字的编码消息,确定解码它的方法总数。

例如,给定编码消息“12”,它可以被解码为“AB”(1 2)或“L”(12)。 解码“12”的方式数为2。

我的解决方案:

如果找到拆分,我的解决方案的重点是倒退并乘以选项数量。 通过拆分,我的意思是数字可以用两种方式解释。 例如:11 可以用两种方式解释“aa”或“k”。

public class Solution {
    public int numDecodings(String s) {
        if (s.isEmpty() || s.charAt(0) == '0') return 0;
        int decodings = 1;
        boolean used = false; // Signifies that the prev was already use as a decimal
        for (int index = s.length()-1 ; index > 0 ; index--) {
            char curr = s.charAt(index);
            char prev = s.charAt(index-1);
            if (curr == '0') {
                if (prev != '1' && prev != '2') {
                    return 0;
                }
                index--; // Skip prev because it is part of curr
                used = false;
            } else {
                if (prev == '1' || (prev == '2' && curr <= '6')) {
                    decodings = decodings * 2;
                    if (used) {
                        decodings = decodings - 1;
                    }
                    used = true;
                } else {
                    used = false;
                }
            }
        }
        return decodings;
    }
}

失败发生在以下输入上:

Input:"4757562545844617494555774581341211511296816786586787755257741178599337186486723247528324612117156948"
Output: 3274568
Expected: 589824

这是一个非常有趣的问题。 首先,我将展示我将如何解决这个问题。 我们将看到使用递归并没有那么复杂,使用动态规划可以解决问题。 我们将生成一个通用解决方案,该解决方案不会对每个代码点的上限26进行硬编码。

关于术语的说明:我将使用术语代码点(CP) 不是在 Unicode 意义上,而是指代126 每个代码点都表示为可变数量的字符。 我还将以明显的含义使用术语编码文本(ET) 和明文(CT)。 在谈论序列或数组时,第一个元素称为head 其余元素是tail

理论前奏

  • EC ""一个解码:CT ""
  • EC "3"可以解构为'3' + "" ,并且有一个解码。
  • EC "23"可以解构为'2' + "3"'23' + "" 每个尾部都有一个解码,所以整个EC有两个解码。
  • EC "123"可以解构为'1' + "23"'12' + "3" 尾部分别有两个一个解码。 整个EC有3个解码。 解构'123' + ""无效,因为123 > 26是我们的上限。
  • ... 等等任何长度的 EC。

所以给定一个像"123"这样的字符串,我们可以通过在开头找到所有有效的 CP 并总结每个尾部的解码次数来获得解码次数。

其中最困难的部分是找到有效的头部。 我们可以通过查看上限的字符串表示来获得头部的最大长度。 在我们的例子中,头部最多可以有两个字符长。 但并非所有适当长度的头部都是有效的,因为它们也必须≤ 26

朴素的递归实现

现在我们已经为一个简单(但有效)的递归实现完成了所有必要的工作:

static final int upperLimit  = 26;
static final int maxHeadSize = ("" + upperLimit).length();

static int numDecodings(String encodedText) {
    // check base case for the recursion
    if (encodedText.length() == 0) {
        return 1;
    }

    // sum all tails
    int sum = 0;
    for (int headSize = 1; headSize <= maxHeadSize && headSize <= encodedText.length(); headSize++) {
        String head = encodedText.substring(0, headSize);
        String tail = encodedText.substring(headSize);
        if (Integer.parseInt(head) > upperLimit) {
            break;
        }
        sum += numDecodings(tail);
    }

    return sum;
}

缓存递归实现

显然这不是很有效,因为(对于更长的 ET),相同的尾巴将被多次分析。 此外,我们创建了很多临时字符串,但我们暂时先放着。 我们可以轻松做的一件事是记住特定尾部的解码次数。 为此,我们使用与输入字符串长度相同的数组:

static final int upperLimit  = 26;
static final int maxHeadSize = ("" + upperLimit).length();

static int numDecodings(String encodedText) {
    return numDecodings(encodedText, new Integer[1 + encodedText.length()]);
}

static int numDecodings(String encodedText, Integer[] cache) {
    // check base case for the recursion
    if (encodedText.length() == 0) {
        return 1;
    }

    // check if this tail is already known in the cache
    if (cache[encodedText.length()] != null) {
        return cache[encodedText.length()];
    }

    // cache miss -- sum all tails
    int sum = 0;
    for (int headSize = 1; headSize <= maxHeadSize && headSize <= encodedText.length(); headSize++) {
        String head = encodedText.substring(0, headSize);
        String tail = encodedText.substring(headSize);
        if (Integer.parseInt(head) > upperLimit) {
            break;
        }
        sum += numDecodings(tail, cache);  // pass the cache through
    }

    // update the cache
    cache[encodedText.length()] = sum;
    return sum;
}

请注意,我们使用的是Integer[] ,而不是int[] 这样,我们可以使用null测试来检查不存在的条目。 这个解决方案不仅是正确的,而且速度也非常快——朴素递归的运行时间为O(解码次数) ,而记忆版本的运行时间为O(字符串长度)

走向 DP 解决方案

当您在头脑中运行上面的代码时,您会注意到对整个字符串的第一次调用会发生缓存未命中,然后计算第一个尾部的解码次数,该尾部每次也未命中缓存。 我们可以通过从输入的末尾开始首先评估尾部来避免这种情况。 因为所有的尾部都会在整个字符串之前被评估,我们可以删除对缓存未命中的检查。 现在我们也没有任何递归的理由,因为之前所有的结果都已经在缓存中了。

static final int upperLimit  = 26;
static final int maxHeadSize = ("" + upperLimit).length();

static int numDecodings(String encodedText) {
    int[] cache = new int[encodedText.length() + 1];

    // base case: the empty string at encodedText.length() is 1:
    cache[encodedText.length()] = 1;

    for (int position = encodedText.length() - 1; position >= 0; position--) {
        // sum directly into the cache
        for (int headSize = 1; headSize <= maxHeadSize && headSize + position <= encodedText.length(); headSize++) {
            String head = encodedText.substring(position, position + headSize);
            if (Integer.parseInt(head) > upperLimit) {
                break;
            }
            cache[position] += cache[position + headSize];
        }
    }

    return cache[0];
}

通过注意到我们只查询缓存中的最后一个maxHeadSize元素,可以进一步优化该算法。 因此,我们可以使用固定大小的队列来代替数组。 那时,我们将有一个在 *O(input length) 时间和O(maxHeadSize)空间中运行的动态编程解决方案。

upperLimit = 26专业化upperLimit = 26

上述算法尽可能保持通用,但我们可以手动将其专门化为特定的upperLimit 这很有用,因为它允许我们进行各种优化。 然而,这引入了“幻数”,使代码更难维护。 因此,在非关键软件中应该避免这种手动专业化(并且上述算法已经尽可能快)。

static int numDecodings(String encodedText) {
    // initialize the cache
    int[] cache = {1, 0, 0};

    for (int position = encodedText.length() - 1; position >= 0; position--) {
        // rotate the cache
        cache[2] = cache[1];
        cache[1] = cache[0];
        cache[0] = 0;

        // headSize == 1
        if (position + 0 < encodedText.length()) {
            char c = encodedText.charAt(position + 0);

            // 1 .. 9
            if ('1' <= c && c <= '9') {
                cache[0] += cache[1];
            }
        }

        // headSize == 2
        if (position + 1 < encodedText.length()) {
            char c1 = encodedText.charAt(position + 0);
            char c2 = encodedText.charAt(position + 1);

            // 10 .. 19
            if ('1' == c1) {
                cache[0] += cache[2];
            }
            // 20 .. 26
            else if ('2' == c1 && '0' <= c2 && c2 <= '6') {
                cache[0] += cache[2];
            }
        }
    }

    return cache[0];
}

与您的代码比较

代码表面上是相似的。 但是,您对字符的解析更加复杂。 您引入了一个used变量,如果设置了该变量,将减少解码计数以考虑双字符 CP。 这是错误的,但我不确定为什么。 主要问题是您几乎每一步都将计数加倍。 正如我们所见,前面的计数是相加的,并且很可能会有所不同。

这表明您在没有适当准备的情况下编写了代码。 你可以编写很多种软件,而不必考虑太多,但在设计算法时离不开仔细分析。 对我来说,在纸上设计算法并绘制每个步骤的图表通常很有帮助(沿着这个答案的“理论前奏”)。 当您对将要实现的语言考虑太多而对可能错误的假设考虑太少时,这尤其有用。

我建议您阅读“归纳证明”以了解如何编写正确的递归算法。 一旦有了递归解决方案,您就可以随时将其转换为迭代版本。

所以这里有一些更简单的方法可以解决您的问题。 这与计算斐波那契非常接近,不同之处在于每个较小规模的子问题都有条件检查。 空间复杂度为 O(1),时间为 O(n)

代码是用 C++ 编写的。

   int numDecodings(string s)
   {
    if( s.length() == 0 ) return 0;


    int j  = 0;
    int p1 = (s[j] != '0' ? 1 : 0);         // one step prev form j=1
    int p2 = 1;                             // two step prev from j=1, empty
    int p = p1;

    for( int j = 1; j < s.length(); j++ )
    {
        p = 0;

        if( s[j] != '0' ) 
            p += p1;    


        if( isValidTwo(s, j-1, j) )
            p += p2;

        if( p==0 )                  // no further decoding necessary, 
            break;                  // as the prefix 0--j is has no possible decoding.

        p2 = p1;                    // update prev for next j+1;
        p1 = p;

    }

    return p;
    }

    bool isValidTwo(string &s, int i, int j)
    {
        int val= 10*(s[i]-'0')+s[j]-'0';

        if ( val <= 9 ) 
        return false;

        if ( val > 26 ) 
        return false;

        return true;

    }

这是我解决问题的代码。 我使用DP ,我认为很清楚理解。

Java编写

public class Solution {
        public int numDecodings(String s) {
            if(s == null || s.length() == 0){
                return 0;
            }
            int n = s.length();
            int[] dp = new int[n+1];
            dp[0] = 1;
            dp[1] = s.charAt(0) != '0' ? 1 : 0;

            for(int i = 2; i <= n; i++){
                int first = Integer.valueOf(s.substring(i-1,i));
                int second = Integer.valueOf(s.substring(i-2,i));
                if(first >= 1 && first <= 9){
                    dp[i] += dp[i-1];
                }
                if(second >= 10 && second <= 26){
                    dp[i] += dp[i-2];
                }

            }
            return dp[n];

        }

}

由于我自己也在努力解决这个问题,因此这是我的解决方案和推理。 可能我会主要重复阿蒙写的内容,但也许有人会发现它有帮助。 它也是c#而不是java。

假设我们输入了“12131”并且想要获得所有可能的解码字符串。 直接的递归解决方案会从左到右进行迭代,获得有效的 1 位和 2 位数字头,并递归调用尾部的函数。

我们可以使用树来可视化它:

在此处输入图片说明

有 5 个叶子,这是所有可能的解码字符串的数量。 还有3片空叶子,因为数字31不能解码成字母,所以这些叶子是无效的。

算法可能如下所示:

public IList<string> Decode(string s)
{
    var result = new List<string>();

    if (s.Length <= 2)
    {
        if (s.Length == 1)
        {
            if (s[0] != '0')
                result.Add(this.ToASCII(s));
        }
        else if (s.Length == 2)
        {
            if (s[0] != '0' && s[1] != '0')
                result.Add(this.ToASCII(s.Substring(0, 1)) + this.ToASCII(s.Substring(1, 1)));
            if (s[0] != '0' && int.Parse(s) > 0 && int.Parse(s) <= 26)
                result.Add(this.ToASCII(s));
        }
    }
    else
    {
        for (int i = 1; i <= 2; ++i)
        {
            string head = s.Substring(0, i);
            if (head[0] != '0' && int.Parse(head) > 0 && int.Parse(head) <= 26)
            {
                var tails = this.Decode(s.Substring(i));
                foreach (var tail in tails)
                    result.Add(this.ToASCII(head) + tail);
            }
        }
    }

    return result;
}

public string ToASCII(string str)
{
    int number = int.Parse(str);
    int asciiChar = number + 65 - 1; // A in ASCII = 65
    return ((char)asciiChar).ToString();
}

我们必须处理以 0 开头(“0”、“03”等)并且大于 26 的数字。

因为在这个问题中我们只需要计算解码方式,而不是实际的字符串,我们可以简化这段代码:

public int DecodeCount(string s)
{
    int count = 0;

    if (s.Length <= 2)
    {
        if (s.Length == 1)
        {
            if (s[0] != '0')
                count++;
        }
        else if (s.Length == 2)
        {
            if (s[0] != '0' && s[1] != '0')
                count++;
            if (s[0] != '0' && int.Parse(s) > 0 && int.Parse(s) <= 26)
                count++;
        }
    }
    else
    {
        for (int i = 1; i <= 2; ++i)
        {
            string head = s.Substring(0, i);
            if (head[0] != '0' && int.Parse(head) > 0 && int.Parse(head) <= 26)
                count += this.DecodeCount(s.Substring(i));
        }
    }

    return count;
}

该算法的问题在于我们多次计算同一输入字符串的结果。 例如有 3 个以 31 结尾的节点:ABA31、AU31、LA31。 还有 2 个以 131 结尾的节点:AB131、L131。 我们知道,如果节点以 31 结尾,则它只有一个子节点,因为 31 只能以一种方式解码为 CA。 同样,我们知道如果字符串以 131 结尾,则它有 2 个孩子,因为 131 可以解码为 ACA 或 LA。 因此,我们可以将它缓存在 map 中,而不是重新计算它,其中 key 是字符串(例如:“131”),而 value 是解码方式的数量:

public int DecodeCountCached(string s, Dictionary<string, int> cache)
{
    if (cache.ContainsKey(s))
        return cache[s];

    int count = 0;

    if (s.Length <= 2)
    {
        if (s.Length == 1)
        {
            if (s[0] != '0')
                count++;
        }
        else if (s.Length == 2)
        {
            if (s[0] != '0' && s[1] != '0')
                count++;
            if (s[0] != '0' && int.Parse(s) > 0 && int.Parse(s) <= 26)
                count++;
        }
    }
    else
    {
        for (int i = 1; i <= 2; ++i)
        {
            string head = s.Substring(0, i);
            if (head[0] != '0' && int.Parse(head) > 0 && int.Parse(head) <= 26)
                count += this.DecodeCountCached(s.Substring(i), cache);
        }
    }

    cache[s] = count;
    return count;
}

我们可以进一步完善它。 我们可以使用长度代替字符串作为键,因为缓存的总是输入字符串的尾部。 因此,我们可以缓存尾部的长度,而不是缓存字符串:“1”、“31”、“131”、“2131”、“12131”:1、2、3、4、5:

public int DecodeCountDPTopDown(string s, Dictionary<int, int> cache)
{
    if (cache.ContainsKey(s.Length))
        return cache[s.Length];

    int count = 0;

    if (s.Length <= 2)
    {
        if (s.Length == 1)
        {
            if (s[0] != '0')
                count++;
        }
        else if (s.Length == 2)
        {
            if (s[0] != '0' && s[1] != '0')
                count++;
            if (s[0] != '0' && int.Parse(s) > 0 && int.Parse(s) <= 26)
                count++;
        }
    }
    else
    {
        for (int i = 1; i <= 2; ++i)
        {
            string head = s.Substring(0, i);
            if (s[0] != '0' && int.Parse(head) > 0 && int.Parse(head) <= 26)
                count += this.DecodeCountDPTopDown(s.Substring(i), cache);
        }
    }

    cache[s.Length] = count;
    return count;
}

这是递归自顶向下的动态规划方法。 我们从头开始,然后递归计算尾部的解决方案,并记住这些结果以供进一步使用。

我们可以将其转化为自底向上的迭代 DP 解决方案。 我们从最后开始,像之前的解决方案一样缓存切片的结果。 我们可以使用数组代替 map,因为键是整数:

public int DecodeCountBottomUp(string s)
{
    int[] chache = new int[s.Length + 1];
    chache[0] = 0; // for empty string;

    for (int i = 1; i <= s.Length; ++i)
    {
        string tail = s.Substring(s.Length - i, i);

        if (tail.Length == 1)
        {
            if (tail[0] != '0')
                chache[i]++;
        }
        else if (tail.Length == 2)
        {
            if (tail[0] != '0' && tail[1] != '0')
                chache[i]++;
            if (tail[0] != '0' && int.Parse(tail) > 0 && int.Parse(tail) <= 26)
                chache[i]++;
        }
        else
        {
            if (tail[0] != '0')
                chache[i] += chache[i - 1];

            if (tail[0] != '0' && int.Parse(tail.Substring(0, 2)) > 0 && int.Parse(tail.Substring(0, 2)) <= 26)
                chache[i] += chache[i - 2];
        }
    }

    return chache.Last();
}

有些人进一步简化,用值 1 初始化 cache[0],这样他们就可以摆脱 tail.Length==1 和 tail.Length==2 的条件。 对我来说,这是不直观的技巧,因为显然对于空字符串有 0 种解码方式而不是 1 种,因此在这种情况下,必须添加附加条件来处理空输入:

public int DecodeCountBottomUp2(string s)
{
    if (s.Length == 0)
        return 0;

    int[] chache = new int[s.Length + 1];
    chache[0] = 1;
    chache[1] = s.Last() != '0' ? 1 : 0;

    for (int i = 2; i <= s.Length; ++i)
    {
        string tail = s.Substring(s.Length - i, i);

        if (tail[0] != '0')
            chache[i] += chache[i - 1];

        if (tail[0] != '0' && int.Parse(tail.Substring(0, 2)) > 0 && int.Parse(tail.Substring(0, 2)) <= 26)
            chache[i] += chache[i - 2];
    }

    return chache.Last();
}

我的解决方案基于这样一个想法,即特定子字符串中的项目(字符/数字)的排列完全独立于不同子字符串中的相同。 所以我们需要将这些独立的方式相乘以获得方式的总数。

// nc is the number of consecutive 1's or 2's in a substring.
// Returns the number of ways these can be arranged  within 
// themselves to a valid expr.
int ways(int nc){
    int n = pow(2, (nc/2)); //this part can be memorized using map for optimization
    int m = n;
    if (nc%2) {
        m *= 2;
    }
    return n + m - 1;  
}
bool validTens(string A, int i){
    return (A[i] == '1' || (A[i] == '2' && A[i+1] <= '6'));
}
int numDecodings(string A) {
    int ans = 1;
    int nc;

    if ((A.length() == 0)||(A[0] == '0')) return 0;

    for(int i = 1; i < A.length();i++){
        if(A[i] == '0' && validTens(A, i-1) == false) return 0; //invalid string
        while(i < A.length() && validTens(A, i-1)) {
            if(A[i] == '0'){
                //think of '110' or '1210', the last two digits must be together
                if(nc > 0) nc--;  
            }
            else nc++;
            i++;
        }
        ans *= ways(nc);
        nc = 0;
    }
    return ans;
}

空间和时间复杂度为 O(n) 的 Java 解决方案

public int numDecodings(String s) {
    int n = s.length();
    if (n > 0 && s.charAt(0) == '0')
        return 0;
    int[] d = new int[n + 1];
    d[0] = 1;
    d[1] = s.charAt(0) != '0' ? 1 : 0;
    for (int i = 2; i <= n; i++) {
        if (s.charAt(i - 1) > '0')
            d[i] = d[i] + d[i - 1];

        if (s.charAt(i - 2) == '2' && s.charAt(i - 1) < '7')
            d[i] = d[i - 2] + d[i];
        if (s.charAt(i - 2) == '1' && s.charAt(i - 1) <= '9')
            d[i] = d[i - 2] + d[i];
    }
    return d[n];
}

这是一个 O(N) C++ DP 实现。

int numDecodings(string s) {

    if(s[0] == '0') return 0; // Invalid Input
        
    int n = s.length();

    // dp[i] denotes the number of ways to decode the string of length 0 to i
    vector<int> dp(n+1, 0);
        
    // base case : string of 0 or 1 characters will have only 1 way to decode
    dp[0] = dp[1] = 1; 
        
    for(int i = 2; i <= n; i++) {
        // considering the previous number
        if(s[i-1] > '0') dp[i] += dp[i-1];
        // considering the previous two numbers
        if(s[i-2] == '1' || (s[i-2] == '2' && s[i-1] < '7')) dp[i] += dp[i-2];
    }
        
    return dp[n];
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM