Facebook Pixel

616. Add Bold Tag in String πŸ”’

MediumTrieArrayHash TableStringString Matching
Leetcode Link

Problem Description

You are given a string s and an array of strings words. Your task is to add HTML bold tags (<b> and </b>) around substrings in s that match any word in the words array.

The key rules for adding bold tags are:

  1. Find all matching substrings: Any substring of s that exactly matches a word from words should be wrapped with bold tags.

  2. Merge overlapping regions: If two substrings that need to be bolded overlap (share at least one character), they should be wrapped together with a single pair of bold tags instead of having nested or overlapping tags.

  3. Merge consecutive regions: If two bold regions are adjacent (the end of one bold region is immediately followed by the start of another), they should be combined into one continuous bold region.

For example:

  • If s = "abcxyz" and words = ["abc", "xyz"], the result would be "<b>abcxyz</b>" because the two words are consecutive.
  • If s = "aaabbcc" and words = ["aaa", "aab", "bc"], the result would be "<b>aaabbc</b>c" because "aaa" and "aab" overlap (sharing "aa"), and "aab" and "bc" overlap (sharing "b"), creating one continuous bold region.

The solution uses a Trie data structure to efficiently find all matching substrings in s. It first identifies all intervals that need to be bolded, then merges overlapping or consecutive intervals, and finally constructs the result string with the appropriate bold tags inserted.

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

Intuition

The core challenge is efficiently finding all occurrences of words from the words array within the string s, then merging overlapping or adjacent regions.

A naive approach would be to check each word against every position in s, but this becomes inefficient when we have many words or long strings. Instead, we can think of this problem in reverse: for each position in s, we want to find all words that start at that position.

This naturally leads us to use a Trie (prefix tree) structure. Why? Because a Trie allows us to simultaneously check for multiple words while traversing through the string character by character. As we move through s starting from each position, we can traverse the Trie and identify all valid words that begin at that position.

The process breaks down into three logical steps:

  1. Build a Trie from all words to enable efficient multi-pattern matching.

  2. Find all intervals that need to be bolded. For each starting position i in s, we traverse the Trie to find all words that match substrings starting at i. Each match gives us an interval [start, end] that needs bold tags.

  3. Merge intervals that overlap or are consecutive. This is a classic interval merging problem - if the end of one interval touches or overlaps with the start of the next interval (i.e., end + 1 >= next_start), we merge them into a single interval. This ensures we don't have nested tags or immediately adjacent bold regions.

The beauty of this approach is that the Trie allows us to find all matching words in a single pass from each position, making the algorithm efficient even with many words to search for. The interval merging step then cleanly handles the requirement to combine overlapping and adjacent bold regions.

Learn more about Trie patterns.

Solution Approach

The implementation consists of three main components: building a Trie, finding all matching intervals, and merging those intervals.

1. Building the Trie Structure

The Trie class is implemented with:

  • children: An array of size 128 to accommodate all ASCII characters (using ord(c) as the index)
  • is_end: A boolean flag marking if a word ends at this node
def insert(self, word):
    node = self
    for c in word:
        idx = ord(c)
        if node.children[idx] is None:
            node.children[idx] = [Trie](/problems/trie_intro)()
        node = node.children[idx]
    node.is_end = True

All words from the words array are inserted into the Trie for efficient pattern matching.

2. Finding All Matching Intervals

For each position i in string s, we traverse the Trie to find all words that start at position i:

for i in range(n):
    node = [trie](/problems/trie_intro)
    for j in range(i, n):
        idx = ord(s[j])
        if node.children[idx] is None:
            break
        node = node.children[idx]
        if node.is_end:
            pairs.append([i, j])

When we find a complete word (marked by is_end = True), we record the interval [i, j] where i is the start index and j is the end index of the matching substring.

3. Merging Overlapping and Adjacent Intervals

After collecting all intervals, we merge those that overlap or are consecutive:

st, ed = pairs[0]
t = []
for a, b in pairs[1:]:
    if ed + 1 < a:  # No overlap or adjacency
        t.append([st, ed])
        st, ed = a, b
    else:  # Overlap or adjacent
        ed = max(ed, b)
t.append([st, ed])

The merging logic checks if ed + 1 < a, which means there's a gap between intervals. If true, we save the current interval and start a new one. Otherwise, we extend the current interval by updating ed = max(ed, b).

4. Constructing the Final String

Finally, we build the result string by iterating through s and the merged intervals simultaneously:

