Prerequisites

Before attempting this problem, you should be comfortable with:

  • String Matching - Finding all occurrences of substrings within a larger string
  • Boolean Arrays for Marking - Using a boolean array to mark positions that satisfy certain conditions
  • Interval Merging Concepts - Understanding how overlapping or adjacent marked regions should be merged together

1. Mark Bold Characters

Intuition

The key insight is that we need to track which characters should be bold, not which substrings. If we find all occurrences of each word in the string and mark every character position that falls within any match, we can then merge overlapping or adjacent bold regions naturally.

We use a boolean array where each index corresponds to a character in the string. For every word, we find all its occurrences and mark the corresponding positions as bold. When building the result, we only insert <b> at the start of a bold region and </b> at the end, which handles merging automatically.

Algorithm

  1. Create a boolean array bold of size n (length of string s), initialized to false.
  2. For each word in the dictionary:
    • Find all occurrences of the word in s.
    • For each occurrence starting at index start, mark bold[i] = true for all i from start to start + len(word) - 1.
  3. Build the result string by iterating through s:
    • If bold[i] is true and either i == 0 or bold[i-1] is false, insert <b>.
    • Append the character s[i].
    • If bold[i] is true and either i == n-1 or bold[i+1] is false, insert </b>.
  4. Return the result string.
class Solution:
    def addBoldTag(self, s: str, words: List[str]) -> str:
        n = len(s)
        bold = [False] * n
        
        for word in words:
            start = s.find(word)
            while start != -1:
                for i in range(start, start + len(word)):
                    bold[i] = True
                    
                start = s.find(word, start + 1)

        open_tag = "<b>"
        close_tag = "</b>"
        ans = []
        
        for i in range(n):
            if bold[i] and (i == 0 or not bold[i - 1]):
                ans.append(open_tag)
                
            ans.append(s[i])
            
            if bold[i] and (i == n - 1 or not bold[i + 1]):
                ans.append(close_tag)
        
        return "".join(ans)

Time & Space Complexity

The time complexity may differ between languages. It is dependent on how the built-in method is implemented.
For this analysis, we will assume that we are using Java.

  • Time complexity: O(m(n2knk2))O(m \cdot (n^2 \cdot k - n \cdot k^2))
  • Space complexity: O(n)O(n)

Where nn is s.length, mm is words.length, and kk is the average length of the words.


Common Pitfalls

Not Merging Adjacent Bold Regions

A common mistake is inserting separate <b> tags for each word match without merging overlapping or adjacent regions. For example, if "ab" and "bc" both match in "abc", the result should be <b>abc</b>, not <b>ab</b><b>bc</b>.

# Wrong: treating each match independently
for word in words:
    s = s.replace(word, f"<b>{word}</b>")  # Creates nested/separate tags

Off-by-One When Marking Bold Ranges

When marking character positions as bold, forgetting that the end index should be start + len(word) - 1 (inclusive) or using start + len(word) (exclusive) inconsistently leads to incorrect bold boundaries.

# Wrong: marking one character too many or too few
for i in range(start, start + len(word) + 1):  # Off-by-one error
    bold[i] = True

Missing Overlapping Occurrences of the Same Word

Using find() with start + len(word) as the next search position misses overlapping matches. For example, searching for "aa" in "aaa" should find two matches (at index 0 and 1), but skipping by word length finds only one.

# Wrong: skipping by word length misses overlaps
start = s.find(word, start + len(word))  # Should be start + 1