动态规划(3)——LeetCode贴纸拼词、最长公共子序列

239 阅读6分钟

一、贴纸拼词

LeetCode691:

leetcode-cn.com/problems/st…

1、题目描述

给定一个字符串str和一个字符串类型的数组arr,出现的字符都是小写英文。arr每一个字符串,代表一张贴纸,你可以把单个字符剪开使用,目的是拼出str来,返回需要至少多少张贴纸可以完成这个任务。(每种贴纸无数张)

例子: 

str= "babac",arr= {"ba","c","abcd"}

至少需要两张贴纸,"ba"和"abcd",因为使用这两张贴纸,把每一个字符单独剪开,含有2个a、2个b、1个c,是可以拼出str的。所以返回2。

2、从尝试开始

不管什么结果,肯定都会选择某一张贴纸作为第一个选择的贴纸,所以,遍历所有给定的贴纸,每个贴纸都作为第一张产生一个结果,在所有结果中求最少张数即是最后的结果。

/**
 * @author Java和算法学习:周一
 */
public static int minStickers1(String[] stickers, String target) {
    int ans = process1(stickers, target);
    return ans == Integer.MAX_VALUE ? -1 : ans;
}

/**
 * @param stickers 所有贴纸stickers,每一种贴纸都有无穷张
 * @param target 目标
 * @return 最少张数
 */
public static int process1(String[] stickers, String target) {
    // 目标已经完成,还需要0张贴纸
    if (target.length() == 0) {
        return 0;
    }
    int min = Integer.MAX_VALUE;
    // 每一张贴纸都可能作为第一个选择的贴纸,在所有情况中返回最小的
    for (String first : stickers) {
        // 从target中减去当前first字符串后剩下的字符串
        String rest = minus(target, first);
        // 排除first为空字符的情况
        if (rest.length() != target.length()) {
            min = Math.min(min, process1(stickers, rest));
        }
    }
    // 所有情况都枚举完后,都无法拼出target字符串,则min=Integer.MAX_VALUE
    // 能够拼出target,则min还要加上first这张贴纸
    return min + (min == Integer.MAX_VALUE ? 0 : 1);
}

3、优化1

根据给定的所有贴纸,统计出每个贴纸各个字符出现的次数,代替原有的贴纸。只有当前贴纸包含目标字符串的首字符才统计,即通过剪枝来优化。

/**
 * @author Java和算法学习:周一
 */
public static int minStickers2(String[] stickers, String target) {
    int N = stickers.length;
    // 用字符出现次数代替贴纸
    // stickersCounts[i]表示i号贴纸各个字符出现的次数,
    // 0位置表示a出现次数……25表示z出现的次数
    int[][] stickersCounts = new int[N][26];
    for (int i = 0; i < N; i++) {
        char[] charArray = stickers[i].toCharArray();
        for (char c : charArray) {
            stickersCounts[i][c - 'a']++;
        }
    }
    int answer = process2(stickersCounts, target);
    return answer == Integer.MAX_VALUE ? -1 : answer;
}

/**
 * @param stickersCounts 所有贴纸各个字符出现次数的统计
 * @param target         目标字符串
 * @return 拼成目标字符串最少贴纸数
 */
private static int process2(int[][] stickersCounts, String target) {
    // 目标已经完成,还需要0张贴纸
    if (target.length() == 0) {
        return 0;
    }
    // 统计target各个字符出现次数
    char[] targetArray = target.toCharArray();
    int[] targetCounts = new int[26];
    for (char c : targetArray) {
        targetCounts[c - 'a']++;
    }
    // 最小张数
    int min = Integer.MAX_VALUE;
    for (int i = 0; i < stickersCounts.length; i++) {
        // 当前贴纸
        int[] sticker = stickersCounts[i];
        // 剪枝
        // 当前贴纸包含目标字符串的首字符才统计
        if (sticker[targetArray[0] - 'a'] > 0) {
            // 目标字符串减去贴纸后剩余的字符串
            StringBuilder rest = new StringBuilder();
            for (int j = 0; j < 26; j++) {
                if (targetCounts[j] > 0) {
                    int num = targetCounts[j] - sticker[j];
                    for (int k = 0; k < num; k++) {
                        rest.append((char) (j + 'a'));
                    }
                }
            }
            min = Math.min(min, process2(stickersCounts, rest.toString()));
        }
    }
    return min + (min == Integer.MAX_VALUE ? 0 : 1);
}

4、优化2

在优化1的基础上采用傻缓存进一步优化。

因为可变参数是字符串,无法按照之前的做成一个表结构,所以只能采用傻缓存来优化。(只有此种方法LeetCode执行才不会超时)

/**
 * 采用傻缓存优化
 * <p>
 * 因为可变参数是字符串,无法按照之前的做成一个表结构。
 * 前面两种执行都会超时,只有此种不会
 *
 * @author Java和算法学习:周一
 */
public static int minStickers3(String[] stickers, String target) {
    int N = stickers.length;
    int[][] counts = new int[N][26];
    for (int i = 0; i < N; i++) {
        char[] str = stickers[i].toCharArray();
        for (char cha : str) {
            counts[i][cha - 'a']++;
        }
    }
    HashMap<String, Integer> dp = new HashMap<>();
    dp.put(""0);
    int ans = process3(counts, target, dp);
    return ans == Integer.MAX_VALUE ? -1 : ans;
}

/**
 * @param stickers 所有贴纸各个字符出现次数的统计
 * @param t        剩余还需要拼接的字符串
 * @param dp       傻缓存表
 */
