每天一道动态规划之第四天

一、与字符串相关的动态规划

1.1题目

给定一个字符串str,给定一个字符串类型的数组arr,出现的字符都是小写英文arr每一个字符串,代表一张贴纸,你可以把单个字符剪开使用,目的是拼出str来返回需要至少多少张贴纸可以完成这个任务例子: str= “babac,arr = [ba”,“c”“abcd”]把每一个字符单独剪开,含至少需要两张贴纸"ba"和"abcd”,因为使用这两张贴纸,有2个a、2个b、1个c
在这里插入图片描述
先来看一下这个题目吧!
其实用自然智慧来解这道题时,我的脑子直接给出了比较简单的答案。
最少肯定是一张贴纸,但一张贴纸无论是ba c 还是abcd都满足不了;
然后试一试两张呢?ba+ba=baba还差一个c就可以了。
那1张不行,2 张可以,那最少就是两张了。

这种方法可以解决这个最简单的问题,同时也从这个方法中找到一个思路。
没有解决思路的时候,暴力递归/暴力枚举是最常使用的方法。

我用第一张贴纸字符串str2去拟合字符串str中的数据,得到了一个字符串str=str-str1。(这里的减法就是str1分别去掉str2中的字符。)

如果str不为空,还要继续用贴纸去拟合

然后用第二张贴纸字符串str去拟合str,得到了一个字符串str=str-str1。(这里的减法就是str1分别去掉str2中的字符。)

如果str不为空,还要继续用贴纸去拟合…

这不就是有重复调用的函数,那不就是递归了吗?而递归结束的条件就是字符串为空str=“”;

所以面对一个str,我们现在有一个贴纸数组arr,我们会有的第一步就是用一张贴纸去拟合str,但这张贴纸可以是arr数组中的每一张贴纸。

在这里插入图片描述
上图只做了最左边的分支的示例,每个结点都应该继续往下,这很明显就是一颗树状图哈哈。

从最后一层向上看,当str=“c”时,如果用“ba”去拟合,那你无论怎么努力都拟合不了,那就是无效的。如果我们把每一方框都看成一个节点,每个节点都有一个属性叫最小贴纸数min_num。

那么“”的min_num为0—向上一层经过一个边,那就用了一张贴纸,所以要加一。那么上一层的min_num=0+1;

则普遍的公式就是min_num(上层)=min_num(下层)+1;

1.2代码

有上面的思路走下来肯定是一个暴力递归,但是没关系,任何东西都是又暴力开始(和平大概也是,但希望不是~~~~(>_<)~~~~),那我们就来尝试写一下代码吧!

public class solu01 {

    static String str = "baback";
    static String[] sticker = {"ba", "ck", "abcd"};

    public static void main(String[] args) {

        ways1();

    }


    public static void ways1() {
        int min_num = process1(str, sticker);
        min_num=min_num==Integer.MAX_VALUE?0:min_num;
        System.out.println(min_num);

    }


    public static int process1(String target, String[] sticker) {

        if (target.length()==0) {
            return 0;
        }

        int min_num = Integer.MAX_VALUE;
        for (String s : sticker) {
            String str = minus(target, s);
            int min_num_next = Integer.MAX_VALUE;
            ;
            if (!str.equals(target)) {
                min_num_next = process1(str, sticker);
            }
            if (min_num_next != Integer.MAX_VALUE) {
                min_num = Math.min(min_num, min_num_next + 1);
            }

        }
        return min_num;

    }

    public static String minus(String str1, String str2) {

        //可以看到这种贴纸类型的,可以将字符串剪开,其实无所谓顺序如何的,因此可以直接按照abc...这样的顺序来拍一下序
        //有一种很巧妙的办法就是
        int[] count = new int[26];//表示26个字母每一个出现的次数;
        char[] chars1 = str1.toCharArray();//target
        char[] chars2 = str2.toCharArray();

        for (char c : chars1) {
            count[c - 'a']++;
        }
        for (char c : chars2) {
            count[c - 'a']--;
        }


        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < count.length; i++) {
            if (count[i] != 0) {
                for (int j = count[i]; j > 0; j--) {
                    sb.append((char) (i + 'a'));
                }
            }

        }
        return sb.toString();
    }
}

1.3 代码优化

暴力递归之后该是什么呢?一般是用缓存去优化,但这里是一个与之前构建dp表不同的一个优化,他优化的点有两个一个是词频表的建立,一个是剪枝。

1)剪枝

先来说剪枝:
记不记得我们用一张贴纸先去拟合str,但我们这张贴纸就是按放在arr中的顺序都试一遍,没有什么策略。要说策略就是:按顺序都试一遍(很明显这是一句废话。)

但是有些你一眼看过去可能就觉得没必要啊。但这个一眼看过去转换成程序可以理解的描述,就是使用某种策略,直接排除某些选择,但又不影响结果。

比如说str=“babac”,那么我们就可以先去用含有‘b’的贴纸去拟合。

你说没‘b’的贴纸,可能有‘a’啊。

那我用‘b’拟合了,剩下的开头不就是‘a’吗,那还轮不到有‘b’没‘a’的贴纸吗?其实是不影响最后结果的。那么就没有必要把不含‘b’的贴纸放到我们所谓的*“第一张”*。

