数组arr中的字符串是贴纸,每种贴纸任选无数张,想要将target串拼出来,至少需要多少张贴纸?
提示:逻辑上比较明了,但是实现上很难的动态规划题目
这个题,特别像一道动态规划的题目,简直就是一模一样
(1)数组arr里面是面币值,每种面币任选无数张,组成target的办法有多少种
这个题目,是非常非常经典的从左往右的尝试模型
本题读来跟(1)一模一样啊!!!但是上面求的组合方法数,本题要的不是方法数,而是最少的使用贴纸的张数。
但是因为本题arr中放的贴纸,贴纸还能剪开,所以就比较繁琐了!
逻辑上实现很容易,但是代码实现,需要理解哈希表用来记录词频的技巧——这个技巧,之前无数次用了
题目
数组arr中的字符串是贴纸,每种贴纸任选无数张,想要将target串拼出来,至少需要多少张贴纸?
你可以剪开arr[i]串,拿去拼target。
一、审题
示例:
arr= ba c abcd
target=babac
你可以用2张ba,1张c,共3张完成任务
你可以用2张abcd,这样也能拼target,共2张完成任务
最少张数为2
关键在暴力递归函数的定义
其实,你也看见了,示例中,完全不需要关心字符串的顺序,只需要知道target,需要多少字符来拼就行
arr[i]也是可以随意切开的,arr[i]可以重复用(这个重复使用,可以通过不断调用同一个递归函数来模拟,每次进递归函数都是用arr所有的串枚举一遍排头)
不妨设**f(arr,rest)**是咱们要的递归函数,含义是:余下rest=target这个串还没有拼完的情况下,用arr去拼,最少能用多少张搞定?
不妨设最开始ans=无穷张
那么宏观调度上,
(1)我们只需要枚举arr的每一个串,做一次排头,用1张这个排头,就需要把target中相应的字符消除,然后拿剩下的restNext串去继续递归cur=f(arr,restNext),看看这种情况下,我使用的最少张数cur是多少?
(2)每次一个arr[i]做排头,得到的结果cur+1(为啥+1,因为我排头这张也用了哦,你cur是restNext的最少张数呢),都需要跟ans对比,把最小值给ans。最后返回ans最小张数。
这个逻辑自己好好思考一遍!【算法的关键就在捋清逻辑,然后才能下手写代码】
字符串词频的统计处理
f(arr,restNext)这个逻辑也不难吧?
但是图中那个rest-arr[i]=restNext串啊,可不是数字这么简单的
rest-arr[i]=restNext是:rest用arr[i]串消除了arr[i]中的串之后余下没有拼的字符串
咱们在代码中模拟这个过程,需要知道[i]串中的每个字符a–z有多少个?
比如abcda
而arr整个数组,咱们都要统计,方面用
所以啊,咱直接用哈希表map来替换数组arr,
咋做呢?
哈希表map的
key:放arr[i]串,
value:代表是一个1位26长度的数组,每一位是a–z,0–25索引,是arr[i]串的词频。
这个技巧我们用了多次了哦!
比如:
arr= ba c abcd
value是一个定长26的数组,0–25代表a–z字符
索引任意字符串中的一个字符:如果arr[i]中的1个字符串用str来表示,要索引str中的其中一个字符时,用str[i] - ‘a’=0–25来索引(因为字符串存的就是ASCII码值)
同理,target要统计它的词频,也类似,只不过因为target是1个字符串,咱用1个定长26数组来模拟哈希表
比如令tmp = target = 26长度的数组,0–25代表a–z
这个数据处理好了以后,咱们拿1张map(arr)中的贴纸来拼target怎么搞呢?
显然,tmp(target)中的26字符都检查一遍,这仍然是当o(1)的速度,因为26就是常规长度,很短的。
将tmp(target)中每个字符的词频 - 减去 map中所用贴纸的各个对应位的词频,剩下的就是没拼的restNext串
比如:
tmp(target)= 17 7 10【分别代表0 1 2位置的a b c:a还有17个没拼,b还有7个,c还有10个】
实际上target=aaaaaaaaaaaaaaaaabbbbbbbcccccccccc
mapi=10 13 19【分别代表0 1 2位置的a b c:a还有10个可以拿去用,b还有13个,c还有19个】
restNext = rest-arr[i]=tmp(target)- mapi = 17 7 10 - 10 13 19 = 7 0 0 【也就是还有7个a没有拼,还需要去递归找贴纸来拼】
这里:rest-arr[i]=restNext是:rest用arr[i]串消除了arr[i]中的串之后余下没有拼的字符串
如何?这个用纸拼target的过程明白了吧?
我为啥用哈希表替代arr,就是为了统计词频,因为剪,拼,都不需要关心顺序,只需要看词频!
【当然,咱们真的实现代码时,没法往哈希表中放数组,所以呢,用二维数组表示map】
将暴力递归与傻缓存dp表同时用,适用于字符串的动态规划
情况是这样的,别的那些不是字符串的数组,可以用来到arr的i位置,还有rest需要拼,咋操作,得到最小的结果ans,更新给dp[i][rest]
比如(1)数组arr里面是面币值,每种面币任选无数张,组成target的办法有多少种
(1)中可以在i位置决定面币用多少张,要去拼rest值,那很方便整一个二维表来存结果到i rest格子
但是咱们这个题,很特殊!!!
为啥呢,由于我们可以重复用同一张贴纸,还能剪切,而且与顺序无关,所以我们只关心贴纸的字符的词频
而且递归函数中,我们不是像纸币那样,枚举本面币用k张,而去计算结果
我们是不枚举的,枚举行为通过不断调用f(arr,rest),f中每次都枚举arri做一次排头来达到多次使用同一个贴纸的目的
——这里一定要想清楚,这里的枚举,不是赤裸裸的枚举张,而是通过递归调用,枚举每个贴纸做排头的过程来实现
还有,我们关注,target在不断被拼接之后,剩余没拼的next串,
next串其实在咱们拼接的过程中,由于词频减少,next串可能有各种千奇百怪的组合,比如aaac,cbadfg等等等等,
到底有多少next串的可能性,我们不清楚
那么,我们想在暴力递归的过程中,一旦某个next串出现了,咱就统计一下,这个next串,它后续拼完的话,需要的最少张数是多少?
用一个哈希表dp来存,这样每次更新最小值,都放入这个dp表,类似于(1)文章中的dp表
dp表的key:字符串target的各种情况,
value:完成拼接target所需最少的张数。
默认最开始dp已经放了“”空串,只需要0张
这样的话,最后我们要dp.get(target)的结果——作为本题的答案!
这个想法,是非常巧妙的,之前我学的时候也没搞懂为啥这么干,
现在才明白,主要是咱们要枚举next串它的各种可能性,然后保存最小值,这本身也就是动态规划的本质。
okay!
有了这么多关于题目的理解,和相应的处理,咱们开始手撕代码!!
这个玩意一定要自己手撕清楚!!!
public static int f(int[][] map,
HashMap<String, Integer> dp,
String rest){
代码中如何定义呢?
(0)当rest已经在dp中出现了,直接返回dp.get(rest),dp存的就是最小张数,返回【这里dp已经包含了】
那么宏观调度上,
(1)我们只需要枚举arr的每一个串,做一次排头,用1张这个排头,就需要把target中相应的字符消除,然后拿剩下的restNext串去继续递归cur=f(arr,restNext),看看这种情况下,我使用的最少张数cur是多少?
(2)每次一个arr[i]做排头,得到的结果cur+1(为啥+1,因为我排头这张也用了哦,你cur是restNext的最少张数呢),都需要跟ans对比,把最小值给ans。最后返回ans最小张数。
OK:手撕代码:
//不妨设**f(arr,rest)**是咱们要的递归函数,
// 含义是:余下rest=target这个串还没有拼完的情况下,用arr去拼,最少能用多少张搞定?
//不妨设最开始ans=无穷张
//我们想在暴力递归的过程中,一旦某个next串出现了,咱就统计一下,这个next串,它后续拼完的话,需要的最少张数是多少?
//用一个哈希表dp来存,这样每次更新最小值,都放入这个dp表
public static int f(int[][] map,
HashMap<String, Integer> dp,
String rest){
//当rest已经在dp中出现了,**直接返回dp.get(rest),dp存的就是最小张数**,返回【这里dp已经包含了】
if (dp.containsKey(rest)) return dp.get(rest);
//没拼完的话,
//首先,咱们要准备target为tmp,也转化它的词频
int[] tmp = new int[26];
char[] target = rest.toCharArray();
for(char ch:target) {
tmp[ch - 'a']++;
}
int ans = Integer.MAX_VALUE;//先认为需要无穷张贴纸
//那么宏观调度上,
//
//(1)我们只需要枚举arr的每一个串,做一次排头,用1张这个排头,就需要把target中相应的字符消除,
// 然后拿剩下的**restNext串**去继续递归**cur=f(arr,restNext)**,看看这种情况下,我使用的最少张数cur是多少?
for (int i = 0; i < map.length; i++) {//枚举每个贴纸做排头
//咱要拿arri去拼target,如果压根arri中就不存在target的第一个字符,那算了吧
if (map[i][target[0] - 'a'] == 0) continue;//枚举别的贴纸吧
//tmp(target)中的26字符都检查一遍,
// 将tmp(target)中每个字符的词频 - 减去 map中所用贴纸的各个对应位的词频**,剩下的就是没拼的restNext串
StringBuffer sb = new StringBuffer();//准备收集没有没拼的字符
for (int j = 0; j < 26; j++) {//tmp(target)中的26字符都检查一遍,
if (tmp[j] > 0){//确实target还需要拼
//拼调arri中这么多,剩余的就是没有拼的呗
int need = Math.max(0, tmp[j] - map[i][j]);//map多了剩下0个还需要拼,
// map不够,就还有很多要拼
//没有拼的j字符,先储备到next中
for (int k = 0; k < need; k++) {
sb.append((char)(j + 'a'));//j字符,还原ASCII码去字符// 储备到next中
}
}
}
//(2)每次一个arr[i]做排头,得到的结果cur+1
// (**为啥+1**,因为我排头这张也用了哦,你cur是restNext的最少张数呢),
// 都需要跟ans对比,把最小值给ans记录在dp中。最后返回ans最小张数。
String restNext = sb.toString();//储备好的没拼的字符串,继续递归,找最小值
int cur = f(map, dp, restNext);//去看看后续没有拼好的最少需要多少张
if (cur != -1) ans = Math.min(ans, cur + 1);//cur有效,再+1算上我rest已经用了arri这张贴纸哦
}
//所有帖子都枚举排头了,记录rest这个串,最小结果
//当然,得保证ans有效才放,否则就放-1,说明这个拼法压根不行,无效拼接方式
dp.put(rest, ans == Integer.MAX_VALUE ? -1 : ans);
return dp.get(rest);
}
//主函数:
public static int leastTieZhiNum(String[] arr, String target){
if (arr == null || arr.length == 0) return -1;
if (target.compareTo("") == 0 || target == null) return 0;
int N = arr.length;
int[][] map = new int[N][26];//转化arr统计词频
for (int i = 0; i < N; i++) {
char[] str = arr[i].toCharArray();
for (int j = 0; j < str.length; j++) {
//对个串,每个位统计词频+1
map[i][str[j] - 'a']++;
}
}
//dp用来存拼接rest串所需的最少张数
HashMap<String, Integer> dp = new HashMap<>();
dp.put("", 0);//空串的话,直接就0张
return f(map, dp, target);
}
这个逻辑要仔细阅读和实现
测试一下:
public static void test(){
String[] arr = {"bc","c","abcd"};
String target = "babac";//案例
System.out.println(leastTieZhiNum(arr, target));
}
public static void main(String[] args) {
test();
}
完美:
2
总结
提示:重要经验:
1)字符串和普通的数字数组处理方式不一样,一定要理解这里的map和dp表,他们分别的功能
2)直接傻缓存搞定暴力递归,加速算法
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。