数组arr中的字符串是贴纸,每种贴纸任选无数张,想要将target串拼出来,至少需要多少张贴纸

数组arr中的字符串是贴纸,每种贴纸任选无数张,想要将target串拼出来,至少需要多少张贴纸?

提示:逻辑上比较明了,但是实现上很难的动态规划题目

这个题,特别像一道动态规划的题目,简直就是一模一样
(1)数组arr里面是面币值,每种面币任选无数张,组成target的办法有多少种
这个题目,是非常非常经典的从左往右的尝试模型
本题读来跟(1)一模一样啊!!!但是上面求的组合方法数,本题要的不是方法数,而是最少的使用贴纸的张数。
但是因为本题arr中放的贴纸,贴纸还能剪开,所以就比较繁琐了!
逻辑上实现很容易,但是代码实现,需要理解哈希表用来记录词频的技巧——这个技巧,之前无数次用了


题目

数组arr中的字符串是贴纸,每种贴纸任选无数张,想要将target串拼出来,至少需要多少张贴纸?
你可以剪开arr[i]串,拿去拼target。


一、审题

示例:
arr= ba c abcd
target=babac
你可以用2张ba,1张c,共3张完成任务
你可以用2张abcd,这样也能拼target,共2张完成任务
最少张数为2


关键在暴力递归函数的定义

其实,你也看见了,示例中,完全不需要关心字符串的顺序,只需要知道target,需要多少字符来拼就行
arr[i]也是可以随意切开的,arr[i]可以重复用(这个重复使用,可以通过不断调用同一个递归函数来模拟,每次进递归函数都是用arr所有的串枚举一遍排头)

不妨设**f(arr,rest)**是咱们要的递归函数,含义是:余下rest=target这个串还没有拼完的情况下,用arr去拼,最少能用多少张搞定?
不妨设最开始ans=无穷张

那么宏观调度上,
(1)我们只需要枚举arr的每一个串,做一次排头,用1张这个排头,就需要把target中相应的字符消除,然后拿剩下的restNext串去继续递归cur=f(arr,restNext),看看这种情况下,我使用的最少张数cur是多少?
(2)每次一个arr[i]做排头,得到的结果cur+1(为啥+1,因为我排头这张也用了哦,你cur是restNext的最少张数呢),都需要跟ans对比,把最小值给ans。最后返回ans最小张数。
在这里插入图片描述
这个逻辑自己好好思考一遍!【算法的关键就在捋清逻辑,然后才能下手写代码】


字符串词频的统计处理

f(arr,restNext)这个逻辑也不难吧?

但是图中那个rest-arr[i]=restNext串啊,可不是数字这么简单的
rest-arr[i]=restNext是:rest用arr[i]串消除了arr[i]中的串之后余下没有拼的字符串

咱们在代码中模拟这个过程,需要知道[i]串中的每个字符a–z有多少个?
比如abcda
在这里插入图片描述
而arr整个数组,咱们都要统计,方面用
所以啊,咱直接用哈希表map来替换数组arr,

咋做呢?

哈希表map的
key:放arr[i]串,
value:代表是一个1位26长度的数组,每一位是a–z,0–25索引,是arr[i]串的词频。
这个技巧我们用了多次了哦!
比如:
arr= ba c abcd
value是一个定长26的数组,0–25代表a–z字符
在这里插入图片描述
索引任意字符串中的一个字符:如果arr[i]中的1个字符串用str来表示,要索引str中的其中一个字符时,用str[i] - ‘a’=0–25来索引(因为字符串存的就是ASCII码值)
在这里插入图片描述
同理,target要统计它的词频,也类似,只不过因为target是1个字符串,咱用1个定长26数组来模拟哈希表
比如令tmp = target = 26长度的数组,0–25代表a–z

这个数据处理好了以后,咱们拿1张map(arr)中的贴纸来拼target怎么搞呢?
显然,tmp(target)中的26字符都检查一遍,这仍然是当o(1)的速度,因为26就是常规长度,很短的。
将tmp(target)中每个字符的词频 - 减去 map中所用贴纸的各个对应位的词频,剩下的就是没拼的restNext串

