691. 贴纸拼词——记忆化搜索 + 状态压缩Java

目录

1.题目

2.思路

2.1定义记忆数组(状态压缩)

2.2初始化记忆数组

2.3构造dfs()函数

3.代码

时间复杂度——O(2 ^ m * n * (m + k))——155ms

空间复杂度——O(2 ^ m)

4.结果

5.总结

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.位运算,左移,右移,异或。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值