Home/Coding Guide/Google Interview Questions
⌂ Main Page
Coding Interview Preparation Guide

Google Interview Questions

Google interview questions with detailed solutions.

Challenges on this page

Print more slides: * Multiply strings (Google) * Minimum Window Substring (FB) * Median of Two Sorted Arrays * 340. Longest Substring with At Most K Distinct Characters * 23. Merge k Sorted Linked Lists * Merge k Sorted Arrays * 215. Kth Largest Element in an Array

Add these - they are easier than they look: * Dijkstra algo for graphs (shortest path from a source vertex to all other vertices) * Floyd Warshall Algorithm (shortest path between all pair of nodes) * Minimum Spanning Tree

Google Interview Questions

See the following questions in the Facebook notebook that are also part of the Google interview preparation set of questions (avoiding creating duplicates here): 1. Lists and Strings * 15. 3Sum * 3. Longest Substring Without Repeating Characters * 31. Next Permutation * 43. Multiply Strings * 76. Minimum Window Substring

  1. Linked Lists * 2. Add two numbers * 21. Merge two sorted lists * 138. Copy List with Random Pointer

  2. __Trees and Graphs___ * 124. Binary Tree Maximum Path Sum * 200. Number of Islands * 543. Diameter of Binary Tree

Examples Found In Forums

Phone Interview Question 1. Given a quadtree and the fact that each pixel is either white or black, write a program to compute the area of the black pixels assuming the size of picture is 1 by 1.

Python
class QuadTreeNode:
    def __init__(self, color=None, children=None):
        self.color = color  # 'black', 'white', or None
        self.children = children  # List of 4 children (top-left, top-right, bottom-left, bottom-right)


def black_area(node, size=1.0):
    if node.color == 'black':
        return size * size
    if node.color == 'white':
        return 0

    # If not leaf node, recursively calculate area for each child
    area = 0
    for child in node.children:
        area += black_area(child, size / 2)
    return area


# Example Usage:
# Creating a simple quadtree structure:
# Root node is gray (has children), divided into four quadrants.
# Top-left quadrant is completely black.
# Top-right quadrant is completely white.
# Bottom-left quadrant is divided further into four, with one black child.
# Bottom-right quadrant is white.

root = QuadTreeNode(children=[
    QuadTreeNode(color='black'),  # Top-left quadrant
    QuadTreeNode(color='white'),  # Top-right quadrant
    QuadTreeNode(children=[       # Bottom-left quadrant
        QuadTreeNode(color='black'),
        QuadTreeNode(color='white'),
        QuadTreeNode(color='white'),
        QuadTreeNode(color='white')
    ]),
    QuadTreeNode(color='white')   # Bottom-right quadrant
])

# Calculate black area
print("Black Area:", black_area(root))

Phone Interview Question 2. Now calculate the the perimeter of the black pixels, but don't count the edges between pixels of the same color. To be implemented.

Onsite.
Let's define a kind of message called "Broadcast & Shut Down." When a router receives this message, it broadcasts the same message to all other routers within its wireless range. Then, that router shuts down, and can no longer send or receive messages.

For example, Router A is at (0, 0); Router B is at (0, 8); Router C is at (0, 17); Router D is at (11, 0). If the wireless range is 10, when Router A sends a message, it could first reach B; the message from Router B would further reach Router C but Router D would never receive this message.

Given a list of routers' locations (their names and the corresponding 2D coordinates), a source router, a destination router, and the wireless range, tell me whether a message from the source router can reach the destination router with the "Broadcast & Shut Down" message. Write a method / function with appropriate input and output arguments. + develop Class to represent router that has the name and coordinates

Input: list of all routers = [ list of object representing routers ], source router name, destination router name, range
Output: boolean

Python
from typing import List, Set

class Router:
  def __init__(self, name, x, y):
    self.name=name
    self.x=x
    self.y=y

    
def distance(router1: Router, router2: Router) -> float:
    pass
   
def can_broadcast(routers: List[Router], source: str, destination: str, range: int) -> bool:
   ''' Docstring '''
    # edge cases
    if len(ronters) < 2:
        raise ValueError('The num of routers has to be at least 2')
    if not isinstance(source, str) or not isinstance(destination, str):
        raise ValueError('Source and destination should be of type string')
    if not isinstance(range, int):
        raise ValueError('Range should be of type integet')
    if source not in [item.name for item in routers] or destination not in [item.name for item in routers]:
        raise ValueError('The names of routers should be in the list')
   
    # map to facilitate iteration
    router_map = {router.name: router for router in routers}
   
    def dfs(current_router: Router, visited: Set[Router]) -> bool:
        ''' Docstring'''
        if current_router.name == destination:
            return True
     
        # mark current router as bisited
        visited.add(current_router.name)

        # iterate over other routers
        for router in routers:
        if router.name not in visited:
            if dfs(router, visited) and distance(current_router, router) <= range:
                return True

        return False     
     
    return dfs(router_map[source], set())
 
can_broadcat([Router('A', 0, 1), Router('B', 5, 10)], 'A', ' B', 10)

Part I. Arrays and Strings

11. Container With Most Water

You are given an integer array height of length n. There are n vertical lines drawn such that the two endpoints of the ith line are (i, 0) and (i, height[i]).
Find two lines that together with the x-axis form a container, such that the container contains the most water.
Return the maximum amount of water a container can store.
Notice that you may not slant the container.

Input: height = [1,8,6,2,5,4,8,3,7] Output: 49 Explanation: The above vertical lines are represented by array [1,8,6,2,5,4,8,3,7]. In this case, the max area of water (blue section) the container can contain is 49.

image.png

Algorithm

The intuition behind this approach is that the area formed between the lines will always be limited by the height of the shorter line. Further, the farther the lines, the more will be the area obtained.

We take two pointers, one at the beginning and one at the end of the array constituting the length of the lines. Futher, we maintain a variable maxarea to store the maximum area obtained till now. At every step, we find out the area formed between them, update maxarea and move the pointer pointing to the shorter line towards the other end by one step.

Python
# time c. O(n), space c. O(1)
def maxArea(height):
        res = 0
        l = 0
        r = len(height) - 1
        while l < r:
            res = max(res, min(height[l], height[r])*(r-l))
            if height[l] <= height[r]:
                l += 1
            else:
                r -= 1
        return res
        
height = [1,8,6,2,5,4,8,3,8]
maxArea(height)
Output
56

844. Backspace String Compare

Given two strings s and t, return true if they are equal when both are typed into empty text editors. '#' means a backspace character.
Note that after backspacing an empty text, the text will continue empty.

Example 1:
Input: s = "ab#c", t = "ad#c" Output: true Explanation: Both s and t become "ac".

Example 2:
Input: s = "ab##", t = "c#d#" Output: true Explanation: Both s and t become "".

Example 3:
Input: s = "a#c", t = "b" Output: false Explanation: s becomes "c" while t becomes "b".

Constraints: * 1 <= s.length, t.length <= 200 * s and t only contain lowercase letters and '#' characters.

Follow up: Can you solve it in O(n) time and O(1) space?

Python
# easy solution (passed). Time / space c. O(n+m)
def backspaceCompare(s, t):

    if not s and not t:
        return True

    def mod_str(string):
        stack = []
        for c in string:
            if not c == '#':
                stack.append(c)
            else:
                if stack:
                    stack.pop()
        return stack

    return mod_str(s) == mod_str(t)


strings = [ ("ab#c", "ad#c"), ("ab##", "c#d#"), ("a#c","b"),]
for s, t in strings:
    print( backspaceCompare(s, t) )    # True, True, False
Output
True
True
False
Python
# Leetcode solution: they claim it's space c. O(1), but both F(S) and F(T) return a list
import itertools

def backspaceCompare(S, T):
    def F(S):
        skip = 0
        for x in reversed(S):
            if x == '#':
                skip += 1
            elif skip:
                skip -= 1
            else:
                yield x

    return all(x == y for x, y in itertools.zip_longest(F(S), F(T)))

s = "ab#c"
t = "ad#c"
backspaceCompare(s, t)
Output
True

15. 3Sum

Given an integer array nums, return all the triplets [nums[i], nums[j], nums[k]] such that i != j, i != k, and j != k, and nums[i] + nums[j] + nums[k] == 0.
Notice that the solution set must not contain duplicate triplets.

Example 1:
Input: nums = [-1,0,1,2,-1,-4]
Output: [[-1,-1,2],[-1,0,1]]

Example 2:
Input: nums = []
Output: []

Example 3:
Input: nums = [0]
Output: []

Constraints: * 0 <= nums.length <= 3000 * -105 <= nums[i] <= 105

Complexity * Brute force - three nested loops. Time c. O(n^3) * Optimal time c. - sorting = O(nlogn); nested loop = O(n^2)/2 => O(n^2). Therefore, O(nlogn + n^2) => O(n^2) * My previous answer: Space c. - no duplicates and i != j != k => total number of possible triplets is n/3. O(n/3) => O(n) * ChatGPT says O(n**2)

Python
def threeSum(nums: List[int]) -> List[List[int]]:
    '''
        My accepted solution
        Your runtime beats 93.44% of python3 submissions
        Your memory usage beats 98.69 % of python3 submissions
    '''
    if not isinstance(nums, list):
        return None
    n = len(nums)
    if n < 3:
        return []
    nums.sort()        
    res = set()                                               # helps avoid duplicates and decrease scpace complexity

    for i in range( n-1 ):
                
        # If current value > 0, break as the sum > 0
        if nums[i] > 0:
            break
        # It's a duplicate; helps reduce complexity
        if i > 0 and nums[i] == nums[i-1]:    
            continue
                        
        l = i+1
        r = n-1

        while l < r:
            sum_ = nums[i] + nums[l] + nums[r]
            if sum_ < 0:
                l += 1
            elif sum_ > 0:
                r -= 1
            else:             
                res.add( (nums[i], nums[l], nums[r]) )
                l += 1
                r -= 1
                
            # Skip duplicates for the second and third elements
            while l < r and nums[l] == nums[l - 1]:
                l += 1
            while l < r and nums[r] == nums[r + 1]:
                r -= 1

    return list(res)


a1 = [-1, 0, 1, 2, -1, -4]
a2 = []
a3 = [0]

for a in [a1, a2, a3]:
    print( threeSum(a) )

3. Longest Substring Without Repeating Characters

Given a string s, find the length of the longest substring without repeating characters.

Example 1:
Input: s = "abcabcbb"
Output: 3
Explanation: The answer is "abc", with the length of 3.

Example 2:
Input: s = "bbbbb"
Output: 1
Explanation: The answer is "b", with the length of 1.

Example 3:
Input: s = "pwwkew"
Output: 3
Explanation: The answer is "wke", with the length of 3.
Notice that the answer must be a substring, "pwke" is a subsequence and not a substring.

Example 4:
Input: s = ""
Output: 0

Constraints: * 0 <= s.length <= 5 * 104 * s consists of English letters, digits, symbols and spaces

Python
def lengthOfLongestSubstring(s: str) -> int:
    ''' My Solution (Accepted)
        Iterate over each char in string with some logic    
    '''    
    
    curr, max_ = '', ''
    for idx, c in enumerate(s):
        if not curr:                             # add current char if curr is empty
            curr = c                        
        elif curr and c not in curr:             # add current char if it's not in curr
            curr += c
        elif curr and curr[-1] == c:             # reset curr if current char is also the last one in curr
            curr = c
        else:                                    # current char in curr, but not the last one - find its index in s,
            temp_idx = idx - 1                   # but not in curr(!) because curr may be shorter. Make curr include 
            while s[temp_idx] != c:              # everyting after (because there is no c) + c at current idx
                temp_idx -= 1
            curr = s[temp_idx+1 : idx+1]

        max_ = max(curr, max_, key=len)
                
    return len(max_)


def lengthOfLongestSubstring2(s: str) -> int:
    '''LeetCode solution beautified by me
       We use HashSet to store the characters in current window [i,j) (j = i initially).
       Then we slide the index j to the right. If it is not in the HashSet, we slide j further.
       Doing so until s[j] is already in the HashSet. At this point, we found the maximum size
       of substrings without duplicate characters start with index i.
       If we do this for all i, we get our answer
    '''
    mapp = {c:0 for c in s}
    left = 0
    max_len = 0
    for right, c in enumerate(s):
        mapp[c] += 1
        while mapp[c] > 1:
            mapp[ s[left] ] -= 1                 # first this, then l += 1
            left += 1
        max_len = max(max_len, right - left + 1)

    return max_len

    
s1 = 'abcbacbb'
s2 = 'bbbbb'
s3 = 'pwwkew'
s4 = ''

# 3,1,3,0
for s in [s1, s2, s3, s4]:
    print( lengthOfLongestSubstring(s), end=', ' )
    
print()    
for s in [s1, s2, s3, s4]:
    print( lengthOfLongestSubstring2(s), end=', ' )

31. Next Permutation

Implement next permutation, which rearranges numbers into the lexicographically next greater permutation of numbers. If such an arrangement is not possible, it must rearrange it as the lowest possible order (i.e., sorted in ascending order). The replacement must be in place and use only constant extra memory.

Example 1: Input: nums = [1,2,3] Output: [1,3,2]

Example 2: Input: nums = [3,2,1] Output: [1,2,3]

Example 3: Input: nums = [1,1,5] Output: [1,5,1]

Example 4: Input: nums = [1] Output: [1]

Constraints: * 1 <= nums.length <= 100 * 0 <= nums[i] <= 100

image-3.png

image.png

Another explanation (condensed mathematical description): * Find largest index i such that array[i − 1] < array[i]. (If no such i exists, then this is already the last permutation.) * Find largest index j such that j ≥ i and array[j] > array[i − 1]. * Swap array[j] and array[i − 1]. * Reverse the suffix starting at array[i]. image-2.png Source: https://www.nayuki.io/page/next-lexicographical-permutation-algorithm

Python
def nextPermutation(nums: List[int]) -> None:
    '''
        Runtime: 32 ms, faster than 98.61% of Python3 online submissions for Next Permutation
        Memory Usage: 14.3 MB, less than 19.08% of Python3 submissions - IN PLACE, WHAT MEMORY USAGE?
    '''    
    i = len(nums)-2
    while i >= 0 and nums[i+1] <= nums[i]:      # find first nums[i] from right that's > than its right neighbor
        i -= 1
    
    if i >= 0:
        j = len(nums)-1                         # find first nums[j] from right that's > than nums[i]
        while nums[j] <= nums[i]:
            j -= 1
        nums[i], nums[j] = nums[j], nums[i]     # swap them
    
    reverse(nums, i+1)                          # all nums to right of nums[i] are in non-incresing oder -
                                                # reverse them for next lexicographical permutation

def reverse( nums: List[int], start: int) -> None:
        
    i, j = start, len(nums)-1
    while i < j:
        nums[i], nums[j] = nums[j], nums[i]
        i += 1
        j -= 1
        
a1 = [1,2,3]
a2 = [3,2,1]
a3 = [1,1,5]
a4 = [1]

for a in [a1, a2, a3, a4]:
    nextPermutation(a)
    print(a)
Output
[1, 3, 2]
[1, 2, 3]
[1, 5, 1]
[1]

809. Expressive Words

Sometimes people repeat letters to represent extra feeling. For example:

"hello" -> "heeellooo" "hi" -> "hiiii" In these strings like "heeellooo", we have groups of adjacent letters that are all the same: "h", "eee", "ll", "ooo".

You are given a string s and an array of query strings words. A query word is stretchy if it can be made to be equal to s by any number of applications of the following extension operation: choose a group consisting of characters c, and add some number of characters c to the group so that the size of the group is three or more.

For example, starting with "hello", we could do an extension on the group "o" to get "hellooo", but we cannot get "helloo" since the group "oo" has a size less than three. Also, we could do another extension like "ll" -> "lllll" to get "helllllooo". If s = "helllllooo", then the query word "hello" would be stretchy because of these two extension operations: query = "hello" -> "hellooo" -> "helllllooo" = s. Return the number of query strings that are stretchy.

Example 1: Input: s = "heeellooo", words = ["hello", "hi", "helo"] Output: 1 Explanation: We can extend "e" and "o" in the word "hello" to get "heeellooo". We can't extend "helo" to get "heeellooo" because the group "ll" is not size 3 or more.

Example 2: Input: s = "zzzzzyyyyy", words = ["zzyy","zy","zyy"] Output: 3

Constraints: * 1 <= s.length, words.length <= 100 * 1 <= words[i].length <= 100 * s and words[i] consist of lowercase letters.

Python
# My solution (no Leetcode solution).
# Time c. O(len(s)) + O(len(w1) + len(w2) +... + len(unique(w1)) + len(unique(w2)) + ...)) = linear?
# Space c. O(sum of len of unique chars in each word) = linear
def expressiveWords(s, words):

    def unique_chars(string):
        chars = []
        for c in string:
            if not chars:
                chars.append([c, 1])
            elif c == chars[-1][0]:
                chars[-1][1] += 1
            else:
                chars.append([c, 1])
        return chars

    res = 0
    chars_s = unique_chars(s)
    for w in words:
        chars_w = unique_chars(w)
        if [i[0] for i in chars_s] == [j[0] for j in chars_w]:
            include = True
            for k in range(len(chars_s)):
                if chars_s[k][1]<3 and chars_s[k][1] != chars_w[k][1]:    # can't make a stretchy word in this case
                    include = False
                    break
                if chars_s[k][1] < chars_w[k][1]:                # can't make a stretchy word in this case
                    include = False
                    break
            if include:
                res += 1

    return res

s = "heeellooo"
words = ["hello", "hi", "helo"]
expressiveWords(s, words)
Output
1

159. Longest Substring with At Most Two Distinct Characters

340. Longest Substring with At Most K Distinct Characters

Given a string s, return the length of the longest substring that contains at most two distinct characters.

Example 1: Input: s = "eceba" Output: 3 Explanation: The substring is "ece" which its length is 3.

Example 2: Input: s = "ccaabbb" Output: 5 Explanation: The substring is "aabbb" which its length is 5.

Constraints: * 1 <= s.length <= 105 * s consists of English letters.

Algorithm

using the hashmap one knows that the rightmost position of each character, so one has to move left pointer in the position left_idx + 1 to exclude the character from the sliding window

Python
# Time c. O(n), space c. O(1)
# At most k distinct characters
def LongestSubstring(s, k):
    n = len(s)
    if n*k==0:
        return 0
    l, r = 0, 0
    mapp = dict()
    max_len = 2
    for c in s:
        mapp[ s[r] ] = r
        r += 1
        if len(mapp) == k+1:
            del_idx = min(mapp.values())
            del mapp[ s[del_idx] ]
            l = del_idx + 1
        max_len = max(max_len, r - l)
    return max_len


strings = ["ccaabbb", "eceba", 'ababababab', 'abababacbab']
for s in strings:
    print(LongestSubstring(s, 2))     # 5, 3
Output
5
3
10
7
Python
# My solution using two pointers
def lengthOfLongestSubstringTwoDistinct2(s: str, k: int=2) -> int:
        
    n = len(s)
    # edge cases
    if n < 3:
        return n
    
    # initiate variables
    l,r = 0,0
    max_len = 0
    mapp = dict()
        
    # two pointers approac
    while r < n and l <= r:
        mapp[s[r]] = mapp.get(s[r], 0) + 1
        if len(mapp) > k:
            mapp[ s[l] ] -= 1
            if mapp[ s[l] ] == 0:
                del mapp[ s[l] ]
            l += 1
        else:
            max_len = max(max_len, len(s[l:r+1]))         # r-l+1  equals  len(s[l:r+1]))
            r += 1
    return max_len


strings = ["ccaabbb", "eceba", 'ababababab', 'abababacbab']
for s in strings:
    print(lengthOfLongestSubstringTwoDistinct2(s))     # 5, 3
Output
5
3
10
7

55. Jump Game

You are given an integer array nums. You are initially positioned at the array's first index, and each element in the array represents your maximum jump length at that position.
Return true if you can reach the last index, or false otherwise.

Example 1: Input: nums = [2,3,1,1,4] Output: true Explanation: Jump 1 step from index 0 to 1, then 3 steps to the last index.

Example 2: Input: nums = [3,2,1,0,4] Output: false Explanation: You will always arrive at index 3 no matter what. Its maximum jump length is 0, which makes it impossible to reach the last index.

Constraints: * 1 <= nums.length <= 104 * 0 <= nums[i] <= 105

1. Linear time solution (aka DP, but it's just a smart trick)

image-2.png

Python
# greedy solution #1 OR Approach 4: Greedy (Leetcode)
# time O(n), space O(1)
def canJump(nums):
    lastPos = len(nums) - 1
    for i in range (len(nums)-1, -1, -1):
        if i + nums[i] >= lastPos:
            lastPos = i
    
    return lastPos == 0
    
nums = [2,3,1,1,4]        # True
#nums = [3,2,1,0,4]        # False
canJump(nums)
Output
True
Python
# greedy solution #2 (from comments) - LESS INTUITIVE THAN greedy solution #1
# time O(n), space O(1)
def canJump(nums):
    
    if not nums:
        return False
    i = 0
    max_index = nums[i]
    while i < len(nums) and i <= max_index:
        new_index = i + nums[i]
        max_index = max(max_index, new_index)
        i += 1
                
    return True if i==len(nums) else False
    
nums = [2,3,1,1,4]        # True
#nums = [3,2,1,0,4]        # False
canJump(nums)
Output
True

2. Other, less efficient solutions (for learning purposes)

Complexity Analysis (recursion) * Time complexity : O(2^n). There are 2^n (upper bound) ways of jumping from the first position to the last, where n is the length of array nums. For a complete proof, please refer to Appendix A. * Space complexity : O(n). Recursion requires additional memory for the stack frames.

Complexity Analysis (recursion with memoization or top-down dyn. programming) * Time complexity : O(n^2). For every element in the array, say i, we are looking at the next nums[i] elements to its right aiming to find a GOOD index (GOOD=can reach end elem). nums[i] can be at most n, where n is the length of array nums. * Space complexity : O(2n) = O(n). First n originates from recursion. Second n comes from the usage of the memo table.

image.png

A simpler proof for the O(2ⁿ) complexity of the first approach is as follows. The worst possible case is when all the integers are larger than n-1, i.e. you can get to the end from any index of the array. It also means that any possible path (series of jumps) you try will be valid. There are 2^n-2 such possible series of jumps: we always take the first index, then take or not take the 2nd index (2 possibilities), and so on... until finally we always take the last index. The first approach tries out all these possible paths, hence O(2ⁿ) complexity in the worst case.

Python
# MY OWN RECURSIVE SOLUTION WITH MEMOIZATION!
# Time n**2, space O(n)
def canJump(nums):

    def cj_rec(curr_idx):

        if curr_idx == len(nums)-1:
            return True
        elif nums[ curr_idx ] == 0:
            return False
        res = []

        for j in range(1, nums[ curr_idx ]+1):
            new_idx = curr_idx+j
            if new_idx < len(nums):
                if not new_idx in memo:
                    memo[ new_idx ] = cj_rec( new_idx )                    
                res.append(memo[ new_idx ])

        return sum(res) > 0

    idx = 0
    memo = {}
    return cj_rec(idx)
    
nums = [2,3,1,1,4]         # True
#nums = [3,2,1,0,4]        # False
canJump(nums)
Output
True
Python
def canJump_rec(nums):
    ''' 
        My other recursive solution
        Time O(2^n) - see explanation above
        Space?
    '''
    print(nums)
    if len(nums) == 1:
        return True
    res = False
    for n in range(1,nums[0]+1):
        res = canJump_rec(nums[n:])
    return res

Some recursive time complexities
https://stackoverflow.com/questions/13467674/determining-complexity-for-recursive-functions-big-o-notation

int recursiveFun1(int n) { if (n <= 0) return 1; else return 1 + recursiveFun1(n-1); }

This function is being called recursively n times before reaching the base case so its O(n), often called linear.

int recursiveFun2(int n) { if (n <= 0) return 1; else return 1 + recursiveFun2(n-5); }

This function is called n-5 for each time, so we deduct five from n before calling the function, but n-5 is also O(n). (Actually called order of n/5 times. And, O(n/5) = O(n) ).

int recursiveFun3(int n) { if (n <= 0) return 1; else return 1 + recursiveFun3(n/5); }

This function is log(n) base 5, for every time we divide by 5 before calling the function so its O(log(n))(base 5), often called logarithmic and most often Big O notation and complexity analysis uses base 2.

void recursiveFun4(int n, int m, int o) { if (n <= 0) { printf("%d, %d\n",m, o); } else { recursiveFun4(n-1, m+1, o); recursiveFun4(n-1, m, o+1); } }

Here, it's O(2^n), or exponential, since each function call calls itself twice unless it has been recursed n times.

```int recursiveFun5(int n) { for (i = 0; i < n; i += 2) { // do something }

if (n <= 0)
    return 1;
else
    return 1 + recursiveFun5(n-5);

}```

And here the for loop takes n/2 since we're increasing by 2, and the recursion takes n/5 and since the for loop is called recursively, therefore, the time complexity is in

(n/5) * (n/2) = n^2/10,

due to Asymptotic behavior and worst-case scenario considerations or the upper bound that big O is striving for, we are only interested in the largest term so O(n^2).

48. Rotate Image (matrix 90 deg. in place)

You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise).
You have to rotate the image in-place, which means you have to modify the input 2D matrix directly. DO NOT allocate another 2D matrix and do the rotation.

