936. Stamping The Sequence
Problem Description
You are given two strings: stamp
and target
. Initially, you have a string s
of the same length as target
, where all characters are '?'
.
In each turn, you can place the stamp
over s
at any valid position and replace the characters in s
with the corresponding characters from stamp
. The stamp must be fully contained within the boundaries of s
.
For example, if stamp = "abc"
and target = "abcba"
, then s
starts as "?????"
. You can:
- Place
stamp
at index 0 to get"abc??"
- Place
stamp
at index 1 to get"?abc?"
- Place
stamp
at index 2 to get"??abc"
- But you cannot place
stamp
at index 3 since it would go out of bounds
Your goal is to transform s
from all '?'
characters into the target
string by repeatedly stamping. You must do this in at most 10 * target.length
turns.
Return an array containing the starting indices where you placed the stamp at each turn. The indices should be listed in the order you performed the stamping operations. If it's impossible to achieve the target string within the allowed number of turns, return an empty array.
The solution uses reverse thinking - instead of building up from '?'
to target
, it works backwards from target
to all '?'
characters. It employs a topological sorting approach where each possible stamp position is treated as a node with an in-degree representing how many characters match the stamp. When a position can be fully "unstamped" (in-degree becomes 0), it's added to the queue for processing. The algorithm tracks which positions have been covered and builds the answer in reverse order.
Intuition
The key insight is that working forward from all '?'
characters to the target
string is extremely complex because later stamp operations can overwrite earlier ones, making it difficult to determine the correct sequence of operations.
Instead, we can think backwards: start from the completed target
string and work our way back to all '?'
characters. This reverse approach is much cleaner because we're essentially "removing" stamps rather than placing them, and we know that once a position becomes '?'
, it stays that way.
Consider how we would "unstamp" or remove a stamp from the target. We can only remove a stamp at position i
if all characters in that window match either the stamp characters or are already '?'
(previously unstamped). This creates a dependency relationship - some stamps can only be removed after others have been removed first.
This dependency structure naturally leads us to think about topological sorting. Each possible stamp position (window) can be viewed as a node in a graph. The "in-degree" of each window represents how many of its characters still need to match the stamp before we can remove it. When a window's in-degree reaches 0, it means all its characters either match the stamp or are already '?'
, so we can safely remove this stamp.
As we remove each stamp (converting its characters to '?'
), we potentially enable other overlapping stamps to be removed. This is similar to how in topological sorting, processing one node can reduce the in-degrees of its neighbors, potentially making them ready for processing.
The adjacency list g[i]
tracks which windows are affected when position i
in the target becomes '?'
. When we "unstamp" a window and turn some positions to '?'
, we update the in-degrees of all windows that were waiting for those positions to become available.
Since we're working backwards, the final answer needs to be reversed to give us the forward sequence of stamp operations that would build from '?'
to target
.
Solution Approach
The implementation uses a reverse topological sorting approach to find the sequence of stamp operations.
Initialization Phase:
First, we set up the necessary data structures:
indeg[i]
array: Initially set tom
(stamp length) for each of then-m+1
possible stamp positions. This represents how many characters in windowi
differ from the stamp.g[i]
adjacency list: For each positioni
in the target string, we store which windows have a mismatched character at that position.q
queue: Stores windows withindeg = 0
(ready to be unstamped).vis[i]
array: Tracks which positions in the target have been covered (turned to'?'
).
Building the Graph:
For each possible stamp position i
from 0
to n-m
:
- We check each character
j
in the stamp (from0
tom-1
) - If
target[i+j] == stamp[j]
, the characters match, so we decrementindeg[i]
- If they don't match, we add window
i
tog[i+j]
, meaning positioni+j
blocks windowi
from being unstamped - If
indeg[i]
becomes0
after checking all characters, windowi
is ready to be unstamped, so we add it to queueq
Topological Sorting Process:
While the queue is not empty:
- Pop a window
i
from the queue and add it to the answer array - For each position
j
in the stamp (from0
tom-1
):- If position
i+j
hasn't been visited yet:- Mark it as visited (
vis[i+j] = True
) - For each window
k
ing[i+j]
(windows blocked by this position):- Decrement
indeg[k]
by 1 (one less blocking character) - If
indeg[k]
becomes0
, add windowk
to the queue
- Decrement
- Mark it as visited (
- If position
Final Check:
After processing all windows:
- If all positions in the target are visited (
all(vis)
isTrue
), we've successfully covered the entire string - Return the reversed answer array (
ans[::-1]
) since we worked backwards - If any position remains unvisited, it's impossible to form the target, so return an empty array
The time complexity is O(n * (n-m+1))
where n
is the length of the target and m
is the length of the stamp, as each position can be visited at most once and each window can affect up to m
positions.
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 stamp = "abc"
and target = "ababc"
.
Initial Setup:
- Target length n = 5, stamp length m = 3
- Possible stamp positions: 0, 1, 2 (windows of size 3)
- Initialize
indeg = [3, 3, 3]
for all windows - Initialize
g
as empty adjacency lists for each position - Initialize
vis = [False, False, False, False, False]
Building the Graph:
Window 0 (positions 0-2): "aba" vs "abc"
- Position 0: 'a' == 'a' ✓ → decrement indeg[0] to 2
- Position 1: 'b' == 'b' ✓ → decrement indeg[0] to 1
- Position 2: 'a' != 'c' ✗ → add window 0 to g[2]
- Final: indeg[0] = 1
Window 1 (positions 1-3): "bab" vs "abc"
- Position 1: 'b' != 'a' ✗ → add window 1 to g[1]
- Position 2: 'a' != 'b' ✗ → add window 1 to g[2]
- Position 3: 'b' != 'c' ✗ → add window 1 to g[3]
- Final: indeg[1] = 3
Window 2 (positions 2-4): "abc" vs "abc"
- Position 2: 'a' == 'a' ✓ → decrement indeg[2] to 2
- Position 3: 'b' == 'b' ✓ → decrement indeg[2] to 1
- Position 4: 'c' == 'c' ✓ → decrement indeg[2] to 0
- Final: indeg[2] = 0 → add window 2 to queue
State after building:
indeg = [1, 3, 0]
g = {1: [1], 2: [0, 1], 3: [1]}
q = [2]
Topological Sorting:
Round 1: Process window 2
- Add 2 to answer:
ans = [2]
- Mark positions 2, 3, 4 as visited
- Position 2 was blocking windows 0 and 1:
- indeg[0] becomes 0 → add to queue
- indeg[1] becomes 2
- Position 3 was blocking window 1:
- indeg[1] becomes 1
- Updated state:
vis = [F, F, T, T, T]
,q = [0]
Round 2: Process window 0
- Add 0 to answer:
ans = [2, 0]
- Mark positions 0, 1 as visited (position 2 already visited)
- Position 1 was blocking window 1:
- indeg[1] becomes 0 → add to queue
- Updated state:
vis = [T, T, T, T, T]
,q = [1]
Round 3: Process window 1
- Add 1 to answer:
ans = [2, 0, 1]
- All positions already visited, no updates needed
Final Check:
- All positions visited:
vis = [T, T, T, T, T]
✓ - Return reversed answer:
[1, 0, 2]
Verification (forward direction):
- Start with "?????"
- Stamp at position 1: "?abc?"
- Stamp at position 0: "abc??" → "abcc?"
- Stamp at position 2: "ab???" → "ababc" ✓
The algorithm successfully found the stamping sequence by working backwards from the target to all '?' characters.
Solution Implementation
1class Solution:
2 def movesToStamp(self, stamp: str, target: str) -> List[int]:
3 """
4 Find sequence of stamp positions to transform target string to all '?'s.
5 Uses reverse simulation: find which positions can be "unstamped" from target.
6
7 Args:
8 stamp: The stamp string to use
9 target: The target string to achieve
10
11 Returns:
12 List of stamp positions in order, or empty list if impossible
13 """
14 stamp_length, target_length = len(stamp), len(target)
15
16 # Track how many characters at each position still need to match
17 # to successfully place stamp at that position
18 unmatched_count = [stamp_length] * (target_length - stamp_length + 1)
19
20 # Queue for positions that can be stamped (all characters match)
21 ready_positions = deque()
22
23 # Build dependency graph: for each target position, track which
24 # stamp positions depend on it for matching
25 dependencies = [[] for _ in range(target_length)]
26
27 # Initialize unmatched counts and find initially ready positions
28 for stamp_pos in range(target_length - stamp_length + 1):
29 for offset, stamp_char in enumerate(stamp):
30 target_pos = stamp_pos + offset
31
32 if target[target_pos] == stamp_char:
33 # This character already matches
34 unmatched_count[stamp_pos] -= 1
35 if unmatched_count[stamp_pos] == 0:
36 # All characters match, can stamp here
37 ready_positions.append(stamp_pos)
38 else:
39 # This stamp position depends on target_pos being wildcard
40 dependencies[target_pos].append(stamp_pos)
41
42 # Process positions in reverse order (unstamping)
43 stamp_sequence = []
44 is_covered = [False] * target_length
45
46 while ready_positions:
47 # Process next ready position
48 current_stamp_pos = ready_positions.popleft()
49 stamp_sequence.append(current_stamp_pos)
50
51 # Mark all positions covered by this stamp as wildcards
52 for offset in range(stamp_length):
53 target_pos = current_stamp_pos + offset
54
55 if not is_covered[target_pos]:
56 is_covered[target_pos] = True
57
58 # Update all stamp positions that were waiting for this wildcard
59 for dependent_stamp_pos in dependencies[target_pos]:
60 unmatched_count[dependent_stamp_pos] -= 1
61 if unmatched_count[dependent_stamp_pos] == 0:
62 ready_positions.append(dependent_stamp_pos)
63
64 # Check if all positions are covered and return result
65 # Reverse the sequence since we found it backwards (unstamping order)
66 return stamp_sequence[::-1] if all(is_covered) else []
67
1class Solution {
2 public int[] movesToStamp(String stamp, String target) {
3 int stampLength = stamp.length();
4 int targetLength = target.length();
5
6 // Track how many characters need to match for each possible stamp position
7 // Initially, all positions need stampLength matches
8 int[] remainingMatches = new int[targetLength - stampLength + 1];
9 Arrays.fill(remainingMatches, stampLength);
10
11 // Build adjacency list: for each target position, store which stamp positions
12 // would be affected if this character becomes a wildcard
13 List<Integer>[] adjacencyList = new List[targetLength];
14 Arrays.setAll(adjacencyList, i -> new ArrayList<>());
15
16 // Queue for BFS - holds stamp positions that can be applied (all characters match)
17 Deque<Integer> queue = new ArrayDeque<>();
18
19 // Initialize the graph and find initial stamp positions that fully match
20 for (int stampPos = 0; stampPos < targetLength - stampLength + 1; stampPos++) {
21 for (int offset = 0; offset < stampLength; offset++) {
22 if (target.charAt(stampPos + offset) == stamp.charAt(offset)) {
23 // Character matches, decrease remaining matches needed
24 if (--remainingMatches[stampPos] == 0) {
25 // All characters match, this stamp position can be applied
26 queue.offer(stampPos);
27 }
28 } else {
29 // Character doesn't match, add edge from target position to stamp position
30 adjacencyList[stampPos + offset].add(stampPos);
31 }
32 }
33 }
34
35 // Store the sequence of stamp applications
36 List<Integer> stampSequence = new ArrayList<>();
37
38 // Track which target positions have been converted to wildcards
39 boolean[] isWildcard = new boolean[targetLength];
40
41 // BFS to find valid stamp sequence
42 while (!queue.isEmpty()) {
43 int currentStampPos = queue.poll();
44 stampSequence.add(currentStampPos);
45
46 // Mark all characters covered by this stamp as wildcards
47 for (int offset = 0; offset < stampLength; offset++) {
48 if (!isWildcard[currentStampPos + offset]) {
49 isWildcard[currentStampPos + offset] = true;
50
51 // Update all stamp positions that were waiting for this character
52 for (int affectedStampPos : adjacencyList[currentStampPos + offset]) {
53 if (--remainingMatches[affectedStampPos] == 0) {
54 // This stamp position can now be applied
55 queue.offer(affectedStampPos);
56 }
57 }
58 }
59 }
60 }
61
62 // Check if all target positions have been covered
63 for (int i = 0; i < targetLength; i++) {
64 if (!isWildcard[i]) {
65 // Some position wasn't covered, transformation is impossible
66 return new int[0];
67 }
68 }
69
70 // Reverse the sequence since we need to apply stamps in reverse order
71 Collections.reverse(stampSequence);
72
73 // Convert List<Integer> to int[]
74 return stampSequence.stream().mapToInt(Integer::intValue).toArray();
75 }
76}
77
1class Solution {
2public:
3 vector<int> movesToStamp(string stamp, string target) {
4 int stampLen = stamp.size();
5 int targetLen = target.size();
6
7 // indegree[i] represents how many characters at position i need to match
8 // to successfully stamp at position i
9 vector<int> indegree(targetLen - stampLen + 1, stampLen);
10
11 // adjacency list: for each position in target, store which stamp positions
12 // are blocked by a mismatch at this position
13 vector<vector<int>> blockedStamps(targetLen);
14
15 queue<int> readyPositions;
16
17 // Initialize the graph structure
18 // For each possible stamp position i
19 for (int stampPos = 0; stampPos < targetLen - stampLen + 1; ++stampPos) {
20 // Check each character in the stamp
21 for (int offset = 0; offset < stampLen; ++offset) {
22 if (target[stampPos + offset] == stamp[offset]) {
23 // Character matches, decrease the indegree
24 if (--indegree[stampPos] == 0) {
25 // All characters match, this position is ready to stamp
26 readyPositions.push(stampPos);
27 }
28 } else {
29 // Character doesn't match, record this stamp position as blocked
30 // by the character at position stampPos + offset
31 blockedStamps[stampPos + offset].push_back(stampPos);
32 }
33 }
34 }
35
36 vector<int> stampingSequence;
37 vector<bool> covered(targetLen, false); // Track which positions have been covered
38
39 // Process positions that can be stamped (BFS/topological sort)
40 while (!readyPositions.empty()) {
41 int currentStampPos = readyPositions.front();
42 readyPositions.pop();
43
44 // Add this position to the stamping sequence
45 stampingSequence.push_back(currentStampPos);
46
47 // Mark all positions covered by this stamp
48 for (int offset = 0; offset < stampLen; ++offset) {
49 int targetPos = currentStampPos + offset;
50
51 if (!covered[targetPos]) {
52 covered[targetPos] = true;
53
54 // For all stamp positions that were blocked by this character,
55 // decrease their indegree since this position is now covered
56 for (int blockedStampPos : blockedStamps[targetPos]) {
57 if (--indegree[blockedStampPos] == 0) {
58 // This stamp position is now ready
59 readyPositions.push(blockedStampPos);
60 }
61 }
62 }
63 }
64 }
65
66 // Check if all positions in target have been covered
67 for (int i = 0; i < targetLen; ++i) {
68 if (!covered[i]) {
69 // Cannot transform to target
70 return {};
71 }
72 }
73
74 // Reverse the sequence since we built it backwards
75 // (from final state to initial state)
76 reverse(stampingSequence.begin(), stampingSequence.end());
77
78 return stampingSequence;
79 }
80};
81
1/**
2 * Finds the sequence of moves to stamp the target string
3 * @param stamp - The stamp string to use
4 * @param target - The target string to create
5 * @returns Array of starting positions where stamp should be applied, or empty array if impossible
6 */
7function movesToStamp(stamp: string, target: string): number[] {
8 const stampLength: number = stamp.length;
9 const targetLength: number = target.length;
10
11 // Track how many characters need to match for each possible stamp position
12 // Initially, all positions need stampLength matches
13 const inDegree: number[] = Array(targetLength - stampLength + 1).fill(stampLength);
14
15 // Build adjacency list: for each position in target, store which stamp positions affect it
16 const adjacencyList: number[][] = Array.from({ length: targetLength }, () => []);
17
18 // Queue for positions that are ready to be stamped (all characters match)
19 const queue: number[] = [];
20
21 // Initialize the graph by checking each possible stamp position
22 for (let stampPosition = 0; stampPosition < targetLength - stampLength + 1; stampPosition++) {
23 for (let offset = 0; offset < stampLength; offset++) {
24 const targetPosition = stampPosition + offset;
25
26 if (target[targetPosition] === stamp[offset]) {
27 // Character matches, decrease the in-degree
28 inDegree[stampPosition]--;
29 if (inDegree[stampPosition] === 0) {
30 // All characters match at this position, ready to stamp
31 queue.push(stampPosition);
32 }
33 } else {
34 // Character doesn't match, add edge from target position to stamp position
35 adjacencyList[targetPosition].push(stampPosition);
36 }
37 }
38 }
39
40 // Store the result (stamp positions in reverse order)
41 const result: number[] = [];
42
43 // Track which positions in the target have been covered
44 const visited: boolean[] = Array(targetLength).fill(false);
45
46 // Process positions using BFS-like approach
47 while (queue.length > 0) {
48 const currentStampPosition: number = queue.shift()!;
49 result.push(currentStampPosition);
50
51 // Mark all positions covered by this stamp as visited
52 for (let offset = 0; offset < stampLength; offset++) {
53 const targetPosition = currentStampPosition + offset;
54
55 if (!visited[targetPosition]) {
56 visited[targetPosition] = true;
57
58 // Update all stamp positions that were blocked by this target position
59 for (const affectedStampPosition of adjacencyList[targetPosition]) {
60 inDegree[affectedStampPosition]--;
61 if (inDegree[affectedStampPosition] === 0) {
62 // This stamp position is now ready
63 queue.push(affectedStampPosition);
64 }
65 }
66 }
67 }
68 }
69
70 // Check if all positions in target have been covered
71 if (!visited.every(isVisited => isVisited)) {
72 // Not all positions covered, impossible to create target
73 return [];
74 }
75
76 // Reverse to get the correct order (we built it backwards)
77 result.reverse();
78 return result;
79}
80
Time and Space Complexity
Time Complexity: O(n × (n - m + 1))
The algorithm processes each possible stamp position (there are n - m + 1
such positions) and for each position, it may need to update the graph connections and process the characters. Specifically:
- Initial setup: For each of the
n - m + 1
positions, we iterate throughm
characters of the stamp, givingO((n - m + 1) × m)
=O(n × m)
in the worst case. - BFS processing: Each position can be added to the queue at most once. When processing a position from the queue, we mark up to
m
characters as visited and update their connected positions in the graph. Since each character can have at mostn - m + 1
connections in the graph, and there aren
characters total, the worst-case processing isO(n × (n - m + 1))
.
The dominant term is O(n × (n - m + 1))
.
Space Complexity: O(n × (n - m + 1))
The space is used for:
indeg
array:O(n - m + 1)
- Graph
g
: In the worst case, each of then
positions can have connections to up ton - m + 1
stamp positions, givingO(n × (n - m + 1))
- Queue
q
: At mostO(n - m + 1)
elements vis
array:O(n)
ans
list:O(n - m + 1)
The dominant term is the graph storage, which is O(n × (n - m + 1))
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of Already Covered Positions
The Pitfall: When processing a stamp position from the queue, developers often forget to check whether a position has already been covered (turned into a wildcard) before updating dependent stamp positions. This can lead to:
- Double-counting: Decrementing
unmatched_count
multiple times for the same position - Adding stamp positions to the queue prematurely when they shouldn't be ready yet
- Infinite loops or incorrect results
Example of Incorrect Code:
# WRONG: Not checking if position is already covered
for offset in range(stamp_length):
target_pos = current_stamp_pos + offset
is_covered[target_pos] = True # Mark as covered regardless
# This will update dependencies even if already processed!
for dependent_stamp_pos in dependencies[target_pos]:
unmatched_count[dependent_stamp_pos] -= 1
if unmatched_count[dependent_stamp_pos] == 0:
ready_positions.append(dependent_stamp_pos)
The Solution: Always check if a position has been covered before processing its dependencies:
# CORRECT: Check if position is already covered
for offset in range(stamp_length):
target_pos = current_stamp_pos + offset
if not is_covered[target_pos]: # Only process if not yet covered
is_covered[target_pos] = True
# Now safely update dependencies
for dependent_stamp_pos in dependencies[target_pos]:
unmatched_count[dependent_stamp_pos] -= 1
if unmatched_count[dependent_stamp_pos] == 0:
ready_positions.append(dependent_stamp_pos)
2. Misunderstanding the Initial Match Counting
The Pitfall: During initialization, developers might incorrectly count matches or set up the dependency graph wrong. Common mistakes include:
- Starting with
unmatched_count[i] = 0
and incrementing for mismatches (wrong direction) - Forgetting to immediately add positions with zero unmatched count to the queue
- Adding positions to dependencies even when they match
Example of Incorrect Code:
# WRONG: Counting mismatches instead of matches
for stamp_pos in range(target_length - stamp_length + 1):
for offset, stamp_char in enumerate(stamp):
target_pos = stamp_pos + offset
if target[target_pos] != stamp_char:
unmatched_count[stamp_pos] += 1 # Starting from 0 is wrong!
dependencies[target_pos].append(stamp_pos)
# Forgot to check if ready to stamp immediately!
The Solution: Start with the full stamp length and decrement for matches:
# CORRECT: Start with stamp_length and decrement for matches
unmatched_count = [stamp_length] * (target_length - stamp_length + 1)
for stamp_pos in range(target_length - stamp_length + 1):
for offset, stamp_char in enumerate(stamp):
target_pos = stamp_pos + offset
if target[target_pos] == stamp_char:
unmatched_count[stamp_pos] -= 1
if unmatched_count[stamp_pos] == 0:
ready_positions.append(stamp_pos) # Add immediately if ready
else:
dependencies[target_pos].append(stamp_pos)
3. Forgetting to Reverse the Final Answer
The Pitfall: Since the algorithm works backwards (from target to all wildcards), the stamp positions are collected in reverse order. Forgetting to reverse the final answer will give the wrong sequence.
Example of Incorrect Code:
# WRONG: Returning the answer without reversing
return stamp_sequence if all(is_covered) else []
The Solution: Always reverse the answer before returning:
# CORRECT: Reverse the sequence since we found it backwards
return stamp_sequence[::-1] if all(is_covered) else []
These pitfalls are particularly dangerous because they might work correctly for simple test cases but fail on more complex inputs where stamps overlap or the same position needs to be considered multiple times.
A heap is a ...?
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Queue Intro Think of the last time you stood in line to buy movie tickets The first person in line gets their ticket first and any newcomers join the end of the line This system where the first person to arrive is the first to be served is a queue in real
Want a Structured Path to Master System Design Too? Don’t Miss This!