KK's blog

每天积累多一些

0%

LeetCode 368 Largest Divisible Subset

Given a set of distinct positive integers, find the largest subset such that every pair (Si, Sj) of elements in this subset satisfies:

Si % Sj = 0 or Sj % Si = 0.

If there are multiple solutions, return any subset is fine.

Example 1:

**Input:** [1,2,3]
**Output:** [1,2] (of course, [1,3] will also be ok)

Example 2:

**Input:** [1,2,4,8]
**Output:** [1,2,4,8]

题目大意:

一个数组,让我们求这样一个子集合,集合中的任意两个数相互取余均为0。

解题思路:

由于知道子问题有助于求解考虑用DP。它就是LIS的翻版。这道题还需要打印DP路径。

  1. 定义dp[i]为num[i-1]这个数对应的最大可整除子集合个数。
  2. 递归式为dp[i] = max{dp[j-1] + 1}, 0<j<i, 若num[i-1]可被num[j-1]整除
  3. 方向为从左到右。初始值为dp = 1。
  4. path数组记录解的下标+1,每求得一个解dp[i] = dp[j] + 1就记录对应上一层解的下标,也就是到此解的路径。

注意事项:

  1. 初始值dp = 1。

Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public List<Integer> largestDivisibleSubset(int[] nums) {
List<Integer> res = new ArrayList<>();
if(nums == null || nums.length == 0)
return res;
if(nums.length == 1)
return Arrays.asList(nums[0]);
Arrays.sort(nums);
int max = Integer.MIN_VALUE;
int maxPos = -1;
int[] dp = new int[nums.length + 1];
int[] path = new int[nums.length + 1];
for(int i = 0; i < dp.length; i++) // remember to init to 1
dp[i] = 1;
for(int i = 1; i < dp.length; i++) {
for(int j = 1; j < i; j++) {
if(nums[i-1] % nums[j-1] == 0 && dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
path[i] = j;
}
}
if(dp[i] > max) { // the biggest value might not be the last one, keep track of the start pos for the path
max = dp[i];
maxPos = i;
}
}
int pos = maxPos;
for(int i = 0; i < dp[maxPos]; i++) {
res.add(nums[pos-1]);
pos = path[pos];

}
Collections.sort(res);
return res;
}

算法分析:

时间复杂度为O(n2),空间复杂度O(n2)

LeetCode 010 Regular Expression Matching

Given an input string (s) and a pattern (p), implement regular expression matching with support for '.' and '*'.

'.' Matches any single character.
'*' Matches zero or more of the preceding element.

The matching should cover the entire input string (not partial).

Note:

  • s could be empty and contains only lowercase letters a-z.
  • p could be empty and contains only lowercase letters a-z, and characters like . or *.

Example 1:

**Input:**
s = "aa"
p = "a"
**Output:** false
**Explanation:** "a" does not match the entire string "aa".

Example 2:

**Input:**
s = "aa"
p = "a*"
**Output:** true
**Explanation:** '*' means zero or more of the preceding element, 'a'. Therefore, by repeating 'a' once, it becomes "aa".

Example 3:

**Input:**
s = "ab"
p = ".*"
**Output:** true
**Explanation:** ".*" means "zero or more (*) of any character (.)".

Example 4:

**Input:**
s = "aab"
p = "c*a*b"
**Output:** true
**Explanation:** c can be repeated 0 times, a can be repeated 1 time. Therefore, it matches "aab".

Example 5:

**Input:**
s = "mississippi"
p = "mis*is*p*."
**Output:** false

题目大意:

这道求正则表达式匹配的题和那道Leetocde 044 Wildcard Matching很类似,不同点在于*的意义不同,在之前那道题中,
*表示可以代替任意个数的不同字符,而这道题中的*表示之前一个字符(同样字符)可以有0-N个匹配。此题更难一些。

解题思路:

