diff --git a/DIRECTORY.md b/DIRECTORY.md index 8f87f61d..1ae82b52 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -22,6 +22,8 @@ * Subsequence * [Test Is Valid Subsequence](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/arrays/subsequence/test_is_valid_subsequence.py) * Backtracking + * Additive Number + * [Test Additive Number](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/additive_number/test_additive_number.py) * Combination * [Test Combination](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/combination/test_combination.py) * [Test Combination 2](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/combination/test_combination_2.py) @@ -59,6 +61,9 @@ * [Shortest Reach](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/bfs/graphs/shortest_reach/shortest_reach.py) * Smallest Set Of Vertices * [Smallest Set Of Vertices](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/bfs/graphs/smallest_set_of_vertices/smallest_set_of_vertices.py) + * Counting + * Rank Teams By Votes + * [Test Rank Teams By Votes](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/counting/rank_teams_by_votes/test_rank_teams_by_votes.py) * Dollar Bills * [Make Change](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dollar_bills/make_change.py) * Dynamic Programming @@ -114,6 +119,8 @@ * [Test Pascals Triangle](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/pascals_triangle/test_pascals_triangle.py) * Shortest Common Supersequence * [Test Shortest Common Supersequence](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/shortest_common_supersequence/test_shortest_common_supersequence.py) + * Total Appeal Of A String + * [Test Appeal Of A String](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/total_appeal_of_a_string/test_appeal_of_a_string.py) * Unique Paths * [Test Unique Paths](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/unique_paths/test_unique_paths.py) * Word Break @@ -153,6 +160,7 @@ * Network Delay Time * [Test Network Delay Time](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/network_delay_time/test_network_delay_time.py) * Number Of Islands + * [Test Number Of Distinct Islands](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/number_of_islands/test_number_of_distinct_islands.py) * [Test Number Of Islands](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/number_of_islands/test_number_of_islands.py) * [Union Find](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/number_of_islands/union_find.py) * Number Of Provinces @@ -165,6 +173,8 @@ * [Test Reorder Routes](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/reorder_routes/test_reorder_routes.py) * Rotting Oranges * [Test Rotting Oranges](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/rotting_oranges/test_rotting_oranges.py) + * Shortest Path Length + * [Test Shortest Path Length](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/shortest_path_length/test_shortest_path_length.py) * Single Cycle Check * [Test Single Cycle Check](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/single_cycle_check/test_single_cycle_check.py) * Sort Items By Group @@ -249,6 +259,8 @@ * Employee Free Time * [Interval](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/employee_free_time/interval.py) * [Test Employee Free Time](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/employee_free_time/test_employee_free_time.py) + * Find Right Interval + * [Test Find Right Interval](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/find_right_interval/test_find_right_interval.py) * Full Bloom Flowers * [Test Full Bloom Flowers](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/full_bloom_flowers/test_full_bloom_flowers.py) * Insert Interval @@ -266,6 +278,8 @@ * [Test Non Overlapping Intervals](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/non_overlapping_intervals/test_non_overlapping_intervals.py) * Remove Intervals * [Test Remove Covered Intervals](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/remove_intervals/test_remove_covered_intervals.py) + * Remove Min Intervals + * [Test Remove Min Intervals](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/remove_min_intervals/test_remove_min_intervals.py) * Task Scheduler * [Test Task Scheduler](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/task_scheduler/test_task_scheduler.py) * Josephus Circle @@ -755,6 +769,13 @@ * [Circuit Breaker](https://github.com/BrianLusina/PythonSnips/blob/master/design_patterns/circuit_breaker/circuit_breaker.py) * Continuous Median * [Test Continuous Median Handler](https://github.com/BrianLusina/PythonSnips/blob/master/design_patterns/continuous_median/test_continuous_median_handler.py) + * Creational + * Factory + * Notification + * [Email Notification](https://github.com/BrianLusina/PythonSnips/blob/master/design_patterns/creational/factory/notification/email_notification.py) + * [Notification](https://github.com/BrianLusina/PythonSnips/blob/master/design_patterns/creational/factory/notification/notification.py) + * [Notification Factory](https://github.com/BrianLusina/PythonSnips/blob/master/design_patterns/creational/factory/notification/notification_factory.py) + * [Sms Notification](https://github.com/BrianLusina/PythonSnips/blob/master/design_patterns/creational/factory/notification/sms_notification.py) * Event Stream * [Audit Logger](https://github.com/BrianLusina/PythonSnips/blob/master/design_patterns/event_stream/audit_logger.py) * [Batch Event Processor](https://github.com/BrianLusina/PythonSnips/blob/master/design_patterns/event_stream/batch_event_processor.py) diff --git a/algorithms/backtracking/additive_number/README.md b/algorithms/backtracking/additive_number/README.md new file mode 100644 index 00000000..71e0d9c6 --- /dev/null +++ b/algorithms/backtracking/additive_number/README.md @@ -0,0 +1,87 @@ +# Additive Number + +An additive number is a string whose digits can form an additive sequence. + +A valid additive sequence should contain at least three numbers. Except for the first two numbers, each subsequent +number in the sequence must be the sum of the preceding two. + +Given a string containing only digits, return true if it is an additive number or false otherwise. + +> Note: Numbers in the additive sequence cannot have leading zeros, so sequence 1, 2, 03 or 1, 02, 3 is invalid. + +## Examples + +Example 1: +```text +Input: "112358" +Output: true +Explanation: +The digits can form an additive sequence: 1, 1, 2, 3, 5, 8. +1 + 1 = 2, 1 + 2 = 3, 2 + 3 = 5, 3 + 5 = 8 +``` + +Example 2: +```text +Input: "199100199" +Output: true +Explanation: +The additive sequence is: 1, 99, 100, 199. +1 + 99 = 100, 99 + 100 = 199 +``` + +## Constraints + +- 1 <= num.length <= 35 +- num consists only of digits. + +## Topics + +- String +- Backtracking + +## Solution + +The key intuition behind this problem is that once we fix the first two numbers in the sequence, the entire rest of the +sequence is completely determined, each subsequent number must equal the sum of the two preceding ones. Therefore, we +use backtracking to try all possible ways to choose the first and second numbers by varying their lengths, and for each +choice, we greedily verify whether the remaining digits of the string can be partitioned to satisfy the additive property. +The backtrack function processes the string from left to right, extracting candidate numbers of increasing length. For +the first two numbers (count < 2), we freely explore all possible lengths. Once we have at least two numbers, we compute +the `expectedSum` and prune: if `currentNum` exceeds `expectedSum`, we break (longer substrings will only be larger); if +`currentNum` is less, we continue to try a longer substring. If `currentNum` matches, we recurse deeper. The recursion +succeeds when we consume the entire string with at least 3 numbers in the sequence. + +Now, let's look at the solution steps below: + +1. Initialize n as the length of the input string num. +2. Define a recursive helper function `backtrack(start, prev1, prev2, count)` where `start` is the current index in `num`, + `prev1` is the second-to-last number, `prev2` is the last number, and count tracks how many numbers have been placed + so far. +3. **Base case**: If `start` equals `n` (the entire string has been consumed), return true if count ≥ 3, otherwise return + false. +4. Iterate over all possible end positions `end` from `start + 1` to `n` (inclusive) to form candidate substrings + `num[start:end]`. + - If the substring has length greater than 1 and starts with '0', break out of the loop — this handles the leading-zero + constraint, since any longer substring starting from the same position will also have a leading zero. + - Convert the `substring` to an integer `currentNum`. + - If `count ≥ 2` (meaning we already have at least two numbers), compute `expectedSum` as `prev1 + prev2`. + - If `currentNum` > `expectedSum`, break as no need to try longer substrings since they will only yield larger values. + - If `currentNum` < `expectedSum`, continue and try a longer substring that might match the expected sum. + - If `currentNum` = `expectedSum`, proceed to recurse. + - Recursively call `backtrack(end, prev2, currentNum, count + 1)`. If it returns true, propagate true upward immediately. +5. If no valid partition is found after exhausting all candidates, return false. +6. Invoke backtrack(0, 0, 0, 0) and return its result. + +### Time Complexity + +The time complexity is commonly described as O(n^3): O(n^2) choices for the first two numbers, and O(n) to validate the ++rest for each choice. If you also account for substring-to-integer parsing cost, the practical bound can be higher. + +> Note that Python natively handles arbitrarily large integers, which addresses the follow-up question about overflow +> for very large inputs, no special handling is needed. + +### Space Complexity + +The space complexity of the solution is O(n) because the recursion depth is at most O(n) (in the case where each number +is a single digit), and each recursive call uses a constant amount of additional space aside from the call stack. +Substring creation also uses up to O(n) space. diff --git a/algorithms/backtracking/additive_number/__init__.py b/algorithms/backtracking/additive_number/__init__.py new file mode 100644 index 00000000..1b6ee4c0 --- /dev/null +++ b/algorithms/backtracking/additive_number/__init__.py @@ -0,0 +1,73 @@ +def is_additive_number_dfs(num: str) -> bool: + if not num: + return False + + n = len(num) + + def dfs(a: int, b: int, number: str) -> bool: + if not number: + return True + + if a + b > 0 and number[0] == "0": + return False + + for i in range(1, len(number) + 1): + if a + b == int(number[:i]): + if dfs(b, a + b, number[i:]): + return True + return False + + for x in range(1, n - 1): + for y in range(x + 1, n): + if x > 1 and num[0] == "0": + break + if y - x > 1 and num[x] == "0": + continue + if dfs(int(num[:x]), int(num[x:y]), num[y:]): + return True + + return False + + +def is_additive_number_backtrack(num: str) -> bool: + n = len(num) + + def backtrack(start: int, prev1: int, prev2: int, count: int) -> bool: + # Base case: we've consumed the entire string + if start == n: + # Valid only if we have at least 3 numbers in the sequence + return count >= 3 + + # Try every possible next number by varying its length + for end in range(start + 1, n + 1): + # Extract the substring representing the current number + substring = num[start:end] + + # Skip numbers with leading zeros (e.g., "03"), but "0" itself is allowed + if len(substring) > 1 and substring[0] == "0": + break # No point trying longer substrings; they'll also have leading zero + + current_num = int(substring) + + # If we already have at least 2 numbers, validate the additive property + if count >= 2: + expected_sum = prev1 + prev2 + + # If current number is too large, no need to try longer substrings + if current_num > expected_sum: + break + + # If current number doesn't match the expected sum, try next length + if current_num < expected_sum: + continue + + # current_num == expected_sum, so recurse with updated sequence + + # Recurse: prev2 becomes prev1, current_num becomes the new prev2 + if backtrack(end, prev2, current_num, count + 1): + return True + + return False + + # Start backtracking from index 0 with no previous numbers and count = 0 + return backtrack(0, 0, 0, 0) diff --git a/algorithms/backtracking/additive_number/test_additive_number.py b/algorithms/backtracking/additive_number/test_additive_number.py new file mode 100644 index 00000000..c7380812 --- /dev/null +++ b/algorithms/backtracking/additive_number/test_additive_number.py @@ -0,0 +1,34 @@ +import unittest +from algorithms.backtracking.additive_number import ( + is_additive_number_dfs, + is_additive_number_backtrack, +) +from parameterized import parameterized + +IS_ADDITIVE_NUMBER_TEST_CASES = [ + ("112358", True), + ("199100199", True), + ("11235813", True), + ("12345", False), + ("000", True), + ("1", False), + ("0", False), + ("101", True), + ("1023", False), +] + + +class AdditiveNumberTestCase(unittest.TestCase): + @parameterized.expand(IS_ADDITIVE_NUMBER_TEST_CASES) + def test_is_additive_number_dfs(self, num: str, expected: bool): + actual = is_additive_number_dfs(num) + self.assertEqual(expected, actual) + + @parameterized.expand(IS_ADDITIVE_NUMBER_TEST_CASES) + def test_is_additive_number_backtrack(self, num: str, expected: bool): + actual = is_additive_number_backtrack(num) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main()