"Time Limit Exceeded" on LeetCode's Longest Palindromic Subsequence question - python

I'm trying to solve this problem on LeetCode, which reads:
Following the most upvoted Java solution, I came up with the following memoized solution:
import functools
class Solution:
def longestPalindromeSubseq(self, s):
return longest_palindromic_subsequence(s)
#functools.lru_cache(maxsize=None)
def longest_palindromic_subsequence(s):
if not s:
return 0
if len(s) == 1:
return 1
if s[0] == s[-1]:
return 2 + longest_palindromic_subsequence(s[1:-1])
return max(
longest_palindromic_subsequence(s[0:-1]),
longest_palindromic_subsequence(s[1:]))
The problem is that the time limit is exceeded for an input string which appears to have many repeated characters:
As I understand from the cited discussion, without the functools.lru_cache, the time complexity of this algorithm is O(2^N) because, at each reduction of the string length by one character, two recursive calls are made.
However, the discussion states that the memoized solution is O(N^2), which shouldn't exceed the time limit. I don't really see how memoization reduces the time complexity, however, and it doesn't seem to be the case here.
What further puzzles me is that if the solution consists of many repeated characters, it should actually run in O(N) time since each time the first and last characters are the same, only one recursive call is made.
Can someone explain to me why this test is failing?

String slicing in Python is O(n) (n being the length of the slice) while java's substring is O(1) as it merely creates a view on the same underlying char[]. You can take the slices out of the equation, however, by simply operating on the same string with two moving indexes. Moreover, you can move indexes past blocks of identical letters when first and last are not the same:
#functools.lru_cache(maxsize=None)
def longest_palindromic_subsequence(s, start=None, end=None):
if start is None:
start = 0
if end is None:
end = len(s) - 1
if end < start:
return 0
if end == start:
return 1
if s[start] == s[end]:
return 2 + longest_palindromic_subsequence(s, start+1, end-1)
# you can move indexes until you meet a different letter!
start_ = start
end_ = end
while s[start_] == s[start]:
start_ += 1
while s[end_] == s[end]:
end_ -= 1
return max(
longest_palindromic_subsequence(s, start, end_),
longest_palindromic_subsequence(s, start_, end))
Memoizaton should help significantly. Take input "abcde". In the return max(...) part, eventually two recursive calls will be made for "bcd", and even more calls for the further embedded substrings.

Related

Python Solution for 392. Is Subsequence

I have solved solution 392 on LeetCode and one of the topics listed for it is Dynamic Programming. Looking at my code and other solutions online, I wonder what part of the solution is categorized as pertaining to Dynamic Programming. I would appreciate it if someone could enlighten me and help me have a better understanding of this.
The solution explanation is paywalled for me on LeetCode as I don't have premium, so I am trying to open source this understanding.
Solution:
def isSubsequence(self, s: str, t: str) -> bool:
if len(s) == 0:
return True
if len(t) == 0:
return False
temp = ''
count = 0
for i in t:
if count < len(s) and i == s[count]:
temp += i
count += 1
if temp == s:
return True
else:
return False
Link: https://leetcode.com/problems/is-subsequence/
As commented the posted solution is Your approach is an example of a two pointer algorithm
To create a Dynamic Programming problems solution we can be broken into three steps
Find the first solution (base case)
Analyze the solution
Optimize the solution
Step 1: First solution
Here's a recursive solution top/down solution that solves the problem.
Recursive solution breaks into subproblems
if s is empty string problem solved (return True)
if t is empty the problem solved (return False)
if first letters match => return result of matching after first letters in s & t
otherwise, match s after first letter in t
Code
def isSubsequence(s, t):
# Base Cases
if not s:
return True # s is empty
elif not t:
return False # t is empty
# Recursive case
# if first letters match, solve after first letters of s & t
# else find s after first letter of t
return isSubsequence(s[1:], t[1:]) if s[0] == t[0] else isSubsequence(s, t[1:])
Step 2: Analysis
The recursion provides a simple implementation
Normally recusion would be inefficient since it would repeatedly solve the same subproblems over and over
However, subproblems are not repeatedly solved in this case
For instance to find if "ab" is a subsequence of "xaxb" we the following call tree:
isSubsequence("ab", 'xaxb') # to check "ab" against "xaxb"
isSubsequence("ab", "axb") # we check these sequence of subproblems
isSubsequence("b", "xb") # but each is only checked once
isSubsequence("b", "b")
isSubsequenc("", "")
return True
Step 3: Optimization
In this case the solution is already optimized. For other recursive solutons like thiw we would use memoization to optimize
avoids repeatedly solving subsolutions
can use the cache Python 3.9+ or lru_cache (pre Python 3.9) for memoization
Memoized Code (note: not necessary in this case)
from functools import lru_cache
#lru_cache(maxsize=None)
def isSubsequence(s, t):
# Base Cases
if not s:
return True # s is empty
elif not t:
return False # t is empty
# Recursive case
# if first letters match, solve after first letters of s & t
# else find s after first letter of t
return isSubsequence(s[1:], t[1:]) if s[0] == t[0] else isSubsequence(s, t[1:])

