diff --git a/Ex1_combination_sum.py b/Ex1_combination_sum.py new file mode 100644 index 00000000..2f47c404 --- /dev/null +++ b/Ex1_combination_sum.py @@ -0,0 +1,64 @@ +# Time Complexity : O(2^t), where t is the target value +# - In the worst case, every candidate can be repeatedly used to form the target. +# Space Complexity : O(t), for recursion stack and storing current combination +# Did this code successfully run on Leetcode : YES +# Any problem you faced while coding this : No + +from typing import List + +class Solution: + def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]: + # Backtracking approach + res = [] + + def dfs(i: int, curr: List[int], total: int): + if total == target: + res.append(curr.copy()) + return + + if i >= len(candidates) or total > target: + return + + # Choose the current candidate + curr.append(candidates[i]) + dfs(i, curr, total + candidates[i]) + curr.pop() # backtrack + + # Skip the current candidate + dfs(i + 1, curr, total) + + dfs(0, [], 0) + return res + + + # ----------------------------------------- + # Non-backtracking version (pure recursion with copies) + # Time Complexity : O(2^t * k), where t = target, k = average length of combination + # - 2^t because of choice at each candidate (take or skip) + # - k factor because we copy the current combination list at each valid leaf + # Space Complexity : O(t * k) + # - Recursion stack up to depth t (target) + # - Each recursive call creates a new list copy of size up to k + + # res = [] + # def dfs(i, curr, total): + # if total == target: + # res.append(curr.copy()) + # return + # if i >= len(candidates) or total > target: + # return + # new_temp = curr[:] + # new_temp.append(candidates[i]) + # dfs(i, new_temp, total + candidates[i]) + # dfs(i + 1, curr, total) + # dfs(0, [], 0) + # return res + + +if __name__ == "__main__": + sol = Solution() + print(sol.combinationSum([2,3,6,7], 7)) # Output: [[2,2,3],[7]] + print(sol.combinationSum([2,3,5], 8)) # Output: [[2,2,2,2],[2,3,3],[3,5]] + print(sol.combinationSum([2], 1)) # Output: [] + print(sol.combinationSum([1], 1)) # Output: [[1]] + print(sol.combinationSum([1], 2)) # Output: [[1,1]] diff --git a/Ex2_expression_add_operators.py b/Ex2_expression_add_operators.py new file mode 100644 index 00000000..e4790481 --- /dev/null +++ b/Ex2_expression_add_operators.py @@ -0,0 +1,90 @@ +# Time Complexity: O(4^n), where n is the length of the input string `num` +# - At each digit, we branch into 3 possibilities: '+', '-', '*' +# - Plus slicing and integer conversion cost +# Space Complexity: O(n) recursion depth and O(n) space per expression (max) +# Did this code successfully run on Leetcode: YES +# Any problem you faced while coding this: No + +from typing import List + +class Solution: + def addOperators(self, num: str, target: int) -> List[str]: + # Functional style DFS (no mutation) + res = [] + n = len(num) + + def dfs(index, expr, value, prev): + if index == n: + if value == target: + res.append(expr) + return + + curr_num = 0 + for i in range(index, n): + # Skip numbers with leading zero, but allow single "0" + if i > index and num[index] == '0': + break + + curr_str = num[index:i+1] + curr_num = int(curr_str) + + if index == 0: + dfs(i + 1, curr_str, curr_num, curr_num) + else: + dfs(i + 1, expr + '+' + curr_str, value + curr_num, curr_num) + dfs(i + 1, expr + '-' + curr_str, value - curr_num, -curr_num) + dfs(i + 1, expr + '*' + curr_str, value - prev + prev * curr_num, prev * curr_num) + + dfs(0, "", 0, 0) + return res + + # -------------------------------------- + # Alternative: Explicit backtracking with mutation (append/pop) + # Time Complexity: O(4^n), where n is the number of digits in `num` + # - For each digit, we try all possible splits and operators: '+', '-', '*' + # - At most 3 branches per position and n possible splits → exponential combinations + # + # Space Complexity: + # - O(n) for recursion stack (max depth = number of digits) + # - O(n) for building each expression (list `expr`) + # - Result space not included in complexity (can be large depending on output size) + + + # res = [] + # def dfs(i, expr, value, prev): + # if i == len(num): + # if value == target: + # res.append("".join(expr)) + # return + # for j in range(i, len(num)): + # if j > i and num[i] == '0': + # break + # curr_str = num[i:j+1] + # curr_num = int(curr_str) + # if i == 0: + # expr.append(curr_str) + # dfs(j+1, expr, curr_num, curr_num) + # expr.pop() + # else: + # for op in ['+', '-', '*']: + # expr.append(op) + # expr.append(curr_str) + # if op == '+': + # dfs(j+1, expr, value + curr_num, curr_num) + # elif op == '-': + # dfs(j+1, expr, value - curr_num, -curr_num) + # else: + # dfs(j+1, expr, value - prev + prev * curr_num, prev * curr_num) + # expr.pop() + # expr.pop() + # dfs(0, [], 0, 0) + # return res + + +if __name__ == "__main__": + sol = Solution() + print(sol.addOperators("123", 6)) # Output: ["1+2+3", "1*2*3"] + print(sol.addOperators("232", 8)) # Output: ["2*3+2", "2+3*2"] + print(sol.addOperators("105", 5)) # Output: ["1*0+5", "10-5"] + print(sol.addOperators("00", 0)) # Output: ["0+0", "0-0", "0*0"] + print(sol.addOperators("3456237490", 9191)) # Output: []