Example 1:
Input: matrix = [[1,2,3],[4,5,6],[7,8,9]]
Output: [[7,4,1],[8,5,2],[9,6,3]] image.png

Example 2:
Input: matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
Output: [[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]] image-2.png

Constraints: * n == matrix.length == matrix[i].length * 1 <= n <= 20 * -1000 <= matrix[i][j] <= 1000

Elegant Solution: * Transpose (reverse around the main diagonal) * Reflect (reverse from left to right)

Python
# Each operation is O(M): time O(2M) -> O(M), space O(1), where M=num cells in matrix or n**2
class Solution(object):
    def rotate(self, matrix):
        self.transpose(matrix)
        self.reflect(matrix)
        
    def transpose(self, matrix):
        n = len(matrix)
        for i in range(n):
            for j in range(i+1, n):
                matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
                
    def reflect(self, matrix):
        n = len(matrix)
        for i in range(n):
            for j in range(n//2):
                matrix[i][j], matrix[i][-j-1] = matrix[i][-j-1], matrix[i][j]
                
matrix = [[1,2,3],[4,5,6],[7,8,9]]
s = Solution()
s.rotate(matrix)
print(matrix)
Output
[[7, 4, 1], [8, 5, 2], [9, 6, 3]]

Mathematical solution: YAK!

Python
# time O(M), space O(1), where M=num cells in matrix or n**2
from typing import List
class Solution:
    def rotate(self, matrix: List[List[int]]) -> None:
        n = len(matrix[0])
        for i in range(n // 2 + n % 2):
            for j in range(n // 2):
                tmp = matrix[n - 1 - j][i]
                matrix[n - 1 - j][i] = matrix[n - 1 - i][n - j - 1]
                matrix[n - 1 - i][n - j - 1] = matrix[j][n - 1 -i]
                matrix[j][n - 1 - i] = matrix[i][j]
                matrix[i][j] = tmp
                
matrix = [[1,2,3],[4,5,6],[7,8,9]]
s = Solution()
s.rotate(matrix)
print(matrix)
Output
[[7, 4, 1], [8, 5, 2], [9, 6, 3]]

66. Plus One

You are given a large integer represented as an integer array digits, where each digits[i] is the ith digit of the integer. The digits are ordered from most significant to least significant in left-to-right order. The large integer does not contain any leading 0's.
Increment the large integer by one and return the resulting array of digits.

Example 1:
Input: digits = [1,2,3] Output: [1,2,4] Explanation: The array represents the integer 123. Incrementing by one gives 123 + 1 = 124. Thus, the result should be [1,2,4].

Example 2:
Input: digits = [4,3,2,1] Output: [4,3,2,2] Explanation: The array represents the integer 4321. Incrementing by one gives 4321 + 1 = 4322. Thus, the result should be [4,3,2,2].

Example 3:
Input: digits = [9] Output: [1,0] Explanation: The array represents the integer 9. Incrementing by one gives 9 + 1 = 10. Thus, the result should be [1,0].

Constraints: * 1 <= digits.length <= 100 * 0 <= digits[i] <= 9 * digits does not contain any leading 0's.

Python
# time O(n), space O(1)
def plusOne(digits):

        n = len(digits)
        for i in range(n-1, -1, -1):
            if digits[i] == 9:
                digits[i] = 0
            else:
                digits[i] += 1
                return digits
            
        return [1] + digits
    
digits = [9, 9, 9]
plusOne(digits)
Output
[1, 0, 0, 0]
Python
# my similar solution
def plusOne2(arr):
    remainder = 1
    i = len(arr) - 1
    while remainder != 0 and i >=0:
        curr_sum = arr[i]+1
        if curr_sum < 10:
            arr[i] = curr_sum
            remainder = 0
        else:
            arr[i] = 0
            remainder = 1
        i -= 1
    if remainder:
        arr = [1] + arr          
    return arr

digits = [9,9,9,9,9]    # [1, 0, 0, 0]
plusOne2(digits)
Output
[1, 0, 0, 0, 0, 0]

681. Next Closest Time

Given a time represented in the format "HH:MM", form the next closest time by reusing the current digits. There is no limit on how many times a digit can be reused.
You may assume the given input string is always valid. For example, "01:34", "12:09" are all valid. "1:34", "12:9" are all invalid.

Example 1: Input: time = "19:34" Output: "19:39" Explanation: The next closest time choosing from digits 1, 9, 3, 4, is 19:39, which occurs 5 minutes later. It is not 19:33, because this occurs 23 hours and 59 minutes later.

Example 2: Input: time = "23:59" Output: "22:22" Explanation: The next closest time choosing from digits 2, 3, 5, 9, is 22:22. It may be assumed that the returned time is next day's time since it is smaller than the input time numerically.

Constraints: * time.length == 5 * time is a valid time in the form "HH:MM". * 0 <= HH < 24 * 0 <= MM < 60

Python
# My accepted solution (no Leetcode solution)
# time O(n), space - assigning several variables = O(n)
def nextClosestTime1(time):
    '''
        May be incomplete because range(hours+1, 24) doesn't cover everything?
        Or min_option take care of it?
    '''        
    hours, minutes = time.split(':')                                    # O(n) for split()
    options = set(hours + minutes)
    min_option = min(options)                                           # O(n)
    hours, minutes = int(hours), int(minutes)                           # O(n) for int(str)

    for i in range(minutes+1, 60):                                     # max 59 times
        if all(c in options for c in str(i)):                    # 59 * 2 digits * 4 * O(n) for str(int) = O(n)
            minute_part = str(i) if i > 9 else '0'+str(i)        # O(n) for str(i)
            return time[:3] + minute_part

    for i in range(hours+1, 24):
        if all(c in options for c in str(i)):
            hour_part = str(i) if i > 9 else '0'+str(i)
            minute_part = min_option + min_option
            return hour_part + ':' + minute_part

    return min_option + min_option + ':' + min_option + min_option
    
    
# time O(n), space - assigning several variables = O(n)
def nextClosestTime(time):
    '''
        Is this more complete?    
    '''    
    hour, minute = [int(i) for i in time.split(':')]            # O(n) for split()
        
    # special case - minutes within the same hour
    for j in range(minute+1, 60):                               # O(n)
        m = str(j)
        m = '0' + m if len(m) == 1 else m
        if all(c in time for c in list(m)):                     # O(len(m)*n) = O(n)
            return time[:3] + m
    
    # handle other cases - or min_option will suffice?
    found_hour = False
    for i in list(range(hour+1, 24)) + list(range(hour)):       # same as above O(n) estimates
        h = str(i)
        h = '0' + h if len(h) == 1 else h
        if all(c in time for c in list(h)):
            found_hour = True
            res = h + ':'
        if found_hour:
            for j in range(0, 60):
                m = str(j)
                m = '0' + m if len(m) == 1 else m
                if all(c in time for c in list(m)):
                    return res + m
            
    return None    

    
times = ["19:34", "23:59"]
for time in times:
    print(nextClosestTime(time))
Output
19:39
22:22
Python
# same idea from comments
def nextClosestTime(time):
    digits = [int(y) for x in time.split(':') for y in x]
    h, m = time.split(':')
    while True:
        h, m = (str(int(h)+1), '00') if int(m) == 59 else (h, str(int(m)+1))
        h = '00' if int(h) > 23 else h
        h = '0' + h if len(h) == 1 else h
        m = '0' + m if len(m) == 1 else m
        if all([int(x) in digits for x in h+m]):
            return h + ':' + m
        
times = ["19:34", "23:59"]
for time in times:
    print(nextClosestTime(time))
Output
19:39
22:22

163. Missing Ranges

You are given an inclusive range [lower, upper] and a sorted unique integer array nums, where all elements are in the inclusive range.
A number x is considered missing if x is in the range [lower, upper] and x is not in nums.
Return the smallest sorted list of ranges that cover every missing number exactly. That is, no element of nums is in any of the ranges, and each missing number is in one of the ranges.
Each range [a,b] in the list should be output as: * "a->b" if a != b * "a" if a == b

Example 1: Input: nums = [0,1,3,50,75], lower = 0, upper = 99 Output: ["2","4->49","51->74","76->99"] Explanation: The ranges are: [2,2] --> "2" [4,49] --> "4->49" [51,74] --> "51->74" [76,99] --> "76->99"

Example 2: Input: nums = [-1], lower = -1, upper = -1 Output: [] Explanation: There are no missing ranges since there are no missing numbers.

Constraints: * -109 <= lower <= upper <= 109 * 0 <= nums.length <= 100 * lower <= nums[i] <= upper * All the values of nums are unique.

Python
# My solution: time / space O(n)
def findMissingRanges(nums, lower, upper):
        
    if not nums:
        if lower == upper:
            return [str(upper)]
        else:
            return [str(lower) + '->' + str(upper)]
    elif lower == upper and lower == nums[0]:
        return []

    nums2 = list(range(lower, upper+1))
    for idx, i in enumerate(nums2):
        if i in nums:
            nums2[i] = ':'
    nums2 = ' '.join(map(str, nums2))
    nums2 = [ i.strip() for i in nums2.split(':') if i.strip() ]
    stack = []
    for i in nums2:
        i = i.strip().split()
        if len(i) == 1:
            stack.append(i[0])
        else:
            stack.append(i[0] + '->' + i[-1])    
    return stack


# edge cases on website include empty and one-element arrays, sometimes upper or lower overlap with single elem
nums = [0,1,3,50,75]
lower = 0
upper = 99
findMissingRanges(nums, lower, upper)
Output
['2', '4->49', '51->74', '76->99']
Python
# More elegant approach (Leetcode)
def findMissingRanges(nums, lower, upper):
        
        def formatRange(lower, upper):
            if lower == upper:
                return str(lower)
            return str(lower) + "->" + str(upper)

        result = []
        prev = lower - 1
        for i in range(len(nums) + 1):
            curr = nums[i] if i < len(nums) else upper + 1
            if prev + 1 <= curr - 1:
                result.append(formatRange(prev + 1, curr - 1))
            prev = curr
        return result
    
# edge cases on website include empty and one-element arrays, sometimes upper or lower overlap with single elem
nums = [0,1,3,50,75]
lower = 0
upper = 99
findMissingRanges(nums, lower, upper)
Output
['2', '4->49', '51->74', '76->99']

Find And Replace in String

You are given a 0-indexed string s that you must perform k replacement operations on. The replacement operations are given as three 0-indexed parallel arrays, indices, sources, and targets, all of length k.
To complete the ith replacement operation: * Check if the substring sources[i] occurs at index indices[i] in the original string s. * If it does not occur, do nothing. * Otherwise if it does occur, replace that substring with targets[i]. * For example, if s = "abcd", indices[i] = 0, sources[i] = "ab", and targets[i] = "eee", then the result of this replacement will be "eeecd".

All replacement operations must occur simultaneously, meaning the replacement operations should not affect the indexing of each other. The testcases will be generated such that the replacements will not overlap.

For example, a testcase with s = "abc", indices = [0, 1], and sources = ["ab","bc"] will not be generated because the "ab" and "bc" replacements overlap. Return the resulting string after performing all replacement operations on s.

A substring is a contiguous sequence of characters in a string.

Example 1: image.png Input: s = "abcd", indices = [0, 2], sources = ["a", "cd"], targets = ["eee", "ffff"] Output: "eeebffff" Explanation: "a" occurs at index 0 in s, so we replace it with "eee". "cd" occurs at index 2 in s, so we replace it with "ffff".

Example 2: image-2.png Input: s = "abcd", indices = [0, 2], sources = ["ab","ec"], targets = ["eee","ffff"] Output: "eeecd" Explanation: "ab" occurs at index 0 in s, so we replace it with "eee". "ec" does not occur at index 2 in s, so we do nothing.

Constraints: * 1 <= s.length <= 1000 * k == indices.length == sources.length == targets.length * 1 <= k <= 100 * 0 <= indexes[i] < s.length * 1 <= sources[i].length, targets[i].length <= 50 * s consists of only lowercase English letters. * sources[i] and targets[i] consist of only lowercase English letters.

Python
# My solution (accepted)
def findReplaceString(s, indices, sources, targets):

    # exclude targets that don't exist in string
    # collect ranges (start, end index) for remaining sources
    to_discard = []
    full_indices = []
    for i, start in enumerate(indices):
        end = start + len(sources[i])
        if not s[start:end] == sources[i]:
            to_discard.append(i)
        else:
            full_indices.append([start, end])                
    if len(to_discard) == len(indices):
        return s
    targets = [elem for idx, elem in enumerate(targets) if idx not in to_discard]

    # merge ranges and target for convenience and sort (indices are unsorted!)
    for i in range(len(full_indices)):
        full_indices[i].append(targets[i])
    full_indices.sort()

    # fill result w/slices of string or targets
    res = []
    if full_indices[0][0] != 0:
        res.append(s[ :full_indices[0][0] ])
    for i in range(1, len(full_indices)):
        res.append( full_indices[i-1][2] )
        res.append(s[ full_indices[i-1][1] : full_indices[i][0] ])
    res.append(full_indices[-1][2])
    if full_indices[-1][1] != len(s):
        res.append(s[ full_indices[-1][1] : ])

    return ''.join(res)

s = "abcd"
indices = [0, 2]
sources = ["a", "cd"]
targets = ["eee", "ffff"]
findReplaceString(s, indices, sources, targets)
Output
'eeebffff'
Python
# my more concise solution
# time and space c. O(n)
from typing import List

def findReplaceString2(s: str, indices: List[int], sources: List[str], targets: List[str]):
        
    # edge cases
    if not s:
        return s
    assert len(indices) == len(sources) == len(targets), 'Lengths must be the same'
    if not indices:
        return s
        
    # determine valid indices
    valid_idxs = [i for i in range(len(indices)) if sources[i] in s[ indices[i]: ]]
    
    # remove invalid elements
    indices = [ value for idx, value in enumerate(indices) if idx in valid_idxs ]
    sources = [ value for idx, value in enumerate(sources) if idx in valid_idxs ]
    targets = [ value for idx, value in enumerate(targets) if idx in valid_idxs ]
    
    # get index ranges for valid sources
    ranges = []
    for k in range(len(indices)):
        start = indices[k]
        end   = start + len( sources[k] )
        ranges.append([start, end])
          
    # final result - smart iteration over ranges
    res = []
    for idx, r in enumerate(ranges):
        res.append(targets[idx])
        if idx < len(ranges)-1:
            res.append(s[ r[1]:ranges[idx+1][0] ])
        elif idx == len(ranges)-1 and r[1] != len(s):
            res.append(s[ r[1]:len(s)])
    return ''.join(res)
Python
# shorter solution from comments (accepted)
def findReplaceString3(S, indexes, sources, targets):    
    for i, s, t in sorted(zip(indexes, sources, targets), reverse=True):
        S = S[:i] + t + S[i + len(s):] if S[i:i + len(s)] == s else S
    return S
Output
'eeebffff'
Python
s = "abcd"
indices = [0, 2]
sources = ["a", "cd"]
targets = ["eee", "ffff"]
print(findReplaceString(s, indices, sources, targets))    # 'eeebffff'

s = "abcd"
indices = [0, 2]
sources = ["a", "ec"]
targets = ["eee", "ffff"]
print(findReplaceString(s, indices, sources, targets))    # 'eeebcd'

849. Maximize Distance to Closest Person

You are given an array representing a row of seats where seats[i] = 1 represents a person sitting in the ith seat, and seats[i] = 0 represents that the ith seat is empty (0-indexed).
There is at least one empty seat, and at least one person sitting.
Alex wants to sit in the seat such that the distance between him and the closest person to him is maximized.
Return that maximum distance to the closest person.

Example 1: Input: seats = [1,0,0,0,1,0,1] Output: 2 Explanation: If Alex sits in the second open seat (i.e. seats[2]), then the closest person has distance 2. If Alex sits in any other open seat, the closest person has distance 1. Thus, the maximum distance to the closest person is 2.

Example 2: Input: seats = [1,0,0,0] Output: 3 Explanation: If Alex sits in the last seat (i.e. seats[3]), the closest person is 3 seats away. This is the maximum distance possible, so the answer is 3.

Example 3: Input: seats = [0,1] Output: 1

Constraints: * 2 <= seats.length <= 2 * 104 * seats[i] is 0 or 1. * At least one seat is empty. * At least one seat is occupied.

Python
# My solution
import math
from typing import List


def maxDistToClosest(seats: List[int]) -> int:
    max_dist = 0
    persons  = [idx for idx, num in enumerate(seats) if num==1]
    res      = [ math.ceil(abs(persons[i] - persons[i-1])/2) for i in range(1,len(persons)) ]
    if persons[0] != 0:
        res.append(persons[0])        
    if persons[-1] != len(seats)-1:
        res.append(abs(len(seats)-1 - persons[-1]))
    return max(res)


seats_all = [ [1,0,0,0,1,0,1], [1,0,0,0], [0,1] ]
for seats in seats_all:
    print(maxDistToClosest(seats))
Output
2
3
1

Leetcode: Two Pointer

As we iterate through seats, we'll update the closest person sitting to our left, and closest person sitting to our right.
Algorithm: Keep track of prev, the filled seat at or to the left of i, and future, the filled seat at or to the right of i. Then at seat i, the closest person is min(i - prev, future - i), with one exception. i - prev should be considered infinite if there is no person to the left of seat i, and similarly future - i is infinite if there is no one to the right of seat i

Python
def maxDistToClosest(seats):
    people = (i for i, seat in enumerate(seats) if seat)
    prev, future = None, next(people)

    ans = 0
    for i, seat in enumerate(seats):
        if seat:
            prev = i
        else:
            while future is not None and future < i:
                future = next(people, None)

            left = float('inf') if prev is None else i - prev
            right = float('inf') if future is None else future - i
            ans = max(ans, min(left, right))

    return ans

seats_all = [ [1,0,0,0,1,0,1], [1,0,0,0], [0,1] ]
for seats in seats_all:
    print(maxDistToClosest(seats))
Output
2
3
1

20. Valid Parentheses

See the notebook for stacks and queues

23. Merge k Sorted Linked Lists

You are given an array of k linked-lists lists, each linked-list is sorted in ascending order. Merge all the linked-lists into one sorted linked-list and return it.

Example 1:

Input: lists = [[1,4,5],[1,3,4],[2,6]] Output: [1,1,2,3,4,4,5,6] Explanation: The linked-lists are: [ 1->4->5, 1->3->4, 2->6 ] merging them into one sorted list: 1->1->2->3->4->4->5->6 Example 2:

Input: lists = [] Output: [] Example 3:

Input: lists = [[]] Output: []

Constraints: * k == lists.length * 0 <= k <= 10^4 * 0 <= lists[i].length <= 500 * -10^4 <= lists[i][j] <= 10^4 * lists[i] is sorted in ascending order. * The sum of lists[i].length will not exceed 10^4.

Python
# time c. = O(Nlogk), where k = # linked lists. Space c. = O(1)
class Node:
    def __init__(self, data):
        self.val  = data
        self.next = None


# time c. O(Nlogk), k = # lists. Space c. O(1)
def mergeKLists(lists):
    n = len(lists)
    interval = 1
    while interval < n:
        for i in range( 0, n-interval,
                        interval*2 ):
            lists[i] = merge2Lists( lists[i],
                                    lists[i+interval] )
        interval *= 2
    return lists[0] if n > 0 else None

def merge2Lists(l1, l2):
    head = point = Node(0)
    while l1 and l2:
        if l1.val <= l2.val:
            point.next = l1
            l1 = l1.next
        else:
            point.next = l2
            l2 = l1
            l1 = point.next.next
        point = point.next
    if not l1: point.next=l2
    else: point.next=l1
    return head.next


def printList(node):
    while node != None:
        print(node.val, end=" ")
        node = node.next
    print()
Python
N = 3
a = [None] * N
# Linkedlist1
head1 = Node(1)
a[0] = head1
head1.next = Node(3)
head1.next.next = Node(5)
head1.next.next.next = Node(7)
# Limkedlist2
head2 = Node(2)
a[1] = head2
head2.next = Node(4)
head2.next.next = Node(6)
head2.next.next.next = Node(8)
# Linkedlist3
head3 = Node(0)
a[2] = head3
head3.next = Node(9)
head3.next.next = Node(10)
head3.next.next.next = Node(11)
res = mergeKLists(a)
if res != None:
    printList(res)
Output
0 1 2 3 4 5 6 7 8 9 10 11
Python
n = 11
interval = 1
while interval < n:
    for i in range(0, n - interval, interval * 2):
        print(i, interval)
    interval *= 2
Output
0 1
2 1
4 1
6 1
8 1
0 2
4 2
8 2
0 4
0 8

Merge k Sorted Arrays

Python
# time c. O(kN*Logk) since
# using heap (N*Logk) k times;
# space c. O(N) - output array
from heapq import merge
def mergeK(arr, k):
    l = arr[0]
    for i in range(k-1):
        l = list(merge(l, arr[i + 1]))
    return l


arr =[ [2, 6, 12 ], 
       [ 1, 9 ],
       [23, 34, 90, 2000 ], ]
l = mergeK(arr, len(arr))
printArray(l)

42. Trapping Rain Water

Given n non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it can trap after raining.

Example 1:

image.png

Input: height = [0,1,0,2,1,0,1,3,2,1,2,1] Output: 6 Explanation: The above elevation map (black section) is represented by array [0,1,0,2,1,0,1,3,2,1,2,1]. In this case, 6 units of rain water (blue section) are being trapped.

Example 2: Input: height = [4,2,0,3,2,5] Output: 9

Constraints:

n == height.length 1 <= n <= 2 * 10^4 0 <= height[i] <= 10^5

Algorithm

Use stack to store the indices of the bars. Iterate the array: While stack is not empty and \text{height[current]}>\text{height[st.top()]}height[current]>height[st.top()] It means that the stack element can be popped. Pop the top element as \text{top}top. Find the distance between the current element and the element at top of stack, which is to be filled. \text{distance} = \text{current} - \text{st.top}() - 1distance=current−st.top()−1 Find the bounded height \text{bounded_height} = \min(\text{height[current]}, \text{height[st.top()]}) - \text{height[top]}bounded_height=min(height[current],height[st.top()])−height[top] Add resulting trapped water to answer \text{ans} \mathrel{+}= \text{distance} \times \text{bounded_height}ans+=distance×bounded_height Push current index to top of the stack Move \text{current}current to the next position

Python
# time = space = O(n) BETTER TO UNDERSTAND
def max_water(height):
    res = 0
    n = len(height)
    left = [0]*n                                   # left[i] = height tallest bar to left of i'th bar incl. self
    right = [0]*n                                  # right[i] = height tallest bar to right of ith bar incl. self
        
    left[0] = height[0]                                                # pre-fill left arr
    for i in range(1, n):
        left[i] = max(left[i-1], height[i])
      
    right[n-1] = height[n-1]                                           # pre-fill right arr
    for i in range(n-2, -1, -1):
        right[i] = max(right[i+1], height[i])    
    
    for i in range(n):                                                 # amount of water accumulated on i'th bar
        res += min(left[i], right[i]) - height[i]
  
    return res
Python
# time = space = O(n)
def max_water2(height):
    stack = []                                                                      # to store indices    
    n = len(height)
    ans = 0  
    for i in range(n):        
        while stack and height[ stack[-1] ] < height[i]:                # remove bars until condition holds    
            pop_height = height[stack.pop()]                                        # store height of top bar
            if(len(stack) == 0):                                      # If no bars or popped bar has no left boundary
                break            
            distance = i - stack[-1] - 1                              # dist. betw. left & right bounds of popped bar
            min_height = min(height[stack[-1]], height[i])-pop_height               # calculate min height  
            ans += distance * min_height  
        
        stack.append(i)                                  # If stack empty or height of current bar <= stack's top bar  
    return ans
Python
numss = [ [0,1,0,2,1,0,1,3,2,1,2,1], [4,2,0,3,2,5] ]
for nums in numss:
    print( max_water(nums) )
Output
6
9

215. Kth Largest Element in an Array

Given an integer array nums and an integer k, return the kth largest element in the array. Note that it is the kth largest element in the sorted order, not the kth distinct element. You must solve it in O(n) time complexity.

Example 1:

Input: nums = [3,2,1,5,6,4], k = 2 Output: 5 Example 2:

Input: nums = [3,2,3,1,2,4,5,5,6], k = 4 Output: 4

Constraints:

1 <= k <= nums.length <= 10^5 -104 <= nums[i] <= 10^4

Algorithm

Choose a random pivot.

Use a partition algorithm to place the pivot into its perfect position pos in the sorted array, move smaller elements to the left of pivot, and larger or equal ones - to the right.

Compare pos and N - k to choose the side of array to proceed recursively.

! Please notice that this algorithm works well even for arrays with duplicates.

Python
# time = O(n), space = O(1)
def findKthLargest(nums, k):
    def partition(left, right, pivot_idx):
        pivot = nums[pivot_idx]
        # 1. move pivot to end
        nums[pivot_idx], nums[right] = nums[right], nums[pivot_idx]
        # 2. move all smaller elements to the left
        store_idx = left
        for i in range(left, right):
            if nums[i] < pivot:
                nums[store_idx], nums[i] = nums[i], nums[store_idx]
                store_idx += 1
        # 3. move pivot to its final place
        nums[right], nums[store_idx] = nums[store_idx], nums[right]
        return store_idx

    def select_rec(left, right, k_smallest):
        if left == right:    # base case - 1 elem
            return nums[left]
        pivot_idx = random.randint(left, right)
        # find pivot pos in sorted list
        pivot_idx = partition(left, right, pivot_idx)
        # if pivot in final sorted position
        if k_smallest == pivot_idx:
             return nums[k_smallest]        
        elif k_smallest < pivot_idx:    # go left
            return select_rec(left, pivot_idx-1, k_smallest)        
        else:                           # go right
            return select_rec(pivot_idx+1, right, k_smallest)
    # kth largest = (n - k)th smallest 
    return select_rec(0, len(nums)-1, len(nums)-k)

253. Meeting Rooms II

Given an array of meeting time intervals intervals where intervals[i] = [starti, endi], return the minimum number of conference rooms required.

Example 1:

Input: intervals = [[0,30],[5,10],[15,20]] Output: 2 Example 2:

Input: intervals = [[7,10],[2,4]] Output: 1

Constraints:

1 <= intervals.length <= 10^4 0 <= starti < endi <= 10^6

Algorithm

Python
# time = O(NlogN), space = O(N)
class Solution:
    def minMeetingRooms(self, intervals: List[List[int]]) -> int:
        
        # If there are no meetings, we don't need any rooms.
        if not intervals:
            return 0

        used_rooms = 0

        # Separate out the start and the end timings and sort them individually.
        start_timings = sorted([i[0] for i in intervals])
        end_timings = sorted(i[1] for i in intervals)
        L = len(intervals)

        # The two pointers in the algorithm: e_ptr and s_ptr.
        end_pointer = 0
        start_pointer = 0

        # Until all the meetings have been processed
        while start_pointer < L:
            # If there is a meeting that has ended by the time the meeting at `start_pointer` starts
            if start_timings[start_pointer] >= end_timings[end_pointer]:
                # Free up a room and increment the end_pointer.
                used_rooms -= 1
                end_pointer += 1

            # We do this irrespective of whether a room frees up or not.
            # If a room got free, then this used_rooms += 1 wouldn't have any effect. used_rooms would
            # remain the same in that case. If no room was free, then this would increase used_rooms
            used_rooms += 1    
            start_pointer += 1   

        return used_rooms

857. Minimum Cost to Hire K Workers

Solution There are n workers. You are given two integer arrays quality and wage where quality[i] is the quality of the ith worker and wage[i] is the minimum wage expectation for the ith worker.

We want to hire exactly k workers to form a paid group. To hire a group of k workers, we must pay them according to the following rules:

Every worker in the paid group should be paid in the ratio of their quality compared to other workers in the paid group. Every worker in the paid group must be paid at least their minimum wage expectation. Given the integer k, return the least amount of money needed to form a paid group satisfying the above conditions. Answers within 10-5 of the actual answer will be accepted.

Example 1:

Input: quality = [10,20,5], wage = [70,50,30], k = 2 Output: 105.00000 Explanation: We pay 70 to 0th worker and 35 to 2nd worker. Example 2:

Input: quality = [3,1,10,10,1], wage = [4,8,2,2,7], k = 3 Output: 30.66667 Explanation: We pay 4 to 0th worker, 13.33333 to 2nd and 3rd workers separately.

Constraints:

n == quality.length == wage.length 1 <= k <= n <= 104 1 <= quality[i], wage[i] <= 104

Algorithm

Maintain a max heap of quality. (We're using a minheap, with negative values.) We'll also maintain sumq, the sum of this heap.

For each worker in order of ratio, we know all currently considered workers have lower ratio. (This worker will be the 'captain', as described in Approach #1.) We calculate the candidate answer as this ratio times the sum of the smallest K workers in quality.

Python
# time = O(NlogN), space = O(N)
class Solution(object):
    def mincostToHireWorkers(self, quality, wage, K):
        from fractions import Fraction
        workers = sorted((Fraction(w, q), q, w)
                         for q, w in zip(quality, wage))

        ans = float('inf')
        pool = []
        sumq = 0
        for ratio, q, w in workers:
            heapq.heappush(pool, -q)
            sumq += q

            if len(pool) > K:
                sumq += heapq.heappop(pool)

            if len(pool) == K:
                ans = min(ans, ratio * sumq)

        return float(ans)

973. K Closest Points to Origin

Given an array of points where points[i] = [xi, yi] represents a point on the X-Y plane and an integer k, return the k closest points to the origin (0, 0). The distance between two points on the X-Y plane is the Euclidean distance (i.e., √(x1 - x2)2 + (y1 - y2)2). You may return the answer in any order. The answer is guaranteed to be unique (except for the order that it is in).

Example 1: image.png Input: points = [[1,3],[-2,2]], k = 1 Output: [[-2,2]] Explanation: The distance between (1, 3) and the origin is sqrt(10). The distance between (-2, 2) and the origin is sqrt(8). Since sqrt(8) < sqrt(10), (-2, 2) is closer to the origin. We only want the closest k = 1 points from the origin, so the answer is just [[-2,2]].

Example 2: Input: points = [[3,3],[5,-1],[-2,4]], k = 2 Output: [[3,3],[-2,4]] Explanation: The answer [[-2,4],[3,3]] would also be accepted.

Constraints: 1 <= k <= points.length <= 10^4 -104 < xi, yi < 10^4

Algorithm

Let's do the work(i, j, K) of partially sorting the subarray (points[i], points[i+1], ..., points[j]) so that the smallest K elements of this subarray occur in the first K positions (i, i+1, ..., i+K-1).

First, we quickselect by a random pivot element from the subarray. To do this in place, we have two pointers i and j, and move these pointers to the elements that are in the wrong bucket -- then, we swap these elements.

After, we have two buckets [oi, i] and [i+1, oj], where (oi, oj) are the original (i, j) values when calling work(i, j, K). Say the first bucket has 10 items and the second bucket has 15 items. If we were trying to partially sort say, K = 5 items, then we only need to partially sort the first bucket: work(oi, i, 5). Otherwise, if we were trying to partially sort say, K = 17 items, then the first 10 items are already partially sorted, and we only need to partially sort the next 7 items: work(i+1, oj, 7).

Python
# time = space = O(N)
class Solution(object):
    def kClosest(self, points, K):
        dist = lambda i: points[i][0]**2 + points[i][1]**2

        def sort(i, j, K):
            # Partially sorts A[i:j+1] so the first K elements are
            # the smallest K elements.
            if i >= j: return

            # Put random element as A[i] - this is the pivot
            k = random.randint(i, j)
            points[i], points[k] = points[k], points[i]

            mid = partition(i, j)
            if K < mid - i + 1:
                sort(i, mid - 1, K)
            elif K > mid - i + 1:
                sort(mid + 1, j, K - (mid - i + 1))

        def partition(i, j):
            # Partition by pivot A[i], returning an index mid
            # such that A[i] <= A[mid] <= A[j] for i < mid < j.
            oi = i
            pivot = dist(i)
            i += 1

            while True:
                while i < j and dist(i) < pivot:
                    i += 1
                while i <= j and dist(j) >= pivot:
                    j -= 1
                if i >= j: break
                points[i], points[j] = points[j], points[i]

            points[oi], points[j] = points[j], points[oi]
            return j

        sort(0, len(points) - 1, K)
        return points[:K]

Part II. Linked Lists

Three problems conicided with Facebook (see beginning of this notebook)

19. Remove Nth Node From End of List

Given the head of a linked list, remove the nth node from the end of the list and return its head.

Example 1: image.png Input: head = [1,2,3,4,5], n = 2 Output: [1,2,3,5]

Example 2: Input: head = [1], n = 1 Output: []

Example 3: Input: head = [1,2], n = 1 Output: [1]

Constraints:

The number of nodes in the list is sz. 1 <= sz <= 30 0 <= Node.val <= 100 1 <= n <= sz

Follow up: Could you do this in one pass?

Algorithm

We could use two pointers. The first pointer advances the list by n+1n+1 steps from the beginning, while the second pointer starts from the beginning of the list. Now, both pointers are exactly separated by nn nodes apart. We maintain this constant gap by advancing both pointers together until the first pointer arrives past the last node. The second pointer will be pointing at the nnth node counting from the last. We relink the next pointer of the node referenced by the second pointer to point to the node's next next node

Python
# time = O(n), space =O(1)
def removeNthFromEnd(head: Node, n: int) -> Node:
    curr = head
    for i in range(n+1):
        if not curr:
            print('List shorter than', n)
        curr = curr.next
    curr2 = head
    while curr:
        curr = curr.next
        curr2 = curr2.next
    to_delete = curr2.next
    curr2.next = curr2.next.next
    to_delete.next = None
    return head

Part III. Trees and Graphs

127. Word Ladder

A transformation sequence from word beginWord to word endWord using a dictionary wordList is a sequence of words beginWord -> s1 -> s2 -> ... -> sk such that:

Every adjacent pair of words differs by a single letter. Every si for 1 <= i <= k is in wordList. Note that beginWord does not need to be in wordList. sk == endWord Given two words, beginWord and endWord, and a dictionary wordList, return the number of words in the shortest transformation sequence from beginWord to endWord, or 0 if no such sequence exists.

Example 1: Input: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"] Output: 5 Explanation: One shortest transformation sequence is "hit" -> "hot" -> "dot" -> "dog" -> cog", which is 5 words long.

Example 2: Input: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"] Output: 0 Explanation: The endWord "cog" is not in wordList, therefore there is no valid transformation sequence.

Constraints:

1 <= beginWord.length <= 10 endWord.length == beginWord.length 1 <= wordList.length <= 5000 wordList[i].length == beginWord.length beginWord, endWord, and wordList[i] consist of lowercase English letters. beginWord != endWord All the words in wordList are unique.

Algorithm - Bidirectional Breadth First Search

Intuition

The graph formed from the nodes in the dictionary might be too big. The search space considered by the breadth first search algorithm depends upon the branching factor of the nodes at each level. If the branching factor remains the same for all the nodes, the search space increases exponentially along with the number of levels. Consider a simple example of a binary tree. With each passing level in a complete binary tree, the number of nodes increase in powers of 2.

We can considerably cut down the search space of the standard breadth first search algorithm if we launch two simultaneous BFS. One from the beginWord and one from the endWord. We progress one node at a time from both sides and at any point in time if we find a common node in both the searches, we stop the search. This is known as bidirectional BFS and it considerably cuts down on the search space and hence reduces the time and space complexity.

Steps:

The algorithm is very similar to the standard BFS based approach we saw earlier.

The only difference is we now do BFS starting two nodes instead of one. This also changes the termination condition of our search.

We now have two visited dictionaries to keep track of nodes visited from the search starting at the respective ends.

If we ever find a node/word which is in the visited dictionary of the parallel search we terminate our search, since we have found the meet point of this bidirectional search. It's more like meeting in the middle instead of going all the way through.

Termination condition for bidirectional search is finding a word which is already been seen by the parallel search.

The shortest transformation sequence is the sum of levels of the meet point node from both the ends. Thus, for every visited node we save its level as value in the visited dictionary.

Python
# time c. O(n); posted by one of the users in Solutions
from collections import deque

def ladderLength(beginWord, endWord, wordList):
    
    if endWord not in wordList or not endWord or not beginWord or not wordList:
        return 0

    def construct_dict(word_list):
        d = {}
        for word in word_list:
            for i in range(len(word)):
                s = word[:i] + "_" + word[i+1:]
                d[s] = d.get(s, []) + [word]
        return d

    def bfs_words(begin, end, dict_words):
        queue, visited = deque([(begin, 1)]), set()
        while queue:
            word, steps = queue.popleft()
            if word not in visited:
                visited.add(word)
                if word == end:
                    return steps
                for i in range(len(word)):
                    s = word[:i] + "_" + word[i+1:]
                    neigh_words = dict_words.get(s, [])
                    for neigh in neigh_words:
                        if neigh not in visited:
                            queue.append((neigh, steps + 1))
        return 0

    d = construct_dict( set(wordList) | set([beginWord]))        # beginWord may not be in the wordList
    return bfs_words(beginWord, endWord, d)
Python
# official? looks complicated - the above solution from users' Solutions looks prettier
# time = space = O(M×N), where M = length of words, N = total # words in input
from collections import defaultdict
class Solution(object):
    def __init__(self):
        self.length = 0
        # Dictionary to hold combination of words that can be formed,
        # from any given word. By changing one letter at a time.
        self.all_combo_dict = defaultdict(list)

    def visitWordNode(self, queue, visited, others_visited):
        queue_size = len(queue)
        for _ in range(queue_size):
            current_word = queue.popleft()
            for i in range(self.length):
                # Intermediate words for current word
                intermediate_word = current_word[:i] + "*" + current_word[i+1:]

                # Next states are all the words which share the same intermediate state.
                for word in self.all_combo_dict[intermediate_word]:
                    # If the intermediate state/word has already been visited from the
                    # other parallel traversal this means we have found the answer.
                    if word in others_visited:
                        return visited[current_word] + others_visited[word]
                    if word not in visited:
                        # Save the level as the value of the dictionary, to save number of hops.
                        visited[word] = visited[current_word] + 1
                        queue.append(word)
                        
        return None

    def ladderLength(self, beginWord, endWord, wordList):
        """
        :type beginWord: str
        :type endWord: str
        :type wordList: List[str]
        :rtype: int
        """
        if endWord not in wordList or not endWord or not beginWord or not wordList:
            return 0

        # Since all words are of same length.
        self.length = len(beginWord)

        for word in wordList:
            for i in range(self.length):
                # Key is the generic word
                # Value is a list of words which have the same intermediate generic word.
                self.all_combo_dict[word[:i] + "*" + word[i+1:]].append(word)

        # Queues for birdirectional BFS
        queue_begin = collections.deque([beginWord]) # BFS starting from beginWord
        queue_end = collections.deque([endWord]) # BFS starting from endWord

        # Visited to make sure we don't repeat processing same word
        visited_begin = {beginWord : 1}
        visited_end = {endWord : 1}
        ans = None

        # We do a birdirectional search starting one pointer from begin
        # word and one pointer from end word. Hopping one by one.
        while queue_begin and queue_end:
            
            # Progress forward one step from the shorter queue
            if len(queue_begin) <= len(queue_end):
                ans = self.visitWordNode(queue_begin, visited_begin, visited_end)
            else:
                ans = self.visitWordNode(queue_end, visited_end, visited_begin)
            if ans:
                return ans

        return 0
Python
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log","cog"]
print( ladderLength(beginWord, endWord, wordList) )

beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log"]
print( ladderLength(beginWord, endWord, wordList) )
Output
5
0
Python
import collections

s = Solution()
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log","cog"]
print( s.ladderLength(beginWord, endWord, wordList) )

beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log"]
print( s.ladderLength(beginWord, endWord, wordList) )
Output
5
0

210. Course Schedule II

There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai.

For example, the pair [0, 1], indicates that to take course 0 you have to first take course 1. Return the ordering of courses you should take to finish all courses. If there are many valid answers, return any of them. If it is impossible to finish all courses, return an empty array.

Example 1: Input: numCourses = 2, prerequisites = [[1,0]] Output: [0,1] Explanation: There are a total of 2 courses to take. To take course 1 you should have finished course 0. So the correct course order is [0,1].

Example 2: Input: numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]] Output: [0,2,1,3] Explanation: There are a total of 4 courses to take. To take course 3 you should have finished both courses 1 and 2. Both courses 1 and 2 should be taken after you finished course 0. So one correct course order is [0,1,2,3]. Another correct ordering is [0,2,1,3].

Example 3: Input: numCourses = 1, prerequisites = [] Output: [0]

Constraints:

1 <= numCourses <= 2000 0 <= prerequisites.length <= numCourses * (numCourses - 1) prerequisites[i].length == 2 0 <= ai, bi < numCourses ai != bi All the pairs [ai, bi] are distinct.

Algorithm (using nodes' n degrees because DFS, although it has the same time complexity, is logically much more complex to understand)

Initialize a queue, Q to keep a track of all the nodes in the graph with 0 in-degree. Iterate over all the edges in the input and create an adjacency list and also a map of node v/s in-degree. Add all the nodes with 0 in-degree to Q. The following steps are to be done until the Q becomes empty. Pop a node from the Q. Let's call this node, N. For all the neighbors of this node, N, reduce their in-degree by 1. If any of the nodes' in-degree reaches 0, add it to the Q. Add the node N to the list maintaining topologically sorted order. Continue from step 4.1.

Python
# time = space = O(V+E)
from collections import defaultdict, deque
class Solution:

    def findOrder(self, numCourses, prerequisites):
        """
        :type numCourses: int
        :type prerequisites: List[List[int]]
        :rtype: List[int]
        """

        # Prepare the graph
        adj_list = defaultdict(list)
        indegree = {}
        for dest, src in prerequisites:
            adj_list[src].append(dest)

            # Record each node's in-degree
            indegree[dest] = indegree.get(dest, 0) + 1

        # Queue for maintainig list of nodes that have 0 in-degree
        zero_indegree_queue = deque([k for k in range(numCourses) if k not in indegree])

        topological_sorted_order = []

        # Until there are nodes in the Q
        while zero_indegree_queue:

            # Pop one node with 0 in-degree
            vertex = zero_indegree_queue.popleft()
            topological_sorted_order.append(vertex)

            # Reduce in-degree for all the neighbors
            if vertex in adj_list:
                for neighbor in adj_list[vertex]:
                    indegree[neighbor] -= 1

                    # Add neighbor to Q if in-degree becomes 0
                    if indegree[neighbor] == 0:
                        zero_indegree_queue.append(neighbor)

        return topological_sorted_order if len(topological_sorted_order) == numCourses else []
Python
numCourses = 4
prerequisites = [[1,0],[2,0],[3,1],[3,2]]
print(findOrder(numCourses, prerequisites))

222. Count Complete Tree Nodes

Given the root of a complete binary tree, return the number of the nodes in the tree. According to Wikipedia, every level, except possibly the last, is completely filled in a complete binary tree, and all nodes in the last level are as far left as possible. It can have between 1 and 2h nodes inclusive at the last level h. Design an algorithm that runs in less than O(n) time complexity.

Example 1: image.png Input: root = [1,2,3,4,5,6] Output: 6

Example 2: Input: root = [] Output: 0

Example 3: Input: root = [1] Output: 1

Constraints:

The number of nodes in the tree is in the range [0, 5 * 104]. 0 <= Node.val <= 5 * 104 The tree is guaranteed to be complete.

Algorithm (Binary Search)

Return 0 if the tree is empty.

Compute the tree depth d.

Return 1 if d == 0.

The number of nodes in all levels but the last one is 2^d - 1. The number of nodes in the last level could vary from 1 to 2^d. Enumerate potential nodes from 0 to 2^d - 1 and perform the binary search by the node index to check how many nodes are in the last level. Use the function exists(idx, d, root) to check if the node with index idx exists.

Use binary search to implement exists(idx, d, root) as well.

Return 2^d - 1 + the number of nodes in the last level.

Python
# time = O(n), space = O(d)=O(logN) to keep the recursion stack, where d is a tree depth
class Solution:
    def countNodes(self, root: TreeNode) -> int:
        return 1 + self.countNodes(root.right) + self.countNodes(root.left) if root else 0
Python
# time = O(d^2)=O((logN)^2), where d is a tree depth, Space = O(1)
class Solution:
    def compute_depth(self, node: TreeNode) -> int:
        """
        Return tree depth in O(d) time.
        """
        d = 0
        while node.left:
            node = node.left
            d += 1
        return d

    def exists(self, idx: int, d: int, node: TreeNode) -> bool:
        """
        Last level nodes are enumerated from 0 to 2**d - 1 (left -> right).
        Return True if last level node idx exists. 
        Binary search with O(d) complexity.
        """
        left, right = 0, 2**d - 1
        for _ in range(d):
            pivot = left + (right - left) // 2
            if idx <= pivot:
                node = node.left
                right = pivot
            else:
                node = node.right
                left = pivot + 1
        return node is not None
        
    def countNodes(self, root: TreeNode) -> int:
        # if the tree is empty
        if not root:
            return 0
        
        d = self.compute_depth(root)
        # if the tree contains 1 node
        if d == 0:
            return 1
        
        # Last level nodes are enumerated from 0 to 2**d - 1 (left -> right).
        # Perform binary search to check how many nodes exist.
        left, right = 1, 2**d - 1
        while left <= right:
            pivot = left + (right - left) // 2
            if self.exists(pivot, d, root):
                left = pivot + 1
            else:
                right = pivot - 1
        
        # The tree contains 2**d - 1 nodes on the first (d - 1) levels
        # and left nodes on the last level.
        return (2**d - 1) + left

1110. Delete Nodes And Return Forest

Given the root of a binary tree, each node in the tree has a distinct value. After deleting all nodes with a value in to_delete list, we are left with a forest (a disjoint union of trees). Return the roots of the trees in the remaining forest. You may return the result in any order.

Example 1:
Input: root = [1,2,3,4,5,6,7], to_delete = [3,5]
Output: [[1,2,null,4],[6],[7]] image.png

Example 2:
Input: root = [1,2,4,null,3], to_delete = [3]
Output: [[1,2,4]]

Constraints: * The number of nodes in the given tree is at most 1000. * Each node has a distinct value between 1 and 1000. * to_delete.length <= 1000 * to_delete contains distinct values between 1 and 1000.

Solution from Leetcode comments for TreeNode, but there is None for the array representation (asked at my mock interview)

Python
from typing import List
class Node:
    def __init__(self, val: int) -> None:
        self.val = val
        self.left = None
        self.right = None

def delNodes(root: Node, to_delete: List[int]) -> List[Node]:       
    res,to_delete = [],set(to_delete)
    def helper(root):
        ''' Return root if root is not deleted,
            or Null if that subtree was detached '''
        if root:
            # next line executes after all recursive calls on the way up
            root.left,root.right = helper(root.left), helper(root.right)
            if root.val not in to_delete:
                return root
            res.append(root.left)                   # if root is deleted
            res.append(root.right)                  # if root is deleted
    res.append(helper(root))            
    return([ a for a in res if a ])
Python
def print_tree(tree: List[int]) -> None:
    if not tree:
        return
    print(tree.val, end=' ')
    print_tree(tree.left)
    print_tree(tree.right)
Python
root = Node(1)
root.left = Node(2)
root.left.left = Node(4)
root.left.right = Node(5)
root.right = Node(3)
root.right.left = Node(6)
root.right.right = Node(7)

to_delete = [3,5]
forest = delNodes(root, to_delete)
for tree in forest:
    print_tree(tree)
    print()
Output
6 
7 
1 2 4

329. Longest Increasing Path in a Matrix

Given an m x n integers matrix, return the length of the longest increasing path in matrix.From each cell, you can either move in four directions: left, right, up, or down. You may not move diagonally or move outside the boundary (i.e., wrap-around is not allowed).

Example 1: image.png Input: matrix = [[9,9,4],[6,6,8],[2,1,1]] Output: 4 Explanation: The longest increasing path is [1, 2, 6, 9].

Example 2: image-2.png Input: matrix = [[3,4,5],[3,2,6],[2,2,1]] Output: 4 Explanation: The longest increasing path is [3, 4, 5, 6]. Moving diagonally is not allowed.

Example 3: Input: matrix = [[1]] Output: 1

Constraints:

m == matrix.length n == matrix[i].length 1 <= m, n <= 200 0 <= matrix[i][j] <= 231 - 1

Python
# time and space = O(mn)
def longestIncreasingPath(matrix):
    def dfs(i, j):
        if not dp[i][j]:
            val = matrix[i][j]
            dp[i][j] = 1 + max(
                dfs(i - 1, j) if i and val > matrix[i - 1][j] else 0,
                dfs(i + 1, j) if i < M - 1 and val > matrix[i + 1][j] else 0,
                dfs(i, j - 1) if j and val > matrix[i][j - 1] else 0,
                dfs(i, j + 1) if j < N - 1 and val > matrix[i][j + 1] else 0)
        return dp[i][j]
    if not matrix or not matrix[0]: return 0
    M, N = len(matrix), len(matrix[0])
    dp = [[0] * N for i in range(M)]
    return max(dfs(x, y) for x in range(M) for y in range(N))
Python
matrix = [[9,9,4],[6,6,8],[2,1,1]]
print( longestIncreasingPath(matrix) )        # [1, 2, 6, 9]

matrix = [[3,4,5],[3,2,6],[2,2,1]]
print( longestIncreasingPath(matrix) )        # [3, 4, 5, 6]

matrix = [[1]]
print( longestIncreasingPath(matrix) )
Output
4
4
1

394. Decode String

Given an encoded string, return its decoded string. The encoding rule is: k[encoded_string], where the encoded_string inside the square brackets is being repeated exactly k times. Note that k is guaranteed to be a positive integer.

You may assume that the input string is always valid; there are no extra white spaces, square brackets are well-formed, etc. Furthermore, you may assume that the original data does not contain any digits and that digits are only for those repeat numbers, k. For example, there will not be input like 3a or 2[4].

The test cases are generated so that the length of the output will never exceed 10^5.

Example 1: Input: s = "3[a]2[bc]" Output: "aaabcbc"

Example 2: Input: s = "3[a2[c]]" Output: "accaccacc"

Example 3: Input: s = "2[abc]3[cd]ef" Output: "abcabccdcdcdef"

Constraints:

1 <= s.length <= 30 s consists of lowercase English letters, digits, and square brackets '[]'. s is guaranteed to be a valid input. All the integers in s are in the range [1, 300].

Iterative Algorithm
More intuitive

Recursive Algorithm
Build result while next character is letter (a-z) and build the number k while next character is a digit (0-9) by iterating over string s. Ignore the next [ character and recursively find the nested decodedString. Decode the current pattern k[decodedString] and append it to the result. Return the current result. The above steps are repeated recursively for each pattern until the entire string s is traversed.

Base Condition: We must define a base condition that must be satisfied to backtrack from the recursive call. In this case, we would backtrack and return the result when we have traversed the string s or the next character is ] and there is no nested substring.

Python
# time = space = O(n)
def decode_string(s: str) -> str:
    num = 0
    string = ''
    stack = []
    for c in s:
        if c.isdigit():
            num = num*10 + int(c)
        elif c == "[":
            stack.append(string)
            stack.append(num)
            string = ''
            num = 0
        elif c.isalpha():
            string += c
        elif c == ']':
            pre_num = stack.pop()
            pre_string = stack.pop()
            string = pre_string + pre_num * string
    return string
Python
# RECURSIVE - for educational purposes only. DISREGARD
# time = O(maxK⋅n), where maxK = max value of k and n = len of string s.
# We traverse s of size n and iterate k times to decode each pattern of form k[string].
# space = O(n)
def decode_string_rec(s: str) -> str:
        def recurse(s, pos):       
            result = ""
            i, num = pos, 0
            while i < len(s):
                c = s[i]
                if c.isdigit():
                    num = num * 10 + int(c)
                elif c == '[':
                    string, end = recurse(s, i + 1)
                    result += num * string
                    i = end
                    num = 0
                elif c == ']':
                    return result, i
                else:
                    result += c
                i += 1            
            return result, i                
        return recurse(s, 0)[0]
Python
s = "3[a]2[bc]"             # "aaabcbc"
print(decode_string(s))

s = "3[a2[c]]"              # "accaccacc"
print(decode_string(s))

s = "2[abc]3[cd]ef"         # "abcabccdcdcdef"
print(decode_string(s))
Output
aaabcbc
accaccacc
abcabccdcdcdef

399. Evaluate Division

You are given an array of variable pairs equations and an array of real numbers values, where equations[i] = [Ai, Bi] and values[i] represent the equation Ai / Bi = values[i]. Each Ai or Bi is a string that represents a single variable.

You are also given some queries, where queries[j] = [Cj, Dj] represents the jth query where you must find the answer for Cj / Dj = ?.

Return the answers to all queries. If a single answer cannot be determined, return -1.0.

Note: The input is always valid. You may assume that evaluating the queries will not result in division by zero and that there is no contradiction.

Example 1: Input: equations = [["a","b"],["b","c"]], values = [2.0,3.0], queries = [["a","c"],["b","a"],["a","e"],["a","a"],["x","x"]] Output: [6.00000,0.50000,-1.00000,1.00000,-1.00000] Explanation: Given: a / b = 2.0, b / c = 3.0 queries are: a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ? return: [6.0, 0.5, -1.0, 1.0, -1.0 ]

Example 2: Input: equations = [["a","b"],["b","c"],["bc","cd"]], values = [1.5,2.5,5.0], queries = [["a","c"],["c","b"],["bc","cd"],["cd","bc"]] Output: [3.75000,0.40000,5.00000,0.20000]

Example 3: Input: equations = [["a","b"]], values = [0.5], queries = [["a","b"],["b","a"],["a","c"],["x","y"]] Output: [0.50000,2.00000,-1.00000,-1.00000]

Constraints: 1 <= equations.length <= 20 equations[i].length == 2 1 <= Ai.length, Bi.length <= 5 values.length == equations.length 0.0 < values[i] <= 20.0 1 <= queries.length <= 20 queries[i].length == 2 1 <= Cj.length, Dj.length <= 5 Ai, Bi, Cj, Dj consist of lower case English letters and digits.

Algorithm

As one can see, we just transform the problem into a path searching problem in a graph.

More precisely, we can reinterpret the problem as "given two nodes, we are asked to check if there exists a path between them. If so, we should return the cumulative products along the path as the result.

Given the above problem statement, it seems intuitive that one could apply the backtracking algorithm, or sometimes people might call it DFS (Depth-First Search).

Essentially, we can break down the algorithm into two steps overall:

Step 1). we build the graph out of the list of input equations.

Each equation corresponds to two edges in the graph. Step 2). once the graph is built, we then can evaluate the query one by one.

The evaluation of the query is done via searching the path between the given two variables.

Other than the above searching operation, we need to handle two exceptional cases as follows:

Case 1): if either of the nodes does not exist in the graph, i.e. the variables did not appear in any of the input equations, then we can assert that no path exists.

Case 2): if the origin and the destination are the same node, i.e. a/a, we can assume that there exists an invisible self-loop path for each node and the result is one.

