Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions Problem1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
## Problem1
# Combination Sum (https://leetcode.com/problems/combination-sum/)

# Given a set of candidate numbers (candidates) (without duplicates) and a target number (target), find all unique combinations in candidates where the candidate numbers sums to target.

# The same repeated number may be chosen from candidates unlimited number of times.

# Note:

# All numbers (including target) will be positive integers.
# The solution set must not contain duplicate combinations.
# Example 1:

# Input: candidates = [2,3,6,7], target = 7,
# A solution set is:
# [
# [7],
# [2,2,3]
# ]
# Example 2:

# Input: candidates = [2,3,5], target = 8,
# A solution set is:
# [
# [2,2,2,2],
# [2,3,3],
# [3,5]
# ]

# Method1: Using 0-1 Exhaustive Recursion with backtracking
# M -- no. of elements in the array
# N -- the target
# Time Complexity : O(2 ^ (M + N)) -- going through all the nodes
# Space Complexity : O(h) --- for storing the path and at max it would be h, where h is the height of the tree
# Did this code successfully run on GFG : Yes
# Any problem you faced while coding this : No


# Your code here along with comments explaining your approach in three sentences only
# Using 0-1 Exhaustive Recursion, we choose a number in our or we decide to skip a number. If we choose a number, we can choose again
# At every step we have 2 choices. And max height possible is m + n. Here we assume that the final paths that we add to our result are limited.
# So we consider the time complexity for deep copy of final path list as constant

import copy
class Solution(object):
def combinationSum(self, candidates, target):
"""
:type candidates: List[int]
:type target: int
:rtype: List[List[int]]
"""
def helper(candidates, target, idx, path):
# base
if(idx == len(candidates)):
# out of bounds
return

if(target < 0):
return

if(target == 0):
result.append(copy.deepcopy(path))
return

# action
# not choose
helper(candidates, target, idx + 1, path)
# choose
path.append(candidates[idx])
helper(candidates, target - candidates[idx], idx, path)

# backtrack
path.pop()

result = []
helper(candidates, target, 0, [])
return result

sol = Solution()
print("Method1: Using 0-1 Exhaustive Recursion with backtracking")
print(sol.combinationSum([2,3,6,7], 7))
print(sol.combinationSum([2,3,5], 8))
print(sol.combinationSum([2], 1))


# Method2: Using For Loop based recusrion without backtracking
# M -- no. of elements in the array
# N -- the target
# Time Complexity : O((2 ^ (M + N)) * h) -- going through all the nodes and creating a new list at each node. h is the max length of path
# that would be height of the tree
# Space Complexity : O(h ^ 2) = O((m + n) * h) --- At max we go (m + n) height down and max path length would be height of the tree
# Did this code successfully run on GFG : Yes
# Any problem you faced while coding this : No


# Your code here along with comments explaining your approach in three sentences only
# Here in for loop based recursion we have a pivot. At each level we have for loop running from i = pivot to i = len(candidates) - 1.
# It is exactly the same as 0-1 recursion, just the shape of tree is different. Here we are doing it without backtracking so we should
# always pass a new list as path in the further recursive calls as it is a reference. So we create a deepcopy at each node.
# One thing to note is We should not add element and then create deepcopy. It would hamper the no choose case also
# Instead we create a new reference ie new list with deepcopy first and then add the element and pass to the choose case
# In this way our path at the current level would remain same and a new reference would go to further paths

import copy
class Solution(object):
def combinationSum(self, candidates, target):
"""
:type candidates: List[int]
:type target: int
:rtype: List[List[int]]
"""
def helper(candidates, target, pivot, path):
# base
# if(pivot == len(candidates)):
# # out of bounds
# return

if(target < 0):
return

if(target == 0):
# valid case
result.append(path)

# for loop from pivot onwards
for i in range(pivot, len(candidates)):
# next pivot would be i onwards
# when we go to further levels we need to add curr element to the path
# at the same level the path should remain same as before

