题目

我们有 n 种不同的贴纸。每个贴纸上都有一个小写的英文单词。

您想要拼写出给定的字符串 target ,方法是从收集的贴纸中切割单个字母并重新排列它们。如果你愿意,你可以多次使用每个贴纸,每个贴纸的数量是无限的。

返回你需要拼出 target 的最小贴纸数量。如果任务不可能,则返回 -1 。

注意:在所有的测试用例中,所有的单词都是从 1000 个最常见的美国英语单词中随机选择的,并且 target 被选择为两个随机单词的连接。

leetcode链接

思路一:暴力递归

每张贴纸都分别与target目标字符做一次匹配,看剩余rest字符,不断循环,找出最少值。如下图:

动态规划之贴纸拼词问题_字符串

public static int spellWordOne(String target, String[] strs){
    //边界情况
    if(target == null || target.length() == 0 || strs == null || strs.length == 0){
        return 0;
    }
    //返回结果
    int result = stickerToSpellWordOne(target, strs);
    //如果result == Integer.MAX_VALUE,说明不存在,直接返回-1
    return result == Integer.MAX_VALUE ? -1 : result;
}


public static int stickerToSpellWordOne(String target, String[] strs){
    if(target.length() == 0){
        return 0;
    }
    //用来比较值的大小
    int min = Integer.MAX_VALUE;
    //数组中每个贴纸都作为第一个值处理一次
    for (String str : strs) {
        //与目标字符串对比,还剩下的目标字符串为rest
        String rest = deal(target, str);
        //如果长度相等 说明没变化,不要这个str了,要了也只会增大贴纸的数量
        if(rest.length() != target.length()){
            min = Math.min(min, stickerToSpellWordOne(rest, strs));
        }
    }
    //因为第一个字符没算 所以 + 1,有可能strs里面根本就没有符合的卡片
    return min + (min == Integer.MAX_VALUE ? 0 : 1);
}

public static String deal(String target, String str){
    //字符串拆为数组
    char[] targets = target.toCharArray();
    char[] strs = str.toCharArray();
    //新建一个统计数组,统计target 和 str比较后的剩余字符
    int[] count = new int[26];
    //统计targets
    for (int i = 0; i < targets.length; i++) {
        int value = targets[i] - 'a';
        count[value] ++;
    }
    //统计strs
    for (int i = 0; i < strs.length; i++) {
        int value = strs[i] - 'a';
        count[value] --;
    }

    //拼接字符串返回
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 26; i++) {
        // > 0 说明才有剩余的
        if(count[i] > 0){
            for (int j = 0; j < count[i]; j++) {
                sb.append((char)(i + 'a'));
            }
        }
    }
    return sb.toString();
}

思路二:使用词频统计表

这一步最主要的是贪心,只有字符串数组中的子字符包含目标字符(剩余目标字符)的第一个字符,才进行一次去除。

//方法二:词频统计
public static int spellWordTwo(String target, String[] strs){
    //边界情况
    if(target == null || target.length() == 0 || strs == null || strs.length == 0){
        return 0;
    }
    int N = strs.length;
    //用词频表代替贴纸数组,用counts记录字符串数组中每个子数组中字母出现的次数
    int[][] counts = new int[N][26];
    for (int i = 0; i < N; i++) {
        char[] chars = strs[i].toCharArray();
        for (int j = 0; j < chars.length; j++) {
            counts[i][chars[j] - 'a'] ++;
        }
    }
    //返回结果
    int result = stickerToSpellWordTwo(target, counts);
    //如果result == Integer.MAX_VALUE,说明不存在,直接返回-1
    return result == Integer.MAX_VALUE ? -1 : result;
}