Python
# My solution. Complexity - same as below?
from typing import List, Optional, Union

def calcEquation(equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]:
    def divide( a: List[Union[float, str]], b: List[Union[float, str]] ) -> Union[float, str]:
        if a[1] == b[1]:
            return (a[0] / b[0]) * 1.0
        return None

    var_values = dict()
    for i in range(len(equations)):
        a = equations[i][0]
        b = equations[i][1]
        v = values[i]
        var_values[a] = var_values.get(a, []) + [[v, b]]
        var_values[b] = var_values.get(b, []) + [[1/v, a]]
    print(var_values)    
    res = []
    for idx, q in enumerate(queries):
        temp = []
        if q[0] not in var_values or q[1] not in var_values:
            res.append(-1)
            print(f'Query {idx}:', temp)
            continue
        if [q[0], q[1]] in equations:
            j = equations.index([q[0], q[1]])
            res.append(values[j])
            print(f'Query {idx}:', temp)
            continue
        if [q[1], q[0]] in equations:
            j = equations.index([q[1], q[0]])
            res.append(1/values[j])
            print(f'Query {idx}:', temp)
            continue
            
        for a in var_values[q[0]]:
            for b in var_values[q[1]]:
                temp.append(divide(a,b))
        temp = set([i for i in temp if i])
        if len(temp) > 1 or len(temp) < 1:
            print(f'Query {idx}:', temp)
            res.append(-1)
            continue
        res.append(list(temp)[0])
        print(f'Query {idx}:', temp)
    return res