# using deepcopy path
# if we add to the path and send, everywhere the path would get updated
# as it is reference
# We should not add element and then create deepcopy. It would hamper
# the no choose case also
# Instead we create a new reference ie new list with deepcopy first
# and then add the element and pass to the choose case
# In this way our path at the current level would remain same
# And a new reference would go to further paths

# using new path list at every recursive call
# we create a deepcopy first to pass new reference and then add element
# so that our current path at this level remains same
# to avoid baby relay
newPath = copy.deepcopy(path)
newPath.append(candidates[i])

# recurse
# since we can repeat the numbers so i and not (i + 1)
helper(candidates, target - candidates[i], i, newPath)


result = []
helper(candidates, target, 0, [])
return result

sol = Solution()
print("Method2: Using For Loop based recusrion without backtracking")
print(sol.combinationSum([2,3,6,7], 7))
print(sol.combinationSum([2,3,5], 8))
print(sol.combinationSum([2], 1))


# Method3: Using For Loop based recusrion with backtracking
# M -- no. of elements in the array
# N -- the target
# Time Complexity : O(2 ^ (M + N)) -- going through all the nodes
# Space Complexity : O(h) --- for storing the path and at max it would be h, where h is the height of the tree
# Did this code successfully run on GFG : Yes
# Any problem you faced while coding this : No


# Your code here along with comments explaining your approach in three sentences only
# The algo is same as above. Just here instead of creating a new reference at each node, we maintain the same path reference and add to it
# before going to next and pop the last element after coming back to it.

import copy
class Solution(object):
def combinationSum(self, candidates, target):
"""
:type candidates: List[int]
:type target: int
:rtype: List[List[int]]
"""
def helper(candidates, target, pivot, path):
# base
# if(pivot == len(candidates)):
# # out of bounds
# return

if(target < 0):
return

if(target == 0):
# valid case
result.append(copy.deepcopy(path))

# for loop from pivot onwards
for i in range(pivot, len(candidates)):
# next pivot would be i onwards
# when we go to further levels we need to add to the path
# at the same level the path should remain same as before
# if we add to the path and send, everywhere the path would get updated
# as it is reference
# We should not add element and then create deepcopy. It would hamper
# the no choose case also
# Instead we create a new reference ie new list with deepcopy first
# and then add the element and pass to the choose case
# In this way our path at the current level would remain same
# And a new reference would go to further paths
# action
path.append(candidates[i])

# recurse
# since we can repeat the numbers so i and not (i + 1)
helper(candidates, target - candidates[i], i, path)

# if we use same reference then we need to
# backtrack
path.pop()


result = []
helper(candidates, target, 0, [])
return result

sol = Solution()
print("Method3: Using For Loop based recusrion with backtracking")
print(sol.combinationSum([2,3,6,7], 7))
print(sol.combinationSum([2,3,5], 8))
print(sol.combinationSum([2], 1))
125 changes: 125 additions & 0 deletions Problem2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# ## Problem2
# Expression Add Operators(https://leetcode.com/problems/expression-add-operators/)

# Given a string that contains only digits 0-9 and a target value, return all possibilities to add binary operators (not unary) +, -, or * between the digits so they evaluate to the target value.

# Example 1:

# Input: num = "123", target = 6
# Output: ["1+2+3", "1*2*3"]
# Example 2:

# Input: num = "232", target = 8
# Output: ["2*3+2", "2+3*2"]
# Example 3:

# Input: num = "105", target = 5
# Output: ["1*0+5","10-5"]
# Example 4:

# Input: num = "00", target = 0
# Output: ["0+0", "0-0", "0*0"]
# Example 5:

# Input: num = "3456237490", target = 9191
# Output: []