public static int stickerToSpellWordTwo(String target, int[][] counts){
    if(target.length() == 0){
        return 0;
    }
    //用来比较值的大小
    int min = Integer.MAX_VALUE;
    //先统计剩余target字符串中字符个数
    char[] chars = target.toCharArray();
    int[] tcounts = new int[26];
    for (int i = 0; i < target.length(); i++) {
        tcounts[chars[i] - 'a'] ++;
    }
    //字符数组长度N
    int N = counts.length;
    for (int i = 0; i < N; i++) {
        //取出一张贴纸
        int[] count = counts[i];
        //当目标字符串中的第一个字符在 count中才进行,
        // 相当于如果字符数组中的子字符串包含目标数组的第一个字符才继续往下走,因为总有子字符包含,或者都不包含
        if(count[chars[0] - 'a'] > 0){
            StringBuilder builder = new StringBuilder();
            for (int j = 0; j < 26; j++) {
                //目标数组有值
                if(tcounts[j] > 0){
                    //这一步主要是消去目标数组中的字符
                    int num = tcounts[j] - count[j];
                    //如果为负数或0,说明已经没有了,或者目标数组本来就不含这个字符
                    for (int k = 0; k < num; k++) {
                        builder.append((char)(j + 'a'));
                    }
                }
            }
            //现在还剩余的目标数组
            String rest = builder.toString();
            min = Math.min(min, stickerToSpellWordTwo(rest, counts));
        }
    }

    //因为第一个字符没算 所以 + 1,有可能strs里面根本就没有符合的卡片
    return min + (min == Integer.MAX_VALUE ? 0 : 1);
}

方法三:傻缓存法

增加一个HashMap用来存储已经计算过的值

//方法三:傻缓存
public static int spellWordThree(String target, String[] strs){
    //边界情况
    if(target == null || target.length() == 0 || strs == null || strs.length == 0){
        return 0;
    }
    int N = strs.length;
    //用词频表代替贴纸数组,用counts记录字符串数组中每个子数组中字母出现的次数
    int[][] counts = new int[N][26];
    for (int i = 0; i < N; i++) {
        char[] chars = strs[i].toCharArray();
        for (int j = 0; j < chars.length; j++) {
            counts[i][chars[j] - 'a'] ++;
        }
    }
    //用map集合做缓存
    HashMap<String, Integer> dp = new HashMap<>();
    //如果是""字符,就返回0
    dp.put("", 0);
    int result = stickerToSpellWordThree(target, counts, dp);

    //如果result == Integer.MAX_VALUE,说明不存在,直接返回-1
    return result == Integer.MAX_VALUE ? -1 : result;
}


public static int stickerToSpellWordThree(String target, int[][] counts, HashMap<String, Integer> dp){
    if(dp.containsKey(target)){ //""字符会返回0
        return dp.get(target);
    }
    //用来比较值的大小
    int min = Integer.MAX_VALUE;
    //先统计剩余target字符串中字符个数
    char[] chars = target.toCharArray();
    int[] tcounts = new int[26];
    for (int i = 0; i < target.length(); i++) {
        tcounts[chars[i] - 'a'] ++;
    }
    //字符数组长度N
    int N = counts.length;
    for (int i = 0; i < N; i++) {
        //取出一张贴纸
        int[] count = counts[i];
        //当目标字符串中的第一个字符在 count中才进行,
        // 相当于如果字符数组中的子字符串包含目标数组的第一个字符才继续往下走,因为总有子字符包含,或者都不包含
        if(count[chars[0] - 'a'] > 0){
            StringBuilder builder = new StringBuilder();
            for (int j = 0; j < 26; j++) {
                //目标数组有值
                if(tcounts[j] > 0){
                    //这一步主要是消去目标数组中的字符
                    int num = tcounts[j] - count[j];
                    //如果为负数或0,说明已经没有了,或者目标数组本来就不含这个字符
                    for (int k = 0; k < num; k++) {
                        builder.append((char)(j + 'a'));
                    }
                }
            }
            //现在还剩余的目标数组
            String rest = builder.toString();
            min = Math.min(min, stickerToSpellWordThree(rest, counts, dp));
        }
    }

    //因为第一个字符没算 所以 + 1,有可能strs里面根本就没有符合的卡片
    int result = min + (min == Integer.MAX_VALUE ? 0 : 1);
    //做缓存
    dp.put(target, result);
    return result;
}

总结测试

public class StickerToSpellWords {

    //方法一:暴力递归
    public static int spellWordOne(String target, String[] strs){
        //边界情况
        if(target == null || target.length() == 0 || strs == null || strs.length == 0){
            return 0;
        }
        //返回结果
        int result = stickerToSpellWordOne(target, strs);
        //如果result == Integer.MAX_VALUE,说明不存在,直接返回-1
        return result == Integer.MAX_VALUE ? -1 : result;
    }


