Skip to content
Merged
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
21 changes: 21 additions & 0 deletions DIRECTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
87 changes: 87 additions & 0 deletions algorithms/backtracking/additive_number/README.md
Original file line number Diff line number Diff line change
@@ -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.
73 changes: 73 additions & 0 deletions algorithms/backtracking/additive_number/__init__.py
Original file line number Diff line number Diff line change
@@ -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)
34 changes: 34 additions & 0 deletions algorithms/backtracking/additive_number/test_additive_number.py
Original file line number Diff line number Diff line change
@@ -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()
Loading