这是经典题。两字符串匹配题基本就是DP而且知道子问题答案可以推导下一个。

  1. 定义dp[i][j]为字符串s[i-1]和p[j-1]是否能匹配。
  2. 递归式为dp[i][j] = dp[i-1][j-1] && (p[j-1] == . || s[i-1] == p[j-1])
    OR ((dp[i-1][j] && (s[i-1] == p[j-2] || p[j-2] == .)) || dp[i][j-2]) && p[j-1] == *
    第一种情况为非*,通配一样字符或.
    第二种情况为*,如果通配(有条件:与p的前一个字符相等或p为.)就是只移动s,dp[i-1][j]。若不通配就只移动p及其前一个字符
  3. 方向为从左到右,从上到下。初始值为dp[0][0] = true。以及若s为空,p为多个*时候,dp[0][j]=true。

与Wildcard Matching不同之处用黑体标注了:

  1. 用.代替?
  2. *情况,不匹配p移动两位而不是一位。
  3. *情况,匹配带条件而不是无条件。
  4. 初始化用dp[0][j-2]而不是j-1。

注意事项:

  1. 递归式含*不匹配情况dp[i][j-2]。
  2. 初始化s为空,p为多个*。此情况其实与递归式符合,因为i=1开始,所以i=0的时候,dp[i-1][j]为负值省去,
    只取dp[i][j-2]。
  3. 循环中j按理应该从2开始,因为递归式含p[j-2], 这样要初始化第1列(第0列已经初始化为False)。由于题目条件保证星号前一定有其他字符,所以p的第0位不能是星号,所以j可以从1开始
  4. Python中任何矩阵初始化都只能用两重for循环而不能用乘号

Python代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
def isMatch(self, s: str, p: str) -> bool:
dp = [[False for _ in range((len(p) + 1))] for _ in range(len(s) + 1)]
dp[0][0] = True
for j in range(2, len(dp[0])):
dp[0][j] = dp[0][j - 2] and p[j - 1] == '*'

for i in range(1, len(dp)):
for j in range(1, len(dp[0])):
if p[j - 1] != '*':
dp[i][j] = dp[i - 1][j - 1] and (s[i - 1] == p[j - 1] or p[j - 1] == '.')
else:
dp[i][j] = (dp[i - 1][j] and (s[i - 1] == p[j - 2] or p[j - 2] == '.')) or (dp[i][j - 2])
return dp[-1][-1]

Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean isMatch(String s, String p) {
boolean[][] dp = new boolean[s.length() + 1][p.length() + 1];
dp[0][0] = true;
for(int j = 2; j < dp[0].length; j++)
if(p.charAt(j-1) == '*') // remember s="", p="a*"
dp[0][j] = dp[0][j-2];
for(int i = 1; i < dp.length; i++)
for(int j = 1; j < dp[0].length; j++) {
if(p.charAt(j-1) == '*')
dp[i][j] = (dp[i][j-2] || ((s.charAt(i-1) == p.charAt(j-2)
|| p.charAt(j-2) == '.') && dp[i-1][j]));
else
dp[i][j] = dp[i-1][j-1] && (s.charAt(i-1) == p.charAt(j-1)
|| p.charAt(j-1) == '.');
}
return dp[dp.length -1][dp[0].length - 1];
}

算法分析:

时间复杂度为O(n*m),空间复杂度为O(n*m)

LeetCode 044 Wildcard Matching



Given an input string (s) and a pattern (p), implement wildcard pattern matching with support for '?' and '*'.

‘?’ Matches any single character.
‘ Matches any sequence of characters (including the empty sequence).


The matching should cover the entire input string (not partial).

Note: s could be empty and contains only lowercase letters a-z.
p could be empty and contains only lowercase letters a-z, and characters like <font face="monospace">?</font> or ``.

Example 1:

Input:
s = “aa”
p = “a”
Output: false
Explanation: “a” does not match the entire string “aa”.


Example 2:

Input:
s = “aa”
p = “
Output: true
Explanation:
‘ matches any sequence.


Example 3:

Input:
s = “cb”
p = “?a”
Output: false
Explanation: ‘?’ matches ‘c’, but the second letter is ‘a’, which does not match ‘b’.


Example 4:

Input:
s = “adceb”
p = “ab”
Output: true
Explanation: The first ‘‘ matches the empty sequence, while the second ‘‘ matches the substring “dce”.


Example 5:

Input:
s = “acdcb”
p = “ac?b”
*Output:
false


题目大意:

通配符外卡匹配问题,有特殊字符”*“和”?”,其中”?” 能代替任何字符,”*“能代替任何字符串。

