Python Solution for 392. Is Subsequence - python

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:])

Related

How to optimise run time and memory usage of code for finding the longest substring?

I solved the question on LeetCode where you need to find the longest substring without repeating characters, but currently my code is using too much memory (~37Mb) and runs quite slowly compared to other submissions. My solution is this:
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
strings = [0,]
for a in range(len(s)):
sub = s[a]
strings.append(len(sub))
for char in s[a+1:]:
if char not in sub:
sub += char
else:
break
strings.append(len(sub))
return max(strings)
How can I improve the performance of the code? Is there a specific algorithm I should look into? Thanks!
You have to use an algorithm called 'Sliding Window' (which is a bit like a 2-pointer algorithm). In the below solution, the 'start' variable is the pointer used.
Also, please understand the usage of hash-maps/sets/dictionaries. I have used a dictionary since you are able to lookup/search a key in O(1) Time Complexity. You are also able to insert a key in O(1) Time Complexity.
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
if not s:
return 0
dic, ans, start = {}, 0, 0
for i, v in enumerate(s):
if v not in dic or dic[v] < start:
ans = max(ans, i - start + 1)
else:
start = dic[v] + 1
dic[v] = i
return ans
This kind of questions come up quite a bit in tech interviews, so make sure you fully understand the solution I have placed above.

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).

python build a list recursively