Why '==' is fast than manually traversal of two strings comparison in Python3?

I try to solve the problem 28. Implement Str on LeetCode.
However, I have some questions about the time complexity of the two versions of the implemented codes.
# Version 1
class Solution:
def strStr(self, haystack, needle):
len_h = len(haystack)
len_n = len(needle)
if not needle:
return 0
if len_n > len_h:
return -1
i = 0
while i<len_h :
found = True
if haystack[i] == needle[0]:
for j in range(len_n):
if i+j >= len_h or haystack[i+j] != needle[j]:
found = False
break
if found:
return i
i += 1
return -1
In this version, I try to find the needle substring in the haystack using the double loops.
I think the time complexity of the code is O(mn) where m is the length of the haystack and n is the length of the needle.
Unfortunately, the code cannot pass the tests due to the time exceeding.
Then, I try to optimize my code and get version 2 of the code.
# Version 2
class Solution:
def strStr(self, haystack, needle):
len_h = len(haystack)
len_n = len(needle)
if not needle:
return 0
if len_n > len_h:
return -1
i = 0
while i<len_h :
found = True
if haystack[i] == needle[0]:
if haystack[i:i+len_n] == needle:
return i
i += 1
return -1
I compare the needle and the substring of the haystack using string-slice and '==' instead of the manual comparison. Then, the code passes the tests.
Now, I have some questions:
What is the time complexity of the string slice?
What is the time complexity of the check operation (==) between two strings?
Why version 2 is fast than version 1 if the time complexity of the check operation is O(n)?
Thanks for any advice.
str.__eq__(self, other) (that is, equality for strings) is implemented in C and is lightning fast (as fast as any other language once it starts).
Your Python-implemented character-wise string comparison is slow for two reasons. First, the looping logic is implemented in Python, and Python loops are never very fast. Second, when you say needle[j] that is slicing one string to construct another one. That by itself is slow, and you do it in a nested loop, so the overall runtime will be disastrous. You end up calling str.__eq__ once per character, and every time it's called it has to check the length of the strings on each side (it does not know you just sliced a single character).

Double recursion to find smallest multiple using only certain digits