Python
# time = O(M⋅N) where N = # input equations and M = # queries; space = O(n)
from collections import defaultdict
def calcEquation2(equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]:
    def backtrack(curr_node, target_node, acc_product, visited):        # DFS
        visited.add(curr_node)
        ret = -1.0
        neighbors = graph[curr_node]
        if target_node in neighbors:
            ret = acc_product * neighbors[target_node]
        else:
            for neighbor, value in neighbors.items():
                if neighbor in visited:
                    continue
                ret = backtrack(
                    neighbor, target_node, acc_product * value, visited)
                if ret != -1.0:
                    break
        visited.remove(curr_node)
        return ret
    graph = defaultdict(defaultdict)
    for (dividend, divisor), value in zip(equations, values):
        graph[dividend][divisor] = value
        graph[divisor][dividend] = 1 / value

    results = []
    for dividend, divisor in queries:
        if dividend not in graph or divisor not in graph:
            ret = -1.0
        elif dividend == divisor:
            ret = 1.0
        else:
            visited = set()
            ret = backtrack(dividend, divisor, 1, visited)        # DFS: is there a path from dividend to divisor?
        results.append(ret)

    return results
Python
equations = [["a","b"],["b","c"]]
values = [2.0,3.0]
queries = [["a","c"],["b","a"],["a","e"],["a","a"],["x","x"]]
print(calcEquation(equations, values, queries))                  # Output: [6.0, 0.5, -1.0, 1.0, -1.0 ]
print()

equations = [["a","b"],["b","c"],["bc","cd"]]
values = [1.5,2.5,5.0]
queries = [["a","c"],["c","b"],["bc","cd"],["cd","bc"]]          # Output: [3.75000,0.40000,5.00000,0.20000]
print(calcEquation(equations, values, queries))
print()

equations = [["a","b"]]
values = [0.5]
queries = [["a","b"],["b","a"],["a","c"],["x","y"]]              # Output: [0.50000,2.00000,-1.00000,-1.00000]
print(calcEquation(equations, values, queries))
print()
Output
{'a': [[2.0, 'b']], 'b': [[0.5, 'a'], [3.0, 'c']], 'c': [[0.3333333333333333, 'b']]}
Query 0: {6.0}
Query 1: []
Query 2: []
Query 3: {1.0}
Query 4: []
[6.0, 0.5, -1, 1.0, -1]

{'a': [[1.5, 'b']], 'b': [[0.6666666666666666, 'a'], [2.5, 'c']], 'c': [[0.4, 'b']], 'bc': [[5.0, 'cd']], 'cd': [[0.2, 'bc']]}
Query 0: {3.75}
Query 1: []
Query 2: []
Query 3: []
[3.75, 0.4, 5.0, 0.2]

{'a': [[0.5, 'b']], 'b': [[2.0, 'a']]}
Query 0: []
Query 1: []
Query 2: []
Query 3: []
[0.5, 2.0, -1, -1]

753. Cracking the Safe

There is a safe protected by a password. The password is a sequence of n digits where each digit can be in the range [0, k - 1].

The safe has a peculiar way of checking the password. When you enter in a sequence, it checks the most recent n digits that were entered each time you type a digit.

For example, the correct password is "345" and you enter in "012345": After typing 0, the most recent 3 digits is "0", which is incorrect. After typing 1, the most recent 3 digits is "01", which is incorrect. After typing 2, the most recent 3 digits is "012", which is incorrect. After typing 3, the most recent 3 digits is "123", which is incorrect. After typing 4, the most recent 3 digits is "234", which is incorrect. After typing 5, the most recent 3 digits is "345", which is correct and the safe unlocks. Return any string of minimum length that will unlock the safe at some point of entering it.

Example 1: Input: n = 1, k = 2 Output: "10" Explanation: The password is a single digit, so enter each digit. "01" would also unlock the safe.

Example 2: Input: n = 2, k = 2 Output: "01100" Explanation: For each possible password: - "00" is typed in starting from the 4th digit. - "01" is typed in starting from the 1st digit. - "10" is typed in starting from the 3rd digit. - "11" is typed in starting from the 2nd digit. Thus "01100" will unlock the safe. "01100", "10011", and "11001" would also unlock the safe.

Constraints:

1 <= n <= 4 1 <= k <= 10 1 <= kn <= 4096

Hint: We can think of this problem as the problem of finding an Euler path (a path visiting every edge exactly once) on the following graph: there are $$k^{n-1}$$ nodes with each node having $$k$$ edges. It turns out this graph always has an Eulerian circuit (path starting where it ends.) We should visit each node in "post-order" so as to not get stuck in the graph prematurely.

No official solution on Leetcode (see comments)
All the solutions below are from comments

Python
from itertools import product
from collections import defaultdict
def crackSafe(n: int, k: int) -> str:
    if n == 1:
        return ''.join(map(str, range(k)))
    adjlist = defaultdict(list)    
    for comb in product(range(k), repeat=n):                  # create possible passwords
        adjlist[tuple(comb[:-1])].append(tuple(comb[1:]))    
    path = []                                                 # use Hierholzer's to find Euler circuit
    def dfs(node):
        while adjlist[node]:
            dfs(adjlist[node].pop())
        path.append(node[0])    
    dfs(tuple([0]*(n-1)))                                     # start from 0 node which has n-1 zeros
    # adding first digit of each node => need to add n-2 extra zeros
    return ''.join(map(str, [*reversed(path), *([0]*(n-2))]))
Python
# time = space = O(k^n)
def crackSafe2(n, k):
    password_length = k ** n                # total combinations
    password = "0" * n                      # initial pwd
    visited = set()
    visited.add(password)

    def dfs(node):
        nonlocal password, password_length        
        if len(visited) == password_length:        # Base case: if all combs visited - return
            return True        
        for digit in range(k):                     # try each possible digit
            new_password = node[1:] + str(digit)
            if new_password not in visited:
                visited.add(new_password)
                password += str(digit)                
                if dfs(new_password):              #  generate De Bruijn sequence recursively
                    return True
                visited.remove(new_password)
                password = password[:-1]
        return False
    dfs(password)
    return password
Python
def crackSafe3(n: int, k: int) -> str:
    if n == 1:
        return ''.join([str(i) for i in range(k)])
    if k == 1:
        return '0' * n
    suffix_map = {}
    all_combinations = ['0']*(n-1)
    for _ in range(k**n):
        suffix = ''.join(all_combinations[1-n:])
        suffix_map[suffix] = suffix_map.get(suffix, k) - 1
        all_combinations.append(str(suffix_map[suffix]))
    return ''.join(all_combinations)
Python
# Eulerian Path (Trail) Solution
# https://leetcode.com/problems/cracking-the-safe/solutions/3056844/python-eulerian-path-eulerian-trail-solution/
def crackSafe4(n: int, k: int) -> str:
    ava_edge = defaultdict(lambda: k-1)
    res = ['0'] * (n-1)
    suffix = ''.join(res)
    while ava_edge[suffix] >= 0:
        res.append(str(ava_edge[suffix]))
        ava_edge[suffix] -= 1
        suffix = ''.join(res[1-n:] if n > 1 else [])
    return ''.join(res)
Python
n = 1
k = 2        # Output: "10"
print(crackSafe4(n, k))

n = 2
k = 2        # Output: "01100"
print(crackSafe4(n, k))

n = 2
k = 3        # Output:
print(crackSafe4(n, k))
Output
10
01100
0221201100

489. Robot Room Cleaner

You are controlling a robot that is located somewhere in a room. The room is modeled as an m x n binary grid where 0 represents a wall and 1 represents an empty slot.

The robot starts at an unknown location in the room that is guaranteed to be empty, and you do not have access to the grid, but you can move the robot using the given API Robot.

You are tasked to use the robot to clean the entire room (i.e., clean every empty cell in the room). The robot with the four given APIs can move forward, turn left, or turn right. Each turn is 90 degrees.

When the robot tries to move into a wall cell, its bumper sensor detects the obstacle, and it stays on the current cell.

Design an algorithm to clean the entire room using the following APIs:

interface Robot { // returns true if next cell is open and robot moves into the cell. // returns false if next cell is obstacle and robot stays on the current cell. boolean move();

// Robot will stay on the same cell after calling turnLeft/turnRight. // Each turn will be 90 degrees. void turnLeft(); void turnRight();

// Clean the current cell. void clean(); } Note that the initial direction of the robot will be facing up. You can assume all four edges of the grid are all surrounded by a wall.

Custom testing:

The input is only given to initialize the room and the robot's position internally. You must solve this problem "blindfolded". In other words, you must control the robot using only the four mentioned APIs without knowing the room layout and the initial robot's position.

Example 1: Input: room = [[1,1,1,1,1,0,1,1],[1,1,1,1,1,0,1,1],[1,0,1,1,1,1,1,1],[0,0,0,1,0,0,0,0],[1,1,1,1,1,1,1,1]], row = 1, col = 3 Output: Robot cleaned all rooms. Explanation: All grids in the room are marked by either 0 or 1. 0 means the cell is blocked, while 1 means the cell is accessible. The robot initially starts at the position of row=1, col=3. From the top left corner, its position is one row below and three columns right.

Example 2: Input: room = [[1]], row = 0, col = 0 Output: Robot cleaned all rooms.

Constraints:

m == room.length n == room[i].length 1 <= m <= 100 1 <= n <= 200 room[i][j] is either 0 or 1. 0 <= row < m 0 <= col < n room[row][col] == 1 All the empty cells can be visited from the starting position

Algorithm

Time to write down the algorithm for the backtrack function backtrack(cell = (0, 0), direction = 0).

Mark the cell as visited and clean it up.

Explore 4 directions : up, right, down, and left (the order is important since the idea is always to turn right) :

Check the next cell in the chosen direction :

If it's not visited yet and there is no obtacles :

Move forward.

Explore next cells backtrack(new_cell, new_direction).

Backtrack, i.e. go back to the previous cell.

Turn right because now there is an obstacle (or a virtual obstacle) just in front.

Python
# """
# This is the robot's control interface.
# You should not implement it, or speculate about its implementation
# """
#class Robot(object):
#    def move(self):
#        """
#        Returns true if the cell in front is open and robot moves into the cell.
#        Returns false if the cell in front is blocked and robot stays in the current cell.
#        :rtype bool
#        """
#
#    def turnLeft(self):
#        """
#        Robot will stay in the same cell after calling turnLeft/turnRight.
#        Each turn will be 90 degrees.
#        :rtype void
#        """
#
#    def turnRight(self):
#        """
#        Robot will stay in the same cell after calling turnLeft/turnRight.
#        Each turn will be 90 degrees.
#        :rtype void
#        """
#
#    def clean(self):
#        """
#        Clean the current cell.
#        :rtype void
#        """
Python
# time = space = O(N−M), where N = # cells in room, M + # obstacles
class Solution:       
    def cleanRoom(self, robot):
        """
        :type robot: Robot
        :rtype: None
        """
        def go_back():
            robot.turnRight()
            robot.turnRight()
            robot.move()
            robot.turnRight()
            robot.turnRight()
            
        def backtrack(cell = (0, 0), d = 0):
            visited.add(cell)
            robot.clean()
            # going clockwise : 0: 'up', 1: 'right', 2: 'down', 3: 'left'
            for i in range(4):
                new_d = (d + i) % 4
                new_cell = (cell[0] + directions[new_d][0], \
                            cell[1] + directions[new_d][1])
                
                if not new_cell in visited and robot.move():
                    backtrack(new_cell, new_d)
                    go_back()
                # turn the robot following chosen direction : clockwise
                robot.turnRight()
    
        # going clockwise : 0: 'up', 1: 'right', 2: 'down', 3: 'left'
        directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
        visited = set()
        backtrack()

947. Most Stones Removed with Same Row or Column

On a 2D plane, we place n stones at some integer coordinate points. Each coordinate point may have at most one stone. A stone can be removed if it shares either the same row or the same column as another stone that has not been removed. Given an array stones of length n where stones[i] = [xi, yi] represents the location of the ith stone, return the largest possible number of stones that can be removed.

Example 1: Input: stones = [[0,0],[0,1],[1,0],[1,2],[2,1],[2,2]] Output: 5 Explanation: One way to remove 5 stones is as follows: 1. Remove stone [2,2] because it shares the same row as [2,1]. 2. Remove stone [2,1] because it shares the same column as [0,1]. 3. Remove stone [1,2] because it shares the same row as [1,0]. 4. Remove stone [1,0] because it shares the same column as [0,0]. 5. Remove stone [0,1] because it shares the same row as [0,0]. Stone [0,0] cannot be removed since it does not share a row/column with another stone still on the plane.

Example 2: Input: stones = [[0,0],[0,2],[1,1],[2,0],[2,2]] Output: 3 Explanation: One way to make 3 moves is as follows: 1. Remove stone [2,2] because it shares the same row as [2,0]. 2. Remove stone [2,0] because it shares the same column as [0,0]. 3. Remove stone [0,2] because it shares the same row as [0,0]. Stones [0,0] and [1,1] cannot be removed since they do not share a row/column with another stone still on the plane.

Example 3: Input: stones = [[0,0]] Output: 0 Explanation: [0,0] is the only stone on the plane, so you cannot remove it.

Constraints:

1 <= stones.length <= 1000 0 <= xi, yi <= 104 No two stones are at the same coordinate point.

All below solutions are from comments

Python
# matrix for Example 1
m = [
    [1,1,0],
    [1,0,1],
    [0,1,1],
]
Python
# my solution. Building graph - time = n^2?
def remove_stones(stones: List[List[int]]) -> int:
    '''Cannot remove stones whose in_degree = 0 - no adjacent stones'''
    if len(stones) < 2:
        return 0
        
    # build graph and in_degree
    graph, in_degree = dict(), dict()
    for s in stones:
        temp = [i for i in stones if i!=s]
        for i in temp:
            if s[0]==i[0]:
                graph[tuple(s)] = graph.get(tuple(s), []) + [tuple(i)]
                in_degree[tuple(s)] = in_degree.get(tuple(s), 0) + 1
            if s[1]==i[1]:
                graph[tuple(s)] = graph.get(tuple(s), []) + [tuple(i)]
                in_degree[tuple(s)] = in_degree.get(tuple(s), 0) + 1
    start_vertex = [k for k,v in in_degree.items() if v != 0][0]
    print('Initial in_degree:', in_degree)
    print('Start vertex:', start_vertex)

    # DFS while keeping an eye on in_degree
    visited, stack = set(), [start_vertex]
    while stack:
        v = stack.pop()
        if v not in visited and in_degree[v] != 0:
            visited.add(v)
            conns = set(graph[v]) - visited
            for conn in conns:
                in_degree[conn] -= 1
                stack.append(conn)
    print('Graph:', graph)
    print('Visited:', visited)
    print('Final in_degree:', in_degree)
    return len(visited)
Python
# time = O(n^2)?
def dfs(stones, index, visited, n):
    visited[index] = True
    result = 0
    for i in range(n):
        if not visited[i] and (stones[i][0] == stones[index][0] or stones[i][1] == stones[index][1]):
            result += (dfs(stones, i, visited, n) + 1)
    return result

def remove_stones2(stones):
    n = len(stones)
    visited = [False] * n
    result = 0
    for i in range(n):
        if visited[i]:
            continue
        result += dfs(stones, i, visited, n)
    return result
Python
# Time O(N), Space O(N)
from collections import defaultdict

def remove_stones3(stones):
    if len(stones) == 0 or len(stones[0]) == 0:
        return 0

    mapX = defaultdict(list)
    mapY = defaultdict(list)

    # building map of all stone on the same row/col
    for stone in stones:
        mapX[stone[0]].append(stone)
        mapY[stone[1]].append(stone)

    visited = set()
    numComp = 0

    for stone in stones:
        stone_tuple = tuple(stone)
        if stone_tuple not in visited:
            numComp += 1
            dfs2(mapX, mapY, stone_tuple, visited)

    return len(stones) - numComp

def dfs2(mapX, mapY, stone, visited):
    visited.add(stone)
    for s in mapX[stone[0]]:
        s_tuple = tuple(s)
        if s_tuple not in visited:
            dfs2(mapX, mapY, s_tuple, visited)

    for s in mapY[stone[1]]:
        s_tuple = tuple(s)
        if s_tuple not in visited:
            dfs2(mapX, mapY, s_tuple, visited)

A similar approach, with explanation:
Prepare the graph
From every stone:
a. If not visitied, run dfs to remove connected nodes.
b. After removing a node, track that to avoid infinite loop
c. When a strongly connected component is fully traversed, subtract 1 to track that remaining stone
Do this until all stones are traversed

Python
# time = O(n)
def remove_stones4(stones: List[List[int]]) -> int:

    def remove_point(a,b):                           # Function to remove connected points from the ongoing graph. 
        points.discard((a,b))
        for y in x_dic[a]:
            if (a,y) in points:
                remove_point(a,y)
        for x in y_dic[b]:
            if (x,b) in points:
                remove_point(x,b)

    x_dic = defaultdict(list)
    y_dic = defaultdict(list)
    points= {(i,j) for i,j in stones}

    for i,j in stones:                                # Construction of graph by x_coordinates and y_coordinates.
        x_dic[i].append(j)
        y_dic[j].append(i)

    cnt = 0
    for a,b in stones:                                # counting of distinct connected graph.
        if (a,b) in points:
            remove_point(a,b)
            cnt+=1

    return len(stones)-cnt
Python
stones = [[0,0],[0,1],[1,0],[1,2],[2,1],[2,2]]     # Output: 5
# remove [2,2] (shares row w/[2,1]) -> remove [2,1] (shares col w/[0,1]) -> remove [1,2] (shares row w/[1,0])
# remove [1,0] (shares col w/[0,0]) -> remove [0,1] (shares row w/[0,0]) -> cannot remove [0,0]
print(remove_stones4(stones))

stones = [[0,0],[0,2],[1,1],[2,0],[2,2]]           # Output: 3
# remove [2,2] (shares row w/[2,0]) -> remove [2,0] (shares col w/[0,0]) -> remove [0,2] (shares row w/[0,0])
# cannot remove [0,0] and [1,1]
print(remove_stones4(stones))

stones = [[0,0]]                                   # Output: 0
print(remove_stones4(stones))
Output
5
3
0

951. Flip Equivalent Binary Trees

For a binary tree T, we can define a flip operation as follows: choose any node, and swap the left and right child subtrees.

A binary tree X is flip equivalent to a binary tree Y if and only if we can make X equal to Y after some number of flip operations.

Given the roots of two binary trees root1 and root2, return true if the two trees are flip equivalent or false otherwise.

Example 1: image.png Flipped Trees Diagram Input: root1 = [1,2,3,4,5,6,null,null,null,7,8], root2 = [1,3,2,null,6,4,5,null,null,null,null,8,7] Output: true Explanation: We flipped at nodes with values 1, 3, and 5.

Example 2: Input: root1 = [], root2 = [] Output: true

Example 3: Input: root1 = [], root2 = [1] Output: false

Constraints:

The number of nodes in each tree is in the range [0, 100]. Each tree will have unique node values in the range [0, 99].

Algorithm (Recursive)

There are 3 cases:

If root1 or root2 is null, then they are equivalent if and only if they are both null.

Else, if root1 and root2 have different values, they aren't equivalent.

Else, let's check whether the children of root1 are equivalent to the children of root2. There are two different ways to pair these children.

Algorithm (canonical search)

We can use a depth-first search to compare the canonical representation of each tree. If the traversals are the same, the representations are equal. When traversing, we should be careful to encode both when we enter or leave a node.

Python
# level-order traversal
def bfs(root):
    res = []
    if not root:
        return res    
    q = [root]
    while q:
        node = q.pop(0)
        res.append(node.val)
        if node.left:
            q.append(node.left)
        if node.right:
            q.append(node.right)
    return res        
Python
# build tree - time = space = O(n)
class Node:
    def __init__(self, val):
        self.val = val
        self.left = self.right = None

def insertLevelOrder(arr, i, n):
    root = None    
    if i < n:                     # base case
        root = Node(arr[i])
        root.left  = insertLevelOrder(arr, 2*i + 1, n)
        root.right = insertLevelOrder(arr, 2*i + 2, n)         
    return root


arr = [1,2,3,4,5,6,None,None,None,7,8 ]
n = len(arr)
root = insertLevelOrder(arr, 0, n)
bfs(root)
Output
[1, 2, 3, 4, 5, 6, None, None, None, 7, 8]
Python
# recursive
# time = O(min(N_1, N_2)), where N_1, N_2 = lengths of root1 and root2
# space = O(min(H_1, H_2)), where H_1, H_2 = heights of root1 and root2
class Solution(object):
    def flipEquiv(self, root1, root2):
        if root1 is root2:
            return True
        if not root1 or not root2 or root1.val != root2.val:
            return False

