回答答案 - 解码方式

时间:2013-12-02 23:39:19

标签: java

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

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

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

'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

7 个答案:

答案 0 :(得分:33)

这是一个非常有趣的问题。首先,我将展示如何解决这个问题。我们将看到使用递归时并不复杂,并且可以使用动态编程解决问题。我们将生成一个通用解决方案,它不会为每个代码点硬编码26的上限。

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

理论前奏

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

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

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

朴素的递归实施

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

≤ 26

缓存递归实现

显然,这不是非常有效,因为(对于更长的ET),将对多次相同的尾部进行分析。此外,我们创建了许多临时字符串,但我们现在就是这样。我们可以轻松做的一件事是 memoize 特定尾部的解码次数。为此,我们使用与输入字符串长度相同的数组:

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

请注意,我们使用的是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[]的测试来检查不存在的条目。这个解决方案不仅正确,而且速度也非常快 - 天真的递归在 O(解码次数)时运行,而memoized版本在 O(字符串长度)中运行时间。

迈向DP解决方案

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

null

通过注意我们只查询缓存中的最后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]; } 个元素,可以进一步优化此算法。因此,我们可以使用固定大小的队列而不是数组。那时,我们将拥有一个在* O(输入长度​​)时间和 O(maxHeadSize)空间中运行的动态编程解决方案。

maxHeadSize

的专业化

上述算法尽可能保持通用,但我们可以针对特定的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]; } 变量,如果设置该变量,将减少解码计数以便考虑双字符CP。这是错的,但我不确定为什么。主要问题是你几乎每一步都要加倍计数。正如我们所看到的,之前的计数是已添加,并且可能会有所不同。

这表示您未经适当准备就编写了代码。您可以编写多种软件而无需过多考虑,但在设计算法时,您无法进行仔细分析。对我来说,在纸上设计算法通常很有帮助,并绘制每个步骤的图表(沿着这个答案的“理论前奏”)。当您过多地考虑要实现的语言时,这一点尤其有用,而对于可能错误的假设则太少了。

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

答案 1 :(得分:6)

所以这里有一些更简单的方法来解决你的问题。这非常接近于计算Fibonacci,不同之处在于每个较小尺寸的子问题都有条件检查。 空间复杂度为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;

    }

答案 2 :(得分:5)

这是我解决问题的代码。我使用 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];

        }

}

答案 3 :(得分:5)

由于我自己也在努力解决这个问题,这是我的解决方案和推理。可能我会大部分重复amon所写的内容,但也许有人会觉得它很有帮助。它也是c#而不是java。

让我们说我们有输入&#34; 12131&#34;并希望获得所有可能的解码字符串。 直接递归解决方案将从左到右迭代,获得有效的1和2位数头,并递归调用函数尾部。

我们可以使用树形象化它:

enter image description here

有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开始的数字(&#34; 0&#34;,&#34; 03&#34;等),并且大于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是string(例如:&#34; 131&#34;),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;
}

我们可以进一步完善这一点。我们可以使用length来代替使用字符串作为键,因为缓存的内容始终是输入字符串的尾部。因此,不是缓存字符串:&#34; 1&#34;,&#34; 31&#34;,&#34; 131&#34;,&#34; 2131&#34;,&#34; 12131&#34;我们可以缓存尾巴的长度: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();
}

答案 4 :(得分:0)

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

// 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;
}

答案 5 :(得分:0)

时空复杂度为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];
}

答案 6 :(得分:0)

这是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];
}