This is the problem statement my question stems from (from Hackerrank): You are given an integer N. Can you find the least positive integer X made up of only 9's and 0's, such that, X is a multiple of N? X is made up of one or more occurences of 9 and zero or more occurences of 0.
I thought I would use double recursion but I'm having trouble understanding how to make it work. My function takes in "multiple", which is a string (so that I can append either '0' or '9' to it in further function calls and not have to deal with arithmetic), and "divisor".
Basically I wanted to keep calling my function, adding either 9 or 0 to "multiple" at every call, returning the final "multiple" when it's finally divisible by "divisor". I was envisioning it as a tree of function calls splitting every time between function(multiple + '9', divisor) and function(multiple + '0', divisor).
However, it seems once I call return, it doesn't get to the second function call:
#multiple is a string
def rec_nine_zero(multiple, divisor):
if int(multiple) % divisor == 0:
return multiple
else:
return rec_nine_zero(multiple + '9', divisor)
return rec_nine_zero(multiple + '0', divisor)
The below works:
print(rec_nine_zero('9', 1111))
However, if I try this (where the desired multiple is 90):
print(rec_nine_zero('9', 5)
It crashes and tells me basically that the stack blew up, meaning it never got to the second function call.
One problem I can see is that even if I manage to get the return statement to call both functions (with multiple + '9' and multiple + '0'), I feel like all of the branches of the function call tree except one (the one that finally finds the right result) will keep going until the stack says "annnnd...we're done".
EDIT: Based on Prune's answer, here is my new function:
def rec_nine_zero(multiples, divisor):
for multiple in multiples:
if int(multiple) % divisor == 0:
return multiple
new_multiples = []
for multiple in multiples:
new_multiples.append(multiple + '0')
new_multiples.append(multiple + '9')
return rec_nine_zero(new_multiples, divisor)
It blows up because you've done this in the canonical fashion of depth-first search. The first branch you try in a string of 9's, as long as needed to find a solution. Since there isn't any such solution for N=5, you recur until you blow the stack.
Change over to breadth-first. Generate and test your smallest string, '9'. When that fails, recur on the list of strings you want to extend: ["9"].
In the recursion, you append '0' and '9' to each candidate in the list. On the first recursion, this gives you ["90", "99"]. With N=5, you'll return success on this stage. If you have some other number, such as 7, you'll recur with this new list.
On the next step, you'll test the list ["900", "909", "990", "999"], and continue this process until you succeed.
BTW, you can perhaps make this easier if you quit converting between string and int: just start with 9. The next stage will work on 10*x and 10*x+9 for each x in the previous list.
Does that get you moving?
Not the answer but heres another simpler way to do the question -
def rec_nine_zero(divisor):
d = divisor
while True:
if all([True if i in '09' else False for i in `d`]): return d
else: d+=divisor
print rec_nine_zero(111)
Anyways to answer your question -
There are some caveats in your code which might give you some tips about recursion.
First you have a return statement under another return statement in the same construct and because of this the second return statement is never run.
Second thing is you are missing a base case for recursion which is needed to stop the recursion from going on. What your code is doing right now is just appending 9 and 9 and 9 ...., your 1st case works because luckily, 111 has a multiple of 999. In other cases it fails miserably.
And unfortunately, you cannot construct a base case for this problem if you solve it this way. The solution that #Prune gave is the right one.
Hope it helps!
Yeah you got it right! Anyways, I like shortening the code so here you go with the canonical bfs-
def rec_nine_zero(multiples, divisor):
que = [];que.append('9')
while(len(que) != 0):
k = que.pop()
if int(k)%divisor == 0:return k
que.append(k+'9');que.append(k+'0')
return -1
print rec_nine_zero('9', 5)
PS - I'm sure it can be shortened more!

What is the big-Oh runtime of two recursive O(logn) calls?

def f(L):
if len(L) < 1 billion:
return L
else:
return f(L[:len(L) // 2]) + f(L[len(L) // 2:])
L is a list of size n
I know that if it was a single recursive call, then it would be O(logn), but there are two recursive calls here.
But it started to exhibit more of a O(n) runtime as I began to run it on a visualizer.
In my opinion it should be O(logn+logn) = O(2logn) = O(logn). Am I correct?
Consider how many calls you're doing. At the first level of the recursion you'll do 2 calls. For each of those you'll do two more calls. Etc ... This means that at level i of the recursion you'll have made a total of O(2^i) function calls.
How many levels of the recursion are there? This is just the height of a binary tree with n elements, which is O(log_2 n).
So by the time you reach all the leaves of the recursion you will have done O(2^(log_2 n)) = O(n) function calls.
--
Another way of looking at it is that you eventually have to piece back together the entire list, so how could you possibly do that in less than O(n) time?
Your algorithm as it stands is going to be O(n) if len(L) is at least 1 billion because you will break the list into two, and then add the two halves back together. Both slicing and adding are O(n) operations.
If you want to test the runtime of the two recursive calls,
1. Pass in a start and end index, and call
f(L, start, start+(end-start)//2) + f(L, start+(end-start)//2, end)
2. Return end-start or some other O(1) value when end-start is less than 1 billion

How to Convert Recursion to Tail Recursion

Is it always possible to convert a recursion into a tail recursive one?
I am having a hard time converting the following Python function into a tail-recursive one.
def BreakWords(glob):
"""Break a string of characters, glob, into a list of words.
Args:
glob: A string of characters to be broken into words if possible.
Returns:
List of words if glob can be broken down. List can be empty if glob is ''.
None if no such break is possible.
"""
# Base case.
if len(glob) == 0:
return []
# Find a partition.
for i in xrange(1, len(glob) + 1):
left = glob[:i]
if IsWord(left):
right = glob[i:]
remaining_words = BreakWords(right)
if remaining_words is not None:
return [left] + remaining_words
return None
I'n not sure if is always the case, but most of recursive functions can be implemented as tail recursives. Besides Tail Recursion is different from Tail Recursion optimization.
Differences Tail Recursion and "Regular" ones
There are two elements that must be present in a recursive function:
The recursive call
A place to keep count of the return values.
A "regular" recursive function keeps (2) in the stack frame.
The return values in regular recursive function are composed of two types of values:
Other return values
Result of the owns function computation
Let's see a example:
def factorial(n):
if n == 1 return 1
return n * factorial(n-1)
The frame f(5) "stores" the result of it's own computation (5) and the value of f(4), for example. If i call factorial(5), just before the stack calls begin to colapse, i have:
[Stack_f(5): return 5 * [Stack_f(4): 4 * [Stack_f(3): 3 * ... [1[1]]
Notice that each stack stores, besides the values i mentioned, the whole scope of the function. So, the memory usage for a recursive function f is O(x), where x is the number of recursive calls i have to made. So, if i needb 1kb of RAM to calculate factorial(1) or factorial(2), i need ~100k to calculate factorial(100), and so on.
A Tail Recursive function put (2) in it's arguments.
In a Tail Recursion, i pass the result of the partial calculations in each recursive frame to the next one using parameters. Let's see our factorial example, Tail Recursive:
def factorial(n):
def tail_helper(n, acc):
if n == 1 or n == 2: return acc
return tail_helper(n-1, acc + n)
return tail_helper(n,0)
Let's look at it's frames in factorial(4):
[Stack f(4, 5): Stack f(3, 20): [Stack f(2,60): [Stack f(1, 120): 120]]]]
See the differences? In "regular" recursive calls the return functions recursively compose the final value. In Tail Recursion they only reference the base case (last one evaluated). We call accumulator the argument that keeps track of the older values.
Recursion Templates
The regular recursive function go as follows:
def regular(n)
base_case
computation
return (result of computation) combined with (regular(n towards base case))
To transform it in a Tail recursion we:
Introduce a helper function that carries the accumulator
run the helper function inside the main function, with the accumulator set to the base case.
Look:
def tail(n):
def helper(n, accumulator):
if n == base case:
return accumulator
computation
accumulator = computation combined with accumulator
return helper(n towards base case, accumulator)
helper(n, base case)
Your example:
I did something like this:
def BreakWords(glob):
def helper(word, glob, acc_1, acc_2):
if len(word) == 0 and len(glob) == 0:
if not acc_1:
return None
return acc
if len(word) == 0:
word = glob.pop[0]
acc_2 = 0
if IsWord(word.substring[:acc_2]):
acc_1.append(word[:acc_2])
return helper(word[acc_2 + 1:], glob, acc_1, acc_2 + 1)
return helper(word[acc_2 + 1:], glob, acc_1, acc_2 + 1)
return helper("", glob, [], 0)
In order to eliminate the for statement you made, i did my recursive helper function with 2 accumulators. One to store the results, and one to store the position i'm currently trying.
Tail Call optimization
Since no state is being stored on the Non-Border-Cases of the Tail Call stacks, they aren't so important. Some languages/interpreters then substitute the old stack with the new one. So, with no stack frames constraining the number of calls, the Tail Calls behave just like a for-loop.
But unfortunately for you Python isn't one of these cases. You'll get a RunTimeError when the stack gets bigger than 1000. Mr. Guido
thinks that the clarity lost to debugging purposes due to Tail Call Optimization (caused by the frames thrown awy) is more important than the feature. That's a shame. Python has so many cool functional stuff, and tail recursion would be great on top of it :/

Categories

Resources