42. Trapping Rain Water

42. Trapping Rain Water

Question

Given n non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it can trap after raining.

Solution

双指针,设置当前左侧的最大高度left和右侧的最大高度right。

分别从两侧遍历height[]数组,当出现更高的height时更新left和right。
否则记录left和right与height[i]的差值,并记录在数组waterLeft[]和waterRight[]中。

遍历两个数组,添加两者中的最小值到volume。

*由于单个参数只记录了一侧的最大值,因此最大值另一侧的水的体积会被多计算,因此分别从两侧遍历来获得最小值。

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public int trap(int[] height) {
int n = height.length, left = 0, right = 0, volume = 0;
int[] waterLeft = new int[n], waterRight = new int[n];

for(int i = 0; i < n; i++){
if(left <= height[i]) left = height[i];
else waterLeft[i] = left - height[i];
}

for(int i = n - 1; i >= 0; i--){
if(right <= height[i]) right = height[i];
else waterRight[i] = right - height[i];
}

for(int i = 0; i < n; i++){
volume += Math.min(waterLeft[i], waterRight[i]);
}

return volume;
}
}
1770. Max Score from Multiplication Operations

1770. Max Score from Multiplication Operations

Question

You are given two integer arrays nums and multipliers** **of size n and m respectively, where n >= m. The arrays are 1-indexed.

You begin with a score of 0. You want to perform exactly m operations. On the i<sup>th</sup> operation (1-indexed), you will:

  • Choose one integer x from **either the start or the end **of the array nums.
  • Add multipliers[i] * x to your score.
  • Remove x from the array nums.

Return *the maximum score after performing *m operations.

Solution

动态规划,dp[][]数组记录左边取的个数和右边取的个数。

从取1开始到取multipliers的长度位置开始遍历。
然后从left取0个开始,直到left取i个为止遍历。
计算对应的right指针位置。

注意访问数组时需要访问left和right的上一个位置。

如果left为0,则只能取右侧的上一个位置加上右侧的上一个数值乘以mul。
如果right为0,则只能取左侧的上一个位置加上左侧的上一个数值乘以mul。
否则取两者之间的最大值。

最后遍历数组中left + right和为m的位置,并返回最大值。

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public int maximumScore(int[] nums, int[] multipliers) {
int n = nums.length, m = multipliers.length;
int[][] dp = new int[m+1][m+1];

for(int i = 1; i <= m; i++){
int mul = multipliers[i-1];
for(int l = 0; l <= i; l++){
int r = i - l;
int iL = l - 1, iR = n - r;
if(l == 0) dp[l][r] = dp[l][r-1] + mul * nums[iR];
else if(r == 0) dp[l][r] = dp[l-1][r] + mul * nums[iL];
else dp[l][r] = Math.max(dp[l-1][r] + mul * nums[iL], dp[l][r-1] + mul * nums[iR]);
}
}
int ans = Integer.MIN_VALUE;
for(int l = 1; l <= m; l++){
int r = m - l;
ans = Math.max(ans, dp[l][r]);
}
return ans;
}
}
1383. Maximum Performance of a Team

1383. Maximum Performance of a Team

Question

You are given two integers n and k and two integer arrays speed and efficiency both of length n. There are n engineers numbered from 1 to n. speed[i] and efficiency[i] represent the speed and efficiency of the i<sup>th</sup> engineer respectively.

Choose at most k different engineers out of the n engineers to form a team with the maximum performance.

The performance of a team is the sum of their engineers’ speeds multiplied by the minimum efficiency among their engineers.

Return the maximum performance of this team. Since the answer can be a huge number, return it modulo 10<sup>9</sup><span> </span>+ 7.

Solution

排序,将所有工程师根据其效率降序排列。

此时遍历时后一个工程师的效率一定小于等于前一个工程师。

因此,此时遍历到每一个工程师,其前面所有的工程师的efficiency均大于等于其本身。
维护一个所有选取工程师的总速度ttSpd,每次计算并更新max的值。
用最小堆来维护选取的工程师速度,如果优先级队列的尺寸超过k-1,则poll掉队列内最低的速度。

