一、贴纸拼词
LeetCode691:
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、测试
6、所有代码
二、最长公共子序列
LeetCode1143:
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];
}