    public static int stickerToSpellWordOne(String target, String[] strs){
        if(target.length() == 0){
            return 0;
        }
        //用来比较值的大小
        int min = Integer.MAX_VALUE;
        //数组中每个贴纸都作为第一个值处理一次
        for (String str : strs) {
            //与目标字符串对比,还剩下的目标字符串为rest
            String rest = deal(target, str);
            //如果长度相等 说明没变化,不要这个str了,要了也只会增大贴纸的数量
            if(rest.length() != target.length()){
                min = Math.min(min, stickerToSpellWordOne(rest, strs));
            }
        }
        //因为第一个字符没算 所以 + 1,有可能strs里面根本就没有符合的卡片
        return min + (min == Integer.MAX_VALUE ? 0 : 1);
    }

    public static String deal(String target, String str){
        //字符串拆为数组
        char[] targets = target.toCharArray();
        char[] strs = str.toCharArray();
        //新建一个统计数组,统计target 和 str比较后的剩余字符
        int[] count = new int[26];
        //统计targets
        for (int i = 0; i < targets.length; i++) {
            int value = targets[i] - 'a';
            count[value] ++;
        }
        //统计strs
        for (int i = 0; i < strs.length; i++) {
            int value = strs[i] - 'a';
            count[value] --;
        }

        //拼接字符串返回
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 26; i++) {
            // > 0 说明才有剩余的
            if(count[i] > 0){
                for (int j = 0; j < count[i]; j++) {
                    sb.append((char)(i + 'a'));
                }
            }
        }
        return sb.toString();
    }


    //方法二:词频统计
    public static int spellWordTwo(String target, String[] strs){
        //边界情况
        if(target == null || target.length() == 0 || strs == null || strs.length == 0){
            return 0;
        }
        int N = strs.length;
        //用词频表代替贴纸数组,用counts记录字符串数组中每个子数组中字母出现的次数
        int[][] counts = new int[N][26];
        for (int i = 0; i < N; i++) {
            char[] chars = strs[i].toCharArray();
            for (int j = 0; j < chars.length; j++) {
                counts[i][chars[j] - 'a'] ++;
            }
        }
        //返回结果
        int result = stickerToSpellWordTwo(target, counts);
        //如果result == Integer.MAX_VALUE,说明不存在,直接返回-1
        return result == Integer.MAX_VALUE ? -1 : result;
    }


    public static int stickerToSpellWordTwo(String target, int[][] counts){
        if(target.length() == 0){
            return 0;
        }
        //用来比较值的大小
        int min = Integer.MAX_VALUE;
        //先统计剩余target字符串中字符个数
        char[] chars = target.toCharArray();
        int[] tcounts = new int[26];
        for (int i = 0; i < target.length(); i++) {
            tcounts[chars[i] - 'a'] ++;
        }
        //字符数组长度N
        int N = counts.length;
        for (int i = 0; i < N; i++) {
            //取出一张贴纸
            int[] count = counts[i];
            //当目标字符串中的第一个字符在 count中才进行,
            // 相当于如果字符数组中的子字符串包含目标数组的第一个字符才继续往下走,因为总有子字符包含,或者都不包含
            if(count[chars[0] - 'a'] > 0){
                StringBuilder builder = new StringBuilder();
                for (int j = 0; j < 26; j++) {
                    //目标数组有值
                    if(tcounts[j] > 0){
                        //这一步主要是消去目标数组中的字符
                        int num = tcounts[j] - count[j];
                        //如果为负数或0,说明已经没有了,或者目标数组本来就不含这个字符
                        for (int k = 0; k < num; k++) {
                            builder.append((char)(j + 'a'));
                        }
                    }
                }
                //现在还剩余的目标数组
                String rest = builder.toString();
                min = Math.min(min, stickerToSpellWordTwo(rest, counts));
            }
        }

        //因为第一个字符没算 所以 + 1,有可能strs里面根本就没有符合的卡片
        return min + (min == Integer.MAX_VALUE ? 0 : 1);
    }


    //方法三:傻缓存
    public static int spellWordThree(String target, String[] strs){
        //边界情况
        if(target == null || target.length() == 0 || strs == null || strs.length == 0){
            return 0;
        }
        int N = strs.length;
        //用词频表代替贴纸数组,用counts记录字符串数组中每个子数组中字母出现的次数
        int[][] counts = new int[N][26];
        for (int i = 0; i < N; i++) {
            char[] chars = strs[i].toCharArray();
            for (int j = 0; j < chars.length; j++) {
                counts[i][chars[j] - 'a'] ++;
            }
        }
        //用map集合做缓存
        HashMap<String, Integer> dp = new HashMap<>();
        //如果是""字符,就返回0
        dp.put("", 0);
        int result = stickerToSpellWordThree(target, counts, dp);

        //如果result == Integer.MAX_VALUE,说明不存在,直接返回-1
        return result == Integer.MAX_VALUE ? -1 : result;
    }


    public static int stickerToSpellWordThree(String target, int[][] counts, HashMap<String, Integer> dp){
        if(dp.containsKey(target)){ //""字符会返回0
            return dp.get(target);
        }
        //用来比较值的大小
        int min = Integer.MAX_VALUE;
        //先统计剩余target字符串中字符个数
        char[] chars = target.toCharArray();
        int[] tcounts = new int[26];
        for (int i = 0; i < target.length(); i++) {
            tcounts[chars[i] - 'a'] ++;
        }
        //字符数组长度N
        int N = counts.length;
        for (int i = 0; i < N; i++) {
            //取出一张贴纸
            int[] count = counts[i];
            //当目标字符串中的第一个字符在 count中才进行,
            // 相当于如果字符数组中的子字符串包含目标数组的第一个字符才继续往下走,因为总有子字符包含,或者都不包含
            if(count[chars[0] - 'a'] > 0){
                StringBuilder builder = new StringBuilder();
                for (int j = 0; j < 26; j++) {
                    //目标数组有值
                    if(tcounts[j] > 0){
                        //这一步主要是消去目标数组中的字符
                        int num = tcounts[j] - count[j];
                        //如果为负数或0,说明已经没有了,或者目标数组本来就不含这个字符
                        for (int k = 0; k < num; k++) {
                            builder.append((char)(j + 'a'));
                        }
                    }
                }
                //现在还剩余的目标数组
                String rest = builder.toString();
                min = Math.min(min, stickerToSpellWordThree(rest, counts, dp));
            }
        }

        //因为第一个字符没算 所以 + 1,有可能strs里面根本就没有符合的卡片
        int result = min + (min == Integer.MAX_VALUE ? 0 : 1);
        //做缓存
        dp.put(target, result);
        return result;
    }


    public static String[] genetateStringArray(int maxSize){
        if(maxSize == 0){
            return null;
        }
        String[] strs = new String[maxSize];
        for (int i = 0; i < maxSize; i++) {
            //字符串长短
            int maxValue = (int)(21 * Math.random());
            StringBuilder builder = new StringBuilder();
            for (int j = 0; j < maxValue; j++) {
               char sign = (char)(26*Math.random() + 'a');
               builder.append(sign);
            }
            strs[i] = builder.toString();
        }

        return strs;

    }


    public static void main(String[] args) {

        System.out.println("测试开始:");
        for (int j = 0; j < 10000; j++) {
            //字符数组长度
            int maxSize = (int)(101 * Math.random());
            String[] strs = genetateStringArray(maxSize);
            int targetSize = (int)(21*Math.random());
            StringBuilder builder = new StringBuilder();
            for (int i = 0; i < targetSize; i++) {
                char sign = (char)(26*Math.random() + 'a');
                builder.append(sign);
            }
            String target = builder.toString();
            int resultOne = spellWordOne(target, strs);
            int resultTwo = spellWordTwo(target, strs);
            int resultTree = spellWordThree(target, strs);
            if(resultOne != resultTwo || resultOne != resultTree || resultTwo != resultTree){
                System.out.println("不好意思,这次真不行!!!");
                break;
            }
        }
        System.out.println("测试结束");
    }
}