Code

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
class Solution {
public int maxPerformance(int n, int[] speed, int[] efficiency, int k) {
final int mod = (int) Math.pow(10, 9) + 7;
int[][] engineer = new int[n][2];

for(int i = 0; i < n; i++){
engineer[i][0] = speed[i];
engineer[i][1] = efficiency[i];
}

Arrays.sort(engineer, new Comparator<int[]>(){
public int compare(int[] a, int[] b){
return b[1] - a[1];
}
});

long max = 0, ttSpd = 0;

PriorityQueue<Integer> pq = new PriorityQueue<>();
for(int i = 0; i < n; i++){
int s = engineer[i][0], e = engineer[i][1];
ttSpd += s;
max = Math.max(max, ttSpd * e);
pq.add(s);
if(pq.size() == k) ttSpd -= pq.poll();
}

return (int) (max % mod);
}
}

869. Reordered Power of 2

869. Reordered Power of 2

Question

You are given an integer n. We reorder the digits in any order (including the original order) such that the leading digit is not zero.

Return true if and only if we can do this so that the resulting number is a power of two.

Solution

打表,将所有二的指数全部计算出来,并用数组统计各个数字出现的频率。
然后同样统计n中各个数字出现的频率。

如果两者中有频率完全相同的情况,则返回true,反之返回false。

Code

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
class Solution {
public boolean reorderedPowerOf2(int n) {
int[] reorders = new int[10];
int m = 1, digits = 0;
for(int i = n; i > 0; i/=10){
reorders[i % 10]++;
digits++;
}

int size = 0;
List<Integer[]> powerOfTwo = new ArrayList<>();

while(m > 0 && size <= digits){
size = 0;
Integer[] bin = new Integer[10];
Arrays.fill(bin, 0);
for(int i = m; i > 0; i/=10){
size++;
bin[i % 10]++;
}
if(size == digits) powerOfTwo.add(bin);
m *= 2;
}

for(Integer[] bin : powerOfTwo) if(check(bin, reorders)) return true;
return false;
}

private boolean check(Integer[] bin, int[] reorders){
for(int i = 0; i < bin.length; i++) if(bin[i] != reorders[i]) return false;
return true;
}
}

Solution 2

回溯,遍历计算可以组成的所有数字。
打表记录所有二的指数并记录在哈希表内,如果所有组成数字中有哈希表内的数字则返回true,反之返回false。

Code

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class Solution {
HashSet<Integer> visited, set, numbers;
List<Integer> arr, res;
public boolean reorderedPowerOf2(int n) {
int m = 1;
set = new HashSet<>();
visited = new HashSet<>();
numbers = new HashSet<>();
res = new ArrayList<Integer>();
while(m > 0){
set.add(m);
m *= 2;
}

arr = new ArrayList<>();

while(n > 0){
arr.add(n % 10);
n /= 10;
}

for(int i = 0; i< arr.size(); i++){
if(arr.get(i) == 0) continue;
visited.add(i);
reorder(arr.get(i));
visited.remove(i);
}


for(int num : res) if(set.contains(num)) return true;
return false;
}

private void reorder(int num){
if(visited.size() == arr.size()){
res.add(num);
return;
};

if(numbers.contains(num)) return;
numbers.add(num);

for(int i = 0; i < arr.size(); i++){
if(visited.contains(i)) continue;
int next = num * 10 + arr.get(i);
visited.add(i);
reorder(next);
visited.remove(i);
}
}
}
823. Binary Trees With Factors

823. Binary Trees With Factors

Question

Given an array of unique integers, arr, where each integer arr[i] is strictly greater than 1.

We make a binary tree using these integers, and each number may be used for any number of times. Each non-leaf node’s value should be equal to the product of the values of its children.

Return the number of binary trees we can make. The answer may be too large so return the answer modulo 10<sup>9</sup><span> </span>+ 7.

Solution

根据题意,二叉树的根是两个子节点的乘积,因此一个根节点可组成的二叉树总数,相当于其左子节点可组成的二叉树总数,乘以其右子节点可组成的二叉树总数。

因此我们可以先将数组arr排序,用dp[]数组记录以从小到大的数字作为根节点可组成的最大二叉树总数。

由于每个根节点至少可以和自身组成一个节点,因此将dp[]数组初始化为1。

