KK's blog

每天积累多一些

0%

LeetCode 316 Remove Duplicate Letters

Given a string which contains only lowercase letters, remove duplicate letters so that every letter appear once and only once. You must make sure your result is the smallest in lexicographical order among all possible results.

Example:

Given "bcabc"
Return "abc"

Given "cbacdcbc"
Return "acdb"

题目大意:

给定一个只包含小写字母的字符串,从中移除重复字母使得每个字母只出现一次。你必须确保结果的字典序最小。

解题思路:

这题是保持原顺序输出结果,所以考虑用递减栈(递增栈,从栈底递增,求最小即用递增,如求k个最大用最小堆一样)。先看例子bcabc->abc,a入栈倒逼
bc出栈,可解此题。因为既然bc在栈外还有,就可以出栈,保证第一个字母最小(题目要求)。再看cbacdcbc,acd时候b入栈,不能倒逼cd出栈
因为d是唯一一个,所以还要维护一个hashMap来记录每个字母的词频。所以入栈条件为准入栈元素小于栈顶元素且栈顶元素为最后一个(频数>0)。
hashMap作用有两个,第一个为统计词频,第二个为记录未入栈的字母的频数。 resultSet记录stack中所有唯一元素,用于判断是否需要入栈。这是难点,对于已在栈中的重复元素不需要再入栈,因为它在栈中的位置已经是目前
最小的位置,如果要出现更小的结果只能通过非栈内元素倒逼产生新结果。如acabc,第二个a不需要逼c出来,b可以做到这一点,a已在最小位置。

注意事项:

  1. 已在栈内的重复元素不入栈,也不倒逼任何元素出栈,也就是直接忽略它,只要将其频数减一即可,表示已处理。比如abacb,第二个a不能倒逼b。用两个数据结构:set保证不重复加入到栈内,map保证外面还有元素可入栈
  2. 进入循环后频数立刻减一,不要出列时候才做,参见BFS。
  3. 出栈条件:栈不为空,准入栈元素小于栈顶元素,栈顶元素频数>0(表示栈外还有元素可以入栈)。

Python代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
def removeDuplicateLetters(self, s: str) -> str:
char_to_count = collections.Counter(s)
stack, stack_set = [], set()
for i in range(len(s)):
char_to_count[s[i]] -= 1
if s[i] in stack_set:
continue
while stack and s[i] < stack[-1] and char_to_count[stack[-1]] > 0:
stack_set.remove(stack[-1])
stack.pop()
stack.append(s[i])
stack_set.add(s[i])
return ''.join(stack)

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 String removeDuplicateLetters(String s) {
Stack<Character> stack = new Stack<Character>();
Map<Character, Integer> map = new HashMap<Character, Integer>();
Set<Character> result = new HashSet<Character>();

for(int i=0;i<s.length();i++){
Character c = s.charAt(i);
if(map.containsKey(c))
map.put(c, map.get(c)+1);
else
map.put(c, 1);
}

for(int i=0;i<s.length();i++){
Character c = s.charAt(i);
map.put(c, map.get(c)-1);

//Stack已经有c就不加入
if(result.contains(c))
continue;

while(!stack.isEmpty() && c<stack.peek() && map.get(stack.peek())>0){
result.remove(stack.peek());
stack.pop();
}
stack.push(c);
result.add(c);
}

StringBuilder sb = new StringBuilder();
while(!stack.isEmpty())
sb.append(stack.pop());
return sb.reverse().toString();
}

算法分析:

所有元素入栈出栈最多一次,所以时间复杂度为O(n),空间复杂度O(n)

LeetCode

<div>

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.

Example 1:

<pre>Input: envelopes = [[5,4],[6,4],[6,7],[2,3]] Output: 3 Explanation: The maximum number of envelopes you can Russian doll is 3 ([2,3] => [5,4] => [6,7]). </pre>

Example 2:

<pre>Input: envelopes = [[1,1],[1,1],[1,1]] Output: 1 </pre>

Constraints:

  • 1 <= envelopes.length <= 5000
  • envelopes[i].length == 2
  • 1 <= w<sub>i</sub>, h<sub>i</sub> <= 10<sup>4</sup>

</div>

题目大意:

信封套信封,能套上的条件是内信封的宽度和高度均小于外信封。求最多有多少个信封能套在一次

算法思路:

类似于LIS,先排序宽度,高度就变成LIS问题。

注意事项:

  1. 宽度和高度都是严格递增,所以排序envelopes的时候,先顺序排序宽度,再逆序排高度,逆序是防止同一宽度但高度不同的信封成为合法结果,如[3, 4], [3, 5], 高度LIS变成[4, 5]但不合法。还要注意Python语法:envelopes.sort(key=lambda x: (x[0], -x[1]))
  2. 用bisect_left因为,若高度相等,原地替换并不往后加。

Python代码:

