[Medium] 616. Add Bold Tag in String
You are given a string s and an array of strings words. You should add a closed pair of bold tag <b> and </b> to wrap the substrings in s that exist in words. If two such substrings overlap, you should wrap them together by only one pair of closed bold tag. If two consecutive substrings are wrapped, you should combine them.
Return s after adding the bold tags.
Thinking Process
You are given a string s and an array of strings words. You should add a closed pair of bold tag <b> and </b> to wrap the substrings in s that exist in words. If two such substrings overlap, you should wrap them together by only one pair of closed bold tag. If two consecutive substrings are wrapped, you should combine them.
Return s after adding the bold tags.
- Greedy works when local optimal choices lead to global optimum.
- Often sort first to make the greedy choice obvious.
- Prove or sanity-check: would swapping two choices ever help?
Common Approaches
Typical techniques for this pattern:
| Approach | Time | Space | Notes |
|---|---|---|---|
| Sort + greedy (this problem) | O(n log n) | O(1) | Interval scheduling, assignment |
| Local greedy choice | O(n) | O(1) | Jump game, gas station |
| Greedy + heap | O(n log n) | O(n) | Merge streams, room allocation |
| Exchange argument | O(n) | O(1) | Prove greedy choice is safe |
Examples
Example 1:
Input: s = "abcxyz123", words = ["abc","123"]
Output: "<b>abc</b>xyz<b>123</b>"
Example 2:
Input: s = "aaabbcc", words = ["aaa","aab","bc"]
Output: "<b>aaabbc</b>c"
Explanation: The substrings "aaa" and "aab" overlap, so they are wrapped together. Then "bc" is also wrapped, so the result is "<b>aaabbc</b>c".
Constraints
1 <= s.length <= 10000 <= words.length <= 1001 <= words[i].length <= 1000sandwords[i]consist of English letters and digits.- All the values of
wordsare unique.
Algorithm Breakdown
Key Insight: Overlapping Matches
The algorithm handles overlapping matches automatically:
- When multiple words match at overlapping positions, all characters in the union are marked
- Example:
"aaa"at position 0 and"aab"at position 1 both mark positions 1 and 2 - Result: Single continuous bold region from position 0 to 3
Tag Insertion Logic
Opening tag <b>:
class Solution:
def addBoldTag(self, s: str, words: list[str]) -> str:
n = len(s)
if n == 0:
return ""
mask = [False] * n
# Step 1: mark bold positions
for i in range(n):
for word in words:
wlen = len(word)
if i + wlen <= n and s[i:i + wlen] == word:
for j in range(i, i + wlen):
mask[j] = True
# Step 2: build result with tags
res = []
i = 0
while i < n:
if mask[i] and (i == 0 or not mask[i - 1]):
res.append("<b>")
res.append(s[i])
if mask[i] and (i == n - 1 or not mask[i + 1]):
res.append("</b>")
i += 1
return "".join(res)
- Current character is bold
- AND we’re at the start of a bold sequence (first char OR previous not bold)
Closing tag </b>:
if mask[i] == True and (i == 0 or mask[i - 1] == False):
- Current character is bold
- AND we’re at the end of a bold sequence (last char OR next not bold)
String Matching
For each position, we check all words:
if mask[i] == True and (i == n - 1 or mask[i+1] == False):
- Bounds check:
i + word_len <= n - Substring comparison:
s.substr(i, word_len) == word
Complexity
Time Complexity: O(n × m × k)
- Outer loop: O(n) - iterate through each position in string
- Word loop: O(m) - check each word in dictionary
- Substring comparison: O(k) - compare substring of average length k
- Mask marking: O(k) - mark characters (amortized)
- Result building: O(n) - traverse string once
- Total: O(n × m × k) where n = s.length(), m = words.length(), k = average word length
Space Complexity: O(n)
- Mask array: O(n) - boolean array for each character
- Result string: O(n) - output string (with tags)
- Total: O(n)
Key Points
- Boolean Mask: Efficient way to mark which characters should be bold
- Automatic Merging: Overlapping matches automatically merge into single regions
- Tag Boundaries: Insert tags only at boundaries of bold sequences
- All Matches: Check all words at each position to find all matches
- Simple Logic: Straightforward approach that’s easy to understand
Optimization Opportunities
Optimization 1: Early Termination
Skip positions that are too short for any word:
if i + word_len <= n and s.substr(i, word_len) == word:
Optimization 2: Trie for Word Matching
Use a trie to match words more efficiently:
- Build trie from words
- Match using trie traversal
- Reduces substring comparison overhead
Optimization 3: Interval Merging
Instead of marking each character, use intervals:
- Find all match intervals
- Merge overlapping intervals
- Insert tags at interval boundaries
Detailed Example Walkthrough
Example: s = "aaabbcc", words = ["aaa","aab","bc"]
Step 1: Find all matches
Position 0:
- Check "aaa": s.substr(0,3) = "aaa" ✓ → Mark [0,1,2]
- Check "aab": s.substr(0,3) = "aaa" ✗
- Check "bc": s.substr(0,2) = "aa" ✗
Position 1:
- Check "aaa": s.substr(1,3) = "aab" ✗
- Check "aab": s.substr(1,3) = "aab" ✓ → Mark [1,2,3]
- Check "bc": s.substr(1,2) = "ab" ✗
Position 2:
- Check "aaa": s.substr(2,3) = "abb" ✗
- Check "aab": s.substr(2,3) = "abb" ✗
- Check "bc": s.substr(2,2) = "bb" ✗
Position 3:
- Check "aaa": s.substr(3,3) = "bbc" ✗
- Check "aab": s.substr(3,3) = "bbc" ✗
- Check "bc": s.substr(3,2) = "bb" ✗
Position 4:
- Check "aaa": s.substr(4,3) = "bcc" ✗
- Check "aab": s.substr(4,3) = "bcc" ✗
- Check "bc": s.substr(4,2) = "bc" ✓ → Mark [4,5]
Position 5:
- Check "aaa": s.substr(5,3) = "cc" ✗ (too short)
- Check "aab": s.substr(5,3) = "cc" ✗ (too short)
- Check "bc": s.substr(5,2) = "cc" ✗
Final mask: [true, true, true, true, true, true, false]
a a a b b c c
Step 2: Build result with tags
Result: "<b>aaabbc</b>c"
Edge Cases
- Empty string: Return empty string
- No matches: Return original string without tags
- All characters match: Entire string wrapped in one tag
- Overlapping matches: Merged into single region
- Consecutive matches: Merged into single region
Common Mistakes
- Skipping edge cases (empty input, single element, boundaries).
- Off-by-one errors in loops and index ranges.
- Forgetting to handle the case when no valid answer exists.
Related Problems
- 616. Add Bold Tag in String - Current problem
- 758. Bold Words in String - Similar problem
- 28. Find the Index of the First Occurrence in a String - String matching
Tags
String, Array, Greedy, Medium
Key Takeaways
- Greedy works when local optimal choices lead to global optimum.
- Often sort first to make the greedy choice obvious.
- Prove or sanity-check: would swapping two choices ever help?
References
- LC 616: Add Bold Tag in String on LeetCode
- LeetCode Discuss — LC 616: Add Bold Tag in String
- LeetCode Editorial (may require premium)