动态规划

从小到大遍历dp[]数组。
在遍历时,从遍历数值i的左侧选取当前位置的子节点。(根据题意子节点一定小于根节点)
当当前位置arr[i]可以整除arr[left]时,则有可能存在另一个子节点。此时根据哈希表中获得对应的另一个子节点right。如果right存在,则dp[i]更新为dp[i]+dp[left]*dp[right]的值。

最后将dp[]数组中记录的所有数字加和并返回。

Code

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
35
class Solution {
public int numFactoredBinaryTrees(int[] arr) {
final int MOD = (int) Math.pow(10, 9) + 7;
Arrays.sort(arr); //from minium to maxium
HashMap<Integer, Integer> map = new HashMap<>();

for(int i = 0; i < arr.length; i++){
map.put(arr[i], i);
}

long[] dp = new long[arr.length];
Arrays.fill(dp, 1);

for(int i = 0; i < arr.length; i++){
for(int left = 0; left < i; left++){
if(arr[i] % arr[left] == 0){ //make sure divisible
int k = arr[i] / arr[left];
if(map.containsKey(k)){
int right = map.get(k);
dp[i] = (dp[i] + dp[left] * dp[right]) % MOD;
}
}
}
}

int res = 0;

for(long num : dp){
res += num;
res %= MOD;
}

return (int) res;
}
}
745. Prefix and Suffix Search

745. Prefix and Suffix Search

Question

Design a special dictionary with some words that searchs the words in it by a prefix and a suffix.

Implement the WordFilter class:

  • WordFilter(string[] words) Initializes the object with the words in the dictionary.
  • f(string prefix, string suffix) Returns the index of the word in the dictionary, which has the prefix prefix and the suffix suffix. If there is more than one valid index, return the largest of them. If there is no such word in the dictionary, return -1.

Solution

改版的前缀树,用后缀+符号+前缀的形式记录字典树。

TrieNode

用参数id记录TrieNode经历过的最大下标。

27位TrieNode数组children[]记录子TrieNode。

初始化

将words[]中的每个字符串记录在TrieNode上,保存TrieNode的根节点root。
在遍历每个字符串时,需要将每个“后缀+符号+前缀”组成的新字符串记录在字典树中,并更新当前节点的id值。

将后缀部分添加完毕后,加入特殊符号。检查并创建当前节点的curr[26]位置。
然后继续将整个前缀字符串加入字典树,并更新当前节点的id值。

搜索

搜索时,先遍历后缀,查找字典树的路径是否存在,并更新当前节点的位置,如不存在则返回-1。
然后检查并跳过curr.children[26]的位置,如不存在则返回-1。
最后遍历前缀,更新节点位置,如果路径不存在则返回-1。

最后返回当前节点的id即可。

Code

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class WordFilter {
class TrieNode{
int id;
TrieNode[] children;
public TrieNode(){
children = new TrieNode[27];
id = 0;
}
}

TrieNode root;
public WordFilter(String[] words) {
root = new TrieNode();

for(int k = 0; k < words.length; k++){
String word = words[k];
for(int i = 0; i < word.length(); i++){
TrieNode curr = root;
int j = i;
while(j < word.length()){
int index = word.charAt(j) - 'a';
if(curr.children[index] == null) curr.children[index] = new TrieNode();
curr = curr.children[index];
curr.id = k;
j++;
}
if(curr.children[26] == null) curr.children[26] = new TrieNode();
curr = curr.children[26];
curr.id = k;
for(char c : word.toCharArray()){
int index = c - 'a';
if(curr.children[index] == null) curr.children[index] = new TrieNode();
curr = curr.children[index];
curr.id = k;
}
}
}
}

public int f(String prefix, String suffix) {
TrieNode curr = root;
for(char c : suffix.toCharArray()){
int index = c - 'a';
if(curr.children[index] == null) return -1;
curr = curr.children[index];
}

if(curr.children[26] == null) return -1;
curr = curr.children[26];

for(char c : prefix.toCharArray()){
int index = c - 'a';
if(curr.children[index] == null) return -1;
curr = curr.children[index];
}
return curr.id;
}
}

