Violent recursion to dynamic programming (5)

题目
LeetCode Original Question
We have n different kinds of stickers. Each sticker has a lowercase English word on it.

You want to spell out the given string target by cutting individual letters from the collected stickers and rearranging them. You can use each sticker as many times as you want, the number of each sticker is unlimited.

Returns the minimum number of stickers you need to spell target. Returns -1 if the task is impossible.

For example: character target = "aaabc", stickers stickers = {"aad", "sk", "eq", "bc"}, the minimum number of stickers used to spell out target is 3, and stickers are required" aad" 2 cards are used to eliminate aaa, and one bc is used to eliminate the remaining string "bc".

Violent recursion
is still the first method of violent recursion to solve, and then turn to dynamic programming based on violent recursion. The overall idea is this:

  1. Iterate over all stickers, remove the duplicated parts of stickers and target strings, and return the rest of the target string rest.
  2. Compare the returned rest string with the target. If the lengths are not the same, it means that the selected sticker string and the target have duplicate parts. Continue to pass the rest down until the length of the target string is 0.

Code
This code is logically correct, but if you run it in LeetCode, you will find that the time limit is exceeded. Next, we will optimize it first by means of "pruning".

  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();
    }

Pruning optimization
Pruning optimization is a common optimization method in violent recursion.
One of the most important points is to reduce the call of recursive code through some conditional judgments.
For example, in violent recursion, all stickers are traversed every time, which is very inefficient. Pruning optimization is to reduce the traversal of useless stickers. The second optimization is to convert stickers[] into int[][], and convert target Int[], if it satisfies the logical judgment, subtract the number directly, and the efficiency is much faster than the method of traversing after converting to char[] every time with violent recursion.

optimized code

 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);
    }

The principle of pruning optimization based on the code is actually not difficult to understand. If the stickers contain the characters in the target, then in the process of eliminating the target string, the first character in the target will eventually be eliminated in one step. 0, then we will advance the process of eliminating the first string as 0, and it will not affect the specific results.
Of course, the worst result may also be encountered in this process, but the worst case is better than the traversal in violent recursion.

Dynamic programming
This dynamic programming conversion is not the same as the previous conversion. The overall process of the previous conversion is violent programming -> stupid cache -> dynamic programming. However, because its variable parameter is the target string, and depending on the sticker, the possibility of returning the rest string each time is too much, and there is no boundary, even if it is forced to do caching, the cache table may be too large And it is very complicated, so Map is directly added to the code as a cache table.

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;
    }

Guess you like

Origin blog.csdn.net/weixin_43936962/article/details/132624577