Skip to content

Commit 99f5f91

Browse files
authored
Merge pull request #193 from BrianLusina/feat/algorithms-backtracking-additive-number
feat(algorithms, backtracking): is additive number
2 parents 3683dcf + cf0fa07 commit 99f5f91

4 files changed

Lines changed: 215 additions & 0 deletions

File tree

DIRECTORY.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
* Subsequence
2323
* [Test Is Valid Subsequence](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/arrays/subsequence/test_is_valid_subsequence.py)
2424
* Backtracking
25+
* Additive Number
26+
* [Test Additive Number](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/additive_number/test_additive_number.py)
2527
* Combination
2628
* [Test Combination](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/combination/test_combination.py)
2729
* [Test Combination 2](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/combination/test_combination_2.py)
@@ -59,6 +61,9 @@
5961
* [Shortest Reach](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/bfs/graphs/shortest_reach/shortest_reach.py)
6062
* Smallest Set Of Vertices
6163
* [Smallest Set Of Vertices](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/bfs/graphs/smallest_set_of_vertices/smallest_set_of_vertices.py)
64+
* Counting
65+
* Rank Teams By Votes
66+
* [Test Rank Teams By Votes](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/counting/rank_teams_by_votes/test_rank_teams_by_votes.py)
6267
* Dollar Bills
6368
* [Make Change](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dollar_bills/make_change.py)
6469
* Dynamic Programming
@@ -114,6 +119,8 @@
114119
* [Test Pascals Triangle](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/pascals_triangle/test_pascals_triangle.py)
115120
* Shortest Common Supersequence
116121
* [Test Shortest Common Supersequence](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/shortest_common_supersequence/test_shortest_common_supersequence.py)
122+
* Total Appeal Of A String
123+
* [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)
117124
* Unique Paths
118125
* [Test Unique Paths](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/unique_paths/test_unique_paths.py)
119126
* Word Break
@@ -153,6 +160,7 @@
153160
* Network Delay Time
154161
* [Test Network Delay Time](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/network_delay_time/test_network_delay_time.py)
155162
* Number Of Islands
163+
* [Test Number Of Distinct Islands](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/number_of_islands/test_number_of_distinct_islands.py)
156164
* [Test Number Of Islands](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/number_of_islands/test_number_of_islands.py)
157165
* [Union Find](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/number_of_islands/union_find.py)
158166
* Number Of Provinces
@@ -165,6 +173,8 @@
165173
* [Test Reorder Routes](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/reorder_routes/test_reorder_routes.py)
166174
* Rotting Oranges
167175
* [Test Rotting Oranges](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/rotting_oranges/test_rotting_oranges.py)
176+
* Shortest Path Length
177+
* [Test Shortest Path Length](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/shortest_path_length/test_shortest_path_length.py)
168178
* Single Cycle Check
169179
* [Test Single Cycle Check](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/single_cycle_check/test_single_cycle_check.py)
170180
* Sort Items By Group
@@ -249,6 +259,8 @@
249259
* Employee Free Time
250260
* [Interval](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/employee_free_time/interval.py)
251261
* [Test Employee Free Time](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/employee_free_time/test_employee_free_time.py)
262+
* Find Right Interval
263+
* [Test Find Right Interval](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/find_right_interval/test_find_right_interval.py)
252264
* Full Bloom Flowers
253265
* [Test Full Bloom Flowers](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/full_bloom_flowers/test_full_bloom_flowers.py)
254266
* Insert Interval
@@ -266,6 +278,8 @@
266278
* [Test Non Overlapping Intervals](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/non_overlapping_intervals/test_non_overlapping_intervals.py)
267279
* Remove Intervals
268280
* [Test Remove Covered Intervals](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/remove_intervals/test_remove_covered_intervals.py)
281+
* Remove Min Intervals
282+
* [Test Remove Min Intervals](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/remove_min_intervals/test_remove_min_intervals.py)
269283
* Task Scheduler
270284
* [Test Task Scheduler](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/task_scheduler/test_task_scheduler.py)
271285
* Josephus Circle
@@ -755,6 +769,13 @@
755769
* [Circuit Breaker](https://github.com/BrianLusina/PythonSnips/blob/master/design_patterns/circuit_breaker/circuit_breaker.py)
756770
* Continuous Median
757771
* [Test Continuous Median Handler](https://github.com/BrianLusina/PythonSnips/blob/master/design_patterns/continuous_median/test_continuous_median_handler.py)
772+
* Creational
773+
* Factory
774+
* Notification
775+
* [Email Notification](https://github.com/BrianLusina/PythonSnips/blob/master/design_patterns/creational/factory/notification/email_notification.py)
776+
* [Notification](https://github.com/BrianLusina/PythonSnips/blob/master/design_patterns/creational/factory/notification/notification.py)
777+
* [Notification Factory](https://github.com/BrianLusina/PythonSnips/blob/master/design_patterns/creational/factory/notification/notification_factory.py)
778+
* [Sms Notification](https://github.com/BrianLusina/PythonSnips/blob/master/design_patterns/creational/factory/notification/sms_notification.py)
758779
* Event Stream
759780
* [Audit Logger](https://github.com/BrianLusina/PythonSnips/blob/master/design_patterns/event_stream/audit_logger.py)
760781
* [Batch Event Processor](https://github.com/BrianLusina/PythonSnips/blob/master/design_patterns/event_stream/batch_event_processor.py)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Additive Number
2+
3+
An additive number is a string whose digits can form an additive sequence.
4+
5+
A valid additive sequence should contain at least three numbers. Except for the first two numbers, each subsequent
6+
number in the sequence must be the sum of the preceding two.
7+
8+
Given a string containing only digits, return true if it is an additive number or false otherwise.
9+
10+
> Note: Numbers in the additive sequence cannot have leading zeros, so sequence 1, 2, 03 or 1, 02, 3 is invalid.
11+
12+
## Examples
13+
14+
Example 1:
15+
```text
16+
Input: "112358"
17+
Output: true
18+
Explanation:
19+
The digits can form an additive sequence: 1, 1, 2, 3, 5, 8.
20+
1 + 1 = 2, 1 + 2 = 3, 2 + 3 = 5, 3 + 5 = 8
21+
```
22+
23+
Example 2:
24+
```text
25+
Input: "199100199"
26+
Output: true
27+
Explanation:
28+
The additive sequence is: 1, 99, 100, 199.
29+
1 + 99 = 100, 99 + 100 = 199
30+
```
31+
32+
## Constraints
33+
34+
- 1 <= num.length <= 35
35+
- num consists only of digits.
36+
37+
## Topics
38+
39+
- String
40+
- Backtracking
41+
42+
## Solution
43+
44+
The key intuition behind this problem is that once we fix the first two numbers in the sequence, the entire rest of the
45+
sequence is completely determined, each subsequent number must equal the sum of the two preceding ones. Therefore, we
46+
use backtracking to try all possible ways to choose the first and second numbers by varying their lengths, and for each
47+
choice, we greedily verify whether the remaining digits of the string can be partitioned to satisfy the additive property.
48+
The backtrack function processes the string from left to right, extracting candidate numbers of increasing length. For
49+
the first two numbers (count < 2), we freely explore all possible lengths. Once we have at least two numbers, we compute
50+
the `expectedSum` and prune: if `currentNum` exceeds `expectedSum`, we break (longer substrings will only be larger); if
51+
`currentNum` is less, we continue to try a longer substring. If `currentNum` matches, we recurse deeper. The recursion
52+
succeeds when we consume the entire string with at least 3 numbers in the sequence.
53+
54+
Now, let's look at the solution steps below:
55+
56+
1. Initialize n as the length of the input string num.
57+
2. Define a recursive helper function `backtrack(start, prev1, prev2, count)` where `start` is the current index in `num`,
58+
`prev1` is the second-to-last number, `prev2` is the last number, and count tracks how many numbers have been placed
59+
so far.
60+
3. **Base case**: If `start` equals `n` (the entire string has been consumed), return true if count ≥ 3, otherwise return
61+
false.
62+
4. Iterate over all possible end positions `end` from `start + 1` to `n` (inclusive) to form candidate substrings
63+
`num[start:end]`.
64+
- If the substring has length greater than 1 and starts with '0', break out of the loop — this handles the leading-zero
65+
constraint, since any longer substring starting from the same position will also have a leading zero.
66+
- Convert the `substring` to an integer `currentNum`.
67+
- If `count ≥ 2` (meaning we already have at least two numbers), compute `expectedSum` as `prev1 + prev2`.
68+
- If `currentNum` > `expectedSum`, break as no need to try longer substrings since they will only yield larger values.
69+
- If `currentNum` < `expectedSum`, continue and try a longer substring that might match the expected sum.
70+
- If `currentNum` = `expectedSum`, proceed to recurse.
71+
- Recursively call `backtrack(end, prev2, currentNum, count + 1)`. If it returns true, propagate true upward immediately.
72+
5. If no valid partition is found after exhausting all candidates, return false.
73+
6. Invoke backtrack(0, 0, 0, 0) and return its result.
74+
75+
### Time Complexity
76+
77+
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
78+
+rest for each choice. If you also account for substring-to-integer parsing cost, the practical bound can be higher.
79+
80+
> Note that Python natively handles arbitrarily large integers, which addresses the follow-up question about overflow
81+
> for very large inputs, no special handling is needed.
82+
83+
### Space Complexity
84+
85+
The space complexity of the solution is O(n) because the recursion depth is at most O(n) (in the case where each number
86+
is a single digit), and each recursive call uses a constant amount of additional space aside from the call stack.
87+
Substring creation also uses up to O(n) space.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
def is_additive_number_dfs(num: str) -> bool:
2+
if not num:
3+
return False
4+
5+
n = len(num)
6+
7+
def dfs(a: int, b: int, number: str) -> bool:
8+
if not number:
9+
return True
10+
11+
if a + b > 0 and number[0] == "0":
12+
return False
13+
14+
for i in range(1, len(number) + 1):
15+
if a + b == int(number[:i]):
16+
if dfs(b, a + b, number[i:]):
17+
return True
18+
return False
19+
20+
for x in range(1, n - 1):
21+
for y in range(x + 1, n):
22+
if x > 1 and num[0] == "0":
23+
break
24+
if y - x > 1 and num[x] == "0":
25+
continue
26+
if dfs(int(num[:x]), int(num[x:y]), num[y:]):
27+
return True
28+
29+
return False
30+
31+
32+
def is_additive_number_backtrack(num: str) -> bool:
33+
n = len(num)
34+
35+
def backtrack(start: int, prev1: int, prev2: int, count: int) -> bool:
36+
# Base case: we've consumed the entire string
37+
if start == n:
38+
# Valid only if we have at least 3 numbers in the sequence
39+
return count >= 3
40+
41+
# Try every possible next number by varying its length
42+
for end in range(start + 1, n + 1):
43+
# Extract the substring representing the current number
44+
substring = num[start:end]
45+
46+
# Skip numbers with leading zeros (e.g., "03"), but "0" itself is allowed
47+
if len(substring) > 1 and substring[0] == "0":
48+
break # No point trying longer substrings; they'll also have leading zero
49+
50+
current_num = int(substring)
51+
52+
# If we already have at least 2 numbers, validate the additive property
53+
if count >= 2:
54+
expected_sum = prev1 + prev2
55+
56+
# If current number is too large, no need to try longer substrings
57+
if current_num > expected_sum:
58+
break
59+
60+
# If current number doesn't match the expected sum, try next length
61+
if current_num < expected_sum:
62+
continue
63+
64+
# current_num == expected_sum, so recurse with updated sequence
65+
66+
# Recurse: prev2 becomes prev1, current_num becomes the new prev2
67+
if backtrack(end, prev2, current_num, count + 1):
68+
return True
69+
70+
return False
71+
72+
# Start backtracking from index 0 with no previous numbers and count = 0
73+
return backtrack(0, 0, 0, 0)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import unittest
2+
from algorithms.backtracking.additive_number import (
3+
is_additive_number_dfs,
4+
is_additive_number_backtrack,
5+
)
6+
from parameterized import parameterized
7+
8+
IS_ADDITIVE_NUMBER_TEST_CASES = [
9+
("112358", True),
10+
("199100199", True),
11+
("11235813", True),
12+
("12345", False),
13+
("000", True),
14+
("1", False),
15+
("0", False),
16+
("101", True),
17+
("1023", False),
18+
]
19+
20+
21+
class AdditiveNumberTestCase(unittest.TestCase):
22+
@parameterized.expand(IS_ADDITIVE_NUMBER_TEST_CASES)
23+
def test_is_additive_number_dfs(self, num: str, expected: bool):
24+
actual = is_additive_number_dfs(num)
25+
self.assertEqual(expected, actual)
26+
27+
@parameterized.expand(IS_ADDITIVE_NUMBER_TEST_CASES)
28+
def test_is_additive_number_backtrack(self, num: str, expected: bool):
29+
actual = is_additive_number_backtrack(num)
30+
self.assertEqual(expected, actual)
31+
32+
33+
if __name__ == "__main__":
34+
unittest.main()

0 commit comments

Comments
 (0)