ans = []
i = j = 0
while i < n:
    if j == len(t):
        ans.append(s[i:])
        break
    st, ed = t[j]
    if i < st:
        ans.append(s[i:st])  # Add non-bold text
    ans.append('<b>')
    ans.append(s[st : ed + 1])  # Add bold text
    ans.append('</b>')
    j += 1
    i = ed + 1

The algorithm maintains two pointers: i for the current position in string s, and j for the current interval in the merged list. It adds non-bold text between intervals and wraps the intervals with <b> tags.

The time complexity is O(n * m + k) where n is the length of s, m is the average length of words, and k is the total number of matching intervals. The space complexity is O(T + k) where T is the size of the Trie.

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 solution works step by step.

Given:

  • s = "aaabbcc"
  • words = ["aaa", "aab", "bc"]

Step 1: Build the Trie

We insert all words into the Trie structure:

Root
β”œβ”€β”€ a
β”‚   └── a
β”‚       β”œβ”€β”€ a (is_end = True)  // "aaa"
β”‚       └── b (is_end = True)  // "aab"
└── b
    └── c (is_end = True)      // "bc"

Step 2: Find All Matching Intervals

Now we scan through each position in s = "aaabbcc" and find matches:

  • Position 0 ("aaabbcc"):

    • Follow path aβ†’aβ†’a in Trie β†’ Found "aaa" at [0,2]
    • Follow path aβ†’aβ†’b in Trie β†’ Found "aab" at [0,3]
  • Position 1 ("aabbcc"):

    • Follow path aβ†’aβ†’b in Trie β†’ Found "aab" at [1,4]
  • Position 2 ("abbcc"):

    • Follow path aβ†’b in Trie β†’ No match (path doesn't exist)
  • Position 3 ("bbcc"):

    • Follow path bβ†’b in Trie β†’ No match (path doesn't exist)
  • Position 4 ("bcc"):

    • Follow path bβ†’c in Trie β†’ Found "bc" at [4,5]
  • Position 5 ("cc"):

    • Follow path c in Trie β†’ No match (no child 'c' from root)
  • Position 6 ("c"):

    • Follow path c in Trie β†’ No match (no child 'c' from root)

Collected intervals: [[0,2], [0,3], [1,4], [4,5]]

Step 3: Merge Overlapping/Adjacent Intervals

Sort intervals by start position (already sorted): [[0,2], [0,3], [1,4], [4,5]]

Merging process:

  1. Start with [0,2]
  2. Check [0,3]: Since 2+1 β‰₯ 0 (overlapping), merge to [0,3]
  3. Check [1,4]: Since 3+1 β‰₯ 1 (overlapping), merge to [0,4]
  4. Check [4,5]: Since 4+1 β‰₯ 4 (adjacent), merge to [0,5]

Merged intervals: [[0,5]]

Step 4: Construct Final String

With merged interval [0,5] and s = "aaabbcc":

  • Position 0: Start of interval [0,5]
    • Add <b>
    • Add substring from index 0 to 5: "aaabbc"
    • Add </b>
  • Position 6: After interval ends
    • Add remaining string: "c"

Final Result: "<b>aaabbc</b>c"

The overlapping matches "aaa" (positions 0-2), "aab" (positions 0-3 and 1-4), and "bc" (positions 4-5) all merge into one continuous bold region because they share characters or are adjacent, resulting in a single pair of bold tags around "aaabbc".

Solution Implementation

1class Trie:
2    def __init__(self):
3        # Initialize children array for ASCII characters (128 possible values)
4        self.children = [None] * 128
5        # Flag to mark if current node represents end of a word
6        self.is_end = False
7
8    def insert(self, word: str) -> None:
9        """Insert a word into the trie"""
10        node = self
11        for char in word:
12            # Get ASCII value of character
13            index = ord(char)
14            # Create new node if path doesn't exist
15            if node.children[index] is None:
16                node.children[index] = Trie()
17            # Move to next node
18            node = node.children[index]
19        # Mark the end of word
20        node.is_end = True
21
22
23class Solution:
24    def addBoldTag(self, s: str, words: List[str]) -> str:
25        """
26        Add bold tags around substrings found in words list.
27        Merges overlapping or adjacent bold regions.
28        """
29        # Build trie from all words
30        trie = Trie()
31        for word in words:
32            trie.insert(word)
33      
34        n = len(s)
35        # Store all matching intervals as [start, end] pairs
36        intervals = []
37      
38        # Find all matching substrings starting from each position
39        for i in range(n):
40            node = trie
41            for j in range(i, n):
42                index = ord(s[j])
43                # Stop if no matching path in trie
44                if node.children[index] is None:
45                    break
46                node = node.children[index]
47                # If we found a complete word, add interval
48                if node.is_end:
49                    intervals.append([i, j])
50      
51        # Return original string if no matches found
52        if not intervals:
53            return s
54      
55        # Merge overlapping or adjacent intervals
56        merged_intervals = []
57        start, end = intervals[0]
58      
59        for curr_start, curr_end in intervals[1:]:
60            # If intervals are disjoint (not adjacent or overlapping)
61            if end + 1 < curr_start:
62                merged_intervals.append([start, end])
63                start, end = curr_start, curr_end
64            else:
65                # Extend the current interval
66                end = max(end, curr_end)
67        # Add the last interval
68        merged_intervals.append([start, end])
69      
70        # Build result string with bold tags
71        result = []
72        string_index = 0
73        interval_index = 0
74      
75        while string_index < n:
76            # If no more intervals, append remaining string
77            if interval_index == len(merged_intervals):
78                result.append(s[string_index:])
79                break
80          
81            interval_start, interval_end = merged_intervals[interval_index]
82          
83            # Append non-bold portion before current interval
84            if string_index < interval_start:
85                result.append(s[string_index:interval_start])
86          
87            # Append bold portion with tags
88            result.append('<b>')
89            result.append(s[interval_start:interval_end + 1])
90            result.append('</b>')
91          
92            # Move to next interval and update string position
93            interval_index += 1
94            string_index = interval_end + 1
95      
96        return ''.join(result)
97
1class Trie {
2    // Array to store child nodes for all ASCII characters
3    Trie[] children = new Trie[128];
4    // Flag to mark end of a word
5    boolean isEnd;
6
7    /**
8     * Inserts a word into the trie
9     * @param word the word to insert
10     */
11    public void insert(String word) {
12        Trie node = this;
13        // Traverse each character in the word
14        for (char c : word.toCharArray()) {
15            // Create new node if path doesn't exist
16            if (node.children[c] == null) {
17                node.children[c] = new Trie();
18            }
19            // Move to the child node
20            node = node.children[c];
21        }
22        // Mark the end of the word
23        node.isEnd = true;
24    }
25}
26
27class Solution {
28    /**
29     * Adds bold tags around substrings that exist in the words array
30     * @param s the input string
31     * @param words array of words to be bolded
32     * @return string with bold tags added
33     */
34    public String addBoldTag(String s, String[] words) {
35        // Build trie with all words
36        Trie trie = new Trie();
37        for (String word : words) {
38            trie.insert(word);
39        }
40      
41        // Find all matching intervals [start, end]
42        List<int[]> intervals = new ArrayList<>();
43        int n = s.length();
44      
45        // Check each position as potential start of a word
46        for (int i = 0; i < n; ++i) {
47            Trie node = trie;
48            // Try to match words starting at position i
49            for (int j = i; j < n; ++j) {
50                int charIndex = s.charAt(j);
51                // No match found, break
52                if (node.children[charIndex] == null) {
53                    break;
54                }
55                // Move to next node
56                node = node.children[charIndex];
57                // If we found a complete word, record the interval
58                if (node.isEnd) {
59                    intervals.add(new int[] {i, j});
60                }
61            }
62        }
63      
64        // No words found, return original string
65        if (intervals.isEmpty()) {
66            return s;
67        }
68      
69        // Merge overlapping or adjacent intervals
70        List<int[]> mergedIntervals = new ArrayList<>();
71        int start = intervals.get(0)[0];
72        int end = intervals.get(0)[1];
73      
74        for (int j = 1; j < intervals.size(); ++j) {
75            int currentStart = intervals.get(j)[0];
76            int currentEnd = intervals.get(j)[1];
77          
78            // If intervals are not adjacent or overlapping
79            if (end + 1 < currentStart) {
80                mergedIntervals.add(new int[] {start, end});
81                start = currentStart;
82                end = currentEnd;
83            } else {
84                // Merge intervals by extending the end
85                end = Math.max(end, currentEnd);
86            }
87        }
88        // Add the last interval
89        mergedIntervals.add(new int[] {start, end});
90      
91        // Build result string with bold tags
92        int currentPos = 0;
93        int intervalIndex = 0;
94        StringBuilder result = new StringBuilder();
95      
96        while (currentPos < n) {
97            // All intervals processed, append remaining string
98            if (intervalIndex == mergedIntervals.size()) {
99                result.append(s.substring(currentPos));
100                break;
101            }
102          
103            int intervalStart = mergedIntervals.get(intervalIndex)[0];
104            int intervalEnd = mergedIntervals.get(intervalIndex)[1];
105          
106            // Append characters before the bold section
107            if (currentPos < intervalStart) {
108                result.append(s.substring(currentPos, intervalStart));
109            }
110          
111            // Append bold section with tags
112            intervalIndex++;
113            result.append("<b>");
114            result.append(s.substring(intervalStart, intervalEnd + 1));
115            result.append("</b>");
116          
117            // Update position to after the bold section
118            currentPos = intervalEnd + 1;
119        }
120      
121        return result.toString();
122    }
123}
124
1class Trie {
2public:
3    vector<Trie*> children;
4    bool isEnd;
5
6    Trie() {
7        children.resize(128);  // ASCII characters support
8        isEnd = false;
9    }
10
11    // Insert a word into the trie
12    void insert(string word) {
13        Trie* node = this;
14        for (char c : word) {
15            if (!node->children[c]) {
16                node->children[c] = new Trie();
17            }
18            node = node->children[c];
19        }
20        node->isEnd = true;
21    }
22};
23
24class Solution {
25public:
26    string addBoldTag(string s, vector<string>& words) {
27        // Build trie from dictionary words
28        Trie* trie = new Trie();
29        for (const string& word : words) {
30            trie->insert(word);
31        }
32      
33        int n = s.size();
34        vector<pair<int, int>> intervals;
35      
36        // Find all matching intervals using trie
37        for (int i = 0; i < n; ++i) {
38            Trie* node = trie;
39            for (int j = i; j < n; ++j) {
40                int charIndex = s[j];
41                if (!node->children[charIndex]) {
42                    break;  // No matching prefix, stop searching
43                }
44                node = node->children[charIndex];
45                if (node->isEnd) {
46                    intervals.push_back({i, j});  // Found a word match [i, j]
47                }
48            }
49        }
50      
51        // No words found in string
52        if (intervals.empty()) {
53            return s;
54        }
55      
56        // Merge overlapping intervals
57        vector<pair<int, int>> mergedIntervals;
58        int start = intervals[0].first;
59        int end = intervals[0].second;
60      
61        for (int i = 1; i < intervals.size(); ++i) {
62            int currentStart = intervals[i].first;
63            int currentEnd = intervals[i].second;
64          
65            if (end + 1 < currentStart) {
66                // No overlap, save previous interval
67                mergedIntervals.push_back({start, end});
68                start = currentStart;
69                end = currentEnd;
70            } else {
71                // Overlap or adjacent, extend the interval
72                end = max(end, currentEnd);
73            }
74        }
75        mergedIntervals.push_back({start, end});
76      
77        // Build result string with bold tags
78        string result = "";
79        int stringIndex = 0;
80        int intervalIndex = 0;
81      
82        while (stringIndex < n) {
83            if (intervalIndex == mergedIntervals.size()) {
84                // No more intervals, append remaining string
85                result += s.substr(stringIndex);
86                break;
87            }
88          
89            int intervalStart = mergedIntervals[intervalIndex].first;
90            int intervalEnd = mergedIntervals[intervalIndex].second;
91          
92            // Append non-bold part before current interval
93            if (stringIndex < intervalStart) {
94                result += s.substr(stringIndex, intervalStart - stringIndex);
95            }
96          
97            // Append bold part
98            result += "<b>";
99            result += s.substr(intervalStart, intervalEnd - intervalStart + 1);
100            result += "</b>";
101          
102            stringIndex = intervalEnd + 1;
103            ++intervalIndex;
104        }
105      
106        return result;
107    }
108};
109
1// Trie node structure for efficient string matching
2class TrieNode {
3    children: (TrieNode | null)[];
4    isEnd: boolean;
5  
6    constructor() {
7        // Support ASCII characters (128 total)
8        this.children = new Array(128).fill(null);
9        this.isEnd = false;
10    }
11}
12
13// Build trie from dictionary words
14function buildTrie(words: string[]): TrieNode {
15    const root = new TrieNode();
16  
17    // Insert each word into the trie
18    for (const word of words) {
19        let node = root;
20        for (const char of word) {
21            const charCode = char.charCodeAt(0);
22            if (!node.children[charCode]) {
23                node.children[charCode] = new TrieNode();
24            }
25            node = node.children[charCode]!;
26        }
27        node.isEnd = true;
28    }
29  
30    return root;
31}
32
33// Find all matching word intervals in the string using trie
34function findMatchingIntervals(s: string, trie: TrieNode): [number, number][] {
35    const intervals: [number, number][] = [];
36    const n = s.length;
37  
38    // Check each position as a potential start of a word
39    for (let i = 0; i < n; i++) {
40        let node: TrieNode | null = trie;
41      
42        // Try to match the longest possible word starting at position i
43        for (let j = i; j < n; j++) {
44            const charCode = s.charCodeAt(j);
45            node = node.children[charCode];
46          
47            if (!node) {
48                break; // No matching prefix, stop searching
49            }
50          
51            if (node.isEnd) {
52                // Found a complete word match from index i to j (inclusive)
53                intervals.push([i, j]);
54            }
55        }
56    }
57  
58    return intervals;
59}
60
61// Merge overlapping or adjacent intervals
62function mergeIntervals(intervals: [number, number][]): [number, number][] {
63    if (intervals.length === 0) {
64        return [];
65    }
66  
67    const merged: [number, number][] = [];
68    let [start, end] = intervals[0];
69  
70    for (let i = 1; i < intervals.length; i++) {
71        const [currentStart, currentEnd] = intervals[i];
72      
73        // Check if intervals overlap or are adjacent
74        if (end + 1 < currentStart) {
75            // No overlap, save the previous interval
76            merged.push([start, end]);
77            start = currentStart;
78            end = currentEnd;
79        } else {
80            // Overlap or adjacent, extend the current interval
81            end = Math.max(end, currentEnd);
82        }
83    }
84  
85    // Add the last interval
86    merged.push([start, end]);
87  
88    return merged;
89}
90
91// Main function to add bold tags around matching words
92function addBoldTag(s: string, words: string[]): string {
93    // Build trie from dictionary words
94    const trie = buildTrie(words);
95  
96    // Find all matching intervals using trie
97    const intervals = findMatchingIntervals(s, trie);
98  
99    // Return original string if no matches found
100    if (intervals.length === 0) {
101        return s;
102    }
103  
104    // Merge overlapping intervals
105    const mergedIntervals = mergeIntervals(intervals);
106  
107    // Build result string with bold tags
108    let result = "";
109    let stringIndex = 0;
110    let intervalIndex = 0;
111  
112    while (stringIndex < s.length) {
113        // Check if all intervals have been processed
114        if (intervalIndex === mergedIntervals.length) {
115            // No more intervals, append remaining string
116            result += s.substring(stringIndex);
117            break;
118        }
119      
120        const [intervalStart, intervalEnd] = mergedIntervals[intervalIndex];
121      
122        // Append non-bold part before current interval
123        if (stringIndex < intervalStart) {
124            result += s.substring(stringIndex, intervalStart);
125        }
126      
127        // Append bold part with tags
128        result += "<b>";
129        result += s.substring(intervalStart, intervalEnd + 1);
130        result += "</b>";
131      
132        // Move to the position after current interval
133        stringIndex = intervalEnd + 1;
134        intervalIndex++;
135    }
136  
137    return result;
138}
139

Time and Space Complexity

Time Complexity: O(nΒ² * m + n * k)

  • Building the Trie: O(m * L) where m is the number of words and L is the average length of words
  • Finding all matching intervals: O(nΒ²) where n is the length of string s
    • For each starting position i (n positions), we potentially check up to n - i characters
    • In worst case, this gives us O(nΒ²) operations
  • Merging intervals: O(k log k) where k is the number of intervals found (implicit sorting when processing pairs)
    • Although not explicitly sorted in this code, the pairs are processed in order due to how they're generated
    • Merging itself takes O(k) time
  • Building the final string: O(n) to construct the result

Overall: O(m * L + nΒ² + k log k + n) which simplifies to O(nΒ² + m * L) as nΒ² typically dominates

Space Complexity: O(m * L * 128 + k + n)

  • Trie storage: O(m * L * 128) where each Trie node has an array of 128 children
    • In worst case, we store m * L nodes, each with 128 pointers
  • Intervals storage: O(k) where k is the number of matching intervals found
  • Result string construction: O(n) for the answer list before joining
  • Call stack for Trie operations: O(L) maximum depth

Overall: O(m * L * 128 + k + n) which is dominated by the Trie structure's space requirement of O(m * L * 128)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Interval Merging Logic

The Pitfall: One of the most common mistakes is incorrectly determining when intervals should be merged. Developers often forget to handle adjacent intervals (where one interval ends exactly before another begins) or use the wrong comparison operator.

Incorrect Implementation:

# Wrong: Only merges overlapping intervals, misses adjacent ones
if end < curr_start:  # Should be end + 1 < curr_start
    merged_intervals.append([start, end])
    start, end = curr_start, curr_end
else:
    end = max(end, curr_end)

Why It's Wrong: Using end < curr_start only merges intervals that overlap. It misses the case where intervals are adjacent (touching but not overlapping). For example, if we have intervals [0, 2] and [3, 5] representing bold regions for indices 0-2 and 3-5, they should be merged into [0, 5] to create one continuous bold tag <b>...</b> instead of two separate ones <b>...</b><b>...</b>.

Correct Implementation:

# Correct: Merges both overlapping AND adjacent intervals
if end + 1 < curr_start:  # Gap exists between intervals
    merged_intervals.append([start, end])
    start, end = curr_start, curr_end
else:
    end = max(end, curr_end)

2. Not Sorting Intervals Before Merging

The Pitfall: The code assumes intervals are already sorted by start position, but depending on how intervals are collected, they might not be in order.

Problem Scenario:

# If intervals are collected in this order (not sorted):
intervals = [[3, 5], [0, 2], [4, 6]]
# Merging without sorting will produce incorrect results

Solution: Always sort intervals before merging:

# Sort intervals by start position, then by end position
intervals.sort()  # Python's sort handles tuples/lists correctly

3. Off-by-One Errors in String Slicing

The Pitfall: Confusion between inclusive and exclusive indices when building the result string.

Incorrect:

# Wrong: Forgets that interval_end is inclusive
result.append(s[interval_start:interval_end])  # Missing the last character

Correct:

# Correct: Include the character at interval_end
result.append(s[interval_start:interval_end + 1])

4. Handling Empty Input Cases

The Pitfall: Not properly handling edge cases like empty strings or empty word lists.

Solution: Add validation at the beginning:

if not s or not words:
    return s

5. Trie Size Assumption

The Pitfall: Using a fixed-size array of 128 for ASCII characters might fail for Unicode strings or special characters beyond standard ASCII range.

Better Approach for Unicode:

class Trie:
    def __init__(self):
        # Use dictionary instead of fixed array for Unicode support
        self.children = {}
        self.is_end = False
  
    def insert(self, word):
        node = self
        for char in word:
            if char not in node.children:
                node.children[char] = Trie()
            node = node.children[char]
        node.is_end = True

Complete Corrected Solution with Safeguards:

class Solution:
    def addBoldTag(self, s: str, words: List[str]) -> str:
        # Handle edge cases
        if not s or not words:
            return s
      
        # Build trie and find intervals (same as before)
        trie = Trie()
        for word in words:
            trie.insert(word)
      
        n = len(s)
        intervals = []
      
        for i in range(n):
            node = trie
            for j in range(i, n):
                index = ord(s[j])
                if node.children[index] is None:
                    break
                node = node.children[index]
                if node.is_end:
                    intervals.append([i, j])
      
        if not intervals:
            return s
      
        # IMPORTANT: Sort intervals before merging
        intervals.sort()
      
        # Merge with correct adjacency check
        merged = []
        start, end = intervals[0]
      
        for curr_start, curr_end in intervals[1:]:
            if end + 1 < curr_start:  # Correct adjacency check
                merged.append([start, end])
                start, end = curr_start, curr_end
            else:
                end = max(end, curr_end)
        merged.append([start, end])
      
        # Build result with correct string slicing
        result = []
        i = 0
        for start, end in merged:
            if i < start:
                result.append(s[i:start])
            result.append('<b>')
            result.append(s[start:end + 1])  # +1 for inclusive end
            result.append('</b>')
            i = end + 1
      
        if i < n:
            result.append(s[i:])
      
        return ''.join(result)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Consider the classic dynamic programming of longest increasing subsequence:

Find the length of the longest subsequence of a given sequence such that all elements of the subsequence are sorted in increasing order.

For example, the length of LIS for [50, 3, 10, 7, 40, 80] is 4 and LIS is [3, 7, 40, 80].

What is the recurrence relation?


Recommended Readings

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

Load More