Stickers to Spell Word
HardPrompt
We are given n different types of stickers. Each sticker has a lowercase English word on it.
You would like to spell out the given string target by cutting individual letters from your collection of stickers and rearranging them. You can use each sticker more than once if you want, and you have infinite quantities of each sticker.
Return the minimum number of stickers that you need to spell out target. If the task is impossible, return -1.
Note: In all test cases, all words were chosen randomly from the 1000 most common US English words, and target was chosen as a concatenation of two random words.
Example 1:
Input: stickers = ["with","example","science"], target = "thehat"
Output: 3
Explanation:
We can use 2 "with" stickers, and 1 "example" sticker.
After cutting and rearrange the letters of those stickers, we can form the target "thehat".
Also, this is the minimum number of stickers necessary to form the target string.Example 2:
Input: stickers = ["notice","possible"], target = "basicbasic"
Output: -1
Explanation:
We cannot form the target "basicbasic" from cutting letters from the given stickers.
Constraints:
n == stickers.length1 <= n <= 501 <= stickers[i].length <= 101 <= target.length <= 15stickers[i]andtargetconsist of lowercase English letters.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses recursion with memoization, a technique also known as top-down dynamic programming. The problem of finding the minimum stickers for a target is broken down into smaller subproblems: finding the minimum stickers for the remaining parts of the target after applying one sticker. To avoid re-calculating results for the same subproblem (e.g., needing to form "ehat" can be reached via multiple paths), we store the results in a memoization table (a hash map). The key for this table is the string of remaining characters, sorted canonically to ensure that different permutations are treated as the same state.
Algorithm
- Preprocessing: Convert each sticker word into a character frequency map (an array of 26 integers). This avoids repeated counting.
- Memoization: Use a hash map
memoto store the results of subproblems. The key will be the canonical representation of the remaining target characters (a sorted string), and the value will be the minimum number of stickers required. - Recursive Function
solve(target):- Base Case: If
targetis an empty string, 0 stickers are needed. Return 0. - Memoization Check: If
targetis already inmemo, return the stored value. - Recursive Step:
- Initialize
minStickersto a value representing infinity. - Convert the current
targetstring to a frequency maptargetCount. - Iterate through each pre-processed
stickerCount. - Optimization: To ensure progress, only consider stickers that contain the first character of the current (sorted)
targetstring. - For a useful sticker, calculate the
remainingTargetby subtracting the sticker's characters fromtargetCount. Construct a new string from the remaining character counts. Since we build this string character by character from 'a' to 'z', it will be naturally sorted, providing a canonical key for the memoization table. - If the
remainingTargetis shorter than the currenttarget, it means the sticker was helpful. Make a recursive call:res = solve(remainingTarget). - If
resis not infinity, updateminStickers = min(minStickers, 1 + res).
- Initialize
- Save and Return: Store
minStickersinmemofor the currenttargetand return it.
- Base Case: If
- Initial Call: The main function calls
solve(target)and returns the result, or -1 if the result is infinity.
Walkthrough
The core idea is to define a function solve(currentTarget) that computes the minimum stickers needed to spell currentTarget.
To implement this, we first pre-process the input stickers into character frequency maps (e.g., int[26]) for quick lookups. The recursive function solve works as follows:
- Base Case: If the
currentTargetstring is empty, we've successfully spelled the original target. We need 0 more stickers, so we return 0. - Memoization: We use a
HashMap<String, Integer>to store results. Before computing, we check if the result forcurrentTargetis already stored. If so, we return it immediately. - Recursive Logic: We iterate through every sticker. For each sticker, we determine which characters from
currentTargetit can provide. We then generate aremainingTargetstring. If this new string is shorter thancurrentTarget, we recursively callsolve(remainingTarget). The result for the current state is1 +the minimum result from all possible recursive calls.
An important optimization is to only consider stickers that provide at least one of the characters we currently need. A simple way to enforce this is to only try stickers that contain the first character of the canonical (sorted) currentTarget. This guarantees that every sticker we use makes progress.
Finally, we store the computed minimum value in our memoization table before returning it. If after trying all stickers, we cannot reduce the target, we store infinity to signify that this path is a dead end.
class Solution { private Map<String, Integer> memo; private int[][] stickerCounts; public int minStickers(String[] stickers, String target) { stickerCounts = new int[stickers.length][26]; for (int i = 0; i < stickers.length; i++) { for (char c : stickers[i].toCharArray()) { stickerCounts[i][c - 'a']++; } } memo = new HashMap<>(); memo.put("", 0); int result = solve(target); return result == Integer.MAX_VALUE ? -1 : result; } private int solve(String target) { if (memo.containsKey(target)) { return memo.get(target); } int minResult = Integer.MAX_VALUE; int[] targetCount = new int[26]; for (char c : target.toCharArray()) { targetCount[c - 'a']++; } for (int[] stickerCount : stickerCounts) { if (stickerCount[target.charAt(0) - 'a'] == 0) { continue; } StringBuilder remainingTargetBuilder = new StringBuilder(); for (int i = 0; i < 26; i++) { if (targetCount[i] > 0) { int remaining = targetCount[i] - stickerCount[i]; for (int j = 0; j < remaining; j++) { remainingTargetBuilder.append((char) ('a' + i)); } } } String remainingTarget = remainingTargetBuilder.toString(); if (remainingTarget.length() < target.length()) { int res = solve(remainingTarget); if (res != Integer.MAX_VALUE) { minResult = Math.min(minResult, 1 + res); } } } memo.put(target, minResult); return minResult; }}Complexity
Time
O(S * N * T), where S is the number of states (subproblems), N is the number of stickers, and T is the length of the target. The number of states is bounded by `2^T`. For each state, we iterate through N stickers, and for each sticker, we perform an O(T) operation to generate the next state. This gives a total time complexity of O(2^T * N * T).
Space
O(S * T), where S is the number of subproblems and T is the maximum length of a subproblem's string representation. In the worst case, S can be up to `2^T` (where T is the length of the original target), making the space complexity O(2^T * T). This space is used for the memoization table and the recursion call stack.
Trade-offs
Pros
Vastly more efficient than brute-force recursion by avoiding re-computation of subproblems.
The logic directly follows the recursive nature of the problem, making it relatively intuitive to design.
Guaranteed to find the optimal solution.
Cons
Can be slightly slower than an iterative bottom-up approach due to recursion overhead.
String manipulation for memoization keys can add overhead, although using a canonical representation is necessary.
For very deep recursion, it could potentially lead to a stack overflow error, though unlikely with the given constraints (T <= 15).
A more optimized and iterative solution can be achieved using bottom-up dynamic programming with bitmasking. This approach avoids recursion entirely. We use an array, say dp, where the index is a bitmask representing a subset of the characters in the target string. For a target of length T, we'll have 2^T possible subsets (states). dp[mask] stores the minimum number of stickers required to form the subsequence represented by mask. We start with the base case dp[0] = 0 (for an empty set of characters) and iteratively build up the solution for larger and larger subsets until we reach the state representing the full target string.
Algorithm
- State Representation: Use a bitmask of length
T(length oftarget) to represent a subproblem.dp[mask]will store the minimum stickers needed to form the subsequence oftargetwhere thei-th bit ofmaskis 1 if thei-th character is formed. - Initialization: Create a
dparray of size2^T. Initializedp[0] = 0(0 stickers for an empty sequence) and all otherdpentries to infinity. - Preprocessing: As before, convert stickers to character frequency maps (
int[26]). - Iteration:
- Iterate through each
maskfrom0to2^T - 1. - If
dp[mask]is infinity, it's an unreachable state, so skip it. - For the current
mask, iterate through each sticker. - Calculate the
nextMaskthat would result from applying the current sticker to the state represented bymask.- Start with
tempMask = mask. - Make a copy of the sticker's frequency map.
- Iterate through the
targetstring fromi = 0toT-1. - If the
i-th character is needed ((tempMask >> i) & 1 == 0) and the sticker can provide it, set thei-th bit intempMaskand decrement the character's count from the sticker's copied map.
- Start with
- After applying the sticker, we have a
nextMask. Update thedptable:dp[nextMask] = min(dp[nextMask], dp[mask] + 1).
- Iterate through each
- Result: The final answer is
dp[(1<<T) - 1]. If it's infinity, the target is impossible. Return -1.
Walkthrough
In this approach, we represent the state of our problem using a bitmask. A mask is an integer where the i-th bit corresponds to the i-th character of the target string. If the bit is 1, the character is covered; if it's 0, it's not.
We create a dp array of size 2^T, where T is the length of target. dp[mask] will store the minimum stickers needed. We initialize dp[0] to 0 and all other entries to infinity.
The algorithm proceeds by iterating through each mask from 0 up to (1<<T) - 1. For each mask whose dp value is not infinity (meaning it's a reachable state), we try to apply each sticker to transition to a new state.
For a given mask and a sticker, we calculate the nextMask. We start with nextMask = mask. We then iterate through the target's characters. If the i-th character is not yet covered in mask and the sticker has the required letter, we set the i-th bit in nextMask and use up one instance of that letter from the sticker. This greedy application of a sticker's letters is valid because the dp state only cares about which characters are covered, not how they were covered.
After determining the nextMask, we update its dp value with dp[nextMask] = min(dp[nextMask], dp[mask] + 1). This means the cost to reach nextMask is potentially the cost to reach mask plus one more sticker.
After all masks have been processed, dp[(1<<T) - 1] will contain the minimum number of stickers to cover all characters of the target. If the value remains infinity, the target is unreachable.
class Solution { public int minStickers(String[] stickers, String target) { int tLen = target.length(); int n = 1 << tLen; int[] dp = new int[n]; Arrays.fill(dp, Integer.MAX_VALUE); dp[0] = 0; for (int mask = 0; mask < n; mask++) { if (dp[mask] == Integer.MAX_VALUE) { continue; } for (String sticker : stickers) { int nextMask = mask; int[] stickerCount = new int[26]; for (char c : sticker.toCharArray()) { stickerCount[c - 'a']++; } for (int i = 0; i < tLen; i++) { if (((nextMask >> i) & 1) == 0) { char c = target.charAt(i); if (stickerCount[c - 'a'] > 0) { nextMask |= (1 << i); stickerCount[c - 'a']--; } } } if (nextMask != mask) { dp[nextMask] = Math.min(dp[nextMask], dp[mask] + 1); } } } return dp[n - 1] == Integer.MAX_VALUE ? -1 : dp[n - 1]; }}Complexity
Time
O(2^T * N * T), where T is the length of the target and N is the number of stickers. We iterate through `2^T` masks. For each mask, we iterate through N stickers. For each sticker, we iterate through the T characters of the target to compute the next mask.
Space
O(2^T), where T is the length of the target. This space is dominated by the `dp` array of size `2^T`.
Trade-offs
Pros
Generally faster than the recursive top-down approach due to its iterative nature, which avoids function call overhead and can have better cache performance.
Avoids potential stack overflow issues that can arise from deep recursion.
The space complexity is slightly better as it doesn't need to store string keys for a map, just the
dparray.
Cons
The concept of bitmask DP can be less intuitive than a straightforward recursive approach.
The space complexity is exponential,
O(2^T), which is only feasible for smallT(likeT <= 15in this problem).
Solutions
Solution
class Solution { public int minStickers ( String [] stickers , String target ) { Deque < Integer > q = new ArrayDeque <>(); q . offer ( 0 ); int ans = 0 ; int n = target . length (); boolean [] vis = new boolean [ 1 << n ]; vis [ 0 ] = true ; while (! q . isEmpty ()) { for ( int t = q . size (); t > 0 ; -- t ) { int state = q . poll (); if ( state == ( 1 << n ) - 1 ) { return ans ; } for ( String s : stickers ) { int nxt = state ; int [] cnt = new int [ 26 ]; for ( char c : s . toCharArray ()) { ++ cnt [ c - 'a' ]; } for ( int i = 0 ; i < n ; ++ i ) { int idx = target . charAt ( i ) - 'a' ; if (( nxt & ( 1 << i )) == 0 && cnt [ idx ] > 0 ) { nxt |= 1 << i ; -- cnt [ idx ]; } } if (! vis [ nxt ]) { vis [ nxt ] = true ; q . offer ( nxt ); } } } ++ ans ; } return - 1 ; } }class Solution { public: int minStickers ( vector < string >& stickers , string target ) { queue < int > q { { 0 } }; int ans = 0 ; int n = target . size (); vector < bool > vis ( 1 << n ); vis [ 0 ] = true ; while ( ! q . empty ()) { for ( int t = q . size (); t ; -- t ) { int state = q . front (); if ( state == ( 1 << n ) - 1 ) return ans ; q . pop (); for ( auto & s : stickers ) { int nxt = state ; vector < int > cnt ( 26 ); for ( char & c : s ) ++ cnt [ c - 'a' ]; for ( int i = 0 ; i < n ; ++ i ) { int idx = target [ i ] - 'a' ; if ( ! ( nxt & ( 1 << i )) && cnt [ idx ]) { nxt |= 1 << i ; -- cnt [ idx ]; } } if ( ! vis [ nxt ]) { vis [ nxt ] = true ; q . push ( nxt ); } } } ++ ans ; } return - 1 ; } };class Solution : def minStickers ( self , stickers : List [ str ], target : str ) -> int : q = deque ([ 0 ]) ans = 0 n = len ( target ) vis = [ False ] * ( 1 << n ) vis [ 0 ] = True while q : for _ in range ( len ( q )): state = q . popleft () if state == ( 1 << n ) - 1 : return ans for s in stickers : nxt = state cnt = Counter ( s ) for i , c in enumerate ( target ): if not ( nxt & ( 1 << i )) and cnt [ c ]: nxt |= 1 << i cnt [ c ] -= 1 if not vis [ nxt ]: vis [ nxt ] = True q . append ( nxt ) ans += 1 return - 1Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.