        return (self.flipEquiv(root1.left, root2.left) and
                self.flipEquiv(root1.right, root2.right) or
                self.flipEquiv(root1.left, root2.right) and
                self.flipEquiv(root1.right, root2.left))
Python
# canonical search
# complexity - same as recursive
class Solution:
    def flipEquiv(self, root1, root2):
        def dfs(node):
            if node:
                yield node.val
                L = node.left.val if node.left else -1
                R = node.right.val if node.right else -1
                if L < R:
                    yield from dfs(node.left)
                    yield from dfs(node.right)
                else:
                    yield from dfs(node.right)
                    yield from dfs(node.left)
                yield '#'

        return all(x == y for x, y in itertools.zip_longest(
            dfs(root1), dfs(root2)))

Part IV. Recursion

425. Word Squares

Given an array of unique strings words, return all the word squares you can build from words. The same word from words can be used multiple times. You can return the answer in any order.

A sequence of strings forms a valid word square if the kth row and column read the same string, where 0 <= k < max(numRows, numColumns).

For example, the word sequence ["ball","area","lead","lady"] forms a word square because each word reads the same both horizontally and vertically.

Example 1:

Input: words = ["area","lead","wall","lady","ball"] Output: [["ball","area","lead","lady"],["wall","area","lead","lady"]] Explanation: The output consists of two word squares. The order of output does not matter (just the order of words in each word square matters).

Example 2: Input: words = ["abat","baba","atan","atal"] Output: [["baba","abat","baba","atal"],["baba","abat","baba","atan"]] Explanation: The output consists of two word squares. The order of output does not matter (just the order of words in each word square matters).

Constraints:

1 <= words.length <= 1000 1 <= words[i].length <= 4 All words[i] have the same length. words[i] consists of only lowercase English letters. All words[i] are unique.

Algorithm (hashtable)

We build upon the backtracking algorithm that we listed above, and tweak two parts.

In the first part, we add a new function buildPrefixHashTable(words) to build a hashtable out of the input words.

Then in the second part, in the function getWordsWithPrefix() we simply query the hashtable to retrieve all the words that possess the given prefix.

Algorithm (trie)

We build upon the backtracking algorithm that we listed above, and tweak two parts.

In the first part, we add a new function buildTrie(words) to build a Trie out of the input words.

Then in the second part, in the function getWordsWithPrefix(prefix) we simply query the Trie to retrieve all the words that possess the given prefix.

Here are some sample implementations. Note that, we tweak the Trie data structure a bit, in order to further optimize the time and space complexity.

Instead of labeling the word at the leaf node of the Trie, we label the word at each node so that we don't need to perform a further traversal once we reach the last node in the prefix. This trick could help us with the time complexity.

Instead of storing the actual words in the Trie, we keep only the index of the word, which could greatly save the space.

Python
# hashtable
# time = O(N⋅26^L), where N = # input words and L = length of single word; space = O(NL)
class Solution:

    def wordSquares(self, words: List[str]) -> List[List[str]]:

        self.words = words
        self.N = len(words[0])
        self.buildPrefixHashTable(self.words)

        results = []
        word_squares = []
        for word in words:
            word_squares = [word]
            self.backtracking(1, word_squares, results)
        return results

    def backtracking(self, step, word_squares, results):
        if step == self.N:
            results.append(word_squares[:])
            return

        prefix = ''.join([word[step] for word in word_squares])
        for candidate in self.getWordsWithPrefix(prefix):
            word_squares.append(candidate)
            self.backtracking(step+1, word_squares, results)
            word_squares.pop()

    def buildPrefixHashTable(self, words):
        self.prefixHashTable = {}
        for word in words:
            for prefix in (word[:i] for i in range(1, len(word))):
                self.prefixHashTable.setdefault(prefix, set()).add(word)

    def getWordsWithPrefix(self, prefix):
        if prefix in self.prefixHashTable:
            return self.prefixHashTable[prefix]
        else:
            return set([])
Python
# trie
# c. same as for hashtable
class Solution:

    def wordSquares(self, words: List[str]) -> List[List[str]]:

        self.words = words
        self.N = len(words[0])
        self.buildTrie(self.words)

        results = []
        word_squares = []
        for word in words:
            word_squares = [word]
            self.backtracking(1, word_squares, results)
        return results

    def buildTrie(self, words):
        self.trie = {}

        for wordIndex, word in enumerate(words):
            node = self.trie
            for char in word:
                if char in node:
                    node = node[char]
                else:
                    newNode = {}
                    newNode['#'] = []
                    node[char] = newNode
                    node = newNode
                node['#'].append(wordIndex)

    def backtracking(self, step, word_squares, results):
        if step == self.N:
            results.append(word_squares[:])
            return

        prefix = ''.join([word[step] for word in word_squares])
        for candidate in self.getWordsWithPrefix(prefix):
            word_squares.append(candidate)
            self.backtracking(step+1, word_squares, results)
            word_squares.pop()

    def getWordsWithPrefix(self, prefix):
        node = self.trie
        for char in prefix:
            if char not in node:
                return []
            node = node[char]
        return [self.words[wordIndex] for wordIndex in node['#']]

247. Strobogrammatic Number II

Given an integer n, return all the strobogrammatic numbers that are of length n. You may return the answer in any order. A strobogrammatic number is a number that looks the same when rotated 180 degrees (looked at upside down).

Example 1: Input: n = 2 Output: ["11","69","88","96"]

Example 2: Input: n = 1 Output: ["0","1","8"]

Constraints:

1 <= n <= 14

Hint:
Try to use recursion and notice that it should recurse with n - 2 instead of n - 1.

Algorithm

Initialize a data structure reversiblePairs, which contains all pairs of reversible digits.

Call and return the recursive function, generateStroboNumbers(n, finalLength), where the first argument indicates that the current call will generate all n-digit strobogrammatic numbers. The second argument indicates the length of the final strobogrammatic numbers that we will generate and will be used to check if we can add '0' to the beginning and end of a number.

Create a function generateStroboNumbers(n, finalLength) which will return all strobogrammatic numbers of n-digits:

Check for base cases, if n == 0 return an array with an empty string [""], otherwise if n == 1 return ["0", "1", "8"]. Call generateStroboNumbers(n - 2, finalLength) to get all the strobogrammatic numbers of (n-2) digits and store them in subAns. Initialize an empty array currStroboNums to store strobogrammatic numbers of n-digits. For each number in prevStroboNums we append all reversiblePairs at the beginning and the end except when the current reversible pair is '00' and n == finalLength (because we can't append '0' at the beginning of a number) and push this new number in ans. At the end of the function, return all the strobogrammatic numbers, i.e. currStroboNums

Python
# time = N⋅5^(N/2+1), space = N⋅5^(N/2). Iterative approach (level order traversal) has the same complexity 
def findStrobogrammatic(n: int) -> List[str]:
    reversible_pairs = [
        ['0', '0'], ['1', '1'], 
        ['6', '9'], ['8', '8'], ['9', '6']
    ]

    def generate_strobo_numbers(n, final_length):
        if n == 0:
            # 0-digit strobogrammatic number is an empty string.
            return [""]

        if n == 1:
            # 1-digit strobogrammatic numbers.
            return ["0", "1", "8"]

        prev_strobo_nums = generate_strobo_numbers(n - 2, final_length)
        curr_strobo_nums = []

        for prev_strobo_num in prev_strobo_nums:
            for pair in reversible_pairs:
                if pair[0] != '0' or n != final_length:
                    curr_strobo_nums.append(pair[0] + prev_strobo_num + pair[1])

        return curr_strobo_nums

    return generate_strobo_numbers(n, n)
Python
findStrobogrammatic(4)
Output
['1001',
 '6009',
 '8008',
 '9006',
 '1111',
 '6119',
 '8118',
 '9116',
 '1691',
 '6699',
 '8698',
 '9696',
 '1881',
 '6889',
 '8888',
 '9886',
 '1961',
 '6969',
 '8968',
 '9966']

212. Word Search II

Given an m x n board of characters and a list of strings words, return all words on the board.

Each word must be constructed from letters of sequentially adjacent cells, where adjacent cells are horizontally or vertically neighboring. The same letter cell may not be used more than once in a word.

Example 1: image.png Input: board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]], words = ["oath","pea","eat","rain"] Output: ["eat","oath"]

Example 2:

a b c d

Input: board = [["a","b"],["c","d"]], words = ["abcb"] Output: []

Constraints:

m == board.length n == board[i].length 1 <= m, n <= 12 board[i][j] is a lowercase English letter. 1 <= words.length <= 3 * 104 1 <= words[i].length <= 10 words[i] consists of lowercase English letters. All the strings of words are unique.

Algorithm

The overall workflow of the algorithm is intuitive, which consists of a loop over each cell in the board and a recursive function call starting from the cell. Here is the skeleton of the algorithm.

We build a Trie out of the words in the dictionary, which would be used for the matching process later.

Starting from each cell, we start the backtracking exploration (i.e. backtracking(cell)), if there exists any word in the dictionary that starts with the letter in the cell.

During the recursive function call backtracking(cell), we explore the neighbor cells (i.e. neighborCell) around the current cell for the next recursive call backtracking(neighborCell). At each call, we check if the sequence of letters that we traverse so far matches any word in the dictionary, with the help of the Trie data structure that we built at the beginning.

Optimizations: using tries

Python
# time = O(M(4⋅3^L−1)), where M = # cells, L = max len of words; space = O(n)
class Solution:
    def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
        WORD_KEY = '$'
        
        trie = {}
        for word in words:
            node = trie
            for letter in word:
                # retrieve the next node; If not found, create a empty node.
                node = node.setdefault(letter, {})
            # mark the existence of a word in trie node
            node[WORD_KEY] = word
        
        rowNum = len(board)
        colNum = len(board[0])
        
        matchedWords = []
        
        def backtracking(row, col, parent):    
            
            letter = board[row][col]
            currNode = parent[letter]
            
            # check if we find a match of word
            word_match = currNode.pop(WORD_KEY, False)
            if word_match:
                # also we removed the matched word to avoid duplicates,
                #   as well as avoiding using set() for results.
                matchedWords.append(word_match)
            
            # Before the EXPLORATION, mark the cell as visited 
            board[row][col] = '#'
            
            # Explore the neighbors in 4 directions, i.e. up, right, down, left
            for (rowOffset, colOffset) in [(-1, 0), (0, 1), (1, 0), (0, -1)]:
                newRow, newCol = row + rowOffset, col + colOffset     
                if newRow < 0 or newRow >= rowNum or newCol < 0 or newCol >= colNum:
                    continue
                if not board[newRow][newCol] in currNode:
                    continue
                backtracking(newRow, newCol, currNode)
        
            # End of EXPLORATION, we restore the cell
            board[row][col] = letter
        
            # Optimization: incrementally remove the matched leaf node in Trie.
            if not currNode:
                parent.pop(letter)

        for row in range(rowNum):
            for col in range(colNum):
                # starting from each of the cells
                if board[row][col] in trie:
                    backtracking(row, col, trie)
        
        return matchedWords    

351. Android Unlock Patterns

Android devices have a special lock screen with a 3 x 3 grid of dots. Users can set an "unlock pattern" by connecting the dots in a specific sequence, forming a series of joined line segments where each segment's endpoints are two consecutive dots in the sequence. A sequence of k dots is a valid unlock pattern if both of the following are true:

All the dots in the sequence are distinct. If the line segment connecting two consecutive dots in the sequence passes through the center of any other dot, the other dot must have previously appeared in the sequence. No jumps through the center non-selected dots are allowed. For example, connecting dots 2 and 9 without dots 5 or 6 appearing beforehand is valid because the line from dot 2 to dot 9 does not pass through the center of either dot 5 or 6. However, connecting dots 1 and 3 without dot 2 appearing beforehand is invalid because the line from dot 1 to dot 3 passes through the center of dot 2. Here are some example valid and invalid unlock patterns: image.png The 1st pattern [4,1,3,6] is invalid because the line connecting dots 1 and 3 pass through dot 2, but dot 2 did not previously appear in the sequence. The 2nd pattern [4,1,9,2] is invalid because the line connecting dots 1 and 9 pass through dot 5, but dot 5 did not previously appear in the sequence. The 3rd pattern [2,4,1,3,6] is valid because it follows the conditions. The line connecting dots 1 and 3 meets the condition because dot 2 previously appeared in the sequence. The 4th pattern [6,5,4,1,9,2] is valid because it follows the conditions. The line connecting dots 1 and 9 meets the condition because dot 5 previously appeared in the sequence. Given two integers m and n, return the number of unique and valid unlock patterns of the Android grid lock screen that consist of at least m keys and at most n keys.

Two unlock patterns are considered unique if there is a dot in one sequence that is not in the other, or the order of the dots is different.

Example 1:

Input: m = 1, n = 1 Output: 9 Example 2:

Input: m = 1, n = 2 Output: 65

Constraints:

1 <= m, n <= 9

No oficcial solution on Leetcode (see comments)

Python
# https://github.com/xiaoningning/LeetCode-python/blob/master/351%20Android%20Unlock%20Patterns.py
class Solution(object):
    def __init__(self):
        """
        Skip matrix
        Encode rule for 2, 4, 6, 8, 5
        """
        self.skip = [[None for _ in range(10)] for _ in range(10)]
        self.skip[1][3], self.skip[3][1] = 2, 2
        self.skip[1][7], self.skip[7][1] = 4, 4
        self.skip[3][9], self.skip[9][3] = 6, 6
        self.skip[7][9], self.skip[9][7] = 8, 8
        self.skip[4][6], self.skip[6][4] = 5, 5
        self.skip[2][8], self.skip[8][2] = 5, 5
        self.skip[1][9], self.skip[9][1] = 5, 5
        self.skip[3][7], self.skip[7][3] = 5, 5

    def numberOfPatterns(self, m, n):
        """
        NP - O(N!)
        dfs

        Maintain a skip matrix
        :type m: int
        :type n: int
        :rtype: int
        """
        visited = [False for _ in range(10)]
        return sum(
            self.dfs(1, visited, remain) * 4 +
            self.dfs(2, visited, remain) * 4 +
            self.dfs(5, visited, remain)
            for remain in range(m, n+1)
        )

    def dfs(self, cur, visited, remain):
        """
        Return the count of combination
        Optimization - memoization
        """
        if remain == 1:
            return 1

        visited[cur] = True
        ret = 0
        for nxt in range(1, 10):
            if (
                not visited[nxt] and (
                    self.skip[cur][nxt] is None or
                    visited[self.skip[cur][nxt]]
                )
            ):
                ret += self.dfs(nxt, visited, remain - 1)

        visited[cur] = False
        return ret


if __name__ == "__main__":
    assert Solution().numberOfPatterns(1, 2) == 65
    assert Solution().numberOfPatterns(1, 3) == 385

17. Letter Combinations of a Phone Number

Given a string containing digits from 2-9 inclusive, return all possible letter combinations that the number could represent. Return the answer in any order. image.png A mapping of digits to letters (just like on the telephone buttons) is given below. Note that 1 does not map to any letters.

Example 1: Input: digits = "23" Output: ["ad","ae","af","bd","be","bf","cd","ce","cf"]

Example 2: Input: digits = "" Output: []

Example 3: Input: digits = "2" Output: ["a","b","c"]

Constraints:

0 <= digits.length <= 4 digits[i] is a digit in the range ['2', '9'].

Algorithm

As mentioned previously, we need to lock-in letters when we generate new letters. The easiest way to save state like this is to use recursion. Our algorithm will be as follows:

If the input is empty, return an empty array.

Initialize a data structure (e.g. a hash map) that maps digits to their letters, for example, mapping "6" to "m", "n", and "o".

Use a backtracking function to generate all possible combinations.

The function should take 2 primary inputs: the current combination of letters we have, path, and the index we are currently checking. As a base case, if our current combination of letters is the same length as the input digits, that means we have a complete combination. Therefore, add it to our answer, and backtrack. Otherwise, get all the letters that correspond with the current digit we are looking at, digits[index]. Loop through these letters. For each letter, add the letter to our current path, and call backtrack again, but move on to the next digit by incrementing index by 1. Make sure to remove the letter from path once finished with it.

Python
# time = O(N * (4^N)), where N = len of digits, space = O(n)
class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        # If the input is empty, immediately return an empty answer array
        if len(digits) == 0: 
            return []
        
        # Map all the digits to their corresponding letters
        letters = {"2": "abc", "3": "def", "4": "ghi", "5": "jkl", 
                   "6": "mno", "7": "pqrs", "8": "tuv", "9": "wxyz"}
        
        def backtrack(index, path):
            # If the path is the same length as digits, we have a complete combination
            if len(path) == len(digits):
                combinations.append("".join(path))
                return # Backtrack
            
            # Get the letters that the current digit maps to, and loop through them
            possible_letters = letters[digits[index]]
            for letter in possible_letters:
                # Add the letter to our current path
                path.append(letter)
                # Move on to the next digit
                backtrack(index + 1, path)
                # Backtrack by removing the letter before moving onto the next
                path.pop()

        # Initiate backtracking with an empty path and starting index of 0
        combinations = []
        backtrack(0, [])
        return combinations
Python
# from comments
class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        lookup = {
            "2": ["a", "b", "c"],
            "3": ["d", "e", "f"],
            "4": ["g", "h", "i"],
            "5": ["j", "k", "l"],
            "6": ["m", "n", "o"],
            "7": ["p", "q", "r", "s"],
            "8": ["t", "u", "v"],
            "9": ["w", "x", "y", "z"]
        }
        
        letter_lists = []
        for ch in digits:
            letter_lists.append(lookup[ch])
            
        while len(letter_lists) > 1:
            l1 = letter_lists.pop()
            l2 = letter_lists.pop()
            combos = []
            for i in l1:
                for j in l2:
                    combos.append(j + i)
            letter_lists.append(combos)
            
        return [] if not letter_lists else letter_lists[0]

22. Generate Parentheses

Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.

Example 1: Input: n = 3 Output: ["((()))","(()())","(())()","()(())","()()()"]

Example 2: Input: n = 1 Output: ["()"]

Constraints:

1 <= n <= 8

Algorithm (backtracking)

Instead of adding '(' or ')' every time as in Approach 1, let's only add them when we know it will remain a valid sequence. We can do this by keeping track of the number of opening and closing brackets we have placed so far.

We can start an opening bracket if we still have one (of n) left to place. And we can start a closing bracket if it would not exceed the number of opening brackets.

Algorithm (closure number)

To enumerate something, generally we would like to express it as a sum of disjoint subsets that are easier to count.

Consider the closure number of a valid parentheses sequence S: the least index >= 0 so that S[0], S[1], ..., S[2*index+1] is valid. Clearly, every parentheses sequence has a unique closure number. We can try to enumerate them individually.

For each closure number c, we know the starting and ending brackets must be at index 0 and 2c + 1. Then, the 2c elements between must be a valid sequence, plus the rest of the elements must be a valid sequence

Python
# backtracking
# time = space = O((4^n)/sqrt(n))
class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        ans = []
        def backtrack(S = [], left = 0, right = 0):
            if len(S) == 2 * n:
                ans.append("".join(S))
                return
            if left < n:
                S.append("(")
                backtrack(S, left+1, right)
                S.pop()
            if right < left:
                S.append(")")
                backtrack(S, left, right+1)
                S.pop()
        backtrack()
        return ans
Python
# closure number
# same complexity
class Solution(object):
    def generateParenthesis(self, N):
        if N == 0: return ['']
        ans = []
        for c in xrange(N):
            for left in self.generateParenthesis(c):
                for right in self.generateParenthesis(N-1-c):
                    ans.append('({}){}'.format(left, right))
        return ans

Part V. Sorting and Searching

Given two sorted arrays nums1 and nums2 of size m and n respectively, return the median of the two sorted arrays.
The overall run time complexity should be O(log (m+n)).

Example 1:
Input: nums1 = [1,3], nums2 = [2]
Output: 2.00000
Explanation: merged array = [1,2,3] and median is 2.

Example 2:
Input: nums1 = [1,2], nums2 = [3,4]
Output: 2.50000
Explanation: merged array = [1,2,3,4] and median is (2 + 3) / 2 = 2.5.

Constraints: * nums1.length == m * nums2.length == n * 0 <= m <= 1000 * 0 <= n <= 1000 * 1 <= m + n <= 2000 * -10^6 <= nums1[i], nums2[i] <= 10^6

No official solution on Leetcode (see Leetcode comments and Geeks for Geeks)

Algorithm
When we are trying to find the median of a list, everything to the left of this number, which is the median, should be smaller, while everything to the right of this number should be bigger. If we use the same logic concerning two lists, we need to find a partition in such a way that the number from both the list to the left of the partition is smaller, while the number to the right of the partition is bigger. Hence the basic idea here is to find a proper partition. To find a partition, we can use binary search since we have the lists sorted. After we find partitions, we will have four numbers that we need to consider. image.png We need to ensure that we find a partition in such a way that:
X1 < Y2 and Y1 < X2.
Until we find this partition, we need to keep performing our binary search.

Python
# time O(log(min(N,M)), space O(1)
# source: a mix of Leetcode comments + Geeks for Geeks
from typing import List
     
# time O(log(min(N,M)), space O(1). BIN SEARCH
def median(nums1: List[int], nums2: List[int]) -> float:
    ''' 
        Edge cases are all captured except when both arrays
        are empty (currently nan is returned)
    '''   
    # capture edge cases
    if len(nums2) < len(nums1):
        nums1, nums2 = nums2, nums1
    total = len(nums1) + len(nums2)
    half = total // 2
    l, r = 0, len(nums1)-1    
    # median is guaranteed
    while True:
        i = (l + r) // 2    # for nums1
        # subtr. 2 - j starts at 0, i starts at 0
        j = half - i - 2    # for nums2        
        # overflow of indices
        nums1_left  = nums1[i] if i >= 0 else float("-inf")
        nums1_right = nums1[i+1] if (i+1) < len(nums1) else float("inf")
        nums2_left  = nums2[j] if j >= 0 else float("-inf")
        nums2_right = nums2[j+1] if (j+1) < len(nums2) else float("inf")
        # if correct partition is found
        if nums1_left <= nums2_right and nums2_left <= nums1_right:
            if total % 2:
                return min(nums1_right, nums2_right)
            else:
                return ( max(nums1_left, nums2_left) +\
                         min(nums1_right, nums2_right) ) / 2
        # if no correct partition - arrays are in ascending order
        elif nums1_left > nums2_right:
            r = i - 1
        else:
            l = i + 1
                
                
nums1 = [1,3]
nums2 = [2]
print(median(nums1, nums2))

nums1 = [1,2]
nums2 = [3,4]
print(median(nums1, nums2, ))            

nums1 = [-5, 3, 6, 12, 15]
nums2 = [-12, -10, -6, -3, 4, 10]
print(median(nums1, nums2))

nums1 = [3]
nums2 = [4]
print(median(nums1, nums2))

nums1 = []
nums2 = [3,4]
print(median(nums1, nums2))

nums1 = []
nums2 = []
print(median(nums1, nums2))
Output
2
2.5
3
3.5
3.5
nan

34. Find First and Last Position of Element in Sorted Array

Given an array of integers nums sorted in non-decreasing order, find the starting and ending position of a given target value.

If target is not found in the array, return [-1, -1].

You must write an algorithm with O(log n) runtime complexity.

Example 1: Input: nums = [5,7,7,8,8,10], target = 8 Output: [3,4]

Example 2: Input: nums = [5,7,7,8,8,10], target = 6 Output: [-1,-1]

Example 3: Input: nums = [], target = 0 Output: [-1,-1]

Constraints:

0 <= nums.length <= 105 -109 <= nums[i] <= 109 nums is a non-decreasing array. -109 <= target <= 109

Algorithm (bin search)

