2213. Longest Substring of One Repeating Character
Problem Description
You are given a string s
(0-indexed) and need to process k
queries on it. Each query updates a single character in the string.
For each query i
, you:
- Replace the character at position
queryIndices[i]
with the characterqueryCharacters[i]
After each query, you need to find the length of the longest substring that consists of only one repeating character in the entire modified string.
Input:
- A string
s
- A string
queryCharacters
of lengthk
containing the new characters for each query - An array
queryIndices
of lengthk
containing the positions to update for each query
Output:
- An array
lengths
of lengthk
wherelengths[i]
is the length of the longest substring of repeating characters after thei-th
query
Example:
If s = "babacc"
, and we have:
- Query 1: Change index 1 to 'b' → String becomes "bbbacc", longest repeating substring is "bbb" with length 3
- Query 2: Change index 3 to 'c' → String becomes "bbbccc", longest repeating substring is "ccc" with length 3
- And so on...
The problem requires efficiently handling these updates and finding the maximum length of consecutive identical characters after each modification. Since there can be many queries, a naive approach of scanning the entire string after each update would be inefficient, which is why the solution uses a segment tree data structure to maintain and update the required information in O(log n)
time per query.
Intuition
The key insight is that after each character update, we need to quickly find the longest substring of repeating characters in the entire string. A naive approach would scan the entire string after each update, taking O(n)
time per query, which becomes O(k * n)
total.
We need a way to efficiently maintain information about consecutive characters and update it when a single character changes. This is where a segment tree becomes useful - it can handle point updates and range queries in O(log n)
time.
The critical observation is that when we change a single character, it only affects the consecutive character counts in its local region. The rest of the string remains unchanged. So we need a data structure that can:
- Update a single position efficiently
- Maintain information about consecutive characters in different parts of the string
- Combine this information to get the global maximum
For each segment (interval) in our tree, we track three key pieces of information:
lmx
: How many consecutive identical characters start from the left boundary of this segmentrmx
: How many consecutive identical characters end at the right boundary of this segmentmx
: The maximum length of consecutive identical characters within this segment
When we combine two adjacent segments, we need to consider:
- The maximum from the left child segment
- The maximum from the right child segment
- The possibility that the longest sequence spans across both segments (when the rightmost character of the left segment equals the leftmost character of the right segment)
This merging logic allows us to maintain global information about consecutive characters while only updating O(log n)
nodes when a character changes. The segment tree structure naturally maintains the hierarchical relationship between intervals, making it perfect for this type of problem where local changes affect global properties.
Learn more about Segment Tree patterns.
Solution Approach
The solution uses a Segment Tree data structure to efficiently handle point updates and maintain information about the longest consecutive repeating characters.
Segment Tree Node Structure
Each node in the segment tree maintains:
l, r
: The left and right boundaries of the interval this node representslmx
: Length of the longest consecutive characters starting from the left boundaryrmx
: Length of the longest consecutive characters ending at the right boundarymx
: Maximum length of consecutive characters within this interval
Building the Segment Tree
The build
method recursively constructs the tree:
- Create a node for interval
[l, r]
- If it's a leaf node (
l == r
), initialize all values to 1 - Otherwise, recursively build left child for
[l, mid]
and right child for[mid+1, r]
- Call
pushup
to combine information from children
Pushup Operation
The pushup
method is crucial for maintaining correct information after updates. When combining two adjacent segments:
-
Update prefix maximum (
lmx
):- Start with the left child's
lmx
- If the left child is entirely uniform (lmx equals its length) AND the boundary characters match, extend with right child's
lmx
- Start with the left child's
-
Update suffix maximum (
rmx
):- Start with the right child's
rmx
- If the right child is entirely uniform (rmx equals its length) AND the boundary characters match, extend with left child's
rmx
- Start with the right child's
-
Update overall maximum (
mx
):- Take the maximum of: left child's
mx
, right child'smx
- If boundary characters match (
s[left.r - 1] == s[right.l - 1]
), also considerleft.rmx + right.lmx
(the sequence spanning both segments)
- Take the maximum of: left child's
Modify Operation
To update a character at position x
:
- Navigate to the appropriate leaf node using binary search logic
- Update the character in the string
- Call
pushup
on all affected ancestors going back up the tree
Query Operation
The query always asks for the maximum in the entire range [1, n]
, which is simply the mx
value at the root node.
Time Complexity
- Building the tree:
O(n)
- Each modification:
O(log n)
- Each query:
O(1)
(since we query the entire range) - Total for k queries:
O(n + k log n)
Implementation Details
The solution uses 1-based indexing for the segment tree nodes, where:
- Node
u
has left child atu << 1
(2u) and right child atu << 1 | 1
(2u + 1) - The root is at index 1
- Array size is allocated as
4n
to ensure enough space for all nodes
The main solution iterates through each query, performs the modification at the specified index (converting from 0-based to 1-based indexing), and retrieves the maximum consecutive length from the root node.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with s = "aabba"
and process two queries:
- Query 1: Change index 2 to 'a'
- Query 2: Change index 4 to 'b'
Initial Setup: Build a segment tree for string "aabba" (using 1-based indexing internally).
Initial string: "aabba" Tree structure (showing [l,r] intervals): [1,5] / \ [1,3] [4,5] / \ / \ [1,2] [3,3] [4,4][5,5] / \ [1,1][2,2]
For each node, we track (lmx, rmx, mx):
- Leaf nodes [1,1] through [5,5]: all have (1,1,1)
- Node [1,2]: chars 'aa' → (2,2,2) - both are 'a', fully consecutive
- Node [3,3]: char 'b' → (1,1,1)
- Node [1,3]: chars 'aab' → (2,1,2) - lmx=2 ('aa'), rmx=1 ('b'), mx=2
- Node [4,4]: char 'b' → (1,1,1)
- Node [5,5]: char 'a' → (1,1,1)
- Node [4,5]: chars 'ba' → (1,1,1) - different chars, no consecutive
- Root [1,5]: Combining [1,3] and [4,5]:
- Check boundary: s[3]='b', s[4]='b' (they match!)
- lmx = 2 (from left child, not extended since left child not fully uniform)
- rmx = 1 (from right child, not extended since right child not fully uniform)
- mx = max(2, 1, 1+1) = 2 (considering left.mx, right.mx, and left.rmx + right.lmx)
Query 1: Change index 2 to 'a' String becomes "aaaba" (index 2 is position 3 in 1-based indexing)
Modify operation updates node [3,3] and propagates up:
- Node [3,3]: still (1,1,1) but now represents 'a'
- Node [1,3]: chars 'aaa' → pushup recalculates:
- s[2]='a', s[3]='a' (boundary chars match)
- lmx = 2 + 1 = 3 (left child fully uniform, extend with right)
- rmx = 1 + 2 = 3 (right child fully uniform, extend with left)
- mx = max(2, 1, 2+1) = 3
- Root [1,5]: Recalculate with new [1,3]:
- s[3]='a', s[4]='b' (boundary chars don't match)
- lmx = 3 (from left child)
- rmx = 1 (from right child)
- mx = max(3, 1) = 3
Result: Length = 3 (substring "aaa")
Query 2: Change index 4 to 'b' String becomes "aaabb" (index 4 is position 5 in 1-based indexing)
Modify operation updates node [5,5] and propagates up:
- Node [5,5]: still (1,1,1) but now represents 'b'
- Node [4,5]: chars 'bb' → pushup recalculates:
- s[4]='b', s[5]='b' (boundary chars match)
- lmx = 1 + 1 = 2 (left child fully uniform, extend with right)
- rmx = 1 + 1 = 2 (right child fully uniform, extend with left)
- mx = max(1, 1, 1+1) = 2
- Root [1,5]: Recalculate with new [4,5]:
- s[3]='a', s[4]='b' (boundary chars don't match)
- lmx = 3 (from left child)
- rmx = 2 (from right child)
- mx = max(3, 2) = 3
Result: Length = 3 (substring "aaa")
The segment tree efficiently maintains the maximum consecutive length by only updating O(log n) nodes per query, avoiding the need to scan the entire string.
Solution Implementation
1from typing import List, Optional
2
3
4class Node:
5 """Represents a node in the segment tree storing range information."""
6 __slots__ = "l", "r", "lmx", "rmx", "mx"
7
8 def __init__(self, l: int, r: int):
9 self.l = l # Left boundary of the range
10 self.r = r # Right boundary of the range
11 self.lmx = 1 # Maximum consecutive repeating chars from left
12 self.rmx = 1 # Maximum consecutive repeating chars from right
13 self.mx = 1 # Maximum consecutive repeating chars in range
14
15
16class SegmentTree:
17 """Segment tree for efficiently querying and updating longest repeating substring."""
18 __slots__ = "chars", "tree"
19
20 def __init__(self, s: str):
21 self.chars = list(s) # Convert string to list for easy modification
22 n = len(s)
23 self.tree: List[Optional[Node]] = [None] * (n * 4)
24 self.build(1, 1, n)
25
26 def build(self, node_idx: int, left: int, right: int) -> None:
27 """Build the segment tree recursively."""
28 self.tree[node_idx] = Node(left, right)
29
30 if left == right:
31 # Leaf node - single character
32 return
33
34 mid = (left + right) // 2
35 # Build left subtree
36 self.build(node_idx << 1, left, mid)
37 # Build right subtree
38 self.build(node_idx << 1 | 1, mid + 1, right)
39 # Update current node based on children
40 self.pushup(node_idx)
41
42 def query(self, node_idx: int, query_left: int, query_right: int) -> int:
43 """Query the maximum consecutive repeating characters in range [query_left, query_right]."""
44 current_node = self.tree[node_idx]
45
46 # If current range is completely within query range
47 if current_node.l >= query_left and current_node.r <= query_right:
48 return current_node.mx
49
50 mid = (current_node.l + current_node.r) // 2
51 result = 0
52
53 # Query left child if needed
54 if query_left <= mid:
55 result = self.query(node_idx << 1, query_left, query_right)
56
57 # Query right child if needed
58 if query_right > mid:
59 result = max(result, self.query(node_idx << 1 | 1, query_left, query_right))
60
61 return result
62
63 def modify(self, node_idx: int, position: int, new_char: str) -> None:
64 """Modify character at position to new_char (1-indexed position)."""
65 current_node = self.tree[node_idx]
66
67 if current_node.l == current_node.r:
68 # Leaf node - update the character
69 self.chars[position - 1] = new_char
70 return
71
72 mid = (current_node.l + current_node.r) // 2
73
74 if position <= mid:
75 # Update left child
76 self.modify(node_idx << 1, position, new_char)
77 else:
78 # Update right child
79 self.modify(node_idx << 1 | 1, position, new_char)
80
81 # Update current node based on children
82 self.pushup(node_idx)
83
84 def pushup(self, node_idx: int) -> None:
85 """Update parent node information based on its children."""
86 root = self.tree[node_idx]
87 left_child = self.tree[node_idx << 1]
88 right_child = self.tree[node_idx << 1 | 1]
89
90 # Initially inherit values from children
91 root.lmx = left_child.lmx
92 root.rmx = right_child.rmx
93 root.mx = max(left_child.mx, right_child.mx)
94
95 # Calculate range lengths
96 left_range_size = left_child.r - left_child.l + 1
97 right_range_size = right_child.r - right_child.l + 1
98
99 # Check if characters at the boundary are the same
100 if self.chars[left_child.r - 1] == self.chars[right_child.l - 1]:
101 # If left child is completely uniform, extend lmx
102 if left_child.lmx == left_range_size:
103 root.lmx += right_child.lmx
104
105 # If right child is completely uniform, extend rmx
106 if right_child.rmx == right_range_size:
107 root.rmx += left_child.rmx
108
109 # Update maximum by considering the merge at boundary
110 root.mx = max(root.mx, left_child.rmx + right_child.lmx)
111
112
113class Solution:
114 def longestRepeating(
115 self, s: str, queryCharacters: str, queryIndices: List[int]
116 ) -> List[int]:
117 """
118 Process queries to modify string and return longest repeating substring after each query.
119
120 Args:
121 s: Initial string
122 queryCharacters: Characters to update at each query
123 queryIndices: 0-indexed positions to update at each query
124
125 Returns:
126 List of integers representing longest repeating substring after each query
127 """
128 # Initialize segment tree with the string
129 segment_tree = SegmentTree(s)
130 results = []
131
132 # Process each query
133 for index, char in zip(queryIndices, queryCharacters):
134 # Convert 0-indexed to 1-indexed and modify
135 segment_tree.modify(1, index + 1, char)
136 # Query entire range for maximum consecutive repeating characters
137 max_repeating = segment_tree.query(1, 1, len(s))
138 results.append(max_repeating)
139
140 return results
141
1/**
2 * Node class representing a segment in the segment tree
3 * Each node stores information about the longest repeating substring in its range
4 */
5class Node {
6 int left, right; // Range boundaries [left, right]
7 int leftMax, rightMax; // Maximum repeating length starting from left/ending at right
8 int maxLength; // Maximum repeating substring length in this range
9
10 Node(int left, int right) {
11 this.left = left;
12 this.right = right;
13 this.leftMax = 1; // Initially, each position has at least 1 character
14 this.rightMax = 1;
15 this.maxLength = 1;
16 }
17}
18
19/**
20 * Segment Tree for efficiently querying and updating longest repeating substrings
21 */
22class SegmentTree {
23 private char[] characters; // The character array (1-indexed internally)
24 private Node[] tree; // Segment tree nodes
25
26 /**
27 * Constructor initializes the segment tree with the given string
28 * @param s Character array to build the tree from
29 */
30 public SegmentTree(char[] s) {
31 int n = s.length;
32 this.characters = s;
33 this.tree = new Node[n << 2]; // Allocate 4n space for the tree
34 build(1, 1, n); // Build tree with 1-based indexing
35 }
36
37 /**
38 * Recursively builds the segment tree
39 * @param nodeIndex Current node index in the tree
40 * @param rangeLeft Left boundary of current range
41 * @param rangeRight Right boundary of current range
42 */
43 public void build(int nodeIndex, int rangeLeft, int rangeRight) {
44 tree[nodeIndex] = new Node(rangeLeft, rangeRight);
45
46 // Base case: leaf node
47 if (rangeLeft == rangeRight) {
48 return;
49 }
50
51 // Recursive case: build left and right subtrees
52 int mid = (rangeLeft + rangeRight) >> 1;
53 int leftChild = nodeIndex << 1;
54 int rightChild = nodeIndex << 1 | 1;
55
56 build(leftChild, rangeLeft, mid);
57 build(rightChild, mid + 1, rangeRight);
58
59 // Update current node based on children
60 pushup(nodeIndex);
61 }
62
63 /**
64 * Modifies a character at the given position
65 * @param nodeIndex Current node index in the tree
66 * @param position Position to modify (1-indexed)
67 * @param newChar New character value
68 */
69 public void modify(int nodeIndex, int position, char newChar) {
70 Node currentNode = tree[nodeIndex];
71
72 // Base case: reached the target leaf node
73 if (currentNode.left == position && currentNode.right == position) {
74 characters[position - 1] = newChar; // Convert to 0-indexed
75 return;
76 }
77
78 // Recursive case: traverse to the appropriate child
79 int mid = (currentNode.left + currentNode.right) >> 1;
80 int leftChild = nodeIndex << 1;
81 int rightChild = nodeIndex << 1 | 1;
82
83 if (position <= mid) {
84 modify(leftChild, position, newChar);
85 } else {
86 modify(rightChild, position, newChar);
87 }
88
89 // Update current node after modification
90 pushup(nodeIndex);
91 }
92
93 /**
94 * Queries the maximum repeating substring length in the given range
95 * @param nodeIndex Current node index in the tree
96 * @param queryLeft Left boundary of query range
97 * @param queryRight Right boundary of query range
98 * @return Maximum repeating substring length in the range
99 */
100 public int query(int nodeIndex, int queryLeft, int queryRight) {
101 Node currentNode = tree[nodeIndex];
102
103 // Current node's range is completely within query range
104 if (currentNode.left >= queryLeft && currentNode.right <= queryRight) {
105 return currentNode.maxLength;
106 }
107
108 int mid = (currentNode.left + currentNode.right) >> 1;
109 int result = 0;
110 int leftChild = nodeIndex << 1;
111 int rightChild = nodeIndex << 1 | 1;
112
113 // Query left subtree if needed
114 if (queryRight <= mid) {
115 result = query(leftChild, queryLeft, queryRight);
116 }
117
118 // Query right subtree if needed
119 if (queryLeft > mid) {
120 result = Math.max(result, query(rightChild, queryLeft, queryRight));
121 }
122
123 return result;
124 }
125
126 /**
127 * Updates parent node based on its children nodes
128 * Merges information from left and right children
129 * @param nodeIndex Index of the node to update
130 */
131 private void pushup(int nodeIndex) {
132 Node parent = tree[nodeIndex];
133 Node leftChild = tree[nodeIndex << 1];
134 Node rightChild = tree[nodeIndex << 1 | 1];
135
136 // Initial max is the maximum of both children
137 parent.maxLength = Math.max(leftChild.maxLength, rightChild.maxLength);
138
139 // Initialize parent's left and right max from children
140 parent.leftMax = leftChild.leftMax;
141 parent.rightMax = rightChild.rightMax;
142
143 // Calculate the full length of left and right ranges
144 int leftRangeLength = leftChild.right - leftChild.left + 1;
145 int rightRangeLength = rightChild.right - rightChild.left + 1;
146
147 // Check if we can merge across the boundary
148 // If the last character of left child equals first character of right child
149 if (characters[leftChild.right - 1] == characters[rightChild.left - 1]) {
150 // If left child is all same characters, extend parent's leftMax
151 if (leftChild.leftMax == leftRangeLength) {
152 parent.leftMax += rightChild.leftMax;
153 }
154
155 // If right child is all same characters, extend parent's rightMax
156 if (rightChild.rightMax == rightRangeLength) {
157 parent.rightMax += leftChild.rightMax;
158 }
159
160 // Update max length considering the merge at boundary
161 parent.maxLength = Math.max(parent.maxLength,
162 leftChild.rightMax + rightChild.leftMax);
163 }
164 }
165}
166
167/**
168 * Solution class for the longest repeating substring problem with updates
169 */
170class Solution {
171 /**
172 * Processes queries to find longest repeating substring after each character update
173 * @param s Original string
174 * @param queryCharacters String of characters to update
175 * @param queryIndices Array of indices where updates occur
176 * @return Array of maximum repeating substring lengths after each update
177 */
178 public int[] longestRepeating(String s, String queryCharacters, int[] queryIndices) {
179 // Initialize segment tree with the string
180 SegmentTree segmentTree = new SegmentTree(s.toCharArray());
181
182 int numQueries = queryIndices.length;
183 int[] results = new int[numQueries];
184 int stringLength = s.length();
185
186 // Process each query
187 for (int i = 0; i < numQueries; i++) {
188 // Convert to 1-indexed position for the segment tree
189 int updatePosition = queryIndices[i] + 1;
190 char newCharacter = queryCharacters.charAt(i);
191
192 // Update the character at the specified position
193 segmentTree.modify(1, updatePosition, newCharacter);
194
195 // Query the entire string for the maximum repeating substring
196 results[i] = segmentTree.query(1, 1, stringLength);
197 }
198
199 return results;
200 }
201}
202
1class Node {
2public:
3 int left, right; // Range boundaries [left, right]
4 int leftMax, rightMax, max; // leftMax: max repeating from left boundary
5 // rightMax: max repeating from right boundary
6 // max: max repeating in this range
7
8 Node(int l, int r)
9 : left(l)
10 , right(r)
11 , leftMax(1)
12 , rightMax(1)
13 , max(1) {}
14};
15
16class SegmentTree {
17private:
18 string str; // The string we're tracking
19 vector<Node*> tree; // Segment tree nodes
20
21 // Build the segment tree recursively
22 void build(int nodeIdx, int left, int right) {
23 tree[nodeIdx] = new Node(left, right);
24
25 // Leaf node - single character
26 if (left == right) {
27 return;
28 }
29
30 // Build left and right subtrees
31 int mid = (left + right) >> 1;
32 build(nodeIdx << 1, left, mid);
33 build(nodeIdx << 1 | 1, mid + 1, right);
34
35 // Update current node based on children
36 pushUp(nodeIdx);
37 }
38
39 // Update parent node based on its children
40 void pushUp(int nodeIdx) {
41 Node* root = tree[nodeIdx];
42 Node* leftChild = tree[nodeIdx << 1];
43 Node* rightChild = tree[nodeIdx << 1 | 1];
44
45 // Take the maximum from both children
46 root->max = max(leftChild->max, rightChild->max);
47
48 // Initialize left and right max from children
49 root->leftMax = leftChild->leftMax;
50 root->rightMax = rightChild->rightMax;
51
52 // Calculate range sizes
53 int leftRangeSize = leftChild->right - leftChild->left + 1;
54 int rightRangeSize = rightChild->right - rightChild->left + 1;
55
56 // Check if we can merge at the boundary
57 if (str[leftChild->right - 1] == str[rightChild->left - 1]) {
58 // If left child is fully repeating, extend into right child
59 if (leftChild->leftMax == leftRangeSize) {
60 root->leftMax += rightChild->leftMax;
61 }
62
63 // If right child is fully repeating, extend into left child
64 if (rightChild->rightMax == rightRangeSize) {
65 root->rightMax += leftChild->rightMax;
66 }
67
68 // Update max considering the merge at boundary
69 root->max = max(root->max, leftChild->rightMax + rightChild->leftMax);
70 }
71 }
72
73public:
74 // Constructor - build segment tree from string
75 SegmentTree(const string& s)
76 : str(s) {
77 int n = s.size();
78 tree.resize(n * 4); // Allocate space for segment tree
79 build(1, 1, n); // Build tree with 1-indexed positions
80 }
81
82 // Modify character at position x to value v
83 void modify(int nodeIdx, int position, char value) {
84 // Found the leaf node to update
85 if (tree[nodeIdx]->left == position && tree[nodeIdx]->right == position) {
86 str[position - 1] = value; // Update string (convert to 0-indexed)
87 return;
88 }
89
90 // Recursively find the correct leaf
91 int mid = (tree[nodeIdx]->left + tree[nodeIdx]->right) >> 1;
92 if (position <= mid) {
93 modify(nodeIdx << 1, position, value);
94 } else {
95 modify(nodeIdx << 1 | 1, position, value);
96 }
97
98 // Update current node after modification
99 pushUp(nodeIdx);
100 }
101
102 // Query the maximum repeating substring length in range [left, right]
103 int query(int nodeIdx, int left, int right) {
104 // Current node is completely within query range
105 if (tree[nodeIdx]->left >= left && tree[nodeIdx]->right <= right) {
106 return tree[nodeIdx]->max;
107 }
108
109 int mid = (tree[nodeIdx]->left + tree[nodeIdx]->right) >> 1;
110 int result = 0;
111
112 // Query left child only
113 if (right <= mid) {
114 result = query(nodeIdx << 1, left, right);
115 }
116 // Query right child only
117 else if (left > mid) {
118 result = max(result, query(nodeIdx << 1 | 1, left, right));
119 }
120
121 return result;
122 }
123};
124
125class Solution {
126public:
127 vector<int> longestRepeating(string s, string queryCharacters, vector<int>& queryIndices) {
128 // Initialize segment tree with the string
129 SegmentTree segTree(s);
130
131 int numQueries = queryIndices.size();
132 vector<int> result(numQueries);
133 int stringLength = s.size();
134
135 // Process each query
136 for (int i = 0; i < numQueries; ++i) {
137 int position = queryIndices[i] + 1; // Convert to 1-indexed
138 char newChar = queryCharacters[i];
139
140 // Update the character at position
141 segTree.modify(1, position, newChar);
142
143 // Query the entire string for longest repeating substring
144 result[i] = segTree.query(1, 1, stringLength);
145 }
146
147 return result;
148 }
149};
150
1// Node structure for segment tree
2interface SegmentNode {
3 left: number; // Left boundary of the segment
4 right: number; // Right boundary of the segment
5 leftMax: number; // Maximum repeating length starting from left boundary
6 rightMax: number; // Maximum repeating length ending at right boundary
7 max: number; // Maximum repeating length within this segment
8}
9
10// Global variables for the segment tree
11let charArray: string[]; // Character array representation of the string
12let segmentTree: SegmentNode[]; // Segment tree array
13
14/**
15 * Builds the segment tree recursively
16 * @param nodeIndex - Current node index in the tree
17 * @param leftBound - Left boundary of current segment
18 * @param rightBound - Right boundary of current segment
19 */
20function build(nodeIndex: number, leftBound: number, rightBound: number): void {
21 // Initialize current node with boundaries and default max values of 1
22 segmentTree[nodeIndex] = {
23 left: leftBound,
24 right: rightBound,
25 leftMax: 1,
26 rightMax: 1,
27 max: 1
28 };
29
30 // Base case: leaf node
31 if (leftBound === rightBound) {
32 return;
33 }
34
35 // Recursively build left and right subtrees
36 const mid = (leftBound + rightBound) >> 1;
37 const leftChildIndex = nodeIndex << 1;
38 const rightChildIndex = (nodeIndex << 1) | 1;
39
40 build(leftChildIndex, leftBound, mid);
41 build(rightChildIndex, mid + 1, rightBound);
42
43 // Update current node based on children
44 pushup(nodeIndex);
45}
46
47/**
48 * Updates parent node information based on its children
49 * @param nodeIndex - Index of the node to update
50 */
51function pushup(nodeIndex: number): void {
52 const currentNode = segmentTree[nodeIndex];
53 const leftChild = segmentTree[nodeIndex << 1];
54 const rightChild = segmentTree[(nodeIndex << 1) | 1];
55
56 // Update max repeating length from children
57 currentNode.max = Math.max(leftChild.max, rightChild.max);
58
59 // Initially, inherit boundary max values from children
60 currentNode.leftMax = leftChild.leftMax;
61 currentNode.rightMax = rightChild.rightMax;
62
63 // Calculate segment lengths
64 const leftSegmentLength = leftChild.right - leftChild.left + 1;
65 const rightSegmentLength = rightChild.right - rightChild.left + 1;
66
67 // Check if characters at the boundary between left and right segments are the same
68 if (charArray[leftChild.right - 1] === charArray[rightChild.left - 1]) {
69 // If left child is entirely repeating, extend leftMax into right child
70 if (leftChild.leftMax === leftSegmentLength) {
71 currentNode.leftMax += rightChild.leftMax;
72 }
73
74 // If right child is entirely repeating, extend rightMax into left child
75 if (rightChild.rightMax === rightSegmentLength) {
76 currentNode.rightMax += leftChild.rightMax;
77 }
78
79 // Update max considering the merge at boundary
80 currentNode.max = Math.max(currentNode.max, leftChild.rightMax + rightChild.leftMax);
81 }
82}
83
84/**
85 * Modifies a character at a specific position
86 * @param nodeIndex - Current node index in the tree
87 * @param position - Position to modify (1-indexed)
88 * @param newChar - New character value
89 */
90function modify(nodeIndex: number, position: number, newChar: string): void {
91 const currentNode = segmentTree[nodeIndex];
92
93 // Base case: reached the target leaf node
94 if (currentNode.left === position && currentNode.right === position) {
95 charArray[position - 1] = newChar;
96 return;
97 }
98
99 // Recursively modify in appropriate subtree
100 const mid = (currentNode.left + currentNode.right) >> 1;
101 const leftChildIndex = nodeIndex << 1;
102 const rightChildIndex = (nodeIndex << 1) | 1;
103
104 if (position <= mid) {
105 modify(leftChildIndex, position, newChar);
106 } else {
107 modify(rightChildIndex, position, newChar);
108 }
109
110 // Update current node after modification
111 pushup(nodeIndex);
112}
113
114/**
115 * Queries the maximum repeating length in a range
116 * @param nodeIndex - Current node index in the tree
117 * @param queryLeft - Left boundary of query range
118 * @param queryRight - Right boundary of query range
119 * @returns Maximum repeating length in the range
120 */
121function query(nodeIndex: number, queryLeft: number, queryRight: number): number {
122 const currentNode = segmentTree[nodeIndex];
123
124 // Current segment is completely within query range
125 if (currentNode.left >= queryLeft && currentNode.right <= queryRight) {
126 return currentNode.max;
127 }
128
129 const mid = (currentNode.left + currentNode.right) >> 1;
130 const leftChildIndex = nodeIndex << 1;
131 const rightChildIndex = (nodeIndex << 1) | 1;
132
133 let result = 0;
134
135 // Query is entirely in left subtree
136 if (queryRight <= mid) {
137 result = query(leftChildIndex, queryLeft, queryRight);
138 }
139 // Query is entirely in right subtree
140 else if (queryLeft > mid) {
141 result = Math.max(result, query(rightChildIndex, queryLeft, queryRight));
142 }
143 // Query spans both subtrees
144 else {
145 result = Math.max(
146 query(leftChildIndex, queryLeft, queryRight),
147 query(rightChildIndex, queryLeft, queryRight)
148 );
149 }
150
151 return result;
152}
153
154/**
155 * Main function to process queries and return longest repeating substring lengths
156 * @param s - Input string
157 * @param queryCharacters - Characters to update at each query
158 * @param queryIndices - Indices to update at each query (0-indexed)
159 * @returns Array of maximum repeating lengths after each query
160 */
161function longestRepeating(s: string, queryCharacters: string, queryIndices: number[]): number[] {
162 // Initialize global variables
163 charArray = s.split('');
164 const stringLength = s.length;
165 segmentTree = Array(stringLength * 4)
166 .fill(null)
167 .map(() => ({ left: 0, right: 0, leftMax: 1, rightMax: 1, max: 1 }));
168
169 // Build the segment tree
170 build(1, 1, stringLength);
171
172 // Process queries
173 const queryCount = queryIndices.length;
174 const results: number[] = new Array(queryCount);
175
176 for (let i = 0; i < queryCount; i++) {
177 // Convert to 1-indexed position
178 const updatePosition = queryIndices[i] + 1;
179 const updateChar = queryCharacters[i];
180
181 // Modify the character at the specified position
182 modify(1, updatePosition, updateChar);
183
184 // Query the entire range for maximum repeating length
185 results[i] = query(1, 1, stringLength);
186 }
187
188 return results;
189}
190
Time and Space Complexity
Time Complexity: O((n + q) × log n)
where n
is the length of string s
and q
is the number of queries.
-
Building the segment tree: The
build
method is called once and visits each node in the tree exactly once. Since there areO(n)
nodes in total (approximately4n
nodes for a segment tree), and each node performsO(1)
work except for thepushup
operation which is alsoO(1)
, the build time isO(n)
. -
Query operation: Each
query
call traverses at mostO(log n)
levels of the tree (the height of the tree), performingO(1)
work at each level. Since we performq
queries, the total query time isO(q × log n)
. -
Modify operation: Each
modify
call traverses from root to leaf, which isO(log n)
levels, and then callspushup
on the way back up, visitingO(log n)
nodes. Eachpushup
operation isO(1)
. Since we performq
modifications, the total modification time isO(q × log n)
. -
Total time complexity:
O(n + q × log n)
. Whenq
is proportional ton
, this becomesO(n × log n)
, which matches the reference answer.
Space Complexity: O(n)
-
Segment tree storage: The tree array
tr
has size4n
, storingNode
objects. EachNode
has constant space (5 integer attributes), so the tree requiresO(n)
space. -
String storage: The string
s
is converted to a list of characters, requiringO(n)
space. -
Output array: The answer array stores
q
integers, requiringO(q)
space. -
Total space complexity:
O(n + q)
. Whenq
is proportional ton
, this becomesO(n)
.
Note: The reference answer states O(n × log n)
for space complexity, but the actual implementation uses an array-based segment tree with 4n
nodes, which is O(n)
space, not O(n × log n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Boundary Character Comparison in Pushup
One of the most common mistakes is incorrectly handling the boundary character comparison when merging two segments. The critical issue is using wrong indices when checking if adjacent segments can be merged.
Pitfall Code:
# WRONG: Using 0-based index directly if self.chars[left_child.r] == self.chars[right_child.l]: # This will cause index out of bounds or compare wrong characters
Correct Code:
# CORRECT: Convert to 0-based index by subtracting 1 if self.chars[left_child.r - 1] == self.chars[right_child.l - 1]: # Properly compares the last char of left segment with first char of right segment
2. Not Handling Single Character Updates Properly
Another pitfall is forgetting that when we update a character, it affects not just the containing segment but all ancestor segments up to the root. Some implementations might update only the leaf node without propagating changes upward.
Pitfall Implementation:
def modify(self, node_idx: int, position: int, new_char: str) -> None:
current_node = self.tree[node_idx]
if current_node.l == current_node.r:
self.chars[position - 1] = new_char
return # WRONG: Returns without updating ancestors
# Recursive calls without pushup
mid = (current_node.l + current_node.r) // 2
if position <= mid:
self.modify(node_idx << 1, position, new_char)
else:
self.modify(node_idx << 1 | 1, position, new_char)
# MISSING: self.pushup(node_idx)
3. Incorrect Calculation of Merged Sequence Length
When two segments can be merged at their boundary, the total length of the merged sequence must consider both the suffix of the left segment and the prefix of the right segment.
Pitfall Code:
# WRONG: Only considering one side or using wrong values
root.mx = max(root.mx, left_child.mx + right_child.mx) # Incorrect!
Correct Code:
# CORRECT: Use suffix of left (rmx) and prefix of right (lmx)
root.mx = max(root.mx, left_child.rmx + right_child.lmx)
4. Index Conversion Between 0-based and 1-based
The segment tree uses 1-based indexing internally, but Python strings use 0-based indexing. Mixing these up is a frequent source of errors.
Common Mistakes:
# WRONG: Forgetting to convert when accessing the character array char_at_position = self.chars[position] # Should be position - 1 # WRONG: Not converting query indices in the main solution segment_tree.modify(1, index, char) # Should be index + 1
5. Not Checking Complete Uniformity Before Extension
When updating lmx
and rmx
in pushup, we must verify that a child segment is completely uniform before extending the parent's prefix/suffix.
Pitfall Code:
# WRONG: Extending without checking if child is completely uniform if self.chars[left_child.r - 1] == self.chars[right_child.l - 1]: root.lmx = left_child.lmx + right_child.lmx # Always extends!
Correct Code:
# CORRECT: Only extend if the entire left child is uniform if self.chars[left_child.r - 1] == self.chars[right_child.l - 1]: if left_child.lmx == left_child.r - left_child.l + 1: # Check uniformity root.lmx = left_child.lmx + right_child.lmx
Solution to Avoid These Pitfalls:
- Always draw out index mappings between your segment tree (1-based) and array (0-based)
- Test with small examples where you can manually verify the pushup calculations
- Add assertions during development to catch index errors early
- Visualize the merge cases on paper before coding the pushup logic
- Use consistent naming for indices to avoid confusion (e.g., always suffix with
_0based
or_1based
during development)
Which data structure is used in a depth first search?
Recommended Readings
Segment Tree Faster Range Queries For this article we want to introduce the idea of a Segment Tree Segment Trees allow us to quickly perform range queries as well as range updates Suppose we have an array and we want to know the sum of a particular range of numbers as well
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!