比如:
tmp(target)= 17 7 10【分别代表0 1 2位置的a b c:a还有17个没拼,b还有7个,c还有10个】
实际上target=aaaaaaaaaaaaaaaaabbbbbbbcccccccccc
mapi=10 13 19【分别代表0 1 2位置的a b c:a还有10个可以拿去用,b还有13个,c还有19个】
restNext = rest-arr[i]=tmp(target)- mapi = 17 7 10 - 10 13 19 = 7 0 0 【也就是还有7个a没有拼,还需要去递归找贴纸来拼】
在这里插入图片描述
这里:rest-arr[i]=restNext是:rest用arr[i]串消除了arr[i]中的串之后余下没有拼的字符串

如何?这个用纸拼target的过程明白了吧?
我为啥用哈希表替代arr,就是为了统计词频,因为剪,拼,都不需要关心顺序,只需要看词频!

【当然,咱们真的实现代码时,没法往哈希表中放数组,所以呢,用二维数组表示map】
在这里插入图片描述


将暴力递归与傻缓存dp表同时用,适用于字符串的动态规划

情况是这样的,别的那些不是字符串的数组,可以用来到arr的i位置,还有rest需要拼,咋操作,得到最小的结果ans,更新给dp[i][rest]
比如(1)数组arr里面是面币值,每种面币任选无数张,组成target的办法有多少种
(1)中可以在i位置决定面币用多少张,要去拼rest值,那很方便整一个二维表来存结果到i rest格子

但是咱们这个题,很特殊!!!
为啥呢,由于我们可以重复用同一张贴纸,还能剪切,而且与顺序无关,所以我们只关心贴纸的字符的词频
而且递归函数中,我们不是像纸币那样,枚举本面币用k张,而去计算结果
我们是不枚举的,枚举行为通过不断调用f(arr,rest),f中每次都枚举arri做一次排头来达到多次使用同一个贴纸的目的
——这里一定要想清楚,这里的枚举,不是赤裸裸的枚举张,而是通过递归调用,枚举每个贴纸做排头的过程来实现

还有,我们关注,target在不断被拼接之后,剩余没拼的next串,
next串其实在咱们拼接的过程中,由于词频减少,next串可能有各种千奇百怪的组合,比如aaac,cbadfg等等等等,
到底有多少next串的可能性,我们不清楚
那么,我们想在暴力递归的过程中,一旦某个next串出现了,咱就统计一下,这个next串,它后续拼完的话,需要的最少张数是多少?
用一个哈希表dp来存,这样每次更新最小值,都放入这个dp表,类似于(1)文章中的dp表

dp表的key:字符串target的各种情况,
value:完成拼接target所需最少的张数。

默认最开始dp已经放了“”空串,只需要0张
这样的话,最后我们要dp.get(target)的结果——作为本题的答案!
在这里插入图片描述
这个想法,是非常巧妙的,之前我学的时候也没搞懂为啥这么干,
现在才明白,主要是咱们要枚举next串它的各种可能性,然后保存最小值,这本身也就是
动态规划
的本质。

okay!
有了这么多关于题目的理解,和相应的处理,咱们开始手撕代码!!
这个玩意一定要自己手撕清楚!!!