Define a function called findBound which takes three arguments: the array, the target to search for, and a boolean value isFirst which indicates if we are trying to find the first or the last occurrence of target. We use 2 variables to keep track of the subarray that we are scanning. Let's call them begin and end. Initially, begin is set to 0 and end is set to the last index of the array.
We iterate until begin is greater than or equal to end.
At each step, we calculate the middle element mid = (begin + end) / 2. We use the value of the middle element to decide which half of the array we need to search.
nums[mid] == target
isFirst is true ~ This implies that we are trying to find the first occurrence of the element. If mid == begin or nums[mid - 1] != target, then we return mid as the first occurrence of the target. Otherwise, we update end = mid - 1 isFirst is false ~ This implies we are trying to find the last occurrence of the element. If mid == end or nums[mid + 1] != target, then we return mid as the last occurrence of the target. Otherwise, we update begin = mid + 1 nums[mid] > target ~ We update end = mid - 1 since we must discard the right side of the array as the middle element is greater than target.
nums[mid] < target ~ We update begin = mid + 1 since we must discard the left side of the array as the middle element is less than target.
We return a value of -1 at the end of our function which indicates that target was not found in the array.

Python
# time = O(logN), space = O(1)
class Solution:
    def searchRange(self, nums: List[int], target: int) -> List[int]:
        
        lower_bound = self.findBound(nums, target, True)
        if (lower_bound == -1):
            return [-1, -1]
        
        upper_bound = self.findBound(nums, target, False)
        
        return [lower_bound, upper_bound]
        
    def findBound(self, nums: List[int], target: int, isFirst: bool) -> int:
        
        N = len(nums)
        begin, end = 0, N - 1
        while begin <= end:
            mid = int((begin + end) / 2)    
            
            if nums[mid] == target:
                
                if isFirst:
                    # we found our lower bound
                    if mid == begin or nums[mid - 1] < target:
                        return mid
                    # search on left side for bound
                    end = mid - 1
                else:                    
                    # we found our upper bound.
                    if mid == end or nums[mid + 1] > target:
                        return mid
                    # search on right side for bound
                    begin = mid + 1
            
            elif nums[mid] > target:
                end = mid - 1
            else:
                begin = mid + 1
        
        return -1

56. Merge Intervals

Given an array of intervals where intervals[i] = [starti, endi], merge all overlapping intervals, and return an array of the non-overlapping intervals that cover all the intervals in the input.

Example 1: Input: intervals = [[1,3],[2,6],[8,10],[15,18]] Output: [[1,6],[8,10],[15,18]] Explanation: Since intervals [1,3] and [2,6] overlap, merge them into [1,6].

Example 2: Input: intervals = [[1,4],[4,5]] Output: [[1,5]] Explanation: Intervals [1,4] and [4,5] are considered overlapping.

Constraints:

1 <= intervals.length <= 104 intervals[i].length == 2 0 <= starti <= endi <= 104

Algorithm

First, we sort the list as described. Then, we insert the first interval into our merged list and continue considering each interval in turn as follows: If the current interval begins after the previous interval ends, then they do not overlap and we can append the current interval to merged. Otherwise, they do overlap, and we merge them by updating the end of the previous interval if it is less than the end of the current interval.

A simple proof by contradiction shows that this algorithm always produces the correct answer. First, suppose that the algorithm at some point fails to merge two intervals that should be merged. This would imply that there exists some triple of indices ii, jj, and kk in a list of intervals \text{ints}ints such that i < j < ki<j<k and (\text{ints[i]}ints[i], \text{ints[k]}ints[k]) can be merged, but neither (\text{ints[i]}ints[i], \text{ints[j]}ints[j]) nor (\text{ints[j]}ints[j], \text{ints[k]}ints[k]) can be merged. From this scenario follow several inequalities:

\begin{aligned} \text{ints[i].end} < \text{ints[j].start} \ \text{ints[j].end} < \text{ints[k].start} \ \text{ints[i].end} \geq \text{ints[k].start} \ \end{aligned} ints[i].end<ints[j].start ints[j].end<ints[k].start ints[i].end≥ints[k].start ​

We can chain these inequalities (along with the following inequality, implied by the well-formedness of the intervals: \text{ints[j].start} \leq \text{ints[j].end}ints[j].start≤ints[j].end) to demonstrate a contradiction:

\begin{aligned} \text{ints[i].end} < \text{ints[j].start} \leq \text{ints[j].end} < \text{ints[k].start} \ \text{ints[i].end} \geq \text{ints[k].start} \end{aligned} ints[i].end<ints[j].start≤ints[j].end<ints[k].start ints[i].end≥ints[k].start ​

Therefore, all mergeable intervals must occur in a contiguous run of the sorted list.

Python
# time = O(nlogn), space = O(logn)
class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:

        intervals.sort(key=lambda x: x[0])

        merged = []
        for interval in intervals:
            # if the list of merged intervals is empty or if the current
            # interval does not overlap with the previous, simply append it.
            if not merged or merged[-1][1] < interval[0]:
                merged.append(interval)
            else:
            # otherwise, there is overlap, so we merge the current and previous
            # intervals.
                merged[-1][1] = max(merged[-1][1], interval[1])

        return merged

57. Insert Interval

You are given an array of non-overlapping intervals intervals where intervals[i] = [starti, endi] represent the start and the end of the ith interval and intervals is sorted in ascending order by starti. You are also given an interval newInterval = [start, end] that represents the start and end of another interval.

Insert newInterval into intervals such that intervals is still sorted in ascending order by starti and intervals still does not have any overlapping intervals (merge overlapping intervals if necessary).

Return intervals after the insertion.

Example 1: Input: intervals = [[1,3],[6,9]], newInterval = [2,5] Output: [[1,5],[6,9]]

Example 2: Input: intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8] Output: [[1,2],[3,10],[12,16]] Explanation: Because the new interval [4,8] overlaps with [3,5],[6,7],[8,10].

Constraints:

0 <= intervals.length <= 104 intervals[i].length == 2 0 <= starti <= endi <= 105 intervals is sorted by starti in ascending order. newInterval.length == 2 0 <= start <= end <= 105

Algorithm

Here is the algorithm :

Add to the output all the intervals starting before newInterval.

Add to the output newInterval. Merge it with the last added interval if newInterval starts before the last added interval.

Add the next intervals one by one. Merge with the last added interval if the current interval starts before the last added interval.

Python
# time = space = O(n)
class Solution:
    def insert(self, intervals: 'List[Interval]', newInterval: 'Interval') -> 'List[Interval]':
        # init data
        new_start, new_end = newInterval
        idx, n = 0, len(intervals)
        output = []
        
        # add all intervals starting before newInterval
        while idx < n and new_start > intervals[idx][0]:
            output.append(intervals[idx])
            idx += 1
            
        # add newInterval
        # if there is no overlap, just add the interval
        if not output or output[-1][1] < new_start:
            output.append(newInterval)
        # if there is an overlap, merge with the last interval
        else:
            output[-1][1] = max(output[-1][1], new_end)
        
        # add next intervals, merge with newInterval if needed
        while idx < n:
            interval = intervals[idx]
            start, end = interval
            idx += 1
            # if there is no overlap, just add an interval
            if output[-1][1] < start:
                output.append(interval)
            # if there is an overlap, merge with the last interval
            else:
                output[-1][1] = max(output[-1][1], end)
        return output

242. Valid Anagram

See the strings notebook

315. Count of Smaller Numbers After Self

Given an integer array nums, return an integer array counts where counts[i] is the number of smaller elements to the right of nums[i].

Example 1: Input: nums = [5,2,6,1] Output: [2,1,1,0] Explanation: To the right of 5 there are 2 smaller elements (2 and 1). To the right of 2 there is only 1 smaller element (1). To the right of 6 there is 1 smaller element (1). To the right of 1 there is 0 smaller element.

Example 2: Input: nums = [-1] Output: [0]

Example 3: Input: nums = [-1,-1] Output: [0,0]

Constraints:

1 <= nums.length <= 105 -104 <= nums[i] <= 104

Algorithm (mergesort)

Implement a merge sort function.

For each element i, the function records the number of elements jumping from i's right to i's left during the merge sort. Merge sort nums, store the number of elements jumping from right to left in result.

Alternatively, one can sort the indices with corresponding values in nums. That is to say, we are going to sort list [0, 1, ..., n-1] according to the comparator nums[i]. This helps to track the indices and update result. You can find additional details in the implementations below. Return result.

Python
# time = O(nlogn), space = O(n)
class Solution:
    def countSmaller(self, nums: List[int]) -> List[int]:
        n = len(nums)
        arr = [[v, i] for i, v in enumerate(nums)]  # record value and index
        result = [0] * n

        def merge_sort(arr, left, right):
            # merge sort [left, right) from small to large, in place
            if right - left <= 1:
                return
            mid = (left + right) // 2
            merge_sort(arr, left, mid)
            merge_sort(arr, mid, right)
            merge(arr, left, right, mid)

        def merge(arr, left, right, mid):
            # merge [left, mid) and [mid, right)
            i = left  # current index for the left array
            j = mid  # current index for the right array
            # use temp to temporarily store sorted array
            temp = []
            while i < mid and j < right:
                if arr[i][0] <= arr[j][0]:
                    # j - mid numbers jump to the left side of arr[i]
                    result[arr[i][1]] += j - mid
                    temp.append(arr[i])
                    i += 1
                else:
                    temp.append(arr[j])
                    j += 1
            # when one of the subarrays is empty
            while i < mid:
                # j - mid numbers jump to the left side of arr[i]
                result[arr[i][1]] += j - mid
                temp.append(arr[i])
                i += 1
            while j < right:
                temp.append(arr[j])
                j += 1
            # restore from temp
            for i in range(left, right):
                arr[i] = temp[i - left]

        merge_sort(arr, 0, n)

        return result

852. Peak Index in a Mountain Array

An array arr a mountain if the following properties hold:

arr.length >= 3 There exists some i with 0 < i < arr.length - 1 such that: arr[0] < arr[1] < ... < arr[i - 1] < arr[i] arr[i] > arr[i + 1] > ... > arr[arr.length - 1] Given a mountain array arr, return the index i such that arr[0] < arr[1] < ... < arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[arr.length - 1].

You must solve it in O(log(arr.length)) time complexity.

Example 1: Input: arr = [0,1,0] Output: 1

Example 2: Input: arr = [0,2,1,0] Output: 1

Example 3: Input: arr = [0,10,5,2] Output: 1

Constraints:

3 <= arr.length <= 105 0 <= arr[i] <= 106 arr is guaranteed to be a mountain array

Algorithm (linear scan)

The mountain increases until it doesn't. The point at which it stops increasing is the peak.

Python
# linear scan
# time = O(n), space O(1)
class Solution(object):
    def peakIndexInMountainArray(self, A):
        for i in xrange(len(A)):
            if A[i] > A[i+1]:
                return i
Python
# binary search
# time = O(logn), space O(1)
class Solution(object):
    def peakIndexInMountainArray(self, A):
        lo, hi = 0, len(A) - 1
        while lo < hi:
            mi = (lo + hi) / 2
            if A[mi] < A[mi + 1]:
                lo = mi + 1
            else:
                hi = mi
        return lo

Dynamic Programming

5. Longest Palindromic Substring

Given a string s, return the longest palindromic substring in s. A string is called a palindrome string if the reverse of that string is the same as the original string.

Example 1: Input: s = "babad" Output: "bab" Explanation: "aba" is also a valid answer.

Example 2: Input: s = "cbbd" Output: "bb"

Constraints:

1 <= s.length <= 1000 s consist of only digits and English letters.

Python
# Java
# time = O(n^2), space = O(1)
class Solution {
    public String longestPalindrome(String s) {
        if (s == null || s.length() < 1) return "";
        int start = 0, end = 0;
        for (int i = 0; i < s.length(); i++) {
            int len1 = expandAroundCenter(s, i, i);
            int len2 = expandAroundCenter(s, i, i + 1);
            int len = Math.max(len1, len2);
            if (len > end - start) {
                start = i - (len - 1) / 2;
                end = i + len / 2;
            }
        }
        return s.substring(start, end + 1);
    }

    private int expandAroundCenter(String s, int left, int right) {
        int L = left, R = right;
        while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) {
            L--;
            R++;
        }
        return R - L - 1;
    }
}

53. Maximum Subarray

Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum. A subarray is a contiguous part of an array.

Example 1: Input: nums = [-2,1,-3,4,-1,2,1,-5,4] Output: 6 Explanation: [4,-1,2,1] has the largest sum = 6.

Example 2: Input: nums = [1] Output: 1

Example 3: Input: nums = [5,4,-1,7,8] Output: 23

Constraints:

1 <= nums.length <= 105 -104 <= nums[i] <= 104

Follow up: If you have figured out the O(n) solution, try coding another solution using the divide and conquer approach, which is more subtle.

Algorithm

Initialize 2 integer variables. Set both of them equal to the first value in the array.

currentSubarray will keep the running count of the current subarray we are focusing on. maxSubarray will be our final return value. Continuously update it whenever we find a bigger subarray. Iterate through the array, starting with the 2nd element (as we used the first element to initialize our variables). For each number, add it to the currentSubarray we are building. If currentSubarray becomes negative, we know it isn't worth keeping, so throw it away. Remember to update maxSubarray every time we find a new maximum.

Return maxSubarray

Python
# time = O(n), space = O(1)
class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        # Initialize our variables using the first element.
        current_subarray = max_subarray = nums[0]
        
        # Start with the 2nd element since we already used the first one.
        for num in nums[1:]:
            # If current_subarray is negative, throw it away. Otherwise, keep adding to it.
            current_subarray = max(num, current_subarray + num)
            max_subarray = max(max_subarray, current_subarray)
        
        return max_subarray

121. Best Time to Buy and Sell Stock

You are given an array prices where prices[i] is the price of a given stock on the ith day. You want to maximize your profit by choosing a single day to buy one stock and choosing a different day in the future to sell that stock. Return the maximum profit you can achieve from this transaction. If you cannot achieve any profit, return 0.

Example 1: Input: prices = [7,1,5,3,6,4] Output: 5 Explanation: Buy on day 2 (price = 1) and sell on day 5 (price = 6), profit = 6-1 = 5. Note that buying on day 2 and selling on day 1 is not allowed because you must buy before you sell.

Example 2: Input: prices = [7,6,4,3,1] Output: 0 Explanation: In this case, no transactions are done and the max profit = 0.

Constraints:

1 <= prices.length <= 105 0 <= prices[i] <= 104

Algorithm Say the given array is:

[7, 1, 5, 3, 6, 4] If we plot the numbers of the given array on a graph, we get: image.png Profit Graph

The points of interest are the peaks and valleys in the given graph. We need to find the largest price following each valley, which difference could be the max profit. We can maintain two variables - minprice and maxprofit corresponding to the smallest valley and maximum profit (maximum difference between selling price and minprice) obtained so far respectively.

Python
# time = O(n), space = O(1)
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        min_price = float('inf')
        max_profit = 0
        for i in range(len(prices)):
            if prices[i] < min_price:
                min_price = prices[i]
            elif prices[i] - min_price > max_profit:
                max_profit = prices[i] - min_price
                
        return max_profit

152. Maximum Product Subarray

Given an integer array nums, find a contiguous non-empty subarray within the array that has the largest product, and return the product. The test cases are generated so that the answer will fit in a 32-bit integer. A subarray is a contiguous subsequence of the array.

Example 1: Input: nums = [2,3,-2,4] Output: 6 Explanation: [2,3] has the largest product 6.

Example 2: Input: nums = [-2,0,-1] Output: 0 Explanation: The result cannot be 2, because [-2,-1] is not a subarray.

Constraints:

1 <= nums.length <= 2 * 104 -10 <= nums[i] <= 10 The product of any prefix or suffix of nums is guaranteed to fit in a 32-bit integer.

Algorithm 2: Dynamic Programming Intuition

Rather than looking for every possible subarray to get the largest product, we can scan the array and solve smaller subproblems.

Let's see this problem as a problem of getting the highest combo chain. The way combo chains work is that they build on top of the previous combo chains that you have acquired. The simplest case is when the numbers in nums are all positive numbers. In that case, you would only need to keep on multiplying the accumulated result to get a bigger and bigger combo chain as you progress.

However, two things can disrupt your combo chain:

Zeros Negative numbers Zeros will reset your combo chain. A high score which you have achieved will be recorded in placeholder result. You will have to restart your combo chain after zero. If you encounter another combo chain which is higher than the recorded high score in result, you just need to update the result.

Negative numbers are a little bit tricky. A single negative number can flip the largest combo chain to a very small number. This may sound like your combo chain has been completely disrupted but if you encounter another negative number, your combo chain can be saved. Unlike zero, you still have a hope of saving your combo chain as long as you have another negative number in nums (Think of this second negative number as an antidote for the poison that you just consumed). However, if you encounter a zero while you are looking your another negative number to save your combo chain, you lose the hope of saving that combo chain.

While going through numbers in nums, we will have to keep track of the maximum product up to that number (we will call max_so_far) and minimum product up to that number (we will call min_so_far). The reason behind keeping track of max_so_far is to keep track of the accumulated product of positive numbers. The reason behind keeping track of min_so_far is to properly handle negative numbers.

max_so_far is updated by taking the maximum value among:

Current number. This value will be picked if the accumulated product has been really bad (even compared to the current number). This can happen when the current number has a preceding zero (e.g. [0,4]) or is preceded by a single negative number (e.g. [-3,5]). Product of last max_so_far and current number. This value will be picked if the accumulated product has been steadily increasing (all positive numbers). Product of last min_so_far and current number. This value will be picked if the current number is a negative number and the combo chain has been disrupted by a single negative number before (In a sense, this value is like an antidote to an already poisoned combo chain). min_so_far is updated in using the same three numbers except that we are taking minimum among the above three numbers.

In the animation below, you will observe a negative number -5 disrupting a combo chain but that combo chain is later saved by another negative number -4. The only reason this can be saved is because of min_so_far. You will also observe a zero disrupting a combo chain

Python
# time = O(n), space = O(1)
class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        if len(nums) == 0:
            return 0

        max_so_far = nums[0]
        min_so_far = nums[0]
        result = max_so_far

        for i in range(1, len(nums)):
            curr = nums[i]
            temp_max = max(curr, max_so_far * curr, min_so_far * curr)
            min_so_far = min(curr, max_so_far * curr, min_so_far * curr)

            max_so_far = temp_max

            result = max(max_so_far, result)

        return result

322. Coin Change

You are given an integer array coins representing coins of different denominations and an integer amount representing a total amount of money. Return the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1. You may assume that you have an infinite number of each kind of coin.

Example 1: Input: coins = [1,2,5], amount = 11 Output: 3 Explanation: 11 = 5 + 5 + 1

Example 2: Input: coins = [2], amount = 3 Output: -1

Example 3: Input: coins = [1], amount = 0 Output: 0

Constraints:

1 <= coins.length <= 12 1 <= coins[i] <= 231 - 1 0 <= amount <= 104

Algorithm (DP top down)

The idea of the algorithm is to build the solution of the problem from top to bottom. It applies the idea described above. It use backtracking and cut the partial solutions in the recursive tree, which doesn't lead to a viable solution. This happens when we try to make a change of a coin with a value greater than the amount SS. To improve time complexity we should store the solutions of the already calculated subproblems in a table

Algorithm (DP bottom up)

image-2.png

Python
# DP top down
# time = O(S∗n). where S is the amount, n is denomination count, space O(S)
from functools import lru_cache
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:

        @lru_cache(None)
        def dfs(rem):
            if rem < 0:
                return -1
            if rem == 0:
                return 0
            min_cost = float('inf')
            for coin in coins:
                res = dfs(rem - coin)
                if res != -1:
                    min_cost = min(min_cost, res + 1)
            return min_cost if min_cost != float('inf') else -1

        return dfs(amount)
Python
# DP bottom up
# time = O(S∗n). where S is the amount, n is denomination count, space O(S)
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        dp = [float('inf')] * (amount + 1)
        dp[0] = 0
        
        for coin in coins:
            for x in range(coin, amount + 1):
                dp[x] = min(dp[x], dp[x - coin] + 1)
        return dp[amount] if dp[amount] != float('inf') else -1 

410. Split Array Largest Sum

Given an integer array nums and an integer k, split nums into k non-empty subarrays such that the largest sum of any subarray is minimized. Return the minimized largest sum of the split. A subarray is a contiguous part of the array.

Example 1: Input: nums = [7,2,5,10,8], k = 2 Output: 18 Explanation: There are four ways to split nums into two subarrays. The best way is to split it into [7,2,5] and [10,8], where the largest sum among the two subarrays is only 18.

Example 2: Input: nums = [1,2,3,4,5], k = 2 Output: 9 Explanation: There are four ways to split nums into two subarrays. The best way is to split it into [1,2,3] and [4,5], where the largest sum among the two subarrays is only 9.

Constraints:

1 <= nums.length <= 1000 0 <= nums[i] <= 106 1 <= k <= min(50, nums.length)

Algorithm (top down)

Fill the array prefixSum. The i-th index of prefixSum will have the sum of integers in nums in the range [0, i - 1] with prefix[0] = 0. (We need prefixSum because each time we reach a base case, we must return the sum of the remaining elements, and a prefix sum array allows us to do this in constant time.) Start with index currIndex as 0 and the number of subarrays subarrayCount as m, this represents the subarray with range [0, n - 1] and m subarrays. Select which elements will go in the current subarray by traversing over the indices starting from currIndex to N - subarrayCount. At each index: Use prefixSum to find the sum of the elements in the current subarray (firstSplitSum). Recursively call getMinimumLargestSplitSum to find the minimum largest subarray that can be obtained from the remaining elements. The maximum of these two values (largestSplitSum) will be the largest subarray sum if the first subarray is [currIndex, i]. Repeat this process for all i up to n - subarrayCount, then store the minimum possible largestSplitSum in minimumLargestSplitSum. Return minimumLargestSplitSum; to avoid repeat calculations, also store it in the memoization table memo corresponding to currIndex and subarrayCount. Base case: If subarrayCount is 1, then we know that all of the remaining numbers must go in the current subarray. So instead of making recursive calls according to step 3, when subarrayCount is 1, we can simply return the sum of numbers between currIndex and the end of the array

Algorithm (bottom up)

Fill the array prefixSum. The i-th index of prefixSum will have the sum of integers in nums in the range [0, i - 1] with prefix[0] = 0. Initialize an array memo where memo[currIndex][subarrayCount] will store the result for the subproblem (currIndex, subarrayCount). We need to find the value of memo[0][m] which represents the minimum largest subarray sum starting at index 0 with m subarrays. But we only know what the result will be for the base cases. To fill the memo array, we will iterate subarrayCount over the range [1, m] (starting at 1 because that is our base case) and iterate currIndex over the range [0, n - 1]. For each value of subarrayCount and currIndex, we will update memo[subarrayCount][currIndex]: As the sum of the elements between currIndex and the end of the array if we are at a base case (subarrayCount equals 1). Otherwise, we will use the recurrence relation and the results from previously solved subproblems to calculate memo[subarrayCount][currIndex]. Return the value stored at memo[0][m].

Python
# top down
# time = O(N^2 * M), where N = len of array and M = # subarrays allowed; space = O(NM)
class Solution:
    def splitArray(self, nums: List[int], m: int) -> int:
        n = len(nums)
        
        # Create a prefix sum array of nums.
        prefix_sum = [0] + list(itertools.accumulate(nums))
        
        @functools.lru_cache(None)
        def get_min_largest_split_sum(curr_index: int, subarray_count: int):
            # Base Case: If there is only one subarray left, then all of the remaining numbers
            # must go in the current subarray. So return the sum of the remaining numbers.
            if subarray_count == 1:
                return prefix_sum[n] - prefix_sum[curr_index]
        
            # Otherwise, use the recurrence relation to determine the minimum largest subarray sum
            # between curr_index and the end of the array with subarray_count subarrays remaining.
            minimum_largest_split_sum = prefix_sum[n]
            for i in range(curr_index, n - subarray_count + 1):
                # Store the sum of the first subarray.
                first_split_sum = prefix_sum[i + 1] - prefix_sum[curr_index]

                # Find the maximum subarray sum for the current first split.
                largest_split_sum = max(first_split_sum, 
                                        get_min_largest_split_sum(i + 1, subarray_count - 1))

                # Find the minimum among all possible combinations.
                minimum_largest_split_sum = min(minimum_largest_split_sum, largest_split_sum)

                if first_split_sum >= minimum_largest_split_sum:
                    break
            
            return minimum_largest_split_sum
        
        return get_min_largest_split_sum(0, m)
Python
# bottom up
# time = O(N^2 * M), where N = len of array and M = # subarrays allowed; space = O(NM)
class Solution:
    def splitArray(self, nums: List[int], m: int) -> int:
        n = len(nums)
        memo = [[0] * (m + 1) for _ in range(n)]
        
        # Create a prefix sum array of nums.
        prefix_sum = [0] + list(itertools.accumulate(nums))
        
        for subarray_count in range(1, m + 1):
            for curr_index in range(n):
                # Base Case: If there is only one subarray left, then all of the remaining numbers
                # must go in the current subarray. So return the sum of the remaining numbers.
                if subarray_count == 1:
                    memo[curr_index][subarray_count] = prefix_sum[n] - prefix_sum[curr_index]
                    continue

                # Otherwise, use the recurrence relation to determine the minimum largest subarray sum
                # between curr_index and the end of the array with subarray_count subarrays remaining.
                minimum_largest_split_sum = prefix_sum[n]
                for i in range(curr_index, n - subarray_count + 1):
                    # Store the sum of the first subarray.
                    first_split_sum = prefix_sum[i + 1] - prefix_sum[curr_index]

                    # Find the maximum subarray sum for the current first split.
                    largest_split_sum = max(first_split_sum, memo[i + 1][subarray_count - 1])

                    # Find the minimum among all possible combinations.
                    minimum_largest_split_sum = min(minimum_largest_split_sum, largest_split_sum)

                    if first_split_sum >= minimum_largest_split_sum:
                        break
            
                memo[curr_index][subarray_count] = minimum_largest_split_sum
        
        return memo[0][m]

