暴力递归转动态规划(五)

题目
LeetCode原题

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

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

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

举个例子:字符target = "aaabc",贴纸stickers = {"aad","sk","eq","bc"},拼出target要用到最小贴纸数量是3,其中需要用到贴纸"aad"2张用来消除aaa,一张bc消除剩余字符串"bc"。

暴力递归

依然是先采用暴力递归的方式来解决,并根据暴力递归来转动态规划。整体思路是这样:

  1. 遍历所有贴纸,并去除贴纸和target字符串中重复部分,并返回剩余的target字符串rest。
  2. 将返回rest字符串和target作比较,如果长度不相同 ,则说明选用的贴纸字符串和target有重复部分,将rest继续向下传递,直到target字符串长度为0。

代码

这个代码逻辑上是对的,不过如果在LeetCode中运行会发现超出时间限制,接下来,我们通过"剪枝"的手段来先进行一次优化。

java 复制代码
  public static int minStickers(String[] stickers, String target) {
        int ans = process(stickers, target);
        return ans == Integer.MAX_VALUE ? -1 : ans;
    }

    //process返回最少贴纸数
    public static int process(String[] stickers, String target) {
        //如果当前target字符串剩余长度为0,说明所有字符已经被贴纸消除,return 0
        if (target.length() == 0) {
            return 0;
        }
        //用 Integer.MAX_VALUE来标记是否有最小贴纸数量,如果没有,则min的值不会变
        int min = Integer.MAX_VALUE;
        //遍历所有贴纸
        for (String first : stickers) {
            //rest是target字符串和贴纸刨去重合部分的剩余字符串
            //注意:此处已经使用了一张贴纸
            String rest = minus(target, first);
            //如果长度不相等,说明这张贴纸有被使用
            if (rest.length() != target.length()) {
                //将剩余字符串继续向下递归。
                min = Math.min(min, process(stickers, rest));
            }
        }
        //因为之前遍历贴纸时,使用了一张,如果此时min != Integer.MIN_VALUE,说明target清零了,有贴纸使用,则要把上面使用的那张贴纸加回来
        return min + (min == Integer.MAX_VALUE ? 0 : 1);
    }

    //去除target和贴纸重合字符部分的字符串 并返回剩余字符串
    public static String minus(String s1, String s2) {
        char[] chars1 = s1.toCharArray();
        char[] chars2 = s2.toCharArray();
        int[] counts = new int[26];

        for (char c1 : chars1) {
            counts[c1 - 'a']++;
        }
        for (char c2 : chars2) {
            counts[c2 - 'a']--;
        }
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 26; i++) {
            if (counts[i] > 0) {
                for (int j = 0; j < counts[i]; j++) {
                    sb.append((char) (i + 'a'));
                }
            }
        }
        return sb.toString();
    }

剪枝优化

剪枝优化是暴力递归中很多常见的一种优化方式。

其中很重要的一点就是通过一些条件判断来减少递归代码的调用。

比如说,暴力递归中,每次都遍历所有的stickers,这样效率很低,剪枝优化就是减少无用贴纸的遍历,第二重优化就是将stickers[]转换成int[][],将target转换成int[],如果满足逻辑判断,直接用数量相减,效率也远比暴力递归每一次的转char[]后遍历的方式快很多。

优化后代码

java 复制代码
 public static int minStickers2(String[] stickers, String target) {

        int N = stickers.length;
        int[][] arrs = new int[N][26];
		//主流程中,先将给定的固定贴纸初始化成int[][]
        for (int i = 0; i < N; i++) {
            char[] chars = stickers[i].toCharArray();
            for (char cha : chars) {
                arrs[i][cha - 'a']++;
            }
        }
        int ans = process2(arrs, target);

        return ans == Integer.MAX_VALUE ? -1 : ans;
    }

    public static int process2(int[][] stickers, String target) {
        if (target.length() == 0) {
            return 0;
        }
        int min = Integer.MAX_VALUE;
        int N = stickers.length;
        int[] tcount = new int[26];
        char[] chars = target.toCharArray();
        //将每一次传进来的target字符串转换成int[],方便后面直接相减
        for (char cha : chars) {
            tcount[cha - 'a']++;
        }

        for (int i = 0; i < N; i++) {
        	//获取每一张贴纸
            int[] sticker = stickers[i];
            //只有贴纸中,包含target中[0]位置字符串,才考虑往下进行
            if (sticker[chars[0] - 'a'] > 0) {
                StringBuilder sb = new StringBuilder();
                for (int j = 0; j < 26; j++) {
                	//如果tcount[j] > 0,说明target中有这个字符
                    if (tcount[j] > 0) {
                    	//取值,直接相减
                        int num = tcount[j] - sticker[j];
                        //将target中剩余字符append拼接
                        for (int m = 0; m < num; m++) {
                            sb.append((char) (j + 'a'));
                        }
                    }
                }
                String rest = sb.toString();
                //继续向下传递
                min = Math.min(min, process2(stickers, rest));
            }
        }
        return min + (min == Integer.MAX_VALUE ? 0 : 1);
    }