# Method1: Using For Loop Based Recursion with backtracking
# N -- no. of elements in the array
# Time Complexity : O(4 ^ N) -- at each node we have 4 cases. 0-case when we create number without operators and 1-case when we
# use operators in between. As there are 3 operators that can be used, so 1-case has 3 possibilities. So total 4 cases at each node.
# Also we are creating a substring at each node, so complexity = O((4 ^ N) * h) -- h = n in worst case. But exponential is a dominant
# factor. So O(4 ^ N)
# Space Complexity : O(h1 * h2) + O(h) -- h1 --- max recursion depth that is height of the tree, h2 -- average length of the string
# h -- space occupied by path -- in worst case would be length of num
# Did this code successfully run on GFG : Yes
# Any problem you faced while coding this : No


# Your code here along with comments explaining your approach in three sentences only
# Here we have a 2-level recursion. The first level recursion is to create the possible numbers. Then with each possible number, we can have
# all possible operators before creating numbers at the next level. Here we also evaluate the expression as we go along rather than
# reaching the leaf and then evaluating. For expression evaluation the addition and subtraction cases are simple. But the multiplication or
# division are cases that are to be handled as Multiplication and Division have higher precedence than addition and subtraction.
# We keep a tail variable to keep track of the change that happened from previous level. We initialize the calc and tail variables at the
# first level when pivot = 0. For addition tail = + curr, for subtraction tail = - curr and for multiply tail = tail * curr
# So calc for addition = calc + curr, for subtraction calc = calc - curr and for mutiply calc = (calc - tail) + (calc * tail). For multiply
# we nullify the previous effect by removing tail and giving precedence to multiply by (calc * tail).
# We have to take care of preceeding 0 case in such number problems. When pivot is at digit 0 and i > pivot, we would get all invalid numbers.
# So we should skip those cases and allow pivot = digit 0 and i = pivot case
# This is a backtracking solution as we are maintaining a single path variable


import copy
class Solution(object):
def addOperators(self, num, target):
"""
:type num: str
:type target: int
:rtype: List[str]
"""
def helper(num, target, pivot, path, tail, operators, calc):
# base
if(pivot == len(num)):
if(calc == target):
newPath = copy.deepcopy(path)
result.append("".join(newPath))
return

# logic
# main recursion for the numbers
for i in range(pivot, len(num)):
# preceding 0 case
# when pivot is at 0, only valid case if when i == pivot
# rest cases when i > pivot are all invalid as it would give numbers as "05", "056" ...
if(num[pivot] == "0" and (i > pivot)):
return
# get the number between i and pivot
currStr = (num[pivot:i+1]) # O(h) time and space
currNum = int(currStr) # O(h) time and space
if(pivot == 0):
# we are at first level
# tail = currNum
# calc = currNum
path.append(currStr)
helper(num, target, i + 1, path, currNum, operators, currNum)
path.pop()
else:
# we are at further levels
# calculate the expression as per operators

for j in range(len(operators)):
path.append(operators[j])
path.append(currStr)
if(operators[j] == '+'):
# calc += currNum
# tail = currNum
helper(num, target, i + 1, path, currNum, operators, calc + currNum)

elif(operators[j] == '-'):
# calc -= currNum
# tail = -currNum
helper(num, target, i + 1, path, -currNum, operators, calc - currNum)

elif(operators[j] == '*'):
# calc = (calc - tail) + (tail * currNum)
# tail = tail * currNum
helper(num, target, i + 1, path, tail * currNum, operators, (calc - tail) + (tail * currNum))

path.pop() # to pop the curr number string
path.pop() # to pop the operator


result = []
operators = ['+', '-', '*']
helper(num, target, 0, [], 0, operators, 0)
return result

sol = Solution()
print(sol.addOperators("123", 6))
print(sol.addOperators("232", 8))
print(sol.addOperators("3456237490", 9191))
print(sol.addOperators("105", 5))
print(sol.addOperators("050", 5))
print(sol.addOperators("123", 123))