/**
* Your WordFilter object will be instantiated and called as such:
* WordFilter obj = new WordFilter(words);
* int param_1 = obj.f(prefix,suffix);
*/
968. Binary Tree Cameras

968. Binary Tree Cameras

Question

You are given the root of a binary tree. We install cameras on the tree nodes where each camera at a node can monitor its parent, itself, and its immediate children.

Return the minimum number of cameras needed to monitor all nodes of the tree.

Solution 1

贪心算法,后序遍历。每次递归根据下面两个子节点的状态来决定当前节点的状态。

每个节点一共有三种状态:

  1. 未被监控,返回0。
  2. 摄像头,返回1。
  3. 已被监控,返回2。

递归,当当前节点为空,则返回2,表示已被监控。这样叶子节点则为未被监控的状态。

如果下面的子节点有一个未被监控,则当前节点需要设置相机,返回1,计数res+1。
如果下面的子节点有一个为摄像头,则当前节点已被监控,返回2。
否则下面的节点均为已被监控,此时返回未被监控0。

Code

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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int res = 0; //记录摄像头个数
public int minCameraCover(TreeNode root) {
if(judge(root) == 0) res++; //如果根节点未被监控,则需要增加一个摄像机
return res;
}
public int judge(TreeNode root){
if(root == null) return 2; //根节点为空时返回2,这样叶子节点变为未被监控
int left = judge(root.left);
int right = judge(root.right);

if(left == 0 || right == 0){ //如果左右子节点有未被监控的节点,则当前节点设置为摄像机,结果加一,返回1
res++;
return 1;
}
if(left == 1 || right == 1) return 2; //如果左右子节点中有摄像机,则当前节点已被监控,返回2
return 0; //如果左右子节点都被监控,则当前节点未被监控,返回0
}
}

Solution 2

参考:从递归优化到树形DP

树形DP。
递归,每次返回当前节点所有子节点的最小相机数。
每一个节点有三种状态:

  1. 放置了相机。
  2. 没有相机,但是被父节点parent监控。
  3. 没有相机,但是被子节点son监控。

将三种状态分别传入minCam()方法。

递归方法 minCam()

在递归时传入两个参数hasCamera和isWatched,来判断当前节点是否有相机,是否被监控。
当当前节点为空节点时,如果设置应当有相机(做不到)则返回无限大,消除这个返回值。如果不应该有相机,则返回0。

向下递归子节点。

  • 当当前节点应该有相机时:
    子节点一定被监控因此isWatched为true。
    子节点可以放置或不放置相机,因此hasCamera可以为true或false。
    注意当前位置放置相机,则子节点最多放置一个相机,因此一共有三种组合。
    由于相机至少有一个,因此返回其中的最小值+1。
  • 当当前节点没有放置相机,但被父节点监控时:
    左右节点一共有四种组合,分别同时将子节点的监控状态向下递归。
    返回其中的最小值。
  • 当当前节点没有放置相机,也没有被父节点监控,而是被子节点监控时:
    子节点至少有一个相机,向下递归状态,一共有三种组合。
    返回其中的最小值

然后分别调用minCam(root, true, true)和minCam(root, false, fasle),取两者中的小值,即可得到答案。

树形dp优化剪枝

上面的方法实际采用时会超时,因为我们重复了三次调用一个子树下的三种状态的minCam。
因此我们可以将三种状态下的minCam分别保存在数组中返回。

  1. withCam: 当前子树root有相机。
  2. noCamWatchedByParent: 当前子树root没有相机,被父节点监控。
  3. noCamWatchedBySon: 当前子树root没有相机,被子节点监控。

在每次递归时,获取数组minCam(root.left)和minCam(root.right)。
然后根据左右子节点的三个参数来计算当前节点的新的三个参数,将其返回。

Code

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
35
36
37
38
39
40
41
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int minCameraCover(TreeNode root) {
return Math.min(minCam(root, true, true), minCam(root, false, false));
}