解题思路:

这是经典题。两字符串匹配题基本就是DP而且知道子问题答案可以推导下一个。

  1. 定义dp[i][j]为字符串s[i-1]和p[j-1]是否能匹配。
  2. 递归式为
    1
    2
    dp[i][j] = dp[i-1][j-1] && (p[j-1] == ? || s[i-1] == p[j-1])  if p[j-1] != \* 
    dp[i-1][j] || dp[i][j-1] if p[j-1] == \*

第一种情况为非*,通配一样字符或?
第二种情况为*,如果通配就是只移动s,dp[i-1][j]。若不通配(通配完)就只移动p。

  1. 方向为从左到右,从上到下。初始值为dp[0][0] = true。以及若s为空,p为多个*时候,dp[0][j]=true。

注意事项:

  1. 递归式含*不匹配情况dp[i][j-1],容易忽略。
  2. 初始化s为空,p为多个*。根据递归式来写,i=0时,递归式只剩下dp[i][j-1]。将i = 0带入到内外循环代码实现即可(先写内外循环)
  3. 模板问题: dp初始化先col再row; i循环到len(dp)而不是len(s); 用到p时候是p[i - 1]而不是p[i]

Python代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
def isMatch(self, s: str, p: str) -> bool:
dp = [[False for _ in range(len(p) + 1)] for _ in range(len(s) + 1)] # remember p then s
dp[0][0] = True
for j in range(1, len(dp[0])):
if p[j - 1] == '*':
dp[0][j] = dp[0][j - 1]
for i in range(1, len(dp)): # remember len(dp) not len(s)
for j in range(1, len(dp[0])):
if p[j - 1] != '*': # remember j-1 not j
dp[i][j] = dp[i - 1][j - 1] and (s[i - 1] == p[j - 1] or p[j - 1] == '?')
else:
dp[i][j] = dp[i - 1][j] or dp[i][j - 1]
return dp[-1][-1]

注意事项:

  1. 递归式含*不匹配情况dp[i][j-1],我写的时候忽略了。
  2. 初始化s为空,p为多个*。此情况其实与递归式符合,因为i=1开始,所以i=0的时候,dp[i-1][j]为负值省去,
    只取dp[i][j-1]。
  3. 一开始写的corner case并入到递归式处理。

Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public boolean isMatch(String s, String p) {
//if(s.isEmpty() && p.isEmpty())
//return true;
//if(!s.isEmpty() && p.isEmpty())
//return false;
//if(s.isEmpty() && !p.isEmpty() && isAllStars(p))
//return true;
//if(s.isEmpty() && !p.isEmpty())
//return false;

boolean[][] dp = new boolean[s.length() + 1][p.length() + 1];
dp[0][0] = true;
for(int j = 1; j < dp[0].length; j++)
// remember empty s can match any prefix *** character in p making sure dp[0][j] = true
if(p.charAt(j-1) == '*')
dp[0][j] = dp[0][j-1];
for(int i = 1; i < dp.length; i++)
for(int j = 1; j < dp[0].length; j++)
dp[i][j] = (dp[i-1][j-1] && (p.charAt(j-1) == '?' || s.charAt(i-1) == p.charAt(j-1)))
// miss dp[i][j-1] means no match on *
|| ((dp[i-1][j] || dp[i][j-1]) && p.charAt(j-1) == '*');
return dp[dp.length -1][dp[0].length - 1];
}

算法分析:

时间复杂度为O(n*m),空间复杂度为O(n*m)

算法思路:

DFS将子问题的解存于结果中。cache[st] = result. st是子问题边界。

应用:

用于求所有可能性,且这些可能性有重复,记忆性搜索可以用于剪枝。

  1. Leetcode 139
  2. Leetcode 140

与DP的界线:

  1. 状态转移特别麻烦如有循环依赖, 不是顺序性。如棋盘,上下左右四个方向如1197 Minimum Knight Moves和word ladder
  2. 初始化状态不是特别容易找到

算法步骤:

  1. key为子问题索引st,value为子问题的解。不含path和res因为类似于Catalan,用子问题返回结果来组成此轮结果。f(input, st, endIndex, cache) -> List
  2. 紧跟终结条件,若在cache中,返回子问题的解。
  3. 循环结束,将子问题的结果存于cache。

