93. Restore IP Addresses
Problem Description
The task is to generate all possible valid IP addresses from a given string containing only digits. A valid IP address has four integer sections separated by dots, and each integer must lie within the range of 0 to 255. Importantly, the integers must not have leading zeros, except for the digit "0" itself. To solve this problem, it is necessary to insert dots in the correct positions within the given string without changing the order of digits to create valid IP addresses. Each section of the IP address is essentially a substring of the original string. The challenge is to determine the partitions that yield a valid sequence of four integers, respecting the constraints stated above.
Flowchart Walkthrough
Let's determine the appropriate algorithm for solving Leetcode 93. Restore IP Addresses using the Flowchart. Here's a step-by-step analysis:
Is it a graph?
- No: The task of restoring IP addresses does not involve representing or processing data in the form of graphs with nodes and edges.
Need to solve for kth smallest/largest?
- No: The problem is not related to finding the kth smallest or largest element.
Involves Linked Lists?
- No: This problem does not use linked lists for storing or manipulating data.
Does the problem have small constraints?
- Yes: IP addresses consist of four octets, each ranging from 0 to 255, which places restrictive and manageable constraints on possible combinations.
Brute force / Backtracking?
- Yes: Given the small scope defined by IP address formatting rules, a backtracking approach is appropriate to explore all valid combinations of the segments to format IP addresses correctly.
Conclusion: Following the flowchart for algorithm selection, it suggests using a backtracking method for Leetcode 93. Restore IP Addresses because it fits a problem that relies on exploring multiple combinations within a limited set of constraints. The task is to construct feasible solutions (valid IP addresses) while adhering to specific formatting rules, making backtracking suitable for generating all possible valid IPs from the provided string.
Intuition
To arrive at the solution, we adopt a depth-first search (DFS) strategy, exploring each potential segment step by step and backtracking if a segment turns out to be invalid. The DFS algorithm tries to place the dots in all allowable positions, and upon placing each dot, it checks if the resulting segment is a valid integer between 0 and 255. It is important to note that a leading zero is permissible if and only if the segment is a single zero.
The steps of the algorithm are as follows:
- Proceed with the DFS traversal from the first digit of the string.
- In each function call, iterate over the possible lengths for the next segment, which could be 1, 2, or 3 digits long.
- At each iteration, check if the segment is a valid integer within the IP address criteria (0 to 255 and no leading zeros, unless the integer is 0).
- If the segment is valid, add it to a temporary list and recursively call the function with the updated index.
- If we reach a point where all four segments are valid and we have used all digits of the string, we add the combination to the result list.
- If at any point the segment is invalid or we've got 4 segments but the string is not fully used, we backtrack and try another segment length or configuration.
By exploring each possible dot placement and segment length, and ensuring that each constitutes a valid part of the IP address, we can guarantee that all valid IP addresses formed from the input string are found.
Learn more about Backtracking patterns.
Solution Approach
The provided solution uses a depth-first search (DFS) recursive algorithm to explore all possible combinations of the digits in the string that could result in a valid IP address. The following is a detailed explanation of the implementation:
-
Recursive Function (DFS): The
dfs(i: int)
function is a recursive function that takes one argument,i
, which represents the current index in the strings
. This function is called recursively to explore all potential valid IP segments starting at indexi
. -
Base Case and Result Collection: In the recursive function, there are two base cases. One checks if we are at the end of the string (
i >= n
) and have exactly four segments in the temporary list (len(t) == 4
), which is a condition for a valid IP address. If these conditions are met, we join the segments with dots and append the result to the answer listans
. The other base case handles the situation where reaching the end of the string without having exactly four segments or having four segments but not using all characters, which leads to termination of the current path. -
Checking for Valid Segments: A helper function
check(i: int, j: int)
is used to determine if the substring from indexi
toj
(inclusive) constitutes a valid IP segment. The check function accounts for leading zeros and the valid integer range. A leading zero is only valid if it is a singular zero (e.g., "0"). Any segment leading with a "0" and having additional digits (e.g., "01") is invalid. -
Exploration and Backtracking: The core of the DFS is a for-loop that iterates from the current index
i
to the minimum betweeni + 3
andn
(the length of the string). This accounts for the fact that IP address segments have at most three digits. If a valid segment (substring) is found using thecheck
function, it's added to the current temporary listt
, and the DFS continues from the next indexj + 1
. After the recursive call returns, the added segment is removed (backtracking), and the loop continues to explore the next possible segment. -
Parallel Variables: The solution maintains several variables in parallel:
n
, which is the length of the input strings
;ans
, the list holding all the valid IP addresses found; andt
, which is a temporary list used to store the current segments of the IP address being explored. -
Initiation of DFS: The process begins by calling
dfs(0)
, which triggers the recursive exploration from the first character in the string.
The DFS pattern used here is crucial not only to generate all possible segmentations but also to ensure efficiency by halting early in paths that cannot lead to valid IP addresses. The use of recursion with backtracking enables the algorithm to explore different segment lengths and combinations effectively.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's consider a simple example to illustrate how the solution works with the given string "25525511135".
-
Begin by calling the recursive DFS function from the first character:
dfs(0)
. -
In the first level of recursion, start with an empty list
t = []
. Iterate over string lengths 1-3 starting from the first characters[0]
as long as we remain within the string length (dot placement possibilities), here let's try all three:- Try taking "2" as the first segment:
t = ["2"]
, calldfs(1)
. - Try taking "25" as the first segment:
t = ["25"]
, calldfs(2)
. - Try taking "255" as the first segment:
t = ["255"]
, calldfs(3)
.
- Try taking "2" as the first segment:
-
Following the depth-first search strategy, let's dig into the case where the first segment is "255":
dfs(3)
. -
With
t = ["255"]
, proceed with the next characters[3]
, trying segment lengths of 1-3.- Try taking "2" as the next segment:
t = ["255", "2"]
, calldfs(4)
. - Try taking "25" as the next segment:
t = ["255", "25"]
, is invalid as "25525" is not a possible IP address. Backtracking occurs, no recursive call is made. - Try taking "255" as the next segment:
t = ["255", "255"]
, which is valid, so proceed withdfs(6)
.
- Try taking "2" as the next segment:
-
Now consider
t = ["255", "255"]
. Atdfs(6)
, try out different lengths again.- Taking "1" as the next segment,
t = ["255", "255", "1"]
, calldfs(7)
. - Taking "11" as the next segment,
t = ["255", "255", "11"]
, calldfs(8)
. - Taking "111" as the next segment is not possible here since we would run out of characters for the last segment. No recursive call happens.
- Taking "1" as the next segment,
-
Consider the state
t = ["255", "255", "1"]
. Now calldfs(7)
and follow the same process.- Taking "3" as the next segment:
t = ["255", "255", "1", "3"]
and callingdfs(8)
. - Taking "35" as the next segment:
t = ["255", "255", "1", "35"]
is valid but doesn't use all characters, no recursive call is made. - Taking "135" as the final segment is not possible since the start index would be greater than string length. No recursive call is made.
- Taking "3" as the next segment:
-
In
dfs(8)
witht = ["255", "255", "1", "3"]
, we are at the end of the string, so joint
with dots to get "255.255.1.3" and add this to the answer listans
. -
Now backtrack to the state
t = ["255", "255", "1"]
and attempt the next possible recursion which would have been with the segment "35" or "135". Both these were ruled out in step 6. -
Continue backtracking to previous recursion levels and trying different segment lengths and combinations in this manner until all possibilities have been exhausted.
By the end of the described example, we will have all valid IP addresses that can be formed from the given string stored in the ans
list.
Solution Implementation
1class Solution:
2 def restoreIpAddresses(self, s: str) -> List[str]:
3 # Helper function to check if the part of the string can be a valid IP address segment
4 def is_valid_segment(start: int, end: int) -> bool:
5 if s[start] == "0" and start != end: # Leading zero is not allowed unless the segment is '0'
6 return False
7 return 0 <= int(s[start: end + 1]) <= 255 # Check if the segment is in the range 0-255
8
9 # Recursive function to generate all possible valid IP addresses
10 def backtrack(current_index: int):
11 # If at the end of the string and exactly 4 segments are found, add the IP to the solutions
12 if current_index >= string_length and len(current_ip):
13 solutions.append(".".join(current_ip))
14 return
15 if current_index >= string_length or len(current_ip) >= 4: # If it's not a valid IP, just return
16 return
17 # Try to form a valid segment by choosing 1 to 3 digits
18 for j in range(current_index, min(current_index + 3, string_length)):
19 if is_valid_segment(current_index, j):
20 current_ip.append(s[current_index: j + 1]) # Add the new segment
21 backtrack(j + 1) # Recurse for the next segments
22 current_ip.pop() # Backtrack and remove the last segment before the next iteration
23
24 string_length = len(s) # Store the length of the string
25 solutions = [] # This will store all the valid IP addresses
26 current_ip = [] # Temporary list to store the parts of the current IP address
27 backtrack(0) # Start the recursive backtrack function
28 return solutions
29
30# Example usage
31# sol = Solution()
32# valid_ips = sol.restoreIpAddresses("25525511135")
33# print(valid_ips) # Output would be ['255.255.11.135', '255.255.111.35']
34
1class Solution {
2 private int stringLength; // Length of the input string
3 private String inputString; // The input string representing the digits of the IP address
4 private List<String> validIPAddresses = new ArrayList<>(); // List to hold the valid IP addresses
5 private List<String> currentSegment = new ArrayList<>(); // List to hold the current segments of the IP address being constructed
6
7 // Public method to restore IP addresses from the given string.
8 public List<String> restoreIpAddresses(String s) {
9 stringLength = s.length();
10 inputString = s;
11 backtrack(0); // Begin the depth-first search (DFS) from the first character of the string
12 return validIPAddresses;
13 }
14
15 // Helper method to perform a DFS to build all valid IP addresses.
16 private void backtrack(int index) {
17 // Check if we have processed the entire string and we have exactly 4 segments
18 if (index >= stringLength && currentSegment.size() == 4) {
19 // Join the segments and add the resulting IP address to the list
20 validIPAddresses.add(String.join(".", currentSegment));
21 return;
22 }
23 // If we've processed the entire string or have more than 4 segments, backtrack
24 if (index >= stringLength || currentSegment.size() >= 4) {
25 return;
26 }
27
28 // Initialize an integer to store the numeric value of current segment
29 int segmentValue = 0;
30 // Consider 1 to 3 digit long segments (as an IP segment ranges from 0 to 255)
31 for (int j = index; j < Math.min(index + 3, stringLength); ++j) {
32 segmentValue = segmentValue * 10 + inputString.charAt(j) - '0'; // Convert current segment to integer
33
34 // Check for leading zeroes and if segmentValue is greater than 255
35 if (segmentValue > 255 || (inputString.charAt(index) == '0' && index != j)) {
36 break; // If any of those checks fail, stop exploring further and backtrack
37 }
38
39 // Add the current segment to our list and continue the search
40 currentSegment.add(inputString.substring(index, j + 1));
41 backtrack(j + 1); // Explore further by calling backtrack recursively
42 currentSegment.remove(currentSegment.size() - 1); // Remove the last added segment to backtrack
43 }
44 }
45}
46
1#include <vector>
2#include <string>
3#include <functional>
4
5class Solution {
6public:
7 // Restore IP Addresses from a string.
8 std::vector<std::string> restoreIpAddresses(std::string s) {
9 int length = s.size(); // Get the length of the input string.
10 std::vector<std::string> validIPs; // Store all valid IP addresses.
11 std::vector<std::string> currentIP; // Store the current IP sections.
12
13 // Define the recursive function to perform depth-first search.
14 std::function<void(int)> dfs = [&](int index) {
15 // If index has reached the end of the string and the current IP has exactly 4 parts, a valid IP is found.
16 if (index >= length && currentIP.size() == 4) {
17 validIPs.push_back(currentIP[0] + "." + currentIP[1] + "." + currentIP[2] + "." + currentIP[3]);
18 return;
19 }
20 // If index is beyond the string length or more than 4 parts are formed, there's no valid IP.
21 if (index >= length || currentIP.size() >= 4) {
22 return;
23 }
24 int segmentValue = 0; // Value of the current IP part.
25
26 // Iterate through the string, ensuring to not go beyond length and within the 3 characters segment limit.
27 for (int j = index; j < std::min(length, index + 3); ++j) {
28 segmentValue = segmentValue * 10 + s[j] - '0'; // Calculate the segment value.
29
30 // If the segment value is more than 255 or if it starts with a '0' but is not single digit, it's invalid.
31 if (segmentValue > 255 || (j > index && s[index] == '0')) {
32 break;
33 }
34 // Add the current segment to the currentIP and continue with the next part.
35 currentIP.push_back(s.substr(index, j - index + 1));
36 dfs(j + 1); // Recurse to the next part of the string.
37 currentIP.pop_back(); // Backtrack to explore other possibilities.
38 }
39 };
40 dfs(0); // Start DFS from the 0th index of the input string.
41 return validIPs; // Return all the valid IP addresses.
42 }
43};
44
1function restoreIpAddresses(s: string): string[] {
2 const lengthOfString = s.length;
3 const result: string[] = []; // This will hold all the valid IP addresses
4 const currentIPParts: string[] = []; // Temporarily stores the current parts of the IP address
5
6 // Helper function to perform a depth-first search for IP address segments starting from index 'idx'
7 const findIPAddresses = (idx: number): void => {
8 // Check if we have traversed the string and have exactly 4 parts for an IP address
9 if (idx >= lengthOfString && currentIPParts.length === 4) {
10 result.push(currentIPParts.join('.')); // Join the parts and add them to the result list
11 return;
12 }
13
14 // If we have already 4 parts or have traversed the string, no need to proceed further
15 if (idx >= lengthOfString || currentIPParts.length === 4) {
16 return;
17 }
18
19 let currentSegmentValue = 0;
20 for (let j = idx; j < idx + 3 && j < lengthOfString; ++j) {
21 // Compute the current segment value
22 currentSegmentValue = currentSegmentValue * 10 + s[j].charCodeAt(0) - '0'.charCodeAt(0);
23
24 // Check if the current segment value is greater than 255 or it has a leading zero
25 if (currentSegmentValue > 255 || (j > idx && s[idx] === '0')) {
26 break;
27 }
28
29 // Add the current segment to the current IP parts and proceed recursively
30 currentIPParts.push(currentSegmentValue.toString());
31 findIPAddresses(j + 1); // Recur for the next part of the string
32 currentIPParts.pop(); // Backtrack to try different segment combinations
33 }
34 };
35
36 // Start the depth-first search from the first index
37 findIPAddresses(0);
38
39 // Return the list of valid IP addresses found
40 return result;
41}
42
Time and Space Complexity
The given Python code defines a method to restore possible IP addresses from a string by implementing a depth-first search (DFS) algorithm.
Time Complexity
The time complexity of the algorithm can be considered as O(1)
in terms of the input string's length, since an IP address consists of 4 parts, and each part can have a maximum of 3 digits. The check function is called at each step of the DFS and runs in O(1)
time since it operates on a constant size substring (at most 3 characters).
The DFS function will attempt to place a dot after every 1 to 3 digits, but since IP addresses are fixed length (4 parts of at most 3 digits each), the maximum depth of the recursive call stack will be 4, and there will be at most 3^4
possible combinations to check (3
choices at each of the 4
levels of the decision tree). This results in a total of 81
iterations in the worst case, each taking constant time.
Thus, the overall time complexity is O(1)
since the size of the input is not a factor beyond a certain length (the length must be between 4 and 12 for a valid IP address).
Space Complexity
The space complexity of the solution mainly depends on the size of the recursive call stack and the space used to store the intermediate and final solutions. As previously mentioned, the recursive call stack will have at most 4 levels due to the nature of IP addresses. Plus, a single path t
in the recursion tree is a list that can have at most 4 strings, each up to 3 characters long.
The list ans
will contain all the valid IP addresses we find. In the worst-case scenario, every partitioning will lead to a valid IP address, but this is highly unlikely. However, if we consider every single character as a digit and each digit forms a valid part of an IP address, the maximum number of valid IP addresses would be 3^4
(though actually it would be less due to the leading zero and value >255 restrictions).
Hence, the space complexity for the output list is O(1)
, and the overall space complexity of the algorithm including the recursive call stack and the temporary list t
is also O(1)
, since the problem's constraints limit the input size and, consequently, the recursion depth and output size.
Learn more about how to find time and space complexity quickly using problem constraints.
You are given an array of intervals where intervals[i] = [start_i, end_i]
represent the start and end of the ith
interval. You need to merge all overlapping intervals and return an array of the non-overlapping intervals that cover all the intervals in the input.
Recommended Readings
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
LeetCode Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way we
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!