private int minCam(TreeNode root, boolean hasCamera, boolean isWatched){
if(root == null) return hasCamera ? Integer.MAX_VALUE / 2 : 0;

if(hasCamera){
int min = Math.min(minCam(root.left, false, true) + minCam(root.right, false, true), minCam(root.left, true, true) + minCam(root.right, false, true));
min = Math.min(min, minCam(root.left, false, true) + minCam(root.right, true, true));
return 1 + min;
}
else if(isWatched){
int min = Math.min(minCam(root.left, true, true) + minCam(root.right, true, true), minCam(root.left, true, true) + minCam(root.right, false, false));
min = Math.min(min, minCam(root.left, false, false) + minCam(root.right, true, true));
min = Math.min(min, minCam(root.left, false, false) + minCam(root.right, false, false));
return min;
}
else{
int min = Math.min(minCam(root.left, true, true) + minCam(root.right, true, true), minCam(root.left, true, true) + minCam(root.right, false, false));
min = Math.min(min, minCam(root.left, false, false) + minCam(root.right, true, true));
return min;
}
}
}

Code

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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int minCameraCover(TreeNode root) {
int[] res = minCam(root);
return Math.min(res[0], res[2]);
}

private int[] minCam(TreeNode root){
if(root == null){
return new int[] {Integer.MAX_VALUE/2, 0, 0};
}
int[] left = minCam(root.left);
int[] right = minCam(root.right);

int withCam = 1 + Math.min(left[1] + right[1], Math.min(left[0] + right[1], left[1] + right[0]) );
int noCamWatchedByParent = Math.min( left[0] + right[0], Math.min( left[0] + right[2], Math.min( left[2] + right[0], left[2] + right[2] )));
int noCamWatchedBySon = Math.min( left[0] + right[0], Math.min( left[0] + right[2], left[2] + right[0]));
return new int[] {withCam, noCamWatchedByParent, noCamWatchedBySon};
}
}
318. Maximum Product of Word Lengths

318. Maximum Product of Word Lengths

Question

Given a string array words, return the maximum value of length(word[i]) * length(word[j]) where the two words do not share common letters. If no such two words exist, return 0.

Solution

正常的思路可能是用哈希表保存字符串中出现过的字符。此时表的大小是26也是一个常数。

我们可以采用位运算进行状态压缩,通过一个整数的二进制位保存二十六个字符的存在状态。

状态压缩

我们采用一个整数数组bin[]储存状态压缩后的整数。

遍历每个字符串,在字符串中遍历每一个字符。
初始化整数bit为0。在遍历字符时计算当前字符与字符’a’距离的差n,然后将1左移n位,填入(或运算)整数bit中。

比较

遍历words[]中的两个字符串,当bin[i]与bin[j]进行与运算结果为0时,则两个字符串没有共同字符。
此时更新二者长度乘积的最大值max。

最后返回max即可。

时间复杂度:O(L + n2)。L为所有字符的总长度。

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public int maxProduct(String[] words) {

int max = 0;
int bin[] = new int[words.length];

for(int i = 0; i < words.length; i++){
int bit = 0;
for(int j = 0; j < words[i].length(); j++){
bit |= (1 << (words[i].charAt(j) - 'a'));
}
bin[i] = bit;
}

for(int i = 0; i < words.length; i++){
for(int j = i+1; j < words.length; j++){
if((bin[i] & bin[j]) == 0) max = Math.max(max, words[i].length() * words[j].length());
}
}
return max;
}
}
354. Russian Doll Envelopes

354. Russian Doll Envelopes

Question

You are given a 2D array of integers envelopes where envelopes[i] = [w<sub>i</sub>, h<sub>i</sub>] represents the width and the height of an envelope.

One envelope can fit into another if and only if both the width and height of one envelope are greater than the other envelope’s width and height.

Return the maximum number of envelopes you can Russian doll (i.e., put one inside the other).

Note: You cannot rotate an envelope.

Solution 1

最长递增子数列

这道题是LIS问题(Longest Increasing Subsequence) 的升级版。
后一个数组中的两个元素应该同时严格大于前一个数组中的两个元素。
因此我们需要维护一个宽度和高度同时严格单调递增的数列。

排序

我们可以对数组以宽度进行比较排序。当二者的宽度相等时,以高度的倒序排序
(采用倒序排序是为了下面处理时排除不严格大于上一个数组的情况。)

递增数列