注意事项:

  1. cache是一个解的集合,所以终止条件返回也需要是一个list

Python代码:

1
2
3
4
5
6
7
8
9
10
11
def dfs(self, input, cache) -> List[int]:
if <终止条件>:
return [] # remember to use list
if input in cache:
return cache[input]
res = []
for i in range(len(input)):
anwser = self.dfs(input[:i], cache)
res.append(anwser)
cache[input] = res
return res

算法分析:

时间复杂度为O(解大小),空间复杂度O(解大小).

LeetCode 140 Word Break II



Given a non-empty string s and a dictionary wordDict containing a list of non-empty words, add spaces in s to construct a sentence where each word is a valid dictionary word. Return all such possible sentences.

Note:

The same word in the dictionary may be reused multiple times in the segmentation. You may assume the dictionary does not contain duplicate words.

Example 1:

Input: s = “catsanddog
wordDict = ["cat", "cats", "and", "sand", "dog"]
Output: [ "cats and dog", "cat sand dog" ]


Example 2:

Input: s = “pineapplepenapple”
wordDict = [“apple”, “pen”, “applepen”, “pine”, “pineapple”]
Output: [
“pine apple pen apple”,
“pineapple pen apple”,
“pine applepen apple”
]
Explanation: Note that you are allowed to reuse a dictionary word.


Example 3:

Input: s = “catsandog”
wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
Output: []


题目大意:

一个字符串s,求被“字典集合”(wordDict)中的单词拼接的所有方案。

解题思路:

这是经典题。求所有可能性想到DFS,前面Lintcode 683提到可能会有重复解。所以用Cache。

Cache模板:

  1. key为子问题索引st,value为子问题的解。不含path和res因为类似于Catalan,用子问题返回结果来组成此轮结果。f(input, st, endIndex, cache)
  2. 紧跟终结条件,若在cache中,返回子问题的解。
  3. 循环结束,将子问题的结果存于cache。

注意事项:

  1. 终止条件返回[‘’]而不是[],正如L017,空字符串作为初始结果。返回到上层要strip()
  2. 子问题用f=word + f并不是f=f + word, 这样最后结果避免反转。
  3. s[:i + 1]判断是否在字典中,而不是s[:i],单词包括整个字符串。

Python代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def wordBreak(self, s: str, wordDict: List[str]) -> List[str]:
wordSet = set()
for word in wordDict:
wordSet.add(word)
cache = {}
res = self.dfs(s, wordSet, cache)
return [] if len(res) == 1 and not res[0] else res

def dfs(self, s, wordSet, cache):
if not s:
return ['']
if s in cache:
return cache[s]
res = []
for i in range(len(s)):
if s[:i + 1] not in wordSet:
continue
li = self.dfs(s[i + 1:], wordSet, cache)
for ss in li:
res.append((s[:i + 1] + ' ' + ss).strip())

cache[s] = res
return res

注意事项:

  1. 将两个输入都转换成小写。
  2. 复制子问题的解,不能直接在解List上编辑。

Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public List<String> wordBreak(String s, List<String> wordDict) {
List<String> res = new ArrayList<>();
if(s == null || s.isEmpty())
return res;
Set<String> wordDictLower = new HashSet<>();
for(String c : wordDict)
wordDictLower.add(c.toLowerCase());
s = s.toLowerCase();
Map<Integer, List<String>> cache = new HashMap<>();
return dfs(s, wordDictLower, s.length(), cache);
}

List<String> dfs(String s, Set<String> wordDict, int st, Map<Integer, List<String>> cache) {
if(st == 0)
return new ArrayList<>(Arrays.asList(""));
if(cache.containsKey(st))
return cache.get(st);
List<String> result = new ArrayList<>();
for(int i = 0; i < st; i++) {
String word = s.substring(i, st);
if(!wordDict.contains(word))
continue;

List<String> sub = dfs(s, wordDict, i, cache);
// copy solution for subproblem, don't edit on sub
for(int j = 0; j < sub.size(); j++)
result.add((sub.get(j) + " " + word).trim());
}
cache.put(st, result);
return result;
}

算法分析:

时间复杂度为O(解大小),空间复杂度为O(解大小)

Free mock interview