题目
CC12 拆分词句
描述
给定一个字符串s和一组单词dict,判断s是否可以用空格分割成一个单词序列,使得单词序列中所有的单词都是dict中的单词(序列可以包含一个或多个单词)。
例如:
给定s=“nowcode”;
dict=["now", "code"].
返回true,因为"nowcode"可以被分割成"now code".
思考
这道题,虽然标注为动态规划,但是我们可以用回溯的思想解决这道题
首先,我们先从 str 中,切割出前部一部分包含在 set 里,接着,剩下的部分重复前面的操作
如果发现后面无论切多少,都没有部分可以在 set 中,那么,我们就回溯到之前成功的部分,然后增加成功部分切分的字符创长度,并重复之前的操作,直到成功为止
public boolean wordBreak(String s, Set<String> dict) {
return backTrack(s,dict,0,0);
}
public boolean backTrack(String str,Set<String> dict,int l,int r) {
// 递归终止条件
if (r>=str.length()) return true;
boolean res = false;
for (int i = l; i <str.length(); i++) {
if (dict.contains(subString(str,l,i))) {
res = res || backTrack(str,dict,i+1,i+1);
}
}
return res;
}
// 包头包尾的 subString 函数
public static String subString(String str,int l,int r) {
return str.substring(l,r+1);
}
不出意料的,回溯法成功超时了
其会超时的主要原因,还是以为重复计算
重复计算的例子如下:
输入: s = "AAAleetcodeB", wordDict = ["leet", "code","A", "AA", "AAA"]
for 循环中:
首次递归: s = 'A' + helper('AAleetcodeB'), 最终检查不符合;
二次递归: s = 'AA' + helper('AleetcodeB'), 最终检查不符合;
三次递归: s = 'AAA' + helper('leetcodeB'), 最终检查不符合;
可以发现,在上面的例子中,leetcodeB 是一直被重复计算的
我们如果能将计算过的结果保存起来,下次直接用,就能节省大量时间
记忆有两种办法可供参考:
- 动态规划
- 记忆化数组进行搜索
动态规划:
dp[i]
表示 str(0,i)
是否符合条件
状态转移方程如下:
dp[i] = 对于任意j dp[j] && dict.contains(subString(j+1,i))
题解
使用动态规划的题解
public class CC12_wordBreak {
public static void main(String[] args) {
CC12_wordBreak solu = new CC12_wordBreak();
Set<String> set = new HashSet<>();
set.add("now");
set.add("code");
//set.add("a");
System.out.println(solu.wordBreak("nowcode",set));
}
public boolean wordBreak(String s, Set<String> dict) {
// 状态转移方程 dp[i] = 对于任意j dp[j] && dict.contains(str.subString(j+1,i))
boolean[] dp = new boolean[s.length() + 1];
dp[0]=true;
for (int i = 1; i <=s.length() ; i++) {
for (int j = 0; j < i; j++) {
// 这里为什么是 j 而不是 j+1 开始的
// 是因为我们的 dp[i] 表示的是长度
// 而咱们的 j ,表示的是下标
// 所以下面的 dp[j] 表示的是到长度为 j的部分
// 但是最后一位下标还是 j-1
// 所以,咱们这边的 subString 就应该是从 (j-1)+1 即 j 开始的
String curr = s.substring(j, i);
if (dp[j] && dict.contains(curr)) {
dp[i]=true;
break;
}
}
}
return dp[s.length()];
}
}
CC9 找环形链表入口
描述
给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null。
思考
判断是否有环很容易,我们使用快慢指针就可以解决
问题是怎么找环的入口
其实这是有诀窍的,当快慢指针相碰时,我们再新开一个指针,从起点触发,让 slow 指针和这个新指针同步以一步一格的速度前进,直到二者相碰,相碰的位置就是环的入口
具体推导可以看力扣的题解:链表环入口题解
那有人会问:上面的过程,万一忘记了怎么办?我们其实可以将它总结为一个小短语:快慢遇,二慢碰
题解
知道思路,代码就很好写了
public ListNode EntryNodeOfLoop(ListNode pHead) {
if (pHead==null || pHead.next==null) return null;
ListNode fast = pHead;
ListNode slow = pHead;
while (fast!=null && fast.next!=null) {
fast=fast.next.next;
slow=slow.next;
// 快慢相遇,说明成环
if (fast==slow) {
ListNode tmp = pHead;
while (tmp!=slow) {
tmp=tmp.next;
slow=slow.next;
}
return tmp;
}
}
return null;
}
CC16 分糖果
描述
有N个小朋友站在一排,每个小朋友都有一个评分
你现在要按以下的规则给孩子们分糖果:
- 每个小朋友至少要分得一颗糖果
- 分数高的小朋友要他比旁边得分低的小朋友分得的糖果多
你最少要分发多少颗糖果?
思考
这道题你上网找题解,都说是固定思路,左遍历+右遍历
左规则是 ,从左往右遍历,如果ratings[i]>ratings[i-1]
,那么第 i 个小孩的糖要比第 i-1 个小孩的多一个
右规则和做规则就有少许不同了,从右往左遍历,如果ratings[i]>ratings[i+1]
,这个时候我们不能无脑 +1,而是要判断第 i 个小孩原有的和第 i+1 个小孩 +1后的糖果数的比较,选最大的那个就行。我们不能因为走了一次右规则,就把左规则打破了吧
题解
public int candy (int[] ratings) {
int[] candies = new int[ratings.length];
for (int i = 0; i < candies.length; i++) {
candies[i]=1;
}
for (int i = 1; i < candies.length; i++) {
if (ratings[i]>ratings[i-1]) candies[i]=candies[i-1]+1;
}
for (int i = candies.length-2; i>=0; i--) {
if (ratings[i]>ratings[i+1]) candies[i]=Math.max(candies[i+1]+1,candies[i]);
}
int count = 0;
for (int candy : candies) {
count+=candy;
}
return count;
}
小结
不刷多了,一天就三题