我们可以维护一个递增动态数组arr,记录高度组成的单调递增数列。(LIS)
首先将数组中的第一个元素的高度加入动态数组。
然后遍历数组envelopes[]:

  • 如果当前数组的高度height大于单调递增数列的最大值:
    • 则当前的数组的宽度高度均严格大于数组中最后一个高度对应的数组。
    • 数组是优先以宽度的顺序排列,相等时以高度的倒序排列。由于相同宽度时高度大的在前,因此当当前高度大于之前的最大高度,则宽度也一定大于之前的最大宽度。
  • 如果小于或者等于单调递增数列的最大值:
    • 单调数列中高度的增长越平缓,我们在后面找到更多严格大于height的可能性就越大,因此可以用贪心思想,更新单调数列中的第一个大于当前高度的值。
    • 由于单调递增数列是有序的,因此可以采用二分搜索来寻找第一个大于当前高度的值的下标,并将其更新为当前的height。

排序的时间复杂度为O(nlogn),遍历数组的时间复杂度为O(n),二分搜索的时间复杂度为O(logn)。
总的时间复杂度为O(nlogn)。

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public int maxEnvelopes(int[][] envelopes) {
Arrays.sort(envelopes, (a,b) -> a[0] != b[0] ? a[0] - b[0] : b[1] - a[1]);

List<Integer> arr = new ArrayList<>();
arr.add(envelopes[0][1]);
for(int i = 1; i < envelopes.length; i++){
int height = envelopes[i][1];
if(height > arr.get(arr.size()-1)){
arr.add(height);
}
else{
int index = Collections.binarySearch(arr, height);
if(index < 0) index = -index-1;
arr.set(index, height);
}
}
return arr.size();
}
}

Solution 2

注意这个方法在LeetCode中会超时,仅仅是记录一下思路,也可以采用动态规划的方法。
记忆化搜索剪枝。
记忆化搜索每个envelope。

memorazation()方法,返回当前的envelope中可以包含的最大数量。
当数组memo[i]不等于0时,返回memo[i]。
否则初始化最大值max为1。
遍历数组中的所有元素,如果宽度于长度都大于envelopes[i],则向下递归搜索该元素。
比较并更新max和递归结果的返回值+1。
最后将当前位置memo[i]设置为包含的最大数量max。

遍历所有元素时间复杂度O(n)。
记忆化搜索元素的子项时间复杂度O(n)。
时间复杂度为O(n2)。

Code

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
class Solution {
int[][] dolls;
int[] memo;

public int maxEnvelopes(int[][] envelopes) {
dolls = envelopes;
memo = new int[dolls.length];
int res = 0;
for(int i = 0; i < dolls.length; i++){
if(memo[i] != 0) continue;
memorazation(i);
res = Math.max(res, memo[i]);
}
return res;
}

private int memorazation(int i){
if(memo[i] != 0){
return memo[i];
}
int max = 1;
for(int k = 0; k < dolls.length; k++){
if(dolls[k][0] < dolls[i][0] && dolls[k][1] < dolls[i][1]){
max = Math.max(max, memorazation(k) + 1);
}
}
return memo[i] = max;
}
}
450. Delete Node in a BST

450. Delete Node in a BST

Problem

Given a root node reference of a BST and a key, delete the node with the given key in the BST. Return the root node reference (possibly updated) of the BST.

Basically, the deletion can be divided into two stages:

  1. Search for a node to remove.

  2. If the node is found, delete the node.

Solution

二叉搜索树(BST)的递增排序的访问顺序是中序遍历。(Left -> Root -> Right)

因此二叉搜索树的前驱者(小于当前节点的最大值)和继任者(大于当前节点的最小值)对于修改二叉树至关重要。

辅助方法getPredecessor() 和getSuccessor() 可以获得前驱者和继任者的值。
分别为当前根节点的左子节点的最右子节点(二叉搜索树下的最大值)和当前根节点右子节点的最左子节点。(二叉搜索树下的最小值)

deleteNode() 方法搜索key。如果当前值大于搜索值则搜索并修改其左子节点,反之搜索并修改右子节点。(由于要修改,因此向下递归子节点时需要将返回的结果传入该子节点。)

