3481. Apply Substitutions π
Problem Description
You are given a replacements
mapping and a text
string that may contain placeholders formatted as %var%
, where each var
corresponds to a key in the replacements
mapping. Each replacement value may itself contain one or more such placeholders. Each placeholder is replaced by the value associated with its corresponding replacement key.
Your task is to return the fully substituted text
string which does not contain any placeholders.
The key challenge here is that replacement values can themselves contain placeholders that need to be resolved. This means you need to recursively substitute placeholders until no more remain.
For example:
- If
replacements = [["name", "bob"], ["greeting", "hello %name%"]]
andtext = "%greeting%!"
, you would:- Replace
%greeting%
with"hello %name%"
- Then replace
%name%
with"bob"
- Return
"hello bob!"
- Replace
The solution uses a hash table to store the replacement mappings and a recursive function to handle the nested placeholder substitutions. The function finds placeholders marked by %
symbols, extracts the key between them, recursively resolves the replacement value, and continues processing the rest of the string.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: While this problem doesn't explicitly mention a graph with nodes and edges, we can model it as a graph problem. Each placeholder and its replacement form a directed edge in a dependency graph. For example, if
%greeting%
maps to"hello %name%"
, there's a directed edge fromgreeting
toname
.
Is it a tree?
- No: The dependency graph is not necessarily a tree. It could have multiple paths and potentially cycles (though valid inputs shouldn't have cycles as they would lead to infinite substitution).
Is the problem related to directed acyclic graphs?
- No: While the dependency graph is directed, the main focus isn't on DAG properties like topological ordering.
Is the problem related to shortest paths?
- No: We're not finding shortest paths; we need to fully expand all placeholders.
Does the problem involve connectivity?
- No: We're not checking if nodes are connected or finding connected components.
Does the problem have small constraints?
- Yes: The problem typically has manageable constraints since we're dealing with text substitution, and the recursive depth is limited by the number of nested placeholders.
DFS/backtracking?
- Yes: DFS is the perfect fit here. We need to recursively explore each placeholder, resolve its value (which may contain more placeholders), and continue this process until no placeholders remain.
Conclusion: The flowchart suggests using a DFS (Depth-First Search) approach. Each time we encounter a placeholder, we recursively resolve its replacement value before continuing with the rest of the string. This naturally handles nested placeholders through the recursive call stack.
Intuition
The key insight is recognizing that placeholder substitution is inherently a recursive problem. When we replace a placeholder like %greeting%
with "hello %name%"
, we've created a new problem - we now need to resolve %name%
. This naturally suggests a depth-first approach where we dive deep into each replacement before moving on.
Think of it like unpacking nested boxes. When you open a box and find another box inside, you need to open that inner box first before you can fully unpack everything. Similarly, when we find a placeholder, we need to fully resolve its replacement value (which might contain more placeholders) before we can consider that part of the text complete.
The recursive nature becomes clear when we consider the substitution process:
- We scan the text for a placeholder
%var%
- We look up
var
in our replacements mapping - The replacement itself might contain placeholders, so we need to apply the same process to it
- Only after the replacement is fully resolved can we substitute it back into the original text
- We continue this process for the rest of the text
This is exactly what DFS does - it explores one path completely before backtracking. In our case, we explore one placeholder's replacement chain completely before moving to the next placeholder in the text.
The beauty of using recursion here is that it naturally handles arbitrary nesting depths. Whether a placeholder resolves directly to plain text or goes through multiple levels of substitution, the same recursive logic applies. The recursion naturally terminates when we encounter text with no placeholders, giving us our base case.
Learn more about Depth-First Search, Breadth-First Search, Graph and Topological Sort patterns.
Solution Approach
The implementation uses a hash table combined with recursion to efficiently handle the placeholder substitutions.
Step 1: Build the Hash Table
We first convert the replacements
list into a dictionary d
where each key-value pair represents a placeholder variable and its replacement text:
d = {s: t for s, t in replacements}
This gives us O(1) lookup time for each placeholder variable.
Step 2: Define the Recursive DFS Function
The core logic is in the dfs
function that processes a string s
:
-
Find the first placeholder's start: Use
s.find("%")
to locate the first%
symbol. If no%
is found (returns -1), the string has no placeholders, so we return it as-is. -
Find the placeholder's end: From position
i + 1
, find the next%
symbol usings.find("%", i + 1)
. If we can't find a closing%
, the string is malformed or incomplete, so we return it as-is. -
Extract the placeholder key: The variable name is between the two
%
symbols:key = s[i + 1 : j]
-
Recursively resolve the replacement: Look up
d[key]
and recursively calldfs
on it to resolve any nested placeholders:replacement = dfs(d[key])
-
Reconstruct the string: Build the result by concatenating:
- The text before the placeholder:
s[:i]
- The fully resolved replacement:
replacement
- The recursively processed remainder:
dfs(s[j + 1:])
- The text before the placeholder:
Step 3: Process the Input Text
Call dfs(text)
to start the recursive substitution process on the input text.
The algorithm elegantly handles nested placeholders because when we call dfs(d[key])
, it will fully resolve all placeholders in that replacement value before returning. This ensures that by the time we substitute a replacement into our text, it's already fully expanded.
The time complexity depends on the depth and structure of nested placeholders, but each character in the final expanded text is processed once, making it efficient for typical use cases.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to see how the recursive DFS approach works.
Given:
replacements = [["name", "Alice"], ["day", "Monday"], ["greeting", "Hi %name%, happy %day%!"]]
text = "Message: %greeting%"
Step 1: Build Hash Table
d = {"name": "Alice", "day": "Monday", "greeting": "Hi %name%, happy %day%!"}
Step 2: Process text = "Message: %greeting%"
Call dfs("Message: %greeting%")
:
- Find first
%
at position 9 - Find closing
%
at position 18 - Extract key:
"greeting"
(characters between positions 10-17) - Look up
d["greeting"]
="Hi %name%, happy %day%!"
- Recursively call
dfs("Hi %name%, happy %day%!")
Step 3: Process nested replacement "Hi %name%, happy %day%!"
In dfs("Hi %name%, happy %day%!")
:
- Find first
%
at position 3 - Find closing
%
at position 8 - Extract key:
"name"
- Look up
d["name"]
="Alice"
- Recursively call
dfs("Alice")
β returns"Alice"
(no placeholders) - Reconstruct:
"Hi " + "Alice" + dfs(", happy %day%!")
Step 4: Continue with remaining text ", happy %day%!"
In dfs(", happy %day%!")
:
- Find first
%
at position 8 - Find closing
%
at position 12 - Extract key:
"day"
- Look up
d["day"]
="Monday"
- Recursively call
dfs("Monday")
β returns"Monday"
(no placeholders) - Reconstruct:
", happy " + "Monday" + dfs("!")
dfs("!")
returns"!"
(no placeholders)- Result:
", happy Monday!"
Step 5: Build final result
Working backwards through our recursive calls:
dfs(", happy %day%!")
returns", happy Monday!"
dfs("Hi %name%, happy %day%!")
returns"Hi Alice, happy Monday!"
dfs("Message: %greeting%")
returns"Message: " + "Hi Alice, happy Monday!" + ""
Final Output: "Message: Hi Alice, happy Monday!"
The key insight is how recursion naturally handles the nested placeholders - we fully resolve each replacement value before substituting it back, ensuring no placeholders remain in the final text.
Solution Implementation
1class Solution:
2 def applySubstitutions(self, replacements: List[List[str]], text: str) -> str:
3 """
4 Apply variable substitutions to a text string.
5 Variables are enclosed in % symbols (e.g., %variable_name%).
6
7 Args:
8 replacements: List of [variable_name, replacement_value] pairs
9 text: Input text containing variables to be replaced
10
11 Returns:
12 Text with all variables replaced by their corresponding values
13 """
14
15 def recursive_replace(string: str) -> str:
16 """
17 Recursively find and replace variables in the string.
18 Handles nested replacements where replacement values may contain other variables.
19
20 Args:
21 string: The string to process
22
23 Returns:
24 String with all variables replaced
25 """
26 # Find the first occurrence of '%' marking the start of a variable
27 start_index = string.find("%")
28 if start_index == -1:
29 # No variables found, return the string as-is
30 return string
31
32 # Find the closing '%' for this variable
33 end_index = string.find("%", start_index + 1)
34 if end_index == -1:
35 # No closing '%' found, return the string as-is
36 return string
37
38 # Extract the variable name between the two '%' symbols
39 variable_name = string[start_index + 1 : end_index]
40
41 # Recursively process the replacement value in case it contains variables
42 replacement_value = recursive_replace(replacement_dict[variable_name])
43
44 # Construct the result:
45 # - Everything before the variable
46 # - The replacement value
47 # - Recursively process everything after the variable
48 return (string[:start_index] +
49 replacement_value +
50 recursive_replace(string[end_index + 1:]))
51
52 # Convert the list of replacements to a dictionary for O(1) lookup
53 replacement_dict = {key: value for key, value in replacements}
54
55 # Start the recursive replacement process
56 return recursive_replace(text)
57
1class Solution {
2 // Map to store variable replacements: variable name -> replacement text
3 private final Map<String, String> replacementMap = new HashMap<>();
4
5 /**
6 * Applies variable substitutions to the given text.
7 * Variables are denoted by %variableName% format.
8 *
9 * @param replacements List of [variableName, replacementText] pairs
10 * @param text The text containing variables to be replaced
11 * @return The text with all variables substituted
12 */
13 public String applySubstitutions(List<List<String>> replacements, String text) {
14 // Build the replacement map from the input list
15 for (List<String> replacement : replacements) {
16 replacementMap.put(replacement.get(0), replacement.get(1));
17 }
18
19 // Recursively process the text to replace all variables
20 return processText(text);
21 }
22
23 /**
24 * Recursively processes the text to replace variables with their values.
25 * Handles nested variable replacements through recursive calls.
26 *
27 * @param text The text to process
28 * @return The processed text with variables replaced
29 */
30 private String processText(String text) {
31 // Find the first occurrence of '%' which marks the start of a variable
32 int startIndex = text.indexOf("%");
33 if (startIndex == -1) {
34 // No variables found, return the text as is
35 return text;
36 }
37
38 // Find the closing '%' for this variable
39 int endIndex = text.indexOf("%", startIndex + 1);
40 if (endIndex == -1) {
41 // No closing '%' found, return the text as is
42 return text;
43 }
44
45 // Extract the variable name between the '%' markers
46 String variableName = text.substring(startIndex + 1, endIndex);
47
48 // Get the replacement value for this variable (empty string if not found)
49 // Recursively process the replacement to handle nested variables
50 String replacementValue = processText(replacementMap.getOrDefault(variableName, ""));
51
52 // Construct the result:
53 // prefix (before variable) + replacement value + recursively process remainder
54 return text.substring(0, startIndex) +
55 replacementValue +
56 processText(text.substring(endIndex + 1));
57 }
58}
59
1class Solution {
2public:
3 string applySubstitutions(vector<vector<string>>& replacements, string text) {
4 // Build a mapping from placeholder names to their replacement values
5 unordered_map<string, string> placeholderMap;
6 for (const auto& replacement : replacements) {
7 placeholderMap[replacement[0]] = replacement[1];
8 }
9
10 // Recursive function to process the text and replace placeholders
11 function<string(const string&)> processText = [&](const string& str) -> string {
12 // Find the first '%' character
13 size_t startPos = str.find('%');
14 if (startPos == string::npos) {
15 // No placeholder found, return the string as is
16 return str;
17 }
18
19 // Find the closing '%' character
20 size_t endPos = str.find('%', startPos + 1);
21 if (endPos == string::npos) {
22 // No closing '%' found, return the string as is
23 return str;
24 }
25
26 // Extract the placeholder key between the two '%' characters
27 string placeholderKey = str.substr(startPos + 1, endPos - startPos - 1);
28
29 // Recursively process the replacement value for nested placeholders
30 string replacementValue = processText(placeholderMap[placeholderKey]);
31
32 // Construct the result: prefix + processed replacement + recursively process suffix
33 return str.substr(0, startPos) +
34 replacementValue +
35 processText(str.substr(endPos + 1));
36 };
37
38 // Start processing from the input text
39 return processText(text);
40 }
41};
42
1/**
2 * Applies text substitutions based on key-value replacement pairs.
3 * Replaces all occurrences of %key% in the text with corresponding values.
4 * Supports nested substitutions where values can also contain %key% patterns.
5 *
6 * @param replacements - Array of [key, value] pairs for substitution
7 * @param text - The input text containing %key% patterns to replace
8 * @returns The text with all substitutions applied
9 */
10function applySubstitutions(replacements: string[][], text: string): string {
11 // Build a dictionary from the replacement pairs for O(1) lookup
12 const replacementMap: Record<string, string> = {};
13 for (const [key, value] of replacements) {
14 replacementMap[key] = value;
15 }
16
17 /**
18 * Recursively processes a string to replace all %key% patterns.
19 * Handles nested substitutions by recursively processing replacement values.
20 *
21 * @param currentString - The string to process for substitutions
22 * @returns The processed string with all substitutions applied
23 */
24 const processSubstitutions = (currentString: string): string => {
25 // Find the first occurrence of '%' character
26 const startDelimiterIndex = currentString.indexOf('%');
27 if (startDelimiterIndex === -1) {
28 // No '%' found, return the string as-is
29 return currentString;
30 }
31
32 // Find the closing '%' for this substitution pattern
33 const endDelimiterIndex = currentString.indexOf('%', startDelimiterIndex + 1);
34 if (endDelimiterIndex === -1) {
35 // No closing '%' found, return the string as-is
36 return currentString;
37 }
38
39 // Extract the key between the two '%' characters
40 const substitutionKey = currentString.slice(startDelimiterIndex + 1, endDelimiterIndex);
41
42 // Get the replacement value and recursively process it for nested substitutions
43 // Use empty string if key is not found in the map
44 const substitutionValue = processSubstitutions(replacementMap[substitutionKey] ?? '');
45
46 // Construct the result: prefix + processed replacement + recursively process suffix
47 return currentString.slice(0, startDelimiterIndex) +
48 substitutionValue +
49 processSubstitutions(currentString.slice(endDelimiterIndex + 1));
50 };
51
52 // Start the recursive substitution process
53 return processSubstitutions(text);
54}
55
Time and Space Complexity
Time Complexity: O(m + n Γ L)
The time complexity consists of two main components:
- Building the dictionary from replacements takes
O(m)
time, wherem
is the total length of all replacement mappings - The DFS traversal processes the text, where:
- Finding each placeholder pair (two
find()
operations) takesO(n)
time in worst case, wheren
is the length of the text - For each placeholder found, we recursively process the replacement value, which on average has length
L
- The string slicing and concatenation operations also contribute to the complexity
- In the worst case, we might need to process multiple nested placeholders, leading to
O(n Γ L)
time complexity for the DFS portion
- Finding each placeholder pair (two
Space Complexity: O(m + n Γ L)
The space complexity includes:
O(m)
space for storing the dictionaryd
containing all replacement mappings- The recursion stack depth can go up to
O(L)
in case of nested placeholder substitutions - String slicing creates new strings, and in worst case with nested replacements, the total space used for intermediate strings can be
O(n Γ L)
- The combination of dictionary storage and recursive call stack with string operations results in
O(m + n Γ L)
space complexity
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Infinite Recursion from Circular Dependencies
The most critical pitfall in this solution is the lack of protection against circular references in the replacements. If replacement values reference each other in a cycle, the recursive function will run indefinitely until hitting the recursion limit.
Example of the problem:
replacements = [["A", "%B%"], ["B", "%A%"]] text = "%A%" # This would cause infinite recursion: A -> B -> A -> B -> ...
Solution: Add a visited set to track which variables are currently being resolved:
def recursive_replace(string: str, visiting: set = None) -> str:
if visiting is None:
visiting = set()
start_index = string.find("%")
if start_index == -1:
return string
end_index = string.find("%", start_index + 1)
if end_index == -1:
return string
variable_name = string[start_index + 1 : end_index]
# Check for circular dependency
if variable_name in visiting:
# Return the placeholder as-is or raise an error
return string # or raise ValueError(f"Circular dependency detected: {variable_name}")
# Add to visiting set before recursive call
visiting.add(variable_name)
replacement_value = recursive_replace(replacement_dict[variable_name], visiting)
visiting.remove(variable_name) # Remove after processing
return (string[:start_index] +
replacement_value +
recursive_replace(string[end_index + 1:], visiting))
2. KeyError for Undefined Variables
The current implementation assumes all placeholders have corresponding entries in the replacement dictionary. If a placeholder like %undefined%
appears but isn't in the replacements, the code will crash with a KeyError.
Solution: Check if the variable exists before attempting to access it:
variable_name = string[start_index + 1 : end_index] # Check if variable exists in the dictionary if variable_name not in replacement_dict: # Option 1: Leave the placeholder as-is return (string[:start_index] + string[start_index:end_index + 1] + recursive_replace(string[end_index + 1:])) # Option 2: Replace with empty string # return string[:start_index] + recursive_replace(string[end_index + 1:]) # Option 3: Replace with a default value # return string[:start_index] + "[UNDEFINED]" + recursive_replace(string[end_index + 1:])
3. Performance Issues with Repeated String Concatenation
While not immediately apparent for small inputs, the repeated string concatenation and substring operations can become inefficient for very large texts or deeply nested replacements. Each concatenation creates a new string object.
Solution: For better performance with large texts, consider using a StringBuilder-like approach or processing positions:
def recursive_replace_optimized(string: str) -> str:
result = []
i = 0
while i < len(string):
if string[i] == '%':
# Find closing %
j = string.find('%', i + 1)
if j == -1:
result.append(string[i:])
break
variable_name = string[i + 1:j]
if variable_name in replacement_dict:
# Process replacement recursively
replacement = recursive_replace_optimized(replacement_dict[variable_name])
result.append(replacement)
i = j + 1
else:
result.append(string[i:j + 1])
i = j + 1
else:
# Find next % or end of string
next_percent = string.find('%', i)
if next_percent == -1:
result.append(string[i:])
break
else:
result.append(string[i:next_percent])
i = next_percent
return ''.join(result)
What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Want a Structured Path to Master System Design Too? Donβt Miss This!