Part VII. Design

146. LRU Cache

Design a data structure that follows the constraints of a Least Recently Used (LRU) cache. Implement the LRUCache class:

LRUCache(int capacity) Initialize the LRU cache with positive size capacity. int get(int key) Return the value of the key if the key exists, otherwise return -1. void put(int key, int value) Update the value of the key if the key exists. Otherwise, add the key-value pair to the cache. If the number of keys exceeds the capacity from this operation, evict the least recently used key. The functions get and put must each run in O(1) average time complexity.

Example 1:

Input ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"] [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]] Output [null, null, null, 1, null, -1, null, -1, 3, 4]

Explanation LRUCache lRUCache = new LRUCache(2); lRUCache.put(1, 1); // cache is {1=1} lRUCache.put(2, 2); // cache is {1=1, 2=2} lRUCache.get(1); // return 1 lRUCache.put(3, 3); // LRU key was 2, evicts key 2, cache is {1=1, 3=3} lRUCache.get(2); // returns -1 (not found) lRUCache.put(4, 4); // LRU key was 1, evicts key 1, cache is {4=4, 3=3} lRUCache.get(1); // return -1 (not found) lRUCache.get(3); // return 3 lRUCache.get(4); // return 4

Constraints:

1 <= capacity <= 3000 0 <= key <= 104 0 <= value <= 105 At most 2 * 105 calls will be made to get and put

Intuition

We're asked to implement the structure which provides the following operations in \mathcal{O}(1)O(1) time :

Get the key / Check if the key exists

Put the key

Delete the first added key

The first two operations in O(1) time are provided by the standard hashmap, and the last one - by linked list

Python
class LRUCache(object):

    def __init__(self, capacity):
        """
        :type capacity: int
        """
        

    def get(self, key):
        """
        :type key: int
        :rtype: int
        """
        

    def put(self, key, value):
        """
        :type key: int
        :type value: int
        :rtype: None
        """


# Your LRUCache object will be instantiated and called as such:
# obj = LRUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)
Python
from collections import OrderedDict
class LRUCache(OrderedDict):

    def __init__(self, capacity):
        """
        :type capacity: int
        """
        self.capacity = capacity

    def get(self, key):
        """
        :type key: int
        :rtype: int
        """
        if key not in self:
            return - 1
        
        self.move_to_end(key)
        return self[key]

    def put(self, key, value):
        """
        :type key: int
        :type value: int
        :rtype: void
        """
        if key in self:
            self.move_to_end(key)
        self[key] = value
        if len(self) > self.capacity:
            self.popitem(last = False)
Python
# time = O(1), space = O(capacity)
class DLinkedNode(): 
    def __init__(self):
        self.key = 0
        self.value = 0
        self.prev = None
        self.next = None
            
class LRUCache():
    def _add_node(self, node):
        """
        Always add the new node right after head.
        """
        node.prev = self.head
        node.next = self.head.next

        self.head.next.prev = node
        self.head.next = node

    def _remove_node(self, node):
        """
        Remove an existing node from the linked list.
        """
        prev = node.prev
        new = node.next

        prev.next = new
        new.prev = prev

    def _move_to_head(self, node):
        """
        Move certain node in between to the head.
        """
        self._remove_node(node)
        self._add_node(node)

    def _pop_tail(self):
        """
        Pop the current tail.
        """
        res = self.tail.prev
        self._remove_node(res)
        return res

    def __init__(self, capacity):
        """
        :type capacity: int
        """
        self.cache = {}
        self.size = 0
        self.capacity = capacity
        self.head, self.tail = DLinkedNode(), DLinkedNode()

        self.head.next = self.tail
        self.tail.prev = self.head
        

    def get(self, key):
        """
        :type key: int
        :rtype: int
        """
        node = self.cache.get(key, None)
        if not node:
            return -1

        # move the accessed node to the head;
        self._move_to_head(node)

        return node.value

    def put(self, key, value):
        """
        :type key: int
        :type value: int
        :rtype: void
        """
        node = self.cache.get(key)

        if not node: 
            newNode = DLinkedNode()
            newNode.key = key
            newNode.value = value

            self.cache[key] = newNode
            self._add_node(newNode)

            self.size += 1

            if self.size > self.capacity:
                # pop the tail
                tail = self._pop_tail()
                del self.cache[tail.key]
                self.size -= 1
        else:
            # update the value.
            node.value = value
            self._move_to_head(node)

155. Min Stack

Design a stack that supports push, pop, top, and retrieving the minimum element in constant time.

Implement the MinStack class:

MinStack() initializes the stack object. void push(int val) pushes the element val onto the stack. void pop() removes the element on the top of the stack. int top() gets the top element of the stack. int getMin() retrieves the minimum element in the stack. You must implement a solution with O(1) time complexity for each function.

Example 1:

Input ["MinStack","push","push","push","getMin","pop","top","getMin"] [[],[-2],[0],[-3],[],[],[],[]]

Output [null,null,null,null,-3,null,0,-2]

Explanation MinStack minStack = new MinStack(); minStack.push(-2); minStack.push(0); minStack.push(-3); minStack.getMin(); // return -3 minStack.pop(); minStack.top(); // return 0 minStack.getMin(); // return -2

Constraints:

-2^31 <= val <= 2^31 - 1 Methods pop, top and getMin operations will always be called on non-empty stacks. At most 3 * 104 calls will be made to push, pop, top, and getMin.

Python
# time = O(1), space = O(n)
class MinStack:

    def __init__(self):
        self.stack = []
        

    def push(self, x: int) -> None:
        
        # If the stack is empty, then the min value
        # must just be the first value we add
        if not self.stack:
            self.stack.append((x, x))
            return

        current_min = self.stack[-1][1]
        self.stack.append((x, min(x, current_min)))
        
        
    def pop(self) -> None:
        self.stack.pop()
        

    def top(self) -> int:
        return self.stack[-1][0]
        

    def getMin(self) -> int:
        return self.stack[-1][1]
Python
# time = O(1), space = O(n)
# different approach
class MinStack:

    def __init__(self):
        self.stack = []
        self.min_stack = []        
        

    def push(self, x: int) -> None:
        self.stack.append(x)
        if not self.min_stack or x <= self.min_stack[-1]:
            self.min_stack.append(x)
    
    def pop(self) -> None:
        if self.min_stack[-1] == self.stack[-1]:
            self.min_stack.pop()
        self.stack.pop()

    def top(self) -> int:
        return self.stack[-1]

    def getMin(self) -> int:
        return self.min_stack[-1]
Python
# time = O(1), space = O(n)
# improved different approach
class MinStack:

    def __init__(self):
        self.stack = []
        self.min_stack = []        
        

    def push(self, x: int) -> None:
        
        # We always put the number onto the main stack.
        self.stack.append(x)
        
        # If the min stack is empty, or this number is smaller than
        # the top of the min stack, put it on with a count of 1.
        if not self.min_stack or x < self.min_stack[-1][0]:
            self.min_stack.append([x, 1])
            
        # Else if this number is equal to what's currently at the top
        # of the min stack, then increment the count at the top by 1.
        elif x == self.min_stack[-1][0]:
            self.min_stack[-1][1] += 1

    
    def pop(self) -> None:

        # If the top of min stack is the same as the top of stack
        # then we need to decrement the count at the top by 1.
        if self.min_stack[-1][0] == self.stack[-1]:
            self.min_stack[-1][1] -= 1
            
        # If the count at the top of min stack is now 0, then remove
        # that value as we're done with it.
        if self.min_stack[-1][1] == 0:
            self.min_stack.pop()
            
        # And like before, pop the top of the main stack.
        self.stack.pop()


    def top(self) -> int:
        return self.stack[-1]


    def getMin(self) -> int:
        return self.min_stack[-1][0]   

297. Serialize and Deserialize Binary Tree

Serialization is the process of converting a data structure or object into a sequence of bits so that it can be stored in a file or memory buffer, or transmitted across a network connection link to be reconstructed later in the same or another computer environment.

Design an algorithm to serialize and deserialize a binary tree. There is no restriction on how your serialization/deserialization algorithm should work. You just need to ensure that a binary tree can be serialized to a string and this string can be deserialized to the original tree structure.

Clarification: The input/output format is the same as how LeetCode serializes a binary tree. You do not necessarily need to follow this format, so please be creative and come up with different approaches yourself.

Example 1: image.png Input: root = [1,2,3,null,null,4,5] Output: [1,2,3,null,null,4,5]

Example 2: Input: root = [] Output: []

Constraints:

The number of nodes in the tree is in the range [0, 104]. -1000 <= Node.val <= 1000

Intuition image-2.png The serialization of a Binary Search Tree is essentially to encode its values and more importantly its structure. One can traverse the tree to accomplish the above task. And it is well know that we have two general strategies to do so:

Breadth First Search (BFS)

We scan through the tree level by level, following the order of height, from top to bottom. The nodes on higher level would be visited before the ones with lower levels.

Depth First Search (DFS)

In this strategy, we adopt the depth as the priority, so that one would start from a root and reach all the way down to certain leaf, and then back to root to reach another branch.

The DFS strategy can further be distinguished as preorder, inorder, and postorder depending on the relative order among the root node, left node and right node.

In this task, however, the DFS strategy is more adapted for our needs, since the linkage among the adjacent nodes is naturally encoded in the order, which is rather helpful for the later task of deserialization.

Therefore, in this solution, we demonstrate an example with the preorder DFS strategy.

Python
# time = space = O(n)
# Depth First Search (DFS)
class TreeNode(object):
    """ Definition of a binary tree node."""
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None
        
        
# Serialization 
class Codec:

    def serialize(self, root):
        """ Encodes a tree to a single string.
        :type root: TreeNode
        :rtype: str
        """
        def rserialize(root, string):
            """ a recursive helper function for the serialize() function."""
            # check base case
            if root is None:
                string += 'None,'
            else:
                string += str(root.val) + ','
                string = rserialize(root.left, string)
                string = rserialize(root.right, string)
            return string
        
        return rserialize(root, '')
    
    
# Deserialization 
class Codec:

    def deserialize(self, data):
        """Decodes your encoded data to tree.
        :type data: str
        :rtype: TreeNode
        """
        def rdeserialize(l):
            """ a recursive helper function for deserialization."""
            if l[0] == 'None':
                l.pop(0)
                return None
                
            root = TreeNode(l[0])
            l.pop(0)
            root.left = rdeserialize(l)
            root.right = rdeserialize(l)
            return root

        data_list = data.split(',')
        root = rdeserialize(data_list)
        return root

359. Logger Rate Limiter

Design a logger system that receives a stream of messages along with their timestamps. Each unique message should only be printed at most every 10 seconds (i.e. a message printed at timestamp t will prevent other identical messages from being printed until timestamp t + 10).

All messages will come in chronological order. Several messages may arrive at the same timestamp.

Implement the Logger class:

Logger() Initializes the logger object. bool shouldPrintMessage(int timestamp, string message) Returns true if the message should be printed in the given timestamp, otherwise returns false.

Example 1:

Input ["Logger", "shouldPrintMessage", "shouldPrintMessage", "shouldPrintMessage", "shouldPrintMessage", "shouldPrintMessage", "shouldPrintMessage"] [[], [1, "foo"], [2, "bar"], [3, "foo"], [8, "bar"], [10, "foo"], [11, "foo"]] Output [null, true, true, false, false, false, true]

Explanation Logger logger = new Logger(); logger.shouldPrintMessage(1, "foo"); // return true, next allowed timestamp for "foo" is 1 + 10 = 11 logger.shouldPrintMessage(2, "bar"); // return true, next allowed timestamp for "bar" is 2 + 10 = 12 logger.shouldPrintMessage(3, "foo"); // 3 < 11, return false logger.shouldPrintMessage(8, "bar"); // 8 < 12, return false logger.shouldPrintMessage(10, "foo"); // 10 < 11, return false logger.shouldPrintMessage(11, "foo"); // 11 >= 11, return true, next allowed timestamp for "foo" is 11 + 10 = 21

Constraints:

0 <= timestamp <= 10^9 Every timestamp will be passed in non-decreasing order (chronological order). 1 <= message.length <= 30 At most 10^4 calls will be made to shouldPrintMessage

Algorithm

We initialize a hashtable/dictionary to keep the messages along with the timestamp.

At the arrival of a new message, the message is eligible to be printed with either of the two conditions as follows:

case 1). we have never seen the message before.

case 2). we have seen the message before, and it was printed more than 10 seconds ago.

In both of the above cases, we would then update the entry that is associated with the message in the hashtable, with the latest timestamp

Python
# time = O(1), space = O(n) where n is the size of all incoming messages
class Logger(object):

    def __init__(self):
        """
        Initialize your data structure here.
        """
        self._msg_dict = {}
    
    def shouldPrintMessage(self, timestamp, message):
        """
        Returns true if the message should be printed in the given timestamp, otherwise returns false.
        """
        if message not in self._msg_dict:
            # case 1). add the message to print
            self._msg_dict[message] = timestamp
            return True

        if timestamp - self._msg_dict[message] >= 10:
            # case 2). update the timestamp of the message
            self._msg_dict[message] = timestamp
            return True
        else:
            return False

380. Insert Delete GetRandom O(1)

Implement the RandomizedSet class:

