Coding Interview Patterns Module 16 – Backtracking
Partitioning strings with backtracking
Cut a string into valid pieces with backtracking: restore IPv4 addresses from digits, list palindromic partitions and count them without listing.
What you will learn
- Split a string into valid pieces, such as the parts of an IPv4 address or palindromes
- Eliminate a split as soon as a piece fails or the characters left cannot fill the pieces left
- Build a palindrome table so that each check is O(1)
- Count partitions with memoised suffixes when listing them is too slow
- Validate the digits of an address against leading zeros, the 0 to 255 range and digits from other scripts
Before you start
What you’ll learn · Before you start
What you will learn
- Split a string into valid pieces, such as the parts of an IPv4 address or palindromes
- Eliminate a split as soon as a piece fails or the characters left cannot fill the pieces left
- Build a palindrome table so that each check is O(1)
- Count partitions with memoised suffixes when listing them is too slow
- Validate the digits of an address against leading zeros, the 0 to 255 range and digits from other scripts
Before you start
On this page
Partitioning a string means cutting it into consecutive pieces so that every piece passes a test. Backtracking makes the cuts from left to right: choose the next piece, check it at once, solve the rest, and undo. Restoring IP addresses is the classic case: a string of digits is cut into four parts, each a number from 0 to 255 without a leading zero. In palindromic partitioning every piece must read the same backwards. A length bound stops a split when the characters left cannot fill the pieces left. A precomputed table answers each palindrome check in O(1). Memoised counts for each suffix give the number of partitions without listing them.
Two problems about cutting strings
Problem A: restore the addresses. A log lost the dots of its IPv4 addresses. Write restore_addresses(digits),
which returns every dotted-decimal address that uses all the digits in order. Limits: up to 20 digits; fewer than 4 or
more than 12 give none.
172162541gives 6 addresses, among them 172.16.254.1 and 17.216.25.41.10010gives 1.0.0.10 and 10.0.1.0, and0000gives only 0.0.0.0.
Problem B: palindromic pieces. Write palindrome_partitions(word), which lists every way to cut a word into
palindromes. Limits: up to 40 letters, with at most 100,000 partitions in the answer. noon gives n o o n, n oo n and
noon; malayalam gives 8.
Three splits of 10010 into four parts
Text description of the diagram
The diagram shows the digits 10010 split into four parts by three dots, three times, one split per row.
- The first row is 1, 0, 0 and 10, in solid boxes: the address 1.0.0.10.
- The second row is 10, 0, 1 and 0, in solid boxes: the address 10.0.1.0.
- The third row is 1, 00, 1 and 0, in dashed boxes: it is no address, because the part 00 has a leading zero.
A note at the bottom gives the rule for every part: 1 to 3 digits, a number from 0 to 255, and no leading zero.
The brute force, and where it stops
For A, try every placement of three dots and check the four parts. For B, try every set of cut positions and check every piece:
"""Both problems by brute force: try every way to cut the string, then check every piece."""
import time
from itertools import combinations
def valid_part(part):
"""A part of a dotted-decimal IPv4 address: 1 to 3 digits, no leading zero, at most 255."""
return 1 <= len(part) <= 3 and part.isdigit() and part.isascii() and (part == "0" or part[0] != "0") and int(part) <= 255
def addresses_brute(digits):
"""Every way to put three dots between the digits, kept when all four parts are valid."""
found = []
for a, b, c in combinations(range(1, len(digits)), 3): # the three cut positions
parts = [digits[:a], digits[a:b], digits[b:c], digits[c:]]
if all(valid_part(p) for p in parts):
found.append(".".join(parts))
return found
def palindromes_brute(word):
"""Every set of cut positions, kept when every piece reads the same backwards."""
n, found = len(word), []
for mask in range(1 << max(n - 1, 0)): # bit i: cut after letter i
pieces, start = [], 0
for i in range(n - 1):
if mask >> i & 1:
pieces.append(word[start : i + 1])
start = i + 1
pieces.append(word[start:])
if all(p == p[::-1] for p in pieces):
found.append(pieces)
return found
print(addresses_brute("172162541"), addresses_brute("10010"), addresses_brute("0000"))
print(palindromes_brute("noon"))
print("letters cut sets tried palindromic partitions time")
for n in (14, 16, 18):
word = "abcdefghijklmnopqrstuvwxyz"[:n]
start = time.perf_counter()
found = palindromes_brute(word)
seconds = time.perf_counter() - start
print(f"{n:7} {1 << (n - 1):>15,} {len(found):>23} {seconds:.2f} s")
print(f"40 letters: {1 << 39:,} cut sets to try") Output
['17.216.25.41', '17.216.254.1', '172.16.25.41', '172.16.254.1', '172.162.5.41', '172.162.54.1'] ['1.0.0.10', '10.0.1.0'] ['0.0.0.0']
[['noon'], ['n', 'oo', 'n'], ['n', 'o', 'o', 'n']]
letters cut sets tried palindromic partitions time
14 8,192 1 0.01 s
16 32,768 1 0.09 s
18 131,072 1 0.90 s
40 letters: 549,755,813,888 cut sets to try
This output changes from run to run: the times depend on the computer and on what else it is doing
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brute_force.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
For A the brute force is fine: 12 digits allow only C(11, 3) = 165 placements, and a search over parts of 1 to 3 digits is at most four levels deep with three choices each, 121 calls at most. The difficulty is in the rules. For B each letter doubles the cut sets. Words of distinct letters have a single partition, yet 18 letters cost 131,072 cut sets, and 40 letters would cost 549,755,813,888.
What to notice
A bad piece ruins every split that contains it, so check each piece the moment it is cut. For addresses, the digits left must also fit the parts left, one to three digits each. For palindromes, every single letter passes, so every branch of the search reaches a full partition. The work then follows the size of the answer, and only the checks need to become faster.
Restoring addresses step by step, with a dry run Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Why every address is found exactly once
- The bound and the rules, measured, in Python and JavaScript
- Why the rules matter: the same digits, read differently
- The code in Java, C++ and Go
- Checked against every placement and against ipaddress
- Palindromic partitions with a precomputed table
- Counting partitions without listing them
- Variants
- Edge cases and common bugs
- Exercise: every address hidden in a string of digits
- Every address hidden in a string of digits
- A practice set with answers
- Check yourself
- Interview questions
- Restoring IPv4 addresses from a string of digits is a backtracking search. Why does it never take long, however long the string?
- An address restorer passes its tests, but a colleague notices that it returns addresses with parts like 01 and accepts digits from other scripts. What exactly makes a part valid, why does each rule matter, and which tests would you add?
- A service returns the palindromic partitions of words of up to 40 letters. For most words it answers at once, but for some it never finishes. Which words, why, and what would you change in the contract and in the code?
- A palindromic partitioner checks each piece by comparing letters from both ends. A colleague suggests a precomputed table. When does the table pay for itself, what does it cost, and how would you show the difference?
- Key takeaways
8 runnable examples (C++, Go, Java, JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 20 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- RFC 3986: Uniform Resource Identifier (URI): Generic Syntax, section 3.2.2 (dec-octet) (IETF)
- RFC 6943: Issues in Identifier Comparison for Security Purposes, section 3.1.1 (IPv4 literals) (IETF)
- URL Standard: IPv4 parser (WHATWG)
- ipaddress: IPv4 and IPv6 manipulation library (The Python Standard Library) (Python Software Foundation)
- Numeric types: the digits int() accepts (The Python Standard Library: built-in types) (Python Software Foundation)
- functools.cache (The Python Standard Library) (Python Software Foundation)
- Java SE 25 API: Math.addExact(long, long) (Oracle)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress