From 0f33db0cbebe2e569386b3c39eed8a6f850ba863 Mon Sep 17 00:00:00 2001 From: param07 Date: Wed, 1 Oct 2025 22:39:08 +0530 Subject: [PATCH] solved Backtracking-1 problems --- Problem1.py | 231 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Problem2.py | 125 ++++++++++++++++++++++++++++ 2 files changed, 356 insertions(+) create mode 100644 Problem1.py create mode 100644 Problem2.py diff --git a/Problem1.py b/Problem1.py new file mode 100644 index 00000000..019cb699 --- /dev/null +++ b/Problem1.py @@ -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)) diff --git a/Problem2.py b/Problem2.py new file mode 100644 index 00000000..5825fdda --- /dev/null +++ b/Problem2.py @@ -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)) +