Let's say I have a string
S = "qwertyu"
And I want to build a list using recursion so the list looks like
L = [u, y, t, r, e, w, q]
I tried to write code like this:
def rec (S):
if len(S) > 0:
return [S[-1]].append(rec(S[0:-1]))
Ideally I want to append the last element of a shrinking string until it reaches 0
but all I got as an output is None
I know I'm not doing it right, and I have absolutely no idea what to return when the length of S reaches 0, please show me how I can make this work
(sorry the answer has to use recursion, otherwise it won't bother me)
Thank you very much!!!
There are many simpler ways than using recursion, but here's one recursive way to do it:
def rec (S):
if not S:
return []
else:
temp = list(S[-1])
temp.extend(rec(S[:-1]))
return temp
EDIT:
Notice that the base case ensures that function also works with an empty string. I had to use temp, because you cannot return list(S[-1]).extend(rec(S[:-1])) due to it being a NoneType (it's a method call rather than an object). For the same reason you cannot assign to a variable (hence the two separate lines with temp). A workaround would be to use + to concatenate the two lists, like suggested in Aryerez's answer (however, I'd suggest against his advice to try to impress people with convoluted one liners):
def rec (S):
if not S:
return []
else:
return list(S[-1]) + rec(S[:-1])
In fact using + could be more efficient (although the improvement would most likely be negligible), see answers to this SO question for more details.
This is the simplest solution:
def rec(S):
if len(S) == 1:
return S
return S[-1] + rec(S[:-1])
Or in one-line, if you really want to impress someone :)
def rec(S):
return S if len(S) == 1 else S[-1] + rec(S[:-1])
Since append mutates the list, this is a bit difficult to express recursively. One way you could do this is by using a separate inner function that passes on the current L to the next recursive call.
def rec(S):
def go(S, L):
if len(S) > 0:
L.append(S[-1])
return go(S[0:-1], L)
else:
return L
return go(S, [])
L = [i for i in S[::-1]]
It should work.

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

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.

How to implement a brute force solution to "Finding first unique character in a string"

As described here:
https://leetcode.com/problems/first-unique-character-in-a-string/description/
I attempted one here but couldn't quite finish:
https://paste.pound-python.org/show/JuPLgdgqceMQYh5kk0Sf/
#Given a string, find the first non-repeating character in it and return it's index. If it doesn't exist, return -1.
#xamples:
#s = "leetcode"
#return 0.
#s = "loveleetcode",
#return 2.
#Note: You may assume the string contain only lowercase letters.
class Solution(object):
def firstUniqChar(self, s):
"""
:type s: str
:rtype: int
"""
for i in range(len(s)):
for j in range(i+1,len(s)):
if s[i] == s[j]:
break
#But now what. let's say i have complete loop of j where there's no match with i, how do I return i?
I'm ONLY interested in the brute force N^2 solution, nothing fancier. The idea in the above solution is to start a double loop, where inner loop searches for a match with the outer loop's char, and if there's match, break the inner loop and continue onto the next char on the outer loop.
But the question is, how do I handle when there's NO match, which is when I need to return the outer loop's index as the first unique one.
Can't quite figure out a graceful way to do it, and can handle edge case like a single char string.
Iterate over each char, and check if it appears in any of the following chars. We need to keep track of the characters we've already seen, to avoid falling into edge cases. Try this, it's an O(n^2) solution:
def firstUniqChar(s):
# store already seen chars
seen = []
for i, c in enumerate(s):
# return if char not previously seen and not in rest
if c not in seen and c not in s[i+1:]:
return i
# mark char as seen
seen.append(c)
# no unique chars were found
return -1
For completeness' sake, here's an O(n) solution:
def firstUniqChar(s):
# build frequency table
freq = {}
for i, c in enumerate(s):
if c not in freq:
# store [frequency, index]
freq[c] = [1, i]
else:
# update frequency
freq[c][0] += 1
# find leftmost char with frequency == 1
# it's more efficient to traverse the freq table
# instead of the (potentially big) input string
leftidx = float('+inf')
for f, i in freq.values():
if f == 1 and i < leftidx:
leftidx = i
# handle edge case: no unique chars were found
return leftidx if leftidx != float('+inf') else -1
For example:
firstUniqChar('cc')
=> -1
firstUniqChar('ccdd')
=> -1
firstUniqChar('leetcode')
=> 0
firstUniqChar('loveleetcode')
=> 2
Add an else to the for loop where you return.
for j ...:
...
else:
return i
I'd first like to note that your current algorithm for finding unique characters doesn't work correctly. That's because you can't assume the character at index i is unique just because none of the indexes j found the same character later in the string. The character at index i could be a repeat of an earlier character (which you'd have skipped when the previous j was equal to the current i).
You could fix the algorithm by letting j iterate over the whole range of indexes, and adding an extra check to ignore the matches when the indexes are the same to your if:
for i in range(len(s)):
for j in range(len(s)):
if i != j and s[i] == s[j]:
break
As Ignacio Vazquez-Abrams suggests in his answer, you can then add an else block to the inner for loop to make the code return when no match was found:
else: # this line should be indented to match the "for j" loop
return i
There are also a few ways you can solve this problem more simply if you use the builtin functions and types available in Python.
For instance, you can implement an O(n^2) solution equivalent to the one above using only one explicit loop, and using str.count to replace the inner one:
def firstUniqChar(s):
for i, c in enumerate(s):
if s.count(c) == 1:
return i
return None
I'm also using enumerate to get the character values and indexes together in one step, rather than iterating over a range and indexing later.
There's also a very easy way to make an O(n) solution using collections.Counter, which can do all the counting in one pass before you start checking the characters in order to try to find the first one that is unique:
from collections import Counter
def firstUniqChar(s):
count = Counter(s)
for i, c in enumerate(s):
if count[c] == 1:
return i
return None
I'm not sure your approach will work on an even palindrome, e.g. "redder" (note the second d). Try this instead:
s1 = "leetcode"
s2 = "loveleetcode"
s3 = "redder"
def unique_index(s):
ahead, behind = list(s), set()
for idx, char in enumerate(s):
ahead = ahead[1:]
if (char not in ahead) and (char not in behind):
return idx
behind.add(s[idx])
return -1
assert unique_index(s1) == 0
assert unique_index(s2) == 2
assert unique_index(s3) == -1
For each character, we look ahead and behind. Only characters disjoint from both groups will return an index. As iteration progresses, the list of what is observed ahead shortens, while what is seen behind extends. The default is -1 as stated in the actual leetcode challenge.
A second list is not required. #Óscar López's answer is the simplified answer.

Categories

Resources