根据代码实现的剪枝优化的原理其实也不难理解,如果贴纸中都包含target中的字符,那么在target字符串不断消除的过程中,target中的第一个字符终会有一步会消除为0,那么我们将消除第一个字符串为0的过程提前,也不会影响具体的结果。

当然这个过程中也可能会碰上最坏的结果,但是最坏的情况也是要比暴力递归中的遍历要好的。

动态规划

这个动态规划的转换和之前的不太一样,之前转换的整体流程是 暴力规划 -> 傻缓存 -> 动态规划。但是,因为它的可变参数是target字符串,并且根据贴纸的不同,每一次返回的rest字符串的可能性太多了,并且没有边界,就算是硬要做缓存,可能缓存表也太大并且很复杂,所以代码中直接加Map来当做缓存表。

java 复制代码
public static int minStickers3(String[] stickers, String target) {

        int N = stickers.length;
        int[][] arrs = new int[N][26];

        for (int i = 0; i < N; i++) {
            char[] chars = stickers[i].toCharArray();
            for (char cha : chars) {
                arrs[i][cha - 'a']++;
            }
        }

        HashMap<String,Integer> dp = new HashMap<>();
        int ans = process3(arrs, target,dp);

        return ans == Integer.MAX_VALUE ? -1 : ans;
    }


    public static int process3(int[][] stickers, String target, HashMap<String,Integer> dp) {
        if (dp.containsKey(target)){
            return dp.get(target);
        }
        if (target.length() == 0) {
            return 0;
        }
        int min = Integer.MAX_VALUE;
        int N = stickers.length;
        int[] tcount = new int[26];
        char[] chars = target.toCharArray();
        for (char cha : chars) {
            tcount[cha - 'a']++;
        }

        for (int i = 0; i < N; i++) {
            int[] sticker = stickers[i];
            if (sticker[chars[0] - 'a'] > 0) {
                StringBuilder sb = new StringBuilder();
                for (int j = 0; j < 26; j++) {
                    if (tcount[j] > 0) {
                        int num = tcount[j] - sticker[j];
                        for (int m = 0; m < num; m++) {
                            sb.append((char) (j + 'a'));
                        }
                    }
                }
                String rest = sb.toString();
                min = Math.min(min, process3(stickers, rest,dp));
            }
        }
        int ans = min + (min == Integer.MAX_VALUE ? 0 : 1);
        dp.put(target,ans);
        return ans;
    }
相关推荐
快乐就好ya32 分钟前
Java多线程
java·开发语言
IT学长编程37 分钟前
计算机毕业设计 二手图书交易系统的设计与实现 Java实战项目 附源码+文档+视频讲解
java·spring boot·毕业设计·课程设计·毕业论文·计算机毕业设计选题·二手图书交易系统
CS_GaoMing1 小时前
Centos7 JDK 多版本管理与 Maven 构建问题和注意!
java·开发语言·maven·centos7·java多版本
Indigo_code1 小时前
【数据结构】【顺序表算法】 删除特定值
数据结构·算法
艾伦~耶格尔2 小时前
Spring Boot 三层架构开发模式入门
java·spring boot·后端·架构·三层架构
man20172 小时前
基于spring boot的篮球论坛系统
java·spring boot·后端
阿史大杯茶2 小时前
Codeforces Round 976 (Div. 2 ABCDE题)视频讲解
数据结构·c++·算法
2401_858120532 小时前
Spring Boot框架下的大学生就业招聘平台
java·开发语言
S hh2 小时前
【Linux】进程地址空间
java·linux·运维·服务器·学习
LluckyYH2 小时前
代码随想录Day 58|拓扑排序、dijkstra算法精讲,题目:软件构建、参加科学大会
算法·深度优先·动态规划·软件构建·图论·dfs