public static int process3(int[][] stickers, String t, HashMap<String, Integer> dp) {
    if (dp.containsKey(t)) {
        return dp.get(t);
    }
    char[] target = t.toCharArray();
    int[] tcounts = new int[26];
    for (char cha : target) {
        tcounts[cha - 'a']++;
    }
    int N = stickers.length;
    int min = Integer.MAX_VALUE;
    for (int i = 0; i < N; i++) {
        int[] sticker = stickers[i];
        if (sticker[target[0] - 'a'] > 0) {
            StringBuilder builder = new StringBuilder();
            for (int j = 0; j < 26; j++) {
                if (tcounts[j] > 0) {
                    int nums = tcounts[j] - sticker[j];
                    for (int k = 0; k < nums; k++) {
                        builder.append((char) (j + 'a'));
                    }
                }
            }
            String rest = builder.toString();
            min = Math.min(min, process3(stickers, rest, dp));
        }
    }
    int ans = min + (min == Integer.MAX_VALUE ? 0 : 1);
    dp.put(t, ans);
    return ans;
}

5、测试

LeetCode691.png

6、所有代码

github.com/monday-pro/…

二、最长公共子序列

LeetCode1143:

leetcode-cn.com/problems/lo…

1、题目描述

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

2、从尝试开始

(1)str1和str2都只有一位时,相等最长公共子序列即是1,否则是0

(2)str1只有一位,str2不止一位,str1与str2最后一位相等是1,否则在str2去掉最后一位中再找

(3)str1不只一位,str2只有一位,str1最后一位与str2相等是1,否则在str1去掉最后一位中再找

(4)str1和str2都不只一位

1)一定不要str1最后一位,可能要str2最后一位

2)可能要str1最后一位,一定不要str2最后一位

3)一定要str1最后一位,一定要str2最后一位

1)和2)有重复的情况,但是不影响求最小值;同时还包括一定不要str1最后一位、一定不要str2最后一位的情况

/**
 * @author Java和算法学习:周一
 */
public static int longestCommonSubsequence1(String s1, String s2) {
    if (s1 == null || s2 == null || s1.length() == 0 || s2.length() == 0) {
        return 0;
    }
    return process1(s1.toCharArray(), s2.toCharArray(), s1.length() - 1, s2.length() - 1);
}

private static int process1(char[] s1, char[] s2, int i, int j) {
    if (i == 0 && j == 0) {
        // i、j都只有一位了,这一位相等则最长公共子序列长度为1
        return s1[i] == s2[j] ? 1 : 0;
    } else if (i == 0) {
        // i只有一位了,这一位和当前s2[j]相等则最长公共子序列长度为1,不等则看j-1之前的
        // 因为s1只剩一个字符了,所以s1和s2公共子序列最多长度为1
        // 如果s1[0] == s2[j],那么此时相等已经找到了。公共子序列长度就是1,也不可能更大了
        // 如果s1[0] != s2[j],只是此时不相等而已,
        // 那么s2[0...j-1]上有没有字符等于s1[0]呢?不知道,所以递归继续找
        return s1[i] == s2[j] ? 1 : process1(s1, s2, i, j - 1);
    } else if (j == 0) {
        // j只有一位了,这一位和当前s1[i]相等则最长公共子序列长度为1,不等则看i-1之前的
        return s1[i] == s2[j] ? 1 : process1(s1, s2, i - 1, j);
    } else {
        // 最长公共子序列一定不包含i,可能包含j位置
        int p1 = process1(s1, s2, i - 1, j);
        // 最长公共子序列可能包含i,一定不包含j位置
        int p2 = process1(s1, s2, i, j - 1);
        // p1和p2有重复的情况,但是不影响求最小值;同时还包括一定不包含i、一定不包含j的情况

        // 最长公共子序列包含i、j位置
        int p3 = s1[i] == s2[j] ? 1 + process1(s1, s2, i - 1, j - 1) : 0;
        return Math.max(p1, Math.max(p2, p3));
    }
}

3、动态规划

直接由尝试修改

(1)根据暴力递归,分析可变参数为i,j,同时确定取值范围,生成dp表

(2)根据base case给dp表赋初值

(3)根据暴力递归给dp表剩余位置赋值

(4)根据暴力递归主函数调用,确定动态规划返回值

/**
 * 根据上面的暴力递归修改
 *
 * @author Java和算法学习:周一
 */
public static int longestCommonSubsequence2(String s1, String s2) {
    if (s1 == null || s2 == null || s1.length() == 0 || s2.length() == 0) {
        return 0;
    }
    char[] s1Array = s1.toCharArray();
    char[] s2Array = s2.toCharArray();
    int s1Length = s1.length();
    int s2Length = s2.length();
    // 根据暴力递归,分析可变参数为i,j
    // 取值范围为[0, s1.length-1],[0, s2.length-1]
    int[][] dp = new int[s1Length][s2Length];
    // 根据base case给dp表赋初值
    dp[0][0] = s1Array[0] == s2Array[0] ? 1 : 0;

    for (int j = 1; j < s2Length; j++) {
        dp[0][j] = s1Array[0] == s2Array[j] ? 1 : dp[0][j - 1];
    }
    for (int i = 1; i < s1Length; i++) {
        dp[i][0] = s1Array[i] == s2Array[0] ? 1 : dp[i - 1][0];
    }
    for (int i = 1; i < s1Length; i++) {
        for (int j = 1; j < s2Length; j++) {
            int p1 = dp[i - 1][j];
            int p2 = dp[i][j - 1];
            int p3 = s1Array[i] == s2Array[j] ? 1 + dp[i - 1][j - 1] : 0;
            dp[i][j] = Math.max(p1, Math.max(p2, p3));
        }
    }

    // 根据暴力递归主函数调用,确定动态规划返回值
    return dp[s1Length - 1][s2Length - 1];
}

4、测试

LeetCode1143.png