2)词频表

再来看看词频表,先说词频数组,词频数组是一个长度为26的数组,对于一个单词,统计每个字符输出的长度。比如“abc”的词频表如下图所示。
在这里插入图片描述
那如果我们建立贴纸数组的词频表数组{“ba”,“c”,“abcd”},其词频表:就是
在这里插入图片描述
在第一部分的代码里面,我们每次都要建一次贴纸的词频表和target的词频数组,那么干脆就直接建好了开始用就行。

所以用“剪枝”和“词频表”优化之后的代码为:

public class solu02 {
    static String str = "bababaq";
    static String[] sticker = {"ba", "ck", "abcd"};

    public static void main(String[] args) {


        ways2();

    }



    public static void ways2() {

        //建立词频表
        int[][] sticker_count=new int[sticker.length][26];
        for (int i=0;i<sticker.length;i++) {
            char[] chars = sticker[i].toCharArray();
            for (char aChar : chars) {
                sticker_count[i][aChar-'a']++;

            }
        }

        int min_num = process2(str, sticker_count);
        min_num=min_num== Integer.MAX_VALUE?0:1;
        System.out.println(min_num);

    }

    public static int process2(String s, int[][] sticker_count) {


        if (s.length() == 0) {
            return 0;
        }
        //0.设置要返回的变量
        char[] target=s.toCharArray();
        int min_num = Integer.MAX_VALUE;


        //1.建立target词频数组
        int[] target_count = new int[26];
        for (char c : target) {
            target_count[c - 'a']++;
        }
        //1.遍历贴纸词频表,找到所有分支种的最小值min_num
        int N=sticker_count.length;
        for (int i = 0; i < N; i++) {
            int[] sticker = sticker_count[i];
            //1.1如果这个贴纸包含target的第一个字符,我们可以选用这个贴纸作为第一张。
            if (sticker[target[0] - 'a'] > 0) {
                StringBuilder sb = new StringBuilder();
                //1.2把目标字符串中中的贴纸减掉
                for (int j = 0; j < 26; j++) {
                    if (target_count[j]>0) {
                        int num = target_count[j] - sticker[j];
                        for(int p=0;p<num;p++){
                            sb.append((char)(j+'a'));
                        }

                    }
                }
                //1.3恢复target
                String rest = sb.toString();
                min_num = Math.min(min_num, process2(rest, sticker_count));
            }

        }

        //2.如果返回的是有效值,那么返回的是min_num+1;
        return min_num+(min_num==Integer.MAX_VALUE?0:1);


    }
}

1.4 优化进阶

这里我们可能要想到用缓存法来进行优化,但之前学习的缓存大多都是数组。这个里面一直变换的是谁?是target的字符串,这个字符串的状态如果都列出来占用空间会非常大,因此需要可以说没有严格的位置依赖。没有严格位置依赖但是依赖状态的,或者说状态无法穷举的,缓存就用hashmap来进行。

public class solu03 {
    static String str = "bababakq";
    static String[] sticker = {"ba", "ck", "abcd"};

    public static void main(String[] args) {


        ways3();

    }

    public static void ways3() {

        //
        HashMap<String,Integer> dp=new HashMap<>();

        //建立词频表
        int[][] sticker_count=new int[sticker.length][26];
        for (int i=0;i<sticker.length;i++) {
            char[] chars = sticker[i].toCharArray();
            for (char aChar : chars) {
                sticker_count[i][aChar-'a']++;

            }
        }

        int min_num = process3(str, sticker_count,dp);
        min_num=min_num== Integer.MAX_VALUE?0:min_num;
        System.out.println(min_num);



    }

    public static int process3(String s, int[][] sticker_count, HashMap<String , Integer> dp) {


        if(dp.containsKey(s)){
            return dp.get(s);
        }
        if (s.length() == 0) {
            return 0;
        }
        //0.设置要返回的变量
        char[] target=s.toCharArray();
        int min_num = Integer.MAX_VALUE;


        //1.建立target词频数组
        int[] target_count = new int[26];
        for (char c : target) {
            target_count[c - 'a']++;
        }
        //1.遍历贴纸词频表,找到所有分支种的最小值min_num
        int N=sticker_count.length;
        for (int i = 0; i < N; i++) {
            int[] sticker = sticker_count[i];
            //1.1如果这个贴纸包含target的第一个字符,我们可以选用这个贴纸作为第一张。
            if (sticker[target[0] - 'a'] > 0) {
                StringBuilder sb = new StringBuilder();
                //1.2把目标字符串中中的贴纸减掉
                for (int j = 0; j < 26; j++) {
                    if (target_count[j]>0) {
                        int num = target_count[j] - sticker[j];
                        for(int p=0;p<num;p++){
                            sb.append((char)(j+'a'));
                        }

                    }
                }
                //1.3恢复target
                String rest = sb.toString();
                min_num = Math.min(min_num, process3(rest, sticker_count,dp));
            }

        }


              //2.如果返回的是有效值,那么返回的是min_num+1;
        min_num=min_num+(min_num==Integer.MAX_VALUE?0:1);
        dp.put(s,min_num);

        return min_num;


    }


}


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值