t.
You would like to spell out the given target
string 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.
What is the minimum number of stickers that you need to spell out the target
? If the task is impossible, return -1.
Example 1:
Input:
["with", "example", "science"], "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:
["notice", "possible"], "basicbasic"
Output:
-1
Explanation:
We can't form the target "basicbasic" from cutting letters from the given stickers.
Note:
-
stickers
has length in the range[1, 50]
. -
stickers
consists of lowercase English words (without apostrophes). -
target
has length in the range[1, 15]
, and consists of lowercase English letters. - In all test cases, all words were chosen randomly from the 1000 most common US English words, and the target was chosen as a concatenation of two random words.
- The time limit may be more challenging than usual. It is expected that a 50 sticker test case can be solved within 35ms on average.
-
-
t.
You would like to spell out the given
target
string 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.
What is the minimum number of stickers that you need to spell out the
target
? If the task is impossible, return -1.Example 1:
Input:
["with", "example", "science"], "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:
["notice", "possible"], "basicbasic"
Output:
-1
Explanation:
We can't form the target "basicbasic" from cutting letters from the given stickers.
Note:
-
stickers
has length in the range[1, 50]
. -
stickers
consists of lowercase English words (without apostrophes). -
target
has length in the range[1, 15]
, and consists of lowercase English letters. - In all test cases, all words were chosen randomly from the 1000 most common US English words, and the target was chosen as a concatenation of two random words.
- The time limit may be more challenging than usual. It is expected that a 50 sticker test case can be solved within 35ms on average.
- 思路:最开始DFS + prune,TLE
- 后改为有返回值的递归形式+Memo,因为用的数组表示当前需要求解的字符串,TLE
- 但是后来换成String就AC,应该是数组需要操作2次的缘故,因为数组是引用型变量,在放到递归函数前需要保持一致性
- DFS版:
package l691; class CopyOfSolution { int min = Integer.MAX_VALUE; public int minStickers(String[] stickers, String target) { int[] cnt = new int[26]; for(char c : target.toCharArray()) cnt[c-'a']++; int[][] t = new int[stickers.length][26]; for(int i=0; i<stickers.length; i++) for(char c : stickers[i].toCharArray()) t[i][c-'a']++; int[] upper = new int[stickers.length]; for(int i=0; i<upper.length; i++) { for(int j=0; j<26; j++) if(t[i][j] != 0) upper[i] = Math.max(upper[i], cnt[j]/t[i][j]+1); } int[] cur = new int[26], use = new int[stickers.length]; dfs(t, upper, cnt, 0, cur, use); return min == Integer.MAX_VALUE ? -1 : min; } private void dfs(int[][] t, int[] upper, int[] cnt, int sum, int[] cur ,int[] use) { if(sum >= min) return; boolean ok = true; for(int i=0; i<26; i++) { if(cur[i] < cnt[i]) { ok = false; break; } } if(ok) { min = Math.min(min, sum); return; } for(int i=0; i<upper.length; i++) { if(use[i] < upper[i]) { for(int j=0; j<26; j++) cur[j] += t[i][j]; use[i] ++; dfs(t, upper, cnt, sum+1, cur, use); use[i] --; for(int j=0; j<26; j++) cur[j] -= t[i][j]; } } } }
数组TLE版:
package l691; import java.util.HashMap; import java.util.Map; class TLE { Map<String, Integer> memo = new HashMap<String, Integer>(); public int minStickers(String[] stickers, String target) { int[] need = new int[26]; for(char c : target.toCharArray()) need[c-'a']++; int[][] t = new int[stickers.length][26]; for(int i=0; i<stickers.length; i++) for(char c : stickers[i].toCharArray()) t[i][c-'a']++; StringBuilder sb = new StringBuilder(); for(int i=0; i<26; i++) sb.append(0+" "); memo.put(sb.toString(), 0); return dp(t, need); } private int dp(int[][] t, int[] need) { StringBuilder sb = new StringBuilder(); for(int i=0; i<26; i++) sb.append(Math.max(0, need[i]) + " "); if(memo.containsKey(sb.toString())) return memo.get(sb.toString()); int min = Integer.MAX_VALUE; for(int i=0; i<t.length; i++) { boolean needThisWord = false; for(int j=0; j<26; j++) { if(t[i][j]>0 && need[j] > 0) { needThisWord = true; break; } } if(needThisWord) { for(int j=0; j<26; j++) need[j] -= t[i][j]; int tmp = dp(t, need); if(tmp!=-1) min = Math.min(min, 1+tmp); for(int j=0; j<26; j++) need[j] += t[i][j]; } } memo.put(sb.toString(), min==Integer.MAX_VALUE?-1:min); return min==Integer.MAX_VALUE?-1:min; } }
String AC版:package l691; import java.util.HashMap; import java.util.Map; class Solution { Map<String, Integer> memo = new HashMap<String, Integer>(); public int minStickers(String[] stickers, String target) { int[][] t = new int[stickers.length][26]; for(int i=0; i<stickers.length; i++) for(char c : stickers[i].toCharArray()) t[i][c-'a']++; memo.put("", 0); return dp(t, target); } private int dp(int[][] t, String target) { int[] need = new int[26]; for(char c : target.toCharArray()) need[c-'a']++; if(memo.containsKey(target)) return memo.get(target); int min = Integer.MAX_VALUE; for(int i=0; i<t.length; i++) { boolean needThisWord = false; for(int j=0; j<26; j++) { if(t[i][j]>0 && need[j] > 0) { needThisWord = true; break; } } if(needThisWord) { StringBuilder sb = new StringBuilder(); for(int j=0; j<26; j++) for(int k=0; k<Math.max(0, need[j]-t[i][j]); k++) sb.append((char)(j+'a')); int tmp = dp(t, sb.toString()); if(tmp!=-1) min = Math.min(min, 1+tmp); } } memo.put(target, min==Integer.MAX_VALUE?-1:min); return min==Integer.MAX_VALUE?-1:min; } }
-
There are potentially a lot of overlapping sub problems, but meanwhile we don't exactly know what those sub problems are.DP with memoization works pretty well in such cases(确实,在有些情况下不能用数组DP,只好memo). The workflow is like backtracking, but with memoization. Here I simply use a sorted string of target as the key for the unordered_map DP. A sorted target results in a unique sub problem for possibly different strings.
dp[s] is the minimum stickers required for string s (-1 if impossible). Note s is sorted. clearly, dp[""] = 0, and the problem asks for dp[target].
The DP formula is:
dp[s] = min(1+dp[reduced_s]) for all stickers, here reduced_s is a new string after certain sticker applied
-
Approach #1: Optimized Exhaustive Search [Accepted]
Intuition
A natural answer is to exhaustively search combinations of stickers. Because the data is randomized, there are many heuristics available to us that will make this faster.
-
For all stickers, we can ignore any letters that are not in the target word.
-
When our candidate answer won't be smaller than an answer we have already found, we can stop searching this path.
-
We should try to have our exhaustive search bound the answer as soon as possible, so the effect described in the above point happens more often.
-
When a sticker dominates another, we shouldn't include the dominated sticker in our sticker collection. [Here, we say a sticker
A
dominatesB
ifA.count(letter) >= B.count(letter)
for all letters.]
Algorithm
Firstly, for each sticker, let's create a count of that sticker (a mapping
letter -> sticker.count(letter)
) that does not consider letters not in the target word. LetA
be an array of these counts. Also, let's createt_count
, a count of ourtarget
word.Secondly, let's remove dominated stickers. Because dominance is a transitive relation, we only need to check if a sticker is not dominated by any other sticker once - the ones that aren't dominated are included in our collection.
We are now ready to begin our exhaustive search. A call to
search(ans)
denotes that we want to decide the minimum number of stickers we can used inA
to satisfy the target countt_count
.ans
will store the currently formed answer, andbest
will store the current best answer.If our current answer can't beat our current best answer, we should stop searching. Also, if there are no stickers left and our target is satisfied, we should update our answer.
Otherwise, we want to know the maximum number of this sticker we can use. For example, if this sticker is
'abb'
and our target is'aaabbbbccccc'
, then we could use a maximum of 3 stickers. This is the maximum ofmath.ceil(target.count(letter) / sticker.count(letter))
, taken over allletter
s insticker
. Let's call this quantityused
.After, for the sticker we are currently considering, we try to use
used
of them, thenused - 1
,used - 2
and so on. The reason we do it in this order is so that we can arrive at a value forbest
more quickly, which will stop other branches of our exhaustive search from continuing.The Python version of this solution showcases using
collections.Counter
as a way to simplify some code sections, whereas the Java solution sticks to arrays.Python
class Solution(object): def minStickers(self, stickers, target): t_count = collections.Counter(target) A = [collections.Counter(sticker) & t_count for sticker in stickers] for i in range(len(A) - 1, -1, -1): if any(A[i] == A[i] & A[j] for j in range(len(A)) if i != j): A.pop(i) self.best = len(target) + 1 def search(ans = 0): if ans >= self.best: return if not A: if all(t_count[letter] <= 0 for letter in t_count): self.best = ans return sticker = A.pop() used = max((t_count[letter] - 1) // sticker[letter] + 1 for letter in sticker) used = max(used, 0) for c in sticker: t_count[c] -= used * sticker[c] search(ans + used) for i in range(used - 1, -1, -1): for letter in sticker: t_count[letter] += sticker[letter] search(ans + i) A.append(sticker) search() return self.best if self.best <= len(target) else -1
Java
class Solution { int best; int[][] stickersCount; int[] targetCount; public void search(int ans, int row) { if (ans >= best) return; if (row == stickersCount.length) { for (int c: targetCount) if (c > 0) return; best = ans; return; } int used = 0; for (int i = 0; i < stickersCount[row].length; i++) { if (targetCount[i] > 0 && stickersCount[row][i] > 0) { used = Math.max(used, (targetCount[i] - 1) / stickersCount[row][i] + 1); } } for (int i = 0; i < stickersCount[row].length; i++) { targetCount[i] -= used * stickersCount[row][i]; } search(ans + used, row + 1); while (used > 0) { for (int i = 0; i < stickersCount[row].length; i++) { targetCount[i] += stickersCount[row][i]; } used--; search(ans + used, row + 1); } } public int minStickers(String[] stickers, String target) { int[] targetNaiveCount = new int[26]; for (char c: target.toCharArray()) targetNaiveCount[c - 'a']++; int[] index = new int[26]; int t = 0; for (int i = 0; i < 26; i++) { if (targetNaiveCount[i] > 0) { index[i] = t++; } else { index[i] = -1; } } targetCount = new int[t]; t = 0; for (int c: targetNaiveCount) if (c > 0) { targetCount[t++] = c; } stickersCount = new int[stickers.length][t]; for (int i = 0; i < stickers.length; i++) { for (char c: stickers[i].toCharArray()) { int j = index[c - 'a']; if (j >= 0) stickersCount[i][j]++; } } int anchor = 0; for (int i = 0; i < stickers.length; i++) { for (int j = anchor; j < stickers.length; j++) if (j != i) { boolean dominated = true; for (int k = 0; k < t; k++) { if (stickersCount[i][k] > stickersCount[j][k]) { dominated = false; break; } } if (dominated) { int[] tmp = stickersCount[i]; stickersCount[i] = stickersCount[anchor]; stickersCount[anchor++] = tmp; break; } } } best = target.length() + 1; search(0, anchor); return best <= target.length() ? best : -1; } }
Complexity Analysis
-
Time Complexity: Let N be the number of stickers, and T be the number of letters in the target word. A bound for time complexity is O(NT+1T2): for each sticker, we'll have to try using it up to T+1 times, and updating our target count costs O(T), which we do up to T times. Alternatively, since the answer is bounded at T, we can prove that we can only search up to (T−1N+T−1) times. This would be O((T−1N+T−1)T2).
-
Space Complexity: O(N+T), to store
stickersCount
,targetCount
, and handle the recursive callstack when callingsearch
.
Approach #2: Dynamic Programming [Accepted]
Intuition
Suppose we need
dp[state]
stickers to satisfy alltarget[i]
's for which thei
-th bit ofstate
is set. We would like to knowdp[(1 << len(target)) - 1]
.Algorithm
For each
state
, let's work with it asnow
and look at what happens to it after applying a sticker. For each letter in the sticker that can satisfy an unset bit ofstate
, we set the bit (now |= 1 << i
). At the end, we knownow
is the result of applying that sticker tostate
, and we update ourdp
appropriately.When using Python, we will need some extra techniques from Approach #1 to pass in time.
Python
class Solution(object): def minStickers(self, stickers, target): t_count = collections.Counter(target) A = [collections.Counter(sticker) & t_count for sticker in stickers] for i in range(len(A) - 1, -1, -1): if any(A[i] == A[i] & A[j] for j in range(len(A)) if i != j): A.pop(i) stickers = ["".join(s_count.elements()) for s_count in A] dp = [-1] * (1 << len(target)) dp[0] = 0 for state in xrange(1 << len(target)): if dp[state] == -1: continue for sticker in stickers: now = state for letter in sticker: for i, c in enumerate(target): if (now >> i) & 1: continue if c == letter: now |= 1 << i break if dp[now] == -1 or dp[now] > dp[state] + 1: dp[now] = dp[state] + 1 return dp[-1]
Java
class Solution { public int minStickers(String[] stickers, String target) { int N = target.length(); int[] dp = new int[1 << N]; for (int i = 1; i < 1 << N; i++) dp[i] = -1; for (int state = 0; state < 1 << N; state++) { if (dp[state] == -1) continue; for (String sticker: stickers) { int now = state; for (char letter: sticker.toCharArray()) { for (int i = 0; i < N; i++) { if (((now >> i) & 1) == 1) continue; if (target.charAt(i) == letter) { now |= 1 << i; break; } } } if (dp[now] == -1 || dp[now] > dp[state] + 1) { dp[now] = dp[state] + 1; } } } return dp[(1 << N) - 1]; } }
Complexity Analysis
-
Time Complexity: O(2T∗S∗T) where S be the total number of letters in all stickers, and T be the number of letters in the target word. We can examine each loop carefully to arrive at this conclusion.
-
Space Complexity: O(2T), the space used by
dp
.
-