Recursion Problems

Problems

1. Subsets - LC 78 - Medium

Video - LC78

Clarification Questions

  • Are there any constraints on the size of the input array? This helps determine if we need to worry about exceeding memory limits or if we can optimize for specific input sizes.
  • Can the array contain duplicate elements? This will influence our approach to avoid generating duplicate subsets.
  • Is the order of subsets in the output important? This tells us if we need to maintain a specific order or if any order is acceptable.
  • Should I consider an empty array as valid input? This clarifies an edge case we need to handle.

Thought Process

  • Understanding the Problem: We need to generate all unique combinations of elements from a given set. This is essentially finding the power set.
  • Identifying Potential Approaches:
    • Brute Force: Generate all possible combinations by trying every possible inclusion/exclusion of each element. This might involve nested loops or recursive calls.
    • Optimized: We can use a more elegant recursive approach (backtracking) to systematically explore the combinations, potentially reducing redundant computations.

Code {#code}

from typing import List
class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:

      result  = []

      def helpers(S, i, slate):
	  #Base Case: No more elements left to be considered
	  if i == len(S): # The partial solution is the next subset, to be appended to the result
	      result.append(slate[:])

	  # Recursive Case: S[i] is the next element to examine
	  else:
	      slate.append(S[i])
	      helpers(S, i+1, slate) #delegate the rest of the work to a subordinate
	      slate.pop()
	      helpers(S, i+1, slate)
      helpers(nums, 0, [])
      return result

Recursion Tree

Problem Solving Patterns

  • This is fill in the blanks problem. In each blank decision has to be made whether to include or exclude the value.
  • This problem is very similar to the generalized recursion template. The recursion template assumes permutation so it has a for loop to traverse through the array in the recursive case. This is combination problem (making choice of whether to include or exclude a value for a given blank). This is the only change.
  • There are not many alternative approaches to this other than writing a multi for loop.

Complexity Analysis

\[\begin{eqnarray} \text {Time Complexity} = \text{Work done by Leaf Node workers} + \\ \text{Work done by Internal Node workers} \end{eqnarray}\]

\[\begin{eqnarray} \text {Space Complexity} = \text{Input Space} + \text{Aux Space (Stack Space)} + \\ \text{Output Space} \end{eqnarray}\]

\begin{equation} \text{Input Space} = \text{Given array} = N \end{equation}

\begin{equation} \text{Output Space} = \text{Given array} = N \end{equation}

  • Input Space = N

  • Output size = O(2^n). N/2 = O(2^N * N)

  • Stack Space = freeze the recursion tree at the highest point of stack space i.e N each stack is using constant amount of space. So O(N)

Total space complexity = O(2^N.N)

  • Time Complexity = = 2^N.

  • Each leaf worker is executing work same as the length of the slate. Whatever in slate, we are cloning the size of the slate. on average it is n/2. O(2^N.N)

  • Internal node workers O2^N. Work done by internal node worker is constant. (O(1))

Total complexity is dominated by leaf leverl worker. = O(2^N.N)

  • Time Complexity: O(N * 2^N), where N is the length of the input array. This is because we have 2 choices (include or exclude) for each element, leading to 2^N possible subsets. We also perform O(N) operations to copy each subset into the result.
  • Space Complexity: O(2^N) in the worst case to store all the subsets in the result list. The recursion depth can also reach O(N) in the worst case, contributing to the space complexity.

Test Cases

``` python``

Test cases

test_cases = [ ([1, 2, 3], [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]), ([0], [[], [0]]), ([], [[]]), # Empty set has one subset (itself) ]

for nums, expected in test_cases: result = subsets(nums)

Sort the result and expected output for comparison since order doesn’t matter

result.sort() expected.sort() assert result == expected print(f"Input: {nums}, Output: {result}, Expected: {expected} - Passed!")


### 2.  [Permutations - LC 46 - Medium](https://leetcode.com/problems/permutations/) {#2-dot-permutations-lc-46-medium}



















<div class="plantuml"> </div> #### Code {#code} ```python class Solution: . def permute(self, nums: List[int]) -> List[List[int]]: results = [] def helper(s, i, slate): #Base Case if i == len(s): results.append(slate[:]) #Recursive Case else: for c in range(i, len(s)): # For permutation problems, we have to swap the choice and # swap it back after the recursive call !!!IMPORTANT!!! s[c], s[i] = s[i], s[c] slate.append(s[i]) helper(s, i+1, slate) slate.pop() s[c],s[i] = s[i], s[c] return results helper(nums, 0, []) return results

Recursion Tree

  • Multiple choices and the number of choices come down. result in loop

  • Different form include/exclude of combination problems

  • Since any choice can be picked, it means any index. how to pass the contiguous data down…so swap first and then pass it down

  • Remember to swap it back… if not, somewhere down it may end up in same perumtation and the final result will be missing some other permutations.

  • The need for swapping is a permutation specific issue because of the need for passing continous array

    Space Input + Aux + output input = O(N) aux = O(N) output = O(N!*N)

    Time compleixty : O(N! * N)

3. Subsets with duplicates - LC 90 - Medium

Problem Statement - LC78

Code

from typing import List
class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
	result = []
	nums.sort()

	def helper(S, i, slate):
	    if i == len(S):
		result.append(slate[:])
	    else:
		count = 1
		j = i+1
		while j <= len(S) - 1 and S[i] == S[j]:
		    count += 1
		    j += 1
		for copies in range(0, count+1):
		    for op in range(copies):
			slate.append(S[i])
		    helper(S, i+count, slate)
		    for op in range(copies):
			slate.pop()
	helper(nums, 0, [])
	return result

Recursion Tree

Coding Mistakes

2022-02-27: 😟 😟 😞

  • ❌ Forgot to sort the array. ( we should get the duplicate numbers together before we can process the subsets)
  • ❌ Array overflow. While j <= len(array) should have been j <= len(array) - 1
  • for copies in range(0, count+1): I have to always end the range to count+1 and not count.

4. Letter Case Permutation - LC 784 - Medium

Video - LC784

  • Fill in the blanks problem
  • Each blank can be filled in two ways…meaning it is a combination problem
  • The problem title says says permutation

Important Points

Code

from typing import List

class Solution:
    def letterCasePermutation(self, s: str) -> List[str]:
	result = []

	def helper(s, i):
	    if i == len(s):
		result.append("".join(s))
	    else:
		if s[i].isdigit():

		    helper(s, i+1)
		else:
		    s[i] = s[i].lower()
		    helper(s, i+1)
		    s[i] = s[i].upper()
		    helper(s, i+1)
	helper(list(s), 0)
	return result

Complexity Analysis


Recursion Tree

5. Letter combinations - LC 17 - Medium disappointed

Code:

from typing import List

def letterCombinations(digits: str) -> List[str]:
    result = []
    hmap = {"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']}
    def helper(digitstr, i, slate):
	#Base Case if there is no more string, then return
	if i == len(digitstr):
	    if len(slate) > 0:
		result.append("".join(slate))

	# Recursive Case ....process each letter
	else:
	    for letter in hmap[digitstr[i]]:
		slate.append(letter)
		helper(digitstr, i+1, slate)
		slate.pop()

    helper(digits, 0, [])
    return result

print(letterCombinations("234"))

Coding Mistakes

class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
	result = []
	hmap = {'2':'abc','3':'def', '4':'ghi', '5':'jkl',
	       '6':'mno', '7':'pqrs', '8':'tuv', '9':'wxyz'}

	def helper(s, i, slate):
	    if len(s) == i:
		if len(slate):
		    result.append("".join(slate))
		return

	    #Recursive Case:
	    for j in hmap[s[i]]:
		slate.append(j)
		helper(s, i+1, slate)
		slate.pop()
	helper(digits, 0, [])
	return result

2022-02-28: 😟 😞

  • Major mistake. Forgot the logic for the for loop. Instead of looping through the digit string, I looped through the hashmap.
for letter in hmap[digits[i]]:  # correct way
for letter in hmap[i]: # WRONG
  • In the base case, I should not blindly append the slate to the result. I need to check if the index is > 0. This is because if the input is empty string, it is added to the result. In this case, it should be empty result list.

  • Silly error - Missed semicolon at the end of function definition.


6. Palindromic decomposition: Interview Kickstart Problem

def generate_palindromic_decompositions(s):
    """
    Args:
     s(str)
    Returns:
     list_str
    """
    # Write your code here.

    # Input validation
    if not s or len(s) == 1:
	return [s]

    result = []

    def palindromic_decomposition(s, index, slate):
	# base case
	if index == len(s):
	    result.append('|'.join(slate))
	    return
	# Recursive Case
	# take every possible string from the current position and if it's palndromic go forward, and if it's not prune
	for i in range(index+1, len(s)+1):
	    substring = s[index:i]
	    if is_palindrome(substring):
		slate.append(substring)
		palindromic_decomposition(s, i, slate)
		# at the end of dfs remove what was appended to
		slate.pop()

    palindromic_decomposition(s, 0,[])
    return result

def is_palindrome(string):
    if not string or len(string) == 1: return True
    start, end = 0, len(string)-1
    while start < end:
	if string[start] != string[end]:
	    return False
	start += 1
	end -= 1
    return True

7. Possible to achieve target sum: Interview Kickstart problem


def check_if_sum_possible(arr, k):

    def helper(arr, i, slate,k):
	sum = 0
	for i in range(len(slate)):
	    sum += slate[i]

	if sum == k:
	    return True


	# Base Case:
	if i == len(arr):
	    return False

	# Recursive Case:

	helper(arr, i+1, slate,k)
	slate.append(arr[i])
	helper(arr, i + 1, slate,k)
	slate.pop()

    status = helper(arr, 0, [], k)
    return status

8. Generate Well formed parenthesis LC - 22 - Medium worried

Video - LC22

Important Points

Code

from typing import List
class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
	result = []
	def helper(left, right, slate):

	    if left > right:
		return
	    #Base Case
	    if left == 0 and right == 0:
		result.append("".join(slate))
		return
	    #Recursive Case
	    else:
		if left > 0:
		    slate.append('(')
		    helper(left-1, right, slate)
		    slate.pop()
		if right > 0:
		    slate.append(')')
		    helper(left, right-1, slate)
		    slate.pop()

	helper(n, n, [])
	return result

Complexity Analysis

Coding Mistakes

2022-02-27: 😞

  • I could not initially recollect that in this problem, we need to approach from both left and right side.
  • Missed adding node.val to slate for base case
  • Run time error for case error while referencing targetSum as targetsum.
  • Compilation error for missing out the function keyword 'def' before the helper function

9. Combinations - LC 77 - Medium

Video - LC77

Important Points

Code

from typing import List
class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
	result = []

	def helper(n, i, slate):
	    #Backtracking

	    if len(slate) == k:
		result.append(slate[:])
		return result

	    #Base Case
	    if i >= n+1:
		return

	    #Recursive Case
	    else:
		helper(n, i+1, slate)
		slate.append(i)
		helper(n, i+1, slate)
		slate.pop()

	helper(n, 1, [])
	return result

Checklist What

  • Is this a combinatorial enumeration problem?
  • If so, what are the blanks?
  • How many blanks to be filled?
  • With what are they to be filled?
    • is the logic applied on index?
    • is the logic applied on the input array?
    • is the logic applied on the output slate?
    • At what stage is this applied? while backtracking? or while invoking the recursive function

How

  • Does Pre sorting help?
  • Does multiple passes help?
  • Does sliding window help?
  • Does two or three pointers help?
  • Does saving values to hashmap help?
  • Does DFS or BFS help?
  • Does Backtracking required?

Patterns:

  • No change to the standard recursion template - Permutations, Subsets
  • Addition of backtracking rest everything same - Combinations, Target sums up to K (IK problem)
  • Change to input - Generate Parenthesis
  • Change to index - Subset II, Permutations II
  • Transformation of input in recursive case - Letter case permutation, Letter case to phone combination
  • Extra functions and validations - Palindromic decomposition of string
  • When no changes to the input string, no need for two recursive calls - Letter case permutation, Letter combination problems
  • With mutable approach, leaf level always does the cloning, so focus on them for total compelxity. internal worker only adds to the slate…so contstant amortized time

  • In a complete binary tree, the number of leaf nodes is equal to the number of internal nodes excluding the 1. 50% of nodes are leaf nodes.

Complexity Analysis

10. Permutations II - LC 47 - Medium

Video - LC47

Important Points

Code

class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
	result = []

	def helper(s, i):
	    if len(s) == i:
		result.append(s[:])
		return
	    else:
		hmap = {}

		for j in range(i, len(s)):
		    if s[j] not in hmap:
			hmap[s[j]] = 1
			s[j], s[i] = s[i], s[j]
			helper(s, i+1)
			s[j], s[i] = s[i], s[j]
	helper(nums, 0)
	return result

Complexity Analysis

11. Combinations II - LC 40 - Medium

Important Points

Code

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
	result = []
	candidates.sort()

	def helper(S, i, target, slate):
	    #Backtrack
	    if target < sum(slate):
		return

	    # Add result to global bag and return
	    if target == sum(slate):
		result.append(slate[:])
		return

	    #Base Case
	    if i == len(S):
		return

	    #Recursive Case
	    #Count duplicates and include/exclude equal to duplicate count
	    count = 1
	    j = i+1
	    while j <= len(S) - 1 and S[i] == S[j]:
		count += 1
		j += 1

	    for copies in range(0, count+1):
		for op in range(copies):
		    slate.append(S[i])
		helper(S, i+count,target, slate)
		for op in range(copies):
		    slate.pop()
	helper(candidates, 0,target, [])
	return result

Complexity Analysis

Solutions by type

{{< chart data=“status” >}}

References:

Video References:

Previous
Next