Facebook Pixel

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%"]] and text = "%greeting%!", you would:
    1. Replace %greeting% with "hello %name%"
    2. Then replace %name% with "bob"
    3. Return "hello bob!"

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 from greeting to name.

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. We scan the text for a placeholder %var%
  2. We look up var in our replacements mapping
  3. The replacement itself might contain placeholders, so we need to apply the same process to it
  4. Only after the replacement is fully resolved can we substitute it back into the original text
  5. 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:

  1. 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.

  2. Find the placeholder's end: From position i + 1, find the next % symbol using s.find("%", i + 1). If we can't find a closing %, the string is malformed or incomplete, so we return it as-is.

  3. Extract the placeholder key: The variable name is between the two % symbols:

    key = s[i + 1 : j]
  4. Recursively resolve the replacement: Look up d[key] and recursively call dfs on it to resolve any nested placeholders:

    replacement = dfs(d[key])
  5. 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:])

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 Evaluator

Example 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, where m is the total length of all replacement mappings
  • The DFS traversal processes the text, where:
    • Finding each placeholder pair (two find() operations) takes O(n) time in worst case, where n 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

Space Complexity: O(m + n Γ— L)

The space complexity includes:

  • O(m) space for storing the dictionary d 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)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More