当搜索值等于当前值时,存在三种情况:
1.该子节点存在右子节点。此时我们将当前值设置为继任者的值,然后递归搜索右子节点中继任者的值进行删除。
2.该子节点存在左子节点。此时我们将当前值设置为前驱者的值,然后递归搜索左子节点中前驱者的值进行删除。
3.该子节点是叶子节点。直接删除当前节点。

最后返回修改后的根节点即可。

Code

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
if(root == null) return null;
if(root.val > key) root.left = deleteNode(root.left, key); //如果当前值大于搜索值则搜索左子节点,否则搜索右子节点
else if(root.val < key) root.right = deleteNode(root.right, key);
else{ //如果当前值等于搜索值,根据子节点情况删除节点
if(root.right != null){ //右子节点不为空则将当前root的值设置为继任者的值,然后向下递归删除继任者
root.val = getSuccessor(root);
root.right = deleteNode(root.right, root.val);
}
else if(root.left != null){ //左子节点不为空则将当前root的值设置为前驱者的值,然后向下递归删除继任者
root.val = getPredecessor(root);
root.left = deleteNode(root.left, root.val);
}
else{
root = null;
}
}
return root;
}

private int getSuccessor(TreeNode root){
root = root.right;
while(root.left != null){
root = root.left;
}
return root.val;
}
private int getPredecessor(TreeNode root){
root = root.left;
while(root.right != null){
root = root.right;
}
return root.val;
}
}
300. Longest Increasing Subsequence

300. Longest Increasing Subsequence

Question

Given an integer array nums, return the length of the longest strictly increasing subsequence.

A subsequence is a sequence that can be derived from an array by deleting some or no elements without changing the order of the remaining elements. For example, [3,6,2,7] is a subsequence of the array [0,3,1,6,2,2,7].

Solution

贪心算法

创建一个数组记录达到升序的长度时升序数列的最小末尾值。
如果想要增加数组的长度(即增加最大的升序数列长度),则新的数字必须要大于该数组最后一位的大小。
因此在这种情况下,该数组必然是单调递增的。

二分搜索

将第一个数字填入数组。
然后遍历数组,当新一项大于数组的最后一个值时,最大长度加一,并将其加入数组的尾部。
当新一项小于数组的最后一个值时,该数字需要与之前的数组组成新的升序数列。
我们可以替换掉之前数组中比该数字大的第一个数字,代表新组成的数组。(最长数组之间的数字无所谓,只记录最小的末尾值即可。)
我们可以用二分搜索查到该数字需要加入的index。然后替换掉数组中的数字。
单次二分搜索的时间复杂度为O($logn$)。总的时间复杂度为O($nlogn$)。

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public int lengthOfLIS(int[] nums) {
List<Integer> ans = new ArrayList<>();
int len = 1;
ans.add(nums[0]);

for(int i = 1; i < nums.length; i++){
if(nums[i] > ans.get(ans.size()-1)){
ans.add(nums[i]);
len++;
}
else{
int index = Collections.binarySearch(ans, nums[i]);
if(index < 0){
ans.set(-(index+1), nums[i]);
}
}
}
return len;
}
}

Solution 2

动态规划,创建一个数组dp[]用来记录最大长度,创建max记录最大值。
遍历,先将数组dp[]上的所有位置都填上1。
从0到i-1遍历j。当nums[i] > nums[j]时,更新dp[i]为dp[i]与dp[j]+1中的较大值。
当更新dp[i]时将其和max比较,如果比max大则更新max。
最后返回max值。时间复杂度为O($n_2$)

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
int max = 1;

for(int i = 0; i < n; i++){
dp[i] = 1;
for(int j = i-1; j >= 0; j--){
if(nums[i] > nums[j]){
dp[i] = Math.max(dp[i], dp[j]+1);
if(max < dp[i]){
max = dp[i];
break;
}
}
}
}
return max;
}
}
5. Longest Palindromic Substring

5. Longest Palindromic Substring

Question

Given a string s, return the longest palindromic substring in s.

Solution

马拉车算法,专门用于计算最大回文子字符串长度。

预处理

首先需要对字符串进行预处理,将每两个字符之间(包括两端)加上一个符号’#’。
然后在字符串的前后加入任意两个不同的符号。(防止中心扩展时继续搜索)