1
2
3
4
5
6
7
8
9
10
def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
envelopes.sort(key=lambda x: (x[0], -x[1])) # remember format and reversed order
sorted_height = []
for i in range(len(envelopes)):
index = bisect.bisect_left(sorted_height, envelopes[i][1]) # remember left
if index < len(sorted_height):
sorted_height[index] = envelopes[i][1]
else:
sorted_height.append(envelopes[i][1])
return len(sorted_height)

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
public int maxEnvelopes(int[][] envelopes) {
if(envelopes == null || envelopes.length == 0 ||
envelopes[0] == null || envelopes[0].length != 2) // remember for 2d array
return 0;
Arrays.sort(envelopes, new Comparator<int[]>() {
public int compare(int[] a, int[] b) {
if(a[0] == b[0])
return b[1] - a[1]; // remember the reverse order for [4,5], [4,6] case must strictly increasing for width and height
else
return a[0] - b[0];
}
});
int len = 0;
int[] lis = new int[envelopes.length];
for(int[] e : envelopes) {
int index = Arrays.binarySearch(lis, 0, len, e[1]);
if(index < 0) {
index = -index - 1;
lis[index] = e[1];
}
else
lis[index] = e[1];
if(index == len)
len++;
}

return len;
}

算法分析:

Insert时间复杂度为O(nlogn),空间复杂度O(1)

LeetCode

<div>

There is a ball in a maze with empty spaces (represented as 0) and walls (represented as 1). The ball can go through the empty spaces by rolling up, down, left or right, but it won't stop rolling until hitting a wall. When the ball stops, it could choose the next direction.

Given the m x n maze, the ball's start position and the destination, where start = [start<sub>row</sub>, start<sub>col</sub>] and destination = [destination<sub>row</sub>, destination<sub>col</sub>], return true if the ball can stop at the destination, otherwise return false.

You may assume that the borders of the maze are all walls (see examples).

Example 1:

<pre>Input: maze = [[0,0,1,0,0],[0,0,0,0,0],[0,0,0,1,0],[1,1,0,1,1],[0,0,0,0,0]], start = [0,4], destination = [4,4] Output: true Explanation: One possible way is : left -> down -> left -> down -> right -> down -> right. </pre>

Example 2:

<pre>Input: maze = [[0,0,1,0,0],[0,0,0,0,0],[0,0,0,1,0],[1,1,0,1,1],[0,0,0,0,0]], start = [0,4], destination = [3,2] Output: false Explanation: There is no way for the ball to stop at the destination. Notice that you can pass through the destination but you cannot stop there. </pre>

Example 3:

<pre>Input: maze = [[0,0,0,0,0],[1,1,0,0,1],[0,0,0,0,0],[0,1,0,0,1],[0,1,0,0,0]], start = [4,3], destination = [0,1] Output: false </pre>

Constraints:

  • m == maze.length
  • n == maze[i].length
  • 1 <= m, n <= 100
  • maze[i][j] is 0 or 1.
  • start.length == 2
  • destination.length == 2
  • 0 <= start<sub>row</sub>, destination<sub>row</sub> <= m
  • 0 <= start<sub>col</sub>, destination<sub>col</sub> <= n
  • Both the ball and the destination exist in an empty space, and they will not be in the same position initially.
  • The maze contains at least 2 empty spaces.

</div>

题目大意:

球在玉米迷宫上滚,当遇到边界或玉米会停下,停下后可以转任意方向。求能否停在目标上。

算法思路:

二维数组求目标第一时间想到用BFS,此题求能停下的点而不是所有点。所以属于一组节点作为一层的BFS,也就是说只有能停下的位置才算BFS的一层,才加入都队列,其他停不来的点均忽略。这是此题的难点。

注意事项:

  1. 属于一组节点作为一层的BFS,能停下的点才加入到queue。比标准模板多了Line 10 - 11. 停下包括边界和玉米(maze[x][y] == 1)
  2. 要滚回一步Line 12 - 15,因为line 10循环的终结状态为出界或玉米上。要滚回一步到空地上。

Python代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def hasPath(self, maze: List[List[int]], start: List[int], destination: List[int]) -> bool:
queue = collections.deque([(start[0], start[1])])
visited = set([(start[0], start[1])])
while queue:
node = queue.popleft()
if node[0] == destination[0] and node[1] == destination[1]:
return True
for _dx, _dy in OFFSETS:
x, y = node[0], node[1]
while 0 <= x < len(maze) and 0 <= y < len(maze[0]) and maze[x][y] == 0: # remember maze[x][y] == 0
x, y = x + _dx, y + _dy
if (x - _dx, y - _dy) in visited: # remember
continue
queue.append((x - _dx, y - _dy))
visited.add((x - _dx, y - _dy))
return False

算法分析:

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

LeetCode

<div>

Given a string s, find the longest palindromic subsequence's length in s.

A subsequence is a sequence that can be derived from another sequence by deleting some or no elements without changing the order of the remaining elements.

Example 1:

<pre>Input: s = "bbbab" Output: 4 Explanation: One possible longest palindromic subsequence is "bbbb". </pre>

Example 2:

<pre>Input: s = "cbbd" Output: 2 Explanation: One possible longest palindromic subsequence is "bb". </pre>

Constraints:

  • 1 <= s.length <= 1000
  • s consists only of lowercase English letters.

</div>

题目大意:

求字符串中最长回文序列

算法思路:

