目录
时间复杂度——O(2 ^ m * n * (m + k))——155ms
1.题目
2.思路
看见这个的感觉,普通dfs肯定超时(困难题,普通dfs通常不行),那么就会往记忆化搜索思考。记忆化搜索的模版可以分为三步,dfs()的构思是比较核心的。
1.定义记忆数组或者Map
2.初始化记忆数组,通常赋值为-1.
3.构思dfs()函数
2.1定义记忆数组(状态压缩)
那么定义一个记忆数组还是记忆化Map,取决于如何表示还需要多少target.
假如把target表示为一个长度为26的数组,int[26],那么所有的状态空间为2 ^ 26, 后续还会有别的时间,很有可能会超时。(一开始写得这个版本,就是超时了)
改进一下思路,我们发现这个target长度其实最大为15,我们直接用0 和 1表示target的每一个字符是否存在,那么可以将状态空间压缩为2^15,差不多直接少了1000倍时间,已经看起来不错了。
换句话说,假如我们要表示当前target为“abca", 那么我们直接定义一个记忆化数组,长度为2^4,之后dfs的时候,直接让一个数字mask代表这个target哪些字符还需要被抵消。举个例子:
mask = 0, 代表0000,说明现在target 为空,所有字符都已经由贴纸所抵消。边界条件。
mask = 1, 代表0001,也就是最后一个字符还需要被贴纸抵消;
mask = 2, 代表0010, 说明倒数第二个字符还需要被抵消;
// 记忆化数组
int[]memo;
int m = target.length();
memo = new int[(int)Math.pow(2, m)];
可以用位运算移位表示2^m,计算会更快。1左移m位,就是2^m。
memo = new int[1 << m];
2.2初始化记忆数组
Arrays.fill(memo, -1);
2.3构造dfs()函数
1.边界条件
2.if(memo[mask] == -1){
}
3.返回记忆数组存储的值
比较重要的就是if循环里面的代码了,这里通常就是遍历这个可以选择的物品,然后进行贡献的加减。
在这道题就是遍历不同的贴纸,先计算当前贴纸的26个字符的情况,把字符串转化为长度为26的数组。然后再计算当前贴纸对所需要的target的贡献,也就是说这个贴纸能不能抵消target其实的某些字符。如果可以抵消的话,那么就是继续dfs()。完整代码参见下文。
3.代码
class Solution {
// 记忆化数组
int[]memo;
public int minStickers(String[] stickers, String target) {
int m = target.length();
// 定义记忆化数组, 把1左移m位,代表2^m
// memo = new int[1 << m];
memo = new int[(int)Math.pow(2, m)];
// 初始化记忆数组
Arrays.fill(memo, -1);
int ans = dfs(stickers, target, (1 << m) - 1); // mask最初为2^m - 1
return ans <= m ? ans : -1;
}
public int dfs(String[] stickers, String target, int mask){
// 边界条件
if(mask == 0)return 0;
int m = target.length();
if(memo[mask] == -1){
// 初始化一个不可能得答案,因为一个贴纸至少可以满足target的一个单词,所以最多只可能用m个贴纸
int res = m + 1;
for(String sticker : stickers){
int n = sticker.length();
// 把当前贴纸转化为26个字母数组
int[]cnt = new int[26];
for(int i = 0 ; i < n; i++){
cnt[sticker.charAt(i) - 'a']++;
}
// 计算当前贴纸对target的贡献程度:也就是看贴纸可以抵消target几个字符
int nextMask = mask; //最坏的情况就是这个贴纸没有任何用,一个字符也不能抵消,所以mask不变
for(int i = 0 ; i < m ; i++){
char c = target.charAt(i);
// 如果当前mask第i位是1,代表还可以被抵消,此时还需要满足当前贴纸字符串是否有这个字符c
if(((mask >> i) & 1) == 1 && cnt[c - 'a'] >= 1){
cnt[c - 'a']--;
nextMask = nextMask ^ (1 << i); // 把nextMask的第i位变为0,也就是代表抵消了一个字符。这里使用异或符号。
}
}
// 结束for循环后,判断抵消了几个字符,假如一个字符也没有抵消,那么判断下一个贴纸
if(nextMask == mask) continue;
// 代表抵消了1到多个字符
res = Math.min(res, dfs(stickers, target, nextMask) + 1);
}
memo[mask] = res;
}
// 返回记忆数组保存的值
return memo[mask];
}
}
时间复杂度——O(2 ^ m * n * (m + k))——155ms
记忆化搜索的时间复杂度可以分成两部分的乘积。一个是状态空间数,一个数dfs()代码中的时间复杂度。我们分别来分析一下。
状态空间数,也就是mask的所有取值,从0 -- 2^m - 1,也就是我们记忆数组的大小,这里时间复杂度为O(2^m),m 为target字符串的长度,最大为15,也就是O(2 ^ 15)
dfs()代码中时间复杂度,主要为两层循环,时间复杂度为O(n *(m + k)).n 为贴纸数组的长度,最大为50,m 为target字符串的长度,最大为15, k 为单个贴纸的长度,最大为10,也就是最大时间为O(50 * (10 + 15))
最终时间复杂度为两者相乘,也就是O(2 ^ 15) * O(50 * (10 + 15)) = 4 * 10 ^ 7, ,小于 10 ^ 8.如果选择一开始2 ^ 26,那么肯定就超时了。
空间复杂度——O(2 ^ m)
记忆数组的大小
4.结果
5.总结
1.注意时间复杂度,状态压缩值得学习。
2.位运算,左移,右移,异或。