这个操作使得偶数项的回文字符串变为奇数项的回文字符串,便于接下来进行统一的中心扩展操作。

Manacher算法

参考资料:一文让你彻底明白马拉车算法

用一个数组p[]记录处理后字符串位置的中心展开长度。

初始化一个回文中心的下标位置i,和当前回文中心的右侧范围r。
在进行算法时维护这两个参数。

遍历字符串中除了首尾两个字符以外的位置。

  • 初始化i位置关于c的镜像位置m为2*c-i。
    由于回文字符串的对称特性,此时p[i]的中心拓展长度应等于p[m]。
  • 有几种特例情况,p[i]不等于p[m]:
  1. 如果p[m] + m的和大于r,则以m为中心扩展的范围超过当前c的中心扩展范围的右界。
    此时p[i]无法保证等于p[m]。但是可以确保p[i]至少可以扩展到r,因此将p[i]更新为r-i。
  2. 如果i的位置等于r,则可扩展距离为0。
  3. 如果m的位置为原字符串的左边界,则此时将p[i]赋值为1是不正确的。
    因为p[m]的计算是遇到了边界停止的,而p[i]则没有遇到边界,接下来对i位置从i+p[i]与i-p[i]开始继续进行中心扩展即可。

中心扩展

奇数项的回文字符串由中心扩展,只需要保证i+p[i]+1与i-p[i]-1(两个位置分别表示当前中心扩展的边缘再前进1)的字符相同,则可以继续向下扩展,将p[i]++。

c与r的更新

由于要保证i在r的范围之内,当从i出发的中心扩展范围大于c的中心扩展范围r,需要更新c与r。

时间复杂度

在进行扩展时,每次访问过的位置不会进入中心扩展算法。(比较从中心扩展的边缘开始,即i+p[i]+1与i-p[i]-1)
因此总的时间复杂度为O(n)。

Code

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class Solution {
int n;
int[] p;
String newString;
public String longestPalindrome(String s) {
n = s.length();
StringBuffer sb = new StringBuffer();
if(n == 0) { //对字符串进行处理
sb.append("^$");
}
else{
sb.append("^");
for(int i = 0; i < s.length(); i++){
sb.append('#');
sb.append(s.charAt(i));
}
sb.append("#$");
}
p = new int[sb.length()];
newString = sb.toString();
manacher();

int c = 0;
for(int i = 0; i < sb.length(); i++){
if(p[i] > p[c]) c = i;
}
return s.substring( (c-p[c])/2, (c-p[c])/2+p[c] );
}

private void manacher(){
int c = 0, r = 0;
for(int i = 1; i < p.length - 1; i++){
int m = 2 * c - i;
if(i < r){ //当i在r的范围内时,p[i]的值与p[m]相等。
p[i] = Math.min(r-i, p[m]); //当p[m] + i超过了r时(当前中心拓展边界),p[i]至少可以取值到r-i
}
else{
p[i] = 0; //当i等于r时,该位置拓展距离为0
}

while (newString.charAt(i+p[i]+1) == newString.charAt(i-p[i]-1)) { //中心拓展算法,当两个位置相等时则可拓展距离+1
p[i]++;
}

if(i + p[i] > r){ //当当前位置的右侧边界大于现有边界时,更新位置r
c = i;
r = i + p[i]; //新的右侧边界为新的中心+拓展长度p[i]
}
}
}
}

Solution 2

getLength方法,计算每一个字符位置的回文长度。(将left填成i+1,right填成i则可以搜索偶数回文的长度。)
如果出现更长的回文,则根据返回的长度,和当前的i计算出字符串的范围。

Code

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
class Solution {
public String longestPalindrome(String s) {
int best = 0;
int left = 0;
int right = 0;
for(int i = 0; i < s.length(); i++){
int length = Math.max(getLength(s,i,i), getLength(s,i+1,i));
if(best < length){
left = i-((length-1)/2-1);
right = i+(length/2);
best = length;
}
}
return s.substring(left,right);
}

private int getLength(String s, int left, int right){
while(left >= 0 && right < s.length() && left < s.length()){
if(s.charAt(left) == s.charAt(right)){
left--;
right++;
}
else break;
}

return right - left + 1;
}
}