2

I am trying to solve Longest palindromic substring on Leetcode. I a aware of solutions for this problem like expand around center or dynamic programming bottom up approach. For purely educational purposes I wanted to solve this in top down recursive manner. I am trying to find solution similar to what is described here or here. (problem is slighly different). I have this function:

private (int Start, int End) Longest(string s, int i, int j)

which takes string + start and end position of search. Tuple which is return is start and end of longest palindrom. I am trying to split into these cases:

  1. if s[i] == s[j] investigate Longest(s, i+1, j-1)
  2. Investigate Longest(s, i+1, j)
  3. Investigate Longest(s, i, j - 1)
  4. Return longest (max difference between returned start and end) from these three

Of course I am using Dictionary with tuple (int, int) as key (values of i and j) to remember all computed results to not compute them again.

Full code is below but it is very messy after few several iterations when I was trying to fix algorithm. I believe concreate code is not very important.

Code seems to be returning correct results but fail on Time Limit Exceeded on Leetcode. Is there correct fast recursive solution? I believe there should be as DP bottom up solution exists.

code:

private readonly IDictionary<(int, int), (int, int)> _mem = new Dictionary<(int, int), (int, int)>();

private (int Start, int End) Longest(string s, int i, int j) {
    if (i >= j) {
        return (i, j);
    }

    if (_mem.TryGetValue((i, j), out var ret)) {
        return ret;
    }

    var newI = i + 1;
    var newJ = j - 1;

    ValueTuple<int, int> removingTwo;

    if (s[i] == s[j])
    {
        removingTwo = Longest(s, newI, newJ);

        if (removingTwo.Item1 == newI && removingTwo.Item2 == newJ) {
            removingTwo.Item1--;
            removingTwo.Item2++;
        }
    }
    else {
        removingTwo = (1, 0);
    }

    var removingFirst = Longest(s, newI, j);
    var removingLast = Longest(s, i, newJ);  

    var mT = removingTwo.Item2 - removingTwo.Item1;
    var mF = removingFirst.End - removingFirst.Start;
    var mL = removingLast.End - removingLast.Start;

    var max = Math.Max(mT, mF);
    max = Math.Max(max, mL);

    ValueTuple<int, int> retVal;

    if (max == mT) retVal = removingTwo;
    else if (max == mF) retVal = removingFirst;
    else retVal = removingLast;

    _mem.Add((i, j), retVal);

    return retVal;

}

Edit: working bottom-up solution (copied from geegsforgeegs):

public string LongestPalindrome(string s) {
    if (s.Length == 0) return "";
    var table = new bool[s.Length, s.Length];
    var len = s.Length;
    for (int i = 0; i < len; i++) {
        table[i,i] = true;
    }

    var start = 0;
    var max = 1;
    for (int i = 0; i < len - 1; i++) {
        if (s[i] == s[i + 1]) {
            start = i;
            max = 2;
            table[i, i+1] = true;
        }
    }

    for (int k = 3; k <= len; ++k) { 

              // Fix the starting index 
        for (int i = 0; i < len - k + 1; ++i)  
        { 
            // Get the ending index of substring from 
            // starting index i and length k 
            int j = i + k - 1; 

            // checking for sub-string from ith index to 
            // jth index iff str.charAt(i+1) to  
            // str.charAt(j-1) is a palindrome 
            if (table[i + 1, j - 1] && s[i] == s[j]) { 
                table[i,j] = true; 

                if (k > max) { 
                    start = i; 
                    max = k; 
                } 
            } 
        } 
    } 

    return s.Substring(start, max);
}
Michal
  • 1,192
  • 1
  • 13
  • 31
  • I first tried a solution similar to yours in Python and it exceeded the time limit. Did you try submitting a bottom-up DP solution that also takes O(n^2) time and space? I'm wondering if that would also exceed. – גלעד ברקן Dec 10 '18 at 16:30
  • The bottom up code you've provided runs approximately n^2/2 iterations (~500,000 for a 1000 character string). The best I tried in a recursive solution still made about n^2 iterations (~1,000,000 for a 1000 character string). I wonder if that, combined with the overhead for a dictionary rather than straight table, could make the difference. Have you calculated how many iterations your recursion makes for a 1000 character string? – גלעד ברקן Dec 20 '18 at 16:18

1 Answers1

2

Here's a recursive method in Python that passes the LeetCode test. It may be that they are looking for a constant space solution.

f(i, k) returns (l, j), the largest tuple of length l and its starting index, j. max in this instance is looking at the first element of the returned tuple, which is l, the palindrome's length.

def longestPalindrome(self, s):
  def f(i, k):
    return max(
      # next iteration
      f(i - 1, 1) if k < 2 and i > 0 else (-1,-1),
      f(i - 1, 2) if k < 2 and i > 0 and s[i-1] == s[i] else (-1, -1),

      # a larger palindrome than this one
      f(i - 1, k + 2) if i > 0 and i + k < len(s) and s[i-1] == s[i + k] else (-1, -1),

      # this one
      (k, i)
    )

  (l, j) = f(len(s) - 1, 1)
  return s[j:j+l]
גלעד ברקן
  • 21,095
  • 3
  • 19
  • 57
  • Thank you for sharing this. I am not really strong in algorithms and especially in DP but isn't this more recursive version of expand around center? This line looks like that to me: f(i - 1, k + 2) if i > 0 and i + k < len(s) and s[i-1] == s[i + k] else (-1, -1) Also there is no memonization? – Michal Dec 12 '18 at 20:08
  • @Michal you are exactly correct - it is a version of "expand around center" because this method has constant space, is much faster in practice than O(n^2) unless the input is degenerate, and does not require memoization. I would be interested to know if the LeetCode test would timeout even for a bottom-up DP with O(n^2) time and space but have not tried it. As I commented above, I first tried a memoized recursion similar to yours (perhaps a bit more succinct) that timed out on LeetCode. (That code did not perform more than ~1,000,000 iterations for the ~1000 char strings it timed out on.) – גלעד ברקן Dec 12 '18 at 21:19
  • Thank you for explanation. Bottom-up solution works on LeetCode. I haven't tried but they have listed it official possible solution. Thinking what may be slow in recurive apporach... Hashtable access? I don't think so but don't have any other idea ... – Michal Dec 13 '18 at 14:52
  • @Michal I'm not sure bottom up with O(n^2) time and space works on the test. Why don't you try it and see? Then you'll know if it's an issue with them or our recursive ideas. In the explanation of the solution they also list brute force as Approach 2. It doesn't mean they support it. – גלעד ברקן Dec 13 '18 at 16:40
  • @Michal interesting. Could you please add that code to your question so we can compare with the recursive? – גלעד ברקן Dec 19 '18 at 11:40
  • sure thing added – Michal Dec 20 '18 at 11:48