public static int f(int[][] map,
                        HashMap<String, Integer> dp,
                        String rest){

代码中如何定义呢?
(0)当rest已经在dp中出现了,直接返回dp.get(rest),dp存的就是最小张数,返回【这里dp已经包含了】
那么宏观调度上,
(1)我们只需要枚举arr的每一个串,做一次排头,用1张这个排头,就需要把target中相应的字符消除,然后拿剩下的restNext串去继续递归cur=f(arr,restNext),看看这种情况下,我使用的最少张数cur是多少?
(2)每次一个arr[i]做排头,得到的结果cur+1(为啥+1,因为我排头这张也用了哦,你cur是restNext的最少张数呢),都需要跟ans对比,把最小值给ans。最后返回ans最小张数。

OK:手撕代码:

//不妨设**f(arr,rest)**是咱们要的递归函数,
    // 含义是:余下rest=target这个串还没有拼完的情况下,用arr去拼,最少能用多少张搞定?
    //不妨设最开始ans=无穷张
    //我们想在暴力递归的过程中,一旦某个next串出现了,咱就统计一下,这个next串,它后续拼完的话,需要的最少张数是多少?
    //用一个哈希表dp来存,这样每次更新最小值,都放入这个dp表
    public static int f(int[][] map,
                        HashMap<String, Integer> dp,
                        String rest){
        //当rest已经在dp中出现了,**直接返回dp.get(rest),dp存的就是最小张数**,返回【这里dp已经包含了】
        if (dp.containsKey(rest)) return dp.get(rest);
        //没拼完的话,
        //首先,咱们要准备target为tmp,也转化它的词频
        int[] tmp = new int[26];
        char[] target = rest.toCharArray();
        for(char ch:target) {
            tmp[ch - 'a']++;
        }

        int ans = Integer.MAX_VALUE;//先认为需要无穷张贴纸
        //那么宏观调度上,
        //
        //(1)我们只需要枚举arr的每一个串,做一次排头,用1张这个排头,就需要把target中相应的字符消除,
        // 然后拿剩下的**restNext串**去继续递归**cur=f(arr,restNext)**,看看这种情况下,我使用的最少张数cur是多少?
        for (int i = 0; i < map.length; i++) {//枚举每个贴纸做排头
            //咱要拿arri去拼target,如果压根arri中就不存在target的第一个字符,那算了吧
            if (map[i][target[0] - 'a'] == 0) continue;//枚举别的贴纸吧
            //tmp(target)中的26字符都检查一遍,
            // 将tmp(target)中每个字符的词频 - 减去 map中所用贴纸的各个对应位的词频**,剩下的就是没拼的restNext串
            StringBuffer sb = new StringBuffer();//准备收集没有没拼的字符
            for (int j = 0; j < 26; j++) {//tmp(target)中的26字符都检查一遍,
                if (tmp[j] > 0){//确实target还需要拼
                    //拼调arri中这么多,剩余的就是没有拼的呗
                    int need = Math.max(0, tmp[j] - map[i][j]);//map多了剩下0个还需要拼,
                    // map不够,就还有很多要拼
                    //没有拼的j字符,先储备到next中
                    for (int k = 0; k < need; k++) {
                        sb.append((char)(j + 'a'));//j字符,还原ASCII码去字符// 储备到next中
                    }
                }
            }
            //(2)每次一个arr[i]做排头,得到的结果cur+1
            // (**为啥+1**,因为我排头这张也用了哦,你cur是restNext的最少张数呢),
            // 都需要跟ans对比,把最小值给ans记录在dp中。最后返回ans最小张数。
            String restNext = sb.toString();//储备好的没拼的字符串,继续递归,找最小值
            int cur = f(map, dp, restNext);//去看看后续没有拼好的最少需要多少张
            if (cur != -1) ans = Math.min(ans, cur + 1);//cur有效,再+1算上我rest已经用了arri这张贴纸哦
        }
        //所有帖子都枚举排头了,记录rest这个串,最小结果
        //当然,得保证ans有效才放,否则就放-1,说明这个拼法压根不行,无效拼接方式
        dp.put(rest, ans == Integer.MAX_VALUE ? -1 : ans);

        return dp.get(rest);
    }
    //主函数:
    public static int leastTieZhiNum(String[] arr, String target){
        if (arr == null || arr.length == 0) return -1;
        if (target.compareTo("") == 0 || target == null) return 0;

        int N = arr.length;
        int[][] map = new int[N][26];//转化arr统计词频
        for (int i = 0; i < N; i++) {
            char[] str = arr[i].toCharArray();
            for (int j = 0; j < str.length; j++) {
                //对个串,每个位统计词频+1
                map[i][str[j] - 'a']++;
            }
        }

        //dp用来存拼接rest串所需的最少张数
        HashMap<String, Integer> dp = new HashMap<>();
        dp.put("", 0);//空串的话,直接就0张

        return f(map, dp, target);
    }

这个逻辑要仔细阅读和实现

测试一下:

public static void test(){
        String[] arr = {"bc","c","abcd"};
        String target = "babac";//案例

        System.out.println(leastTieZhiNum(arr, target));
    }

    public static void main(String[] args) {
        test();
    }

完美:

2


总结

提示:重要经验:

1)字符串和普通的数字数组处理方式不一样,一定要理解这里的map和dp表,他们分别的功能
2)直接傻缓存搞定暴力递归,加速算法
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰露可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值