从暴力递归到动态规划5
LeetCode题目链接:https://leetcode.com/problems/stickers-to-spell-word/
目录
题目五
给定一个字符串str,给定一个字符串类型的数组arr,出现的字符都是小写英文arr每一个字符串,代表一张贴纸,你可以把单个字符剪开使用,目的是拼出str来返回需要至少多少张贴纸可以完成这个任务。(每种贴纸数量是无限的)
例子:str= "babac". arr = {"ba" "c" ,"abcd""} 至少需要两张贴纸""ba"和""abcd",因为使用这两张贴纸,把每一个字符单独剪开,含有2个a、2个b、1个c。是可以拼出str的。所以返回2。
暴力递归
-
遍历每一种情况,返回一个最小值;若找不到合适的贴纸,返回0;
-
每次调用一个贴纸,返回减去该贴纸字母后的剩余字符串;
-
当剩余字符串长度为0时,贴纸调用完毕,返回所用贴纸数;
-
求得最小花费贴纸数;
代码如下
//暴力递归
public static int sticker (String[] str, String target) {
if (str == null || str.length==0 || target.length()==0) {
return -1;
}
int min = process(str,target);
return min == Integer.MAX_VALUE ? -1 : min;
}
public static int process(String[] str, String target) {
if (target.length() == 0) {
return 0;
}
int min = Integer.MAX_VALUE;
for (String s: str) {
//返回target减去s后的剩余字符串
String rest = minus(target, s);
//若剩余字符串与目标字符串相等,说明该贴纸中不包含所需字母;
//反之,递归剩余字符串;
if (rest.length() != target.length()) {
min = Math.min(min,process(str,rest));
}
}
return min == Integer.MAX_VALUE ? Integer.MAX_VALUE : min+1;
}
其中minus是一个自定义方法,传入目标字符串和贴纸s;返回目标字符串减去贴纸s后的剩余字符串。
minus方法
public static String minus (String init, String s) {
char[] inits = init.toCharArray();
char[] chars = s.toCharArray();
int[] counts = new int[26];
for (char s1: inits) {
counts[s1-'a']++;
}
for (char s2: chars) {
counts[s2-'a']--;
}
String ans = "";
for (int i = 0; i < counts.length; i++) {
while (counts[i] > 0) {
ans += (char)(i+'a');
counts[i]--;
}
}
return ans;
}
测试
public static void main(String[] args) {
String[] strings = {"with","example","science"};
System.out.println(sticker(strings,"thehat"));
}
//输出
3
动态规划
上述的递归方法在速度和效率上过于低下。我们采用一个二维数组“剪枝”的方法对贴纸进行筛选;减少无用贴纸的计算,增加有效贴纸的命中率,尽可能快的算出花费贴纸的最小数值。
-
二维数组:每行对应一个贴纸,每行有26列对应a~z。记录每个贴纸每个字母的数量。
-
剪枝:当且仅当第一个贴纸包含目标字符串所含任一字母时(有效贴纸),继续选取贴纸;
-
若第一个贴纸不包含任何一个目标字符串中的字母,则直接丢弃(无效贴纸)。因为以该贴纸为首向下查询所得的值一定不是最小值。
代码如下
//动态规划
public static int sticker1 (String[] str, String target) {
if (str == null || str.length==0 || target.length()==0) {
return -1;
}
int N = str.length;
int[][] stickers = new int[N][26];
//二维数组赋值;
for (int i = 0; i < N; i++) {
char[] chars = str[i].toCharArray();
for (char c: chars) {
stickers[i][c-'a']++;
}
}
int min = process1(stickers,target);
return min == Integer.MAX_VALUE ? -1 : min;
}
public static int process1(int[][] sti, String t) {
if (t.length()==0) {
return 0;
}
int min = Integer.MAX_VALUE;
//target的词频数组
int[] tcounts = new int[26];
char[] target = t.toCharArray();
for (char tar: target) {
tcounts[tar-'a']++;
}
int N = sti.length;
for (int i = 0; i < N; i++) {
//选取每个贴纸当做第一个贴纸使用。
int[] sticker = sti[i];
//最关键的一步!!剪枝!
//判断第一个贴纸是否可用。
if (sticker[target[0]-'a'] > 0) {
String rest = "";
for (int j = 0; j < 26; j++) {
if (tcounts[j]>0){
//减去可用贴纸后的剩余字符串;
int nums = tcounts[j] - sticker[j];
while (nums > 0) {
rest += (char)(j+'a');
nums--;
}
}
}
min = Math.min(min,process1(sti,rest));
}
}
return min == Integer.MAX_VALUE ? Integer.MAX_VALUE : min+1;
}
测试
public static void main(String[] args) {
String[] strings = {"with","example","science"};
System.out.println(sticker(strings,"thehat"));
System.out.println(sticker1(strings,"thehat"));
}
//输出
3
3