Review an answer - Decode Ways

前端 未结 7 1246
隐瞒了意图╮
隐瞒了意图╮ 2021-01-31 19:36

I\'m trying to solve a question and my question here is why doesn\'t my solution work?. Here\'s the question and below\'s the answer.

Question taken fr

7条回答
  •  失恋的感觉
    2021-01-31 20:21

    Since I struggled with this problem myself, here is my solution and reasoning. Probably I will mostly repeat what amon wrote, but maybe someone will find it helpful. Also it's c# not java.

    Let's say that we have input "12131" and want to obtain all possible decoded strings. Straightforward recursive solution would do iterate from left to right, obtain valid 1 and 2 digits heads, and invoke function recursively for tail.

    We can visualize it using a tree:

    There are 5 leaves and this is number of all possible decoded strings. There are also 3 empty leaves, because number 31 cannot be decoded into letter, so these leaves are invalid.

    Algorithm might look like this:

    public IList Decode(string s)
    {
        var result = new List();
    
        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();
    }
    

    We have to take care of numbers starting with 0 ("0", "03", etc.), and greater than 26.

    Because in this problem we need only count decoding ways, and not actual strings, we can simplify this code:

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

    The problem with this algorithm is that we compute results for the same input string multiple times. For example there are 3 nodes ending with 31: ABA31, AU31, LA31. Also there are 2 nodes ending with 131: AB131, L131. We know that if node ends with 31 it has only one child, since 31 can be decoded only in one way to CA. Likewise, we know that if string ends with 131 it has 2 children, because 131 can be decoded into ACA or LA. Thus, instead of computing it all over again we can cache it in map, where key is string (eg: "131"), and value is number of decoded ways:

    public int DecodeCountCached(string s, Dictionary 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;
    }
    

    We can refine this even further. Instead of using strings as a keys, we can use length, because what is cached is always tail of input string. So instead of caching strings: "1", "31", "131", "2131", "12131" we can cache lengths of tails: 1, 2, 3, 4, 5:

    public int DecodeCountDPTopDown(string s, Dictionary 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;
    }
    

    This is recursive top-down dynamic programming approach. We start from the begining, and then recursively compute solutions for tails, and memoize those results for further use.

    We can translate it to bottom-up iterative DP solution. We start from the end and cache results for tiles like in previous solution. Instead of map we can use array because keys are integers:

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

    Some people simplify it even further, initializing cache[0] with value 1, so they can get rid of conditions for tail.Length==1 and tail.Length==2. For me it is unintuitive trick though, since clearly for empty string there is 0 decode ways not 1, so in such case additional condition must be added to handle empty input:

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

提交回复
热议问题