一开始看到子序列如LIS就想用DP,dp[i]表示以s[i]为结尾的最长回文子序列。但不容易推导公式,难点是没有限制左边界 所以应该扩展到二维dp[i][j]表示[i, j]之间的最长回文子序列。公式就简单多了

1
2
dp[i][j] = dp[i+1][j-1] + 2,             s[i] == s[j]
= max(dp[i+1][j], dp[i][j-1]), s[i] != s[j]

注意事项:

  1. 难点是想到用二维DP(区间型DP)。用区间型递归模板,注意dp[i + 1][j]并不是i - 1
  2. 初始值为dp[i][i] = 1

Python代码:

1
2
3
4
5
6
7
8
9
10
11
12
def longestPalindromeSubseq(self, s: str) -> int:
dp = [[0 for _ in range(len(s))] for _ in range(len(s))]
for i in range(len(s)):
dp[i][i] = 1
for k in range(1, len(s)):
for i in range(len(s) - k):
j = i + k
if s[i] == s[j]:
dp[i][j] = dp[i + 1][j - 1] + 2
else:
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
return dp[0][-1]

算法分析:

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

LeetCode

<div>

There is a directed graph of n colored nodes and m edges. The nodes are numbered from 0 to n - 1.

You are given a string colors where colors[i] is a lowercase English letter representing the color of the i<sup>th</sup> node in this graph (0-indexed). You are also given a 2D array edges where edges[j] = [a<sub>j</sub>, b<sub>j</sub>] indicates that there is a directed edge from node a<sub>j</sub> to node b<sub>j</sub>.

A valid path in the graph is a sequence of nodes x<sub>1</sub> -> x<sub>2</sub> -> x<sub>3</sub> -> ... -> x<sub>k</sub> such that there is a directed edge from x<sub>i</sub> to x<sub>i+1</sub> for every 1 <= i < k. The color value of the path is the number of nodes that are colored the most frequently occurring color along that path.

Return the largest color value of any valid path in the given graph, or -1 if the graph contains a cycle.

Example 1:

<pre>Input: colors = "abaca", edges = [[0,1],[0,2],[2,3],[3,4]] Output: 3 Explanation: The path 0 -> 2 -> 3 -> 4 contains 3 nodes that are colored "a" (red in the above image). </pre>

Example 2:

<pre>Input: colors = "a", edges = [[0,0]] Output: -1 Explanation: There is a cycle from 0 to 0. </pre>

Constraints:

  • n == colors.length
  • m == edges.length
  • 1 <= n <= 10<sup>5</sup>
  • 0 <= m <= 10<sup>5</sup>
  • colors consists of lowercase English letters.
  • 0 <= a<sub>j</sub>, b<sub>j</sub> < n

</div>

题目大意:

给定一个图,有n个节点,每个节点的颜色已知,用a-z表示,求所有不循环路径上同种颜色的最大节点数,若有循环返回-1.

算法思路:

拓扑排序 + DP

  1. 看到侦测循环考虑用拓扑排序
  2. 拓扑排序的同时,如果知道父亲节点有最大的同种颜色数,容易计算儿子的同种颜色数dp[child]
    dp[child] = max(dp[parent]) for all the parents for the child
    由于有26种颜色,扩展到最大的第i种颜色数dp[child][i]表示以child为结尾的路径上第i种颜色的累计最大节点数
    1
    2
    dp[child][i] = max(dp[parent][i] + 1) if color[child] == color[parent] for all the immediate parents for the child, i = 1..26 
    = max(dp[parent][i]) if color[child] != color[parent]

注意事项:

  1. 见到最值且跟图相关,就考虑用BFS,而且要侦测循环就要用拓扑排序。要记录父节点颜色的累计和,考虑用DP,DP跟颜色相关且颜色都是小写字母,也就是26种
  2. 递归式是所有直接父节点的最大值。因为若有两条路径到达child这个节点,路径1第a种颜色有2个,而路径上第a种颜色有5个。
  3. 计算dp值在知道边的始点和终点上,也就是入度数减一之前。

Python代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def largestPathValue(self, colors: str, edges: List[List[int]]) -> int:
graph = [[] for _ in range(len(colors))]
in_degree = [0] * len(colors)
for _edge in edges:
graph[_edge[0]].append(_edge[1])
in_degree[_edge[1]] += 1
res = []
dp = [[0 for _ in range(26)] for _ in range(len(colors))]
initial_nodes = [i for i in range(len(in_degree)) if in_degree[i] == 0]
for _node in initial_nodes:
dp[_node][ord(colors[_node]) - ord('a')] = 1
queue = collections.deque(initial_nodes)
while queue:
node = queue.popleft()
res.append(node)
for j in graph[node]:
for i in range(26):
dp[j][i] = max(dp[j][i], dp[node][i] + (1 if ord(colors[j]) - ord('a') == i else 0)) # remember max(dp[j][i]
in_degree[j] -= 1
if in_degree[j] == 0:
queue.append(j)
return -1 if len(colors) != len(res) else max(map(max, dp))

算法分析:

时间复杂度为O(V + E),空间复杂度O(V + E)

Free mock interview