题目描述
我们给出了 N
种不同类型的贴纸。每个贴纸上都有一个小写的英文单词。
你希望从自己的贴纸集合中裁剪单个字母并重新排列它们,从而拼写出给定的目标字符串 target
。
如果你愿意的话,你可以不止一次地使用每一张贴纸,而且每一张贴纸的数量都是无限的。
拼出目标 target
所需的最小贴纸数量是多少?如果任务不可能,则返回 -1
。
样例
输入:["with", "example", "science"], "thehat"
输出:3
解释:
我们可以使用 2 个 "with" 贴纸,和 1 个 "example" 贴纸。
把贴纸上的字母剪下来并重新排列后,就可以形成目标 "thehat" 了。
此外,这是形成目标字符串所需的最小贴纸数量。
输入:["notice", "possible"], "basicbasic"
输出:-1
解释:
我们不能通过剪切给定贴纸的字母来形成目标 "basicbasic"。
限制
stickers
长度范围是[1, 50]
。stickers
由小写英文单词组成(不带撇号)。target
的长度在[1, 15]
范围内,由小写字母组成。- 在所有的测试案例中,所有的单词都是从 1000 个最常见的美国英语单词中随机选取的,目标是两个随机单词的串联。
- 时间限制可能比平时更具挑战性。预计 50 个贴纸的测试案例平均可在 35ms 内解决。
算法
(状态压缩动态规划) $O(2^n \cdot nm)$
- 设状态 $f(s)$ 表示拼成了状态为 $s$ 的字符串时所花费的最少的贴纸数量。其中 $s$ 为
[0, 2^n - 1]
的掩码。 - 初始时,$f(0) = 0$,其余待定。
- 转移时,枚举每一个贴纸,将其应用到当前状态上,变成下一个状态 $t$,转移 $f(t) = f(s) + 1$。这里需要维护两个辅助数组,一个是每个
target
中的字符对应的下标位置,另一个是每个贴纸中每个字符出现的次数。 - 最终答案为 $f(2^n - 1)$。
时间复杂度
空间复杂度
C++ 代码
#define N 15
#define M 50
class Solution {
private:
int f[1 << N], seen[M][26];
public:
int minStickers(vector<string>& stickers, string target) {
const int n = target.size();
vector<vector<int>> pos(26);
for (int i = 0; i < n; i++)
pos[target[i] - 'a'].push_back(i);
vector<int> used;
for (int c = 0; c < 26; c++)
if (!pos[c].empty())
used.push_back(c);
const int m = stickers.size();
const int INF = 1000000000;
for (int i = 0; i < m; i++) {
memset(seen[i], 0, sizeof(seen[i]));
for (char c : stickers[i])
seen[i][c - 'a']++;
}
for (int s = 1; s < (1 << n); s++)
f[s] = INF;
f[0] = 0;
for (int s = 0; s < (1 << n) - 1; s++) {
if (f[s] == INF) continue;
for (int i = 0; i < m; i++) {
int t = s;
for (char c : used)
for (int j = 0, k = 0;
j < pos[c].size() && k < seen[i][c];
j++, k++) {
if ((s >> pos[c][j]) & 1) k--;
else t |= 1 << pos[c][j];
}
f[t] = min(f[t], f[s] + 1);
}
}
if (f[(1 << n) - 1] == INF)
return -1;
return f[(1 << n) - 1];
}
};