RandomizedSet() Initializes the RandomizedSet object. bool insert(int val) Inserts an item val into the set if not present. Returns true if the item was not present, false otherwise. bool remove(int val) Removes an item val from the set if present. Returns true if the item was present, false otherwise. int getRandom() Returns a random element from the current set of elements (it's guaranteed that at least one element exists when this method is called). Each element must have the same probability of being returned. You must implement the functions of the class such that each function works in average O(1) time complexity

Example 1:

Input ["RandomizedSet", "insert", "remove", "insert", "getRandom", "remove", "insert", "getRandom"] [[], [1], [2], [2], [], [1], [2], []] Output [null, true, false, true, 2, true, false, 2]

Explanation RandomizedSet randomizedSet = new RandomizedSet(); randomizedSet.insert(1); // Inserts 1 to the set. Returns true as 1 was inserted successfully. randomizedSet.remove(2); // Returns false as 2 does not exist in the set. randomizedSet.insert(2); // Inserts 2 to the set, returns true. Set now contains [1,2]. randomizedSet.getRandom(); // getRandom() should return either 1 or 2 randomly. randomizedSet.remove(1); // Removes 1 from the set, returns true. Set now contains [2]. randomizedSet.insert(2); // 2 was already in the set, so return false. randomizedSet.getRandom(); // Since 2 is the only number in the set, getRandom() will always return 2.

Constraints:

-231 <= val <= 231 - 1 At most 2 * 105 calls will be made to insert, remove, and getRandom. There will be at least one element in the data structure when getRandom is called.

Python
# time = O(1), space = O(n)
from random import choice
class RandomizedSet():
    def __init__(self):
        """
        Initialize your data structure here.
        """
        self.dict = {}
        self.list = []

        
    def insert(self, val: int) -> bool:
        """
        Inserts a value to the set. Returns true if the set did not already contain the specified element.
        """
        if val in self.dict:
            return False
        self.dict[val] = len(self.list)
        self.list.append(val)
        return True
        

    def remove(self, val: int) -> bool:
        """
        Removes a value from the set. Returns true if the set contained the specified element.
        """
        if val in self.dict:
            # move the last element to the place idx of the element to delete
            last_element, idx = self.list[-1], self.dict[val]
            self.list[idx], self.dict[last_element] = last_element, idx
            # delete the last element
            self.list.pop()
            del self.dict[val]
            return True
        return False

    def getRandom(self) -> int:
        """
        Get a random element from the set.
        """
        return choice(self.list)

642. Design Search Autocomplete System

Design a search autocomplete system for a search engine. Users may input a sentence (at least one word and end with a special character '#').

You are given a string array sentences and an integer array times both of length n where sentences[i] is a previously typed sentence and times[i] is the corresponding number of times the sentence was typed. For each input character except '#', return the top 3 historical hot sentences that have the same prefix as the part of the sentence already typed.

Here are the specific rules:

The hot degree for a sentence is defined as the number of times a user typed the exactly same sentence before. The returned top 3 hot sentences should be sorted by hot degree (The first is the hottest one). If several sentences have the same hot degree, use ASCII-code order (smaller one appears first). If less than 3 hot sentences exist, return as many as you can. When the input is a special character, it means the sentence ends, and in this case, you need to return an empty list. Implement the AutocompleteSystem class:

AutocompleteSystem(String[] sentences, int[] times) Initializes the object with the sentences and times arrays. List input(char c) This indicates that the user typed the character c. Returns an empty array [] if c == '#' and stores the inputted sentence in the system. Returns the top 3 historical hot sentences that have the same prefix as the part of the sentence already typed. If there are fewer than 3 matches, return them all.

Example 1:

Input ["AutocompleteSystem", "input", "input", "input", "input"] [[["i love you", "island", "iroman", "i love leetcode"], [5, 3, 2, 2]], ["i"], [" "], ["a"], ["#"]] Output [null, ["i love you", "island", "i love leetcode"], ["i love you", "i love leetcode"], [], []]

Explanation AutocompleteSystem obj = new AutocompleteSystem(["i love you", "island", "iroman", "i love leetcode"], [5, 3, 2, 2]); obj.input("i"); // return ["i love you", "island", "i love leetcode"]. There are four sentences that have prefix "i". Among them, "ironman" and "i love leetcode" have same hot degree. Since ' ' has ASCII code 32 and 'r' has ASCII code 114, "i love leetcode" should be in front of "ironman". Also we only need to output top 3 hot sentences, so "ironman" will be ignored. obj.input(" "); // return ["i love you", "i love leetcode"]. There are only two sentences that have prefix "i ". obj.input("a"); // return []. There are no sentences that have prefix "i a". obj.input("#"); // return []. The user finished the input, the sentence "i a" should be saved as a historical sentence in system. And the following input will be counted as a new search.

Constraints:

n == sentences.length n == times.length 1 <= n <= 100 1 <= sentences[i].length <= 100 1 <= times[i] <= 50 c is a lowercase English letter, a hash '#', or space ' '. Each tested sentence will be a sequence of characters c that end with the character '#'. Each tested sentence will have a length in the range [1, 200]. The words in each input sentence are separated by single spaces. At most 5000 calls will be made to input

No oficial Leetcocde solution (see comments)

Part VIII. Other

7. Reverse Integer

Given a signed 32-bit integer x, return x with its digits reversed. If reversing x causes the value to go outside the signed 32-bit integer range [-231, 231 - 1], then return 0.

Assume the environment does not allow you to store 64-bit integers (signed or unsigned).

Example 1:

Input: x = 123 Output: 321 Example 2:

Input: x = -123 Output: -321 Example 3:

Input: x = 120 Output: 21

Constraints:

-231 <= x <= 231 - 1

Algorithm

Reversing an integer can be done similarly to reversing a string.

We want to repeatedly "pop" the last digit off of xx and "push" it to the back of the \text{rev}rev. In the end, \text{rev}rev will be the reverse of the x. To "pop" and "push" digits without the help of some auxiliary stack/array, we can use math.

//pop operation: pop = x % 10; x /= 10;

//push operation: temp = rev * 10 + pop; rev = temp;

However, this approach is dangerous, because the statement temp=rev⋅10+pop can cause overflow. Luckily, it is easy to check beforehand whether or this statement would cause an overflow

Python
MAX_VALUE = 2147483647
MIN_VALUE = -2147483648

def reverse_integer(num: int) -> int:
    if num < 0:
        num     = abs(num)
        EXTREME = abs(MIN_VALUE)
        pop_max = 8
        sign = -1
    else:
        EXTREME = MAX_VALUE
        pop_max = 7
        sign    = 1
        
    rev = 0
    while num != 0:
        pop = num % 10
        num = num//10
        if rev > EXTREME//10 or (rev == EXTREME//10 and pop > pop_max): return 0
        rev = rev*10 + pop
    return sign*rev

nums = [123, -123, 120]
for num in nums:
    print(reverse_integer(num))
Output
321
-321
21
Python
# time = O(log(x)) - roughly log10(x) digits in x; space = O(1)
# Java
class Solution {
    public int reverse(int x) {
        int rev = 0;
        while (x != 0) {
            int pop = x % 10;
            x /= 10;
            if (rev > Integer.MAX_VALUE/10 || (rev == Integer.MAX_VALUE / 10 && pop > 7)) return 0;
            if (rev < Integer.MIN_VALUE/10 || (rev == Integer.MIN_VALUE / 10 && pop < -8)) return 0;
            rev = rev * 10 + pop;
        }
        return rev;
    }
}

135. Candy

There are n children standing in a line. Each child is assigned a rating value given in the integer array ratings.

You are giving candies to these children subjected to the following requirements:

Each child must have at least one candy. Children with a higher rating get more candies than their neighbors. Return the minimum number of candies you need to have to distribute the candies to the children.

Example 1: Input: ratings = [1,0,2] Output: 5 Explanation: You can allocate to the first, second and third child with 2, 1, 2 candies respectively.

Example 2: Input: ratings = [1,2,2] Output: 4 Explanation: You can allocate to the first, second and third child with 1, 2, 1 candies respectively. The third child gets 1 candy because it satisfies the above two conditions.

Constraints:

n == ratings.length 1 <= n <= 2 * 10^4 0 <= ratings[i] <= 2 * 10^4

Python
# time = space = O(n)
# Java
public class Solution {
    public int candy(int[] ratings) {
        int[] candies = new int[ratings.length];
        Arrays.fill(candies, 1);
        for (int i = 1; i < ratings.length; i++) {
            if (ratings[i] > ratings[i - 1]) {
                candies[i] = candies[i - 1] + 1;
            }
        }
        int sum = candies[ratings.length - 1];
        for (int i = ratings.length - 2; i >= 0; i--) {
            if (ratings[i] > ratings[i + 1]) {
                candies[i] = Math.max(candies[i], candies[i + 1] + 1);
            }
            sum += candies[i];
        }
        return sum;
    }
}
Python
# time = O(n), space = O(1)
# Java
public class Solution {
    public int count(int n) {
        return (n * (n + 1)) / 2;
    }
    public int candy(int[] ratings) {
        if (ratings.length <= 1) {
            return ratings.length;
        }
        int candies = 0;
        int up = 0;
        int down = 0;
        int oldSlope = 0;
        for (int i = 1; i < ratings.length; i++) {
            int newSlope = (ratings[i] > ratings[i - 1]) ? 1 
                : (ratings[i] < ratings[i - 1] ? -1 
                : 0);

            if ((oldSlope > 0 && newSlope == 0) || (oldSlope < 0 && newSlope >= 0)) {
                candies += count(up) + count(down) + Math.max(up, down);
                up = 0;
                down = 0;
            }
            if (newSlope > 0) {
                up++;
            } else if (newSlope < 0) {
                down++;
            } else {
                candies++;
            }

            oldSlope = newSlope;
        }
        candies += count(up) + count(down) + Math.max(up, down) + 1;
        return candies;
    }
}

205. Isomorphic Strings

Given two strings s and t, determine if they are isomorphic.

Two strings s and t are isomorphic if the characters in s can be replaced to get t.

All occurrences of a character must be replaced with another character while preserving the order of characters. No two characters may map to the same character, but a character may map to itself.

Example 1: Input: s = "egg", t = "add" Output: true

Example 2: Input: s = "foo", t = "bar" Output: false

Example 3: Input: s = "paper", t = "title" Output: true

Constraints:

1 <= s.length <= 5 * 10^4 t.length == s.length s and t consist of any valid ascii character

Algorithm

We define a dictionary mapping_s_t which will be used to map characters in string s to characters in string t and another dictionary mapping_t_s which will be used to map characters in string t to characters in string s. Next, we iterate over the two strings one character at a time. Let's assume the character in the first string is c1 and the corresponding character in the second string is c2. If c1 does not have a mapping in mapping_s_t and c2 does not have a mapping in mapping_t_s, we add the corresponding mappings in both the dictionaries and move on to the next character. At this point, we expect both the character mappings to exist in the dictionaries and their values should be mapping_s_t[c1] = c2 and mapping_t_s[c2] = c1. If either of these conditions fails (c1 is not in the dictionary, c2 is not in the dictionary, unexpected mapping), we return false. Return true once both the strings have been exhausted.

Python
# time = O(n), space = O(1)
class Solution:
    def isIsomorphic(self, s: str, t: str) -> bool:
        
        mapping_s_t = {}
        mapping_t_s = {}
        
        for c1, c2 in zip(s, t):
            
            # Case 1: No mapping exists in either of the dictionaries
            if (c1 not in mapping_s_t) and (c2 not in mapping_t_s):
                mapping_s_t[c1] = c2
                mapping_t_s[c2] = c1
            
            # Case 2: Ether mapping doesn't exist in one of the dictionaries or Mapping exists and
            # it doesn't match in either of the dictionaries or both            
            elif mapping_s_t.get(c1) != c2 or mapping_t_s.get(c2) != c1:
                return False
            
        return True

246. Strobogrammatic Number

Given a string num which represents an integer, return true if num is a strobogrammatic number. A strobogrammatic number is a number that looks the same when rotated 180 degrees (looked at upside down).

Example 1: Input: num = "69" Output: true

Example 2: Input: num = "88" Output: true

Example 3: Input: num = "962" Output: false

Constraints:

1 <= num.length <= 50 num consists of only digits. num does not contain any leading zeros except for zero itself

Python
# time = O(n), space = O(1)
class Solution:
    def isStrobogrammatic(self, num: str) -> bool:
        
        rotated_digits = {'0': '0', '1': '1', '8': '8', '6': '9', '9': '6'}
        
        left = 0 
        right = len(num) - 1
        
        while left <= right:
            if num[left] not in rotated_digits \
                    or rotated_digits[num[left]] != num[right]:
                return False
            left += 1
            right -= 1
        return True

299. Bulls and Cows

You are playing the Bulls and Cows game with your friend.

You write down a secret number and ask your friend to guess what the number is. When your friend makes a guess, you provide a hint with the following info:

The number of "bulls", which are digits in the guess that are in the correct position. The number of "cows", which are digits in the guess that are in your secret number but are located in the wrong position. Specifically, the non-bull digits in the guess that could be rearranged such that they become bulls. Given the secret number secret and your friend's guess guess, return the hint for your friend's guess.

The hint should be formatted as "xAyB", where x is the number of bulls and y is the number of cows. Note that both secret and guess may contain duplicate digits.

Example 1: Input: secret = "1807", guess = "7810" Output: "1A3B" Explanation: Bulls are connected with a '|' and cows are underlined: "1807" | "7810"

Example 2: Input: secret = "1123", guess = "0111" Output: "1A1B" Explanation: Bulls are connected with a '|' and cows are underlined: "1123" "1123" | or | "0111" "0111" Note that only one of the two unmatched 1s is counted as a cow since the non-bull digits can only be rearranged to allow one 1 to be a bull.

Constraints:

1 <= secret.length, guess.length <= 1000 secret.length == guess.length secret and guess consist of digits only

Algorithm

Initialize the number of bulls and cows to zero.

Initialize the hashmap to count characters. During the iteration, secret string gives a positive contribution, and guess - negative contribution.

Iterate over the strings: s is the current character in the string secret and g - the current character in the string guess.

If s == g, update bulls counter: bulls += 1.

Otherwise, if s != g:

Update cows by adding 1 if so far guess contains more s characters than secret: h[s] < 0.

Update cows by adding 1 if so far secret contains more g characters than guess: h[g] > 0.

Update the hashmap by marking the presence of s character in the string secret: h[s] += 1.

Update the hashmap by marking the presence of g character in the string guess: h[g] -= 1.

Return the number of bulls and cows.

Python
# time = O(n), space = O(1)
# one pass
class Solution:
    def getHint(self, secret: str, guess: str) -> str:
        h = defaultdict(int)
        bulls = cows = 0

        for idx, s in enumerate(secret):
            g = guess[idx]
            if s == g: 
                bulls += 1
            else:
                cows += int(h[s] < 0) + int(h[g] > 0)
                h[s] += 1
                h[g] -= 1
                
        return "{}A{}B".format(bulls, cows)

308. Range Sum Query 2D - Mutable

Given a 2D matrix matrix, handle multiple queries of the following types:

Update the value of a cell in matrix. Calculate the sum of the elements of matrix inside the rectangle defined by its upper left corner (row1, col1) and lower right corner (row2, col2). Implement the NumMatrix class:

NumMatrix(int[][] matrix) Initializes the object with the integer matrix matrix. void update(int row, int col, int val) Updates the value of matrix[row][col] to be val. int sumRegion(int row1, int col1, int row2, int col2) Returns the sum of the elements of matrix inside the rectangle defined by its upper left corner (row1, col1) and lower right corner (row2, col2).

Example 1: image.png Input ["NumMatrix", "sumRegion", "update", "sumRegion"] [[[[3, 0, 1, 4, 2], [5, 6, 3, 2, 1], [1, 2, 0, 1, 5], [4, 1, 0, 1, 7], [1, 0, 3, 0, 5]]], [2, 1, 4, 3], [3, 2, 2], [2, 1, 4, 3]] Output [null, 8, null, 10]

Explanation NumMatrix numMatrix = new NumMatrix([[3, 0, 1, 4, 2], [5, 6, 3, 2, 1], [1, 2, 0, 1, 5], [4, 1, 0, 1, 7], [1, 0, 3, 0, 5]]); numMatrix.sumRegion(2, 1, 4, 3); // return 8 (i.e. sum of the left red rectangle) numMatrix.update(3, 2, 2); // matrix changes from left image to right image numMatrix.sumRegion(2, 1, 4, 3); // return 10 (i.e. sum of the right red rectangle)

Constraints:

m == matrix.length n == matrix[i].length 1 <= m, n <= 200 -10^5 <= matrix[i][j] <= 10^5 0 <= row < m 0 <= col < n -10^5 <= val <= 10^5 0 <= row1 <= row2 < m 0 <= col1 <= col2 < n At most 10^4 calls will be made to sumRegion and update

Python
# Java
# complex complexity for each method
class NumMatrix {
    private int rows;
    private int cols;
    private int[][] bit; // The BIT matrix

    private int lsb(int n) {
        // the line below allows us to directly capture the right most non-zero bit of a number
        return n & (-n);
    }

    private void updateBIT(int r, int c, int val) {
        // keep adding lsb(i) to i, lsb(j) to j and add val to bit[i][j]
        // Using two nested for loops, one for the rows and one for the columns
        for (int i = r; i <= rows; i += lsb(i)) {
            for (int j = c; j <= cols; j += lsb(j)) {
                this.bit[i][j] += val;
            }
        }
    }

    private int queryBIT(int r, int c) {
        int sum = 0;
        // keep subtracting lsb(i) to i, lsb(j) to j and obtain the final sum as the sum of non-overlapping sub-rectangles
        // Using two nested for loops, one for the rows and one for the columns
        for (int i = r; i > 0; i -= lsb(i)) {
            for (int j = c; j > 0; j -= lsb(j)) {
                sum += this.bit[i][j];
            }
        }
        return sum;
    }

    private void buildBIT(int[][] matrix) {
        for (int i = 1; i <= rows; ++i) {
            for (int j = 1; j <= cols; ++j) {
                // call update function on each of the entries present in the matrix
                int val = matrix[i - 1][j - 1];
                updateBIT(i, j, val);
            }
        }
    }

    public NumMatrix(int[][] matrix) {
        rows = matrix.length;
        if (rows == 0) return;
        cols = matrix[0].length;
        bit = new int[rows + 1][];
        // Using 1 based indexing, hence resizing the bit array to (rows + 1, cols + 1)
        for (int i = 1; i <= rows; ++i)
            bit[i] = new int[cols + 1];
        buildBIT(matrix);
    }

    public void update(int row, int col, int val) {
        int old_val = sumRegion(row, col, row, col);
        // handling 1-based indexing
        row++; col++;
        int diff = val - old_val;
        updateBIT(row, col, diff);
    }

    public int sumRegion(int row1, int col1, int row2, int col2) {
        // handling 1-based indexing
        row1++; col1++; row2++; col2++;
        int a = queryBIT(row2, col2);
        int b = queryBIT(row1 - 1, col1 - 1);
        int c = queryBIT(row2, col1 - 1);
        int d = queryBIT(row1 - 1, col2);
        return (a + b) - (c + d);
    }
};

731. My Calendar II

You are implementing a program to use as your calendar. We can add a new event if adding the event will not cause a triple booking.

A triple booking happens when three events have some non-empty intersection (i.e., some moment is common to all the three events.).

The event can be represented as a pair of integers start and end that represents a booking on the half-open interval [start, end), the range of real numbers x such that start <= x < end.

Implement the MyCalendarTwo class:

MyCalendarTwo() Initializes the calendar object. boolean book(int start, int end) Returns true if the event can be added to the calendar successfully without causing a triple booking. Otherwise, return false and do not add the event to the calendar.

Example 1:

Input ["MyCalendarTwo", "book", "book", "book", "book", "book", "book"] [[], [10, 20], [50, 60], [10, 40], [5, 15], [5, 10], [25, 55]] Output [null, true, true, true, false, true, true]

Explanation MyCalendarTwo myCalendarTwo = new MyCalendarTwo(); myCalendarTwo.book(10, 20); // return True, The event can be booked. myCalendarTwo.book(50, 60); // return True, The event can be booked. myCalendarTwo.book(10, 40); // return True, The event can be double booked. myCalendarTwo.book(5, 15); // return False, The event cannot be booked, because it would result in a triple booking. myCalendarTwo.book(5, 10); // return True, The event can be booked, as it does not use time 10 which is already double booked. myCalendarTwo.book(25, 55); // return True, The event can be booked, as the time in [25, 40) will be double booked with the third event, the time [40, 50) will be single booked, and the time [50, 55) will be double booked with the second event.

Constraints:

0 <= start < end <= 109 At most 1000 calls will be made to book

Algorithm

Evidently, two events [s1, e1) and [s2, e2) do not conflict if and only if one of them starts after the other one ends: either e1 <= s2 OR e2 <= s1. By De Morgan's laws, this means the events conflict when s1 < e2 AND s2 < e1.

If our event conflicts with a double booking, it's invalid. Otherwise, we add conflicts with the calendar to our double bookings, and add the event to our calendar

Python
# time = O(n^2), space = O(n)
class MyCalendarTwo:
    def __init__(self):
        self.calendar = []
        self.overlaps = []

    def book(self, start, end):
        for i, j in self.overlaps:
            if start < j and end > i:
                return False
        for i, j in self.calendar:
            if start < j and end > i:
                self.overlaps.append((max(start, i), min(end, j)))
        self.calendar.append((start, end))
        return True

771. Jewels and Stones

You're given strings jewels representing the types of stones that are jewels, and stones representing the stones you have. Each character in stones is a type of stone you have. You want to know how many of the stones you have are also jewels.

Letters are case sensitive, so "a" is considered a different type of stone from "A".

Example 1: Input: jewels = "aA", stones = "aAAbbbb" Output: 3

Example 2: Input: jewels = "z", stones = "ZZ" Output: 0

Constraints:

1 <= jewels.length, stones.length <= 50 jewels and stones consist of only English letters. All the characters of jewels are unique

Python
# time = O(J.length∗S.length)), space O(1) in Python
# For each stone, check whether it matches any of the jewels. We can check with a linear scan
class Solution(object):
    def numJewelsInStones(self, J, S):
        return sum(s in J for s in S)
Python
# time = O(J.length∗S.length)), space O(J.length)
# For each stone, check whether it matches any of the jewels. We can check efficiently with a Hash Set
class Solution(object):
    def numJewelsInStones(self, J, S):
        Jset = set(J)
        return sum(s in Jset for s in S)
Python
J = "aA"
S = "aAAbbbb"
sum([s in J for s in S])
Output
3

777. Swap Adjacent in LR String

In a string composed of 'L', 'R', and 'X' characters, like "RXXLRXRXL", a move consists of either replacing one occurrence of "XL" with "LX", or replacing one occurrence of "RX" with "XR". Given the starting string start and the ending string end, return True if and only if there exists a sequence of moves to transform one string to the other.

Example 1:

Input: start = "RXXLRXRXL", end = "XRLXXRRLX" Output: true Explanation: We can transform start to end following these steps: RXXLRXRXL -> XRXLRXRXL -> XRLXRXRXL -> XRLXXRRXL -> XRLXXRRLX

Example 2: Input: start = "X", end = "L" Output: false

Constraints:

1 <= start.length <= 10^4 start.length == end.length Both start and end will only consist of characters in 'L', 'R', and 'X'

No offical solution on Leetcode (see Leetcode comments)

843. Guess the Word

You are given an array of unique strings words where words[i] is six letters long. One word of words was chosen as a secret word.

You are also given the helper object Master. You may call Master.guess(word) where word is a six-letter-long string, and it must be from words. Master.guess(word) returns:

-1 if word is not from words, or an integer representing the number of exact matches (value and position) of your guess to the secret word. There is a parameter allowedGuesses for each test case where allowedGuesses is the maximum number of times you can call Master.guess(word).

For each test case, you should call Master.guess with the secret word without exceeding the maximum number of allowed guesses. You will get:

"Either you took too many guesses, or you did not find the secret word." if you called Master.guess more than allowedGuesses times or if you did not call Master.guess with the secret word, or "You guessed the secret word correctly." if you called Master.guess with the secret word with the number of calls to Master.guess less than or equal to allowedGuesses. The test cases are generated such that you can guess the secret word with a reasonable strategy (other than using the bruteforce method).

Example 1:

Input: secret = "acckzz", words = ["acckzz","ccbazz","eiowzz","abcczz"], allowedGuesses = 10 Output: You guessed the secret word correctly. Explanation: master.guess("aaaaaa") returns -1, because "aaaaaa" is not in wordlist. master.guess("acckzz") returns 6, because "acckzz" is secret and has all 6 matches. master.guess("ccbazz") returns 3, because "ccbazz" has 3 matches. master.guess("eiowzz") returns 2, because "eiowzz" has 2 matches. master.guess("abcczz") returns 4, because "abcczz" has 4 matches. We made 5 calls to master.guess, and one of them was the secret, so we pass the test case.

Example 2: Input: secret = "hamada", words = ["hamada","khaled"], allowedGuesses = 10 Output: You guessed the secret word correctly. Explanation: Since there are two words, you can guess both.

Constraints:

1 <= words.length <= 100 words[i].length == 6 words[i] consist of lowercase English letters. All the strings of wordlist are unique. secret exists in words. 10 <= allowedGuesses <= 30

No offical solution on Leetcode (see Leetcode comments)

939. Minimum Area Rectangle

You are given an array of points in the X-Y plane points where points[i] = [xi, yi].

Return the minimum area of a rectangle formed from these points, with sides parallel to the X and Y axes. If there is not any such rectangle, return 0.

Example 1: image.png Input: points = [[1,1],[1,3],[3,1],[3,3],[2,2]] Output: 4

Example 2: image-2.png Input: points = [[1,1],[1,3],[3,1],[3,3],[4,1],[4,3]] Output: 2

Constraints:

1 <= points.length <= 500 points[i].length == 2 0 <= xi, yi <= 4 * 104 All the given points are unique

Algorithm 1

Group the points by x coordinates, so that we have columns of points. Then, for every pair of points in a column (with coordinates (x,y1) and (x,y2)), check for the smallest rectangle with this pair of points as the rightmost edge. We can do this by keeping memory of what pairs of points we've seen before

Algorithm 2

For each pair of points in the array, consider them to be the long diagonal of a potential rectangle. We can check if all 4 points are there using a Set.

For example, if the points are (1, 1) and (5, 5), we check if we also have (1, 5) and (5, 1). If we do, we have a candidate rectangle.

Put all the points in a set. For each pair of points, if the associated rectangle are 4 distinct points all in the set, then take the area of this rectangle as a candidate answer

Python
# time = O(n^2), space = O(n)
class Solution(object):
    def minAreaRect(self, points):
        columns = collections.defaultdict(list)
        for x, y in points:
            columns[x].append(y)
        lastx = {}
        ans = float('inf')

        for x in sorted(columns):
            column = columns[x]
            column.sort()
            for j, y2 in enumerate(column):
                for i in xrange(j):
                    y1 = column[i]
                    if (y1, y2) in lastx:
                        ans = min(ans, (x - lastx[y1,y2]) * (y2 - y1))
                    lastx[y1, y2] = x
        return ans if ans < float('inf') else 0
Python
# time = O(n^2), space = O(n)
class Solution(object):
    def minAreaRect(self, points):
        S = set(map(tuple, points))
        ans = float('inf')
        for j, p2 in enumerate(points):
            for i in xrange(j):
                p1 = points[i]
                if (p1[0] != p2[0] and p1[1] != p2[1] and
                        (p1[0], p2[1]) in S and (p2[0], p1[1]) in S):
                    ans = min(ans, abs(p2[0] - p1[0]) * abs(p2[1] - p1[1]))
        return ans if ans < float('inf') else 0

A Few Problems from the Very First Interview Process Chapter (Simple?)

904. Fruit Into Baskets

Solution You are visiting a farm that has a single row of fruit trees arranged from left to right. The trees are represented by an integer array fruits where fruits[i] is the type of fruit the ith tree produces.

You want to collect as much fruit as possible. However, the owner has some strict rules that you must follow:

You only have two baskets, and each basket can only hold a single type of fruit. There is no limit on the amount of fruit each basket can hold. Starting from any tree of your choice, you must pick exactly one fruit from every tree (including the start tree) while moving to the right. The picked fruits must fit in one of your baskets. Once you reach a tree with fruit that cannot fit in your baskets, you must stop. Given the integer array fruits, return the maximum number of fruits you can pick.

Example 1:

Input: fruits = [1,2,1] Output: 3 Explanation: We can pick from all 3 trees. Example 2:

Input: fruits = [0,1,2,2] Output: 3 Explanation: We can pick from trees [1,2,2]. If we had started at the first tree, we would only pick from trees [0,1]. Example 3:

Input: fruits = [1,2,3,2,2] Output: 4 Explanation: We can pick from trees [2,3,2,2]. If we had started at the first tree, we would only pick from trees [1,2].

Constraints:

1 <= fruits.length <= 105 0 <= fruits[i] < fruits.length

Algorithm Start with an empty window with left and right as its left and right index. We iterate over right and add fruits[right] to this window. If the number is no larger than 2, meaning that we collect no more than 2 types of fruits, this subarray is valid. Otherwise, it is not the right time to expand the window and we must keep its size. Since we have added one fruit from the right side, we should remove one fruit from the left side of the window, and increment left by 1. Once we are done iterating, the difference between left and right stands for the longest valid subarray we encountered, i.e. the maximum number of fruits we can collect. Implementation

Python
# time = space = O(n)
class Solution:
    def totalFruit(self, fruits: List[int]) -> int:
        # Hash map 'basket' to store the types of fruits.
        basket = {}
        left = 0
        
        # Add fruit from the right index (right) of the window.
        for right, fruit in enumerate(fruits):
            basket[fruit] = basket.get(fruit, 0) + 1

            # If the current window has more than 2 types of fruit,
            # we remove one fruit from the left index (left) of the window.
            if len(basket) > 2:
                basket[fruits[left]] -= 1

                # If the number of fruits[left] is 0, remove it from the basket.
                if basket[fruits[left]] == 0:
                    del basket[fruits[left]]
                left += 1
        
        # Once we finish the iteration, the indexes left and right 
        # stands for the longest valid subarray we encountered.
        return right - left + 1

975. Odd Even Jump

You are given an integer array arr. From some starting index, you can make a series of jumps. The (1st, 3rd, 5th, ...) jumps in the series are called odd-numbered jumps, and the (2nd, 4th, 6th, ...) jumps in the series are called even-numbered jumps. Note that the jumps are numbered, not the indices.

You may jump forward from index i to index j (with i < j) in the following way:

During odd-numbered jumps (i.e., jumps 1, 3, 5, ...), you jump to the index j such that arr[i] <= arr[j] and arr[j] is the smallest possible value. If there are multiple such indices j, you can only jump to the smallest such index j. During even-numbered jumps (i.e., jumps 2, 4, 6, ...), you jump to the index j such that arr[i] >= arr[j] and arr[j] is the largest possible value. If there are multiple such indices j, you can only jump to the smallest such index j. It may be the case that for some index i, there are no legal jumps. A starting index is good if, starting from that index, you can reach the end of the array (index arr.length - 1) by jumping some number of times (possibly 0 or more than once).

Return the number of good starting indices.

Example 1:

Input: arr = [10,13,12,14,15] Output: 2 Explanation: From starting index i = 0, we can make our 1st jump to i = 2 (since arr[2] is the smallest among arr[1], arr[2], arr[3], arr[4] that is greater or equal to arr[0]), then we cannot jump any more. From starting index i = 1 and i = 2, we can make our 1st jump to i = 3, then we cannot jump any more. From starting index i = 3, we can make our 1st jump to i = 4, so we have reached the end. From starting index i = 4, we have reached the end already. In total, there are 2 different starting indices i = 3 and i = 4, where we can reach the end with some number of jumps. Example 2:

Input: arr = [2,3,1,1,4] Output: 3 Explanation: From starting index i = 0, we make jumps to i = 1, i = 2, i = 3: During our 1st jump (odd-numbered), we first jump to i = 1 because arr[1] is the smallest value in [arr[1], arr[2], arr[3], arr[4]] that is greater than or equal to arr[0]. During our 2nd jump (even-numbered), we jump from i = 1 to i = 2 because arr[2] is the largest value in [arr[2], arr[3], arr[4]] that is less than or equal to arr[1]. arr[3] is also the largest value, but 2 is a smaller index, so we can only jump to i = 2 and not i = 3 During our 3rd jump (odd-numbered), we jump from i = 2 to i = 3 because arr[3] is the smallest value in [arr[3], arr[4]] that is greater than or equal to arr[2]. We can't jump from i = 3 to i = 4, so the starting index i = 0 is not good. In a similar manner, we can deduce that: From starting index i = 1, we jump to i = 4, so we reach the end. From starting index i = 2, we jump to i = 3, and then we can't jump anymore. From starting index i = 3, we jump to i = 4, so we reach the end. From starting index i = 4, we are already at the end. In total, there are 3 different starting indices i = 1, i = 3, and i = 4, where we can reach the end with some number of jumps. Example 3:

Input: arr = [5,1,3,4,2] Output: 3 Explanation: We can reach the end from starting indices 1, 2, and 4.

Constraints:

1 <= arr.length <= 2 * 10^4 0 <= arr[i] < 10^5

No solution on Leetcode

482. License Key Formatting

You are given a license key represented as a string s that consists of only alphanumeric characters and dashes. The string is separated into n + 1 groups by n dashes. You are also given an integer k.

We want to reformat the string s such that each group contains exactly k characters, except for the first group, which could be shorter than k but still must contain at least one character. Furthermore, there must be a dash inserted between two groups, and you should convert all lowercase letters to uppercase.

Return the reformatted license key.

Example 1:

Input: s = "5F3Z-2e-9-w", k = 4 Output: "5F3Z-2E9W" Explanation: The string s has been split into two parts, each part has 4 characters. Note that the two extra dashes are not needed and can be removed. Example 2:

Input: s = "2-5g-3-J", k = 2 Output: "2-5G-3J" Explanation: The string s has been split into three parts, each part has 2 characters except the first part as it could be shorter as mentioned above.

Constraints:

1 <= s.length <= 105 s consists of English letters, digits, and dashes '-'. 1 <= k <= 104

No solution on Leetcode

929. Unique Email Addresses

Solution Every valid email consists of a local name and a domain name, separated by the '@' sign. Besides lowercase letters, the email may contain one or more '.' or '+'.

For example, in "alice@leetcode.com", "alice" is the local name, and "leetcode.com" is the domain name. If you add periods '.' between some characters in the local name part of an email address, mail sent there will be forwarded to the same address without dots in the local name. Note that this rule does not apply to domain names.

For example, "alice.z@leetcode.com" and "alicez@leetcode.com" forward to the same email address. If you add a plus '+' in the local name, everything after the first plus sign will be ignored. This allows certain emails to be filtered. Note that this rule does not apply to domain names.

For example, "m.y+name@email.com" will be forwarded to "my@email.com". It is possible to use both of these rules at the same time.

Given an array of strings emails where we send one email to each emails[i], return the number of different addresses that actually receive mails.

Example 1:

Input: emails = ["test.email+alex@leetcode.com","test.e.mail+bob.cathy@leetcode.com","testemail+david@lee.tcode.com"] Output: 2 Explanation: "testemail@leetcode.com" and "testemail@lee.tcode.com" actually receive mails. Example 2:

Input: emails = ["a@leetcode.com","b@leetcode.com","c@leetcode.com"] Output: 3

Constraints:

1 <= emails.length <= 100 1 <= emails[i].length <= 100 emails[i] consist of lowercase English letters, '+', '.' and '@'. Each emails[i] contains exactly one '@' character. All local and domain names are non-empty. Local names do not start with a '+' character. Domain names end with the ".com" suffix.

Python
class Solution:
    def numUniqueEmails(self, emails: List[str]) -> int:
        # Hash set to store all the unique emails.
        uniqueEmails = set()

        for email in emails:
            # Split into two parts: local and domain.
            name, domain = email.split('@')

             # Split local by '+' and replace all '.' with ''.
            local = name.split('+')[0].replace('.', '')

            # Concatenate local, '@', and domain.
            uniqueEmails.add(local + '@' + domain)

        return len(uniqueEmails)

158. Read N Characters Given read4 II - Call Multiple Times

No official Leetcode solution. Found only a Java solution in the comments. This was actually asked by Facebook according to the comments. Why would someone aks this? Probably disregard :)

image.png

Constraints: * 1 <= file.length <= 500 * file consist of English letters and digits. * 1 <= queries.length <= 10 * 1 <= queries[i] <= 500

Python
# simple Java solution to convert
private int buffPtr = 0;
    private int buffCnt = 0;
    private char[] buff = new char[4];
    public int read(char[] buf, int n) {
        int ptr = 0;
        while (ptr < n) {
            if (buffPtr == 0) {
                buffCnt = read4(buff);
            }
            if (buffCnt == 0) break;
            while (ptr < n && buffPtr < buffCnt) {
                buf[ptr++] = buff[buffPtr++];
            }
            if (buffPtr >= buffCnt) buffPtr = 0;
        }
        return ptr;
    }