Skip to content

Commit 860687f

Browse files
committed
Update notes of dynamic programming problems
1 parent 54a9987 commit 860687f

21 files changed

+316
-105
lines changed

Algorithms.code-workspace

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"folders": [
3+
{
4+
"path": "."
5+
},
6+
{
7+
"name": ".leetcode",
8+
"path": "../../../.leetcode"
9+
}
10+
],
11+
"settings": {}
12+
}

leetcode/1337.the-k-weakest-rows-in-a-matrix.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ fun kWeakestRows(mat: Array<IntArray>, k: Int): IntArray {
4040
* **Space Complexity**: `O(m)`.
4141

4242
## Optimal Heap + Binary Search
43-
The 1s are always before 0s, so each row is sorted, and we can apply binary search to find the last `1` for counting.
43+
The `1s` are always before `0s`, so each row is sorted, and we can apply binary search to find the last `1` for counting (or the first `0`).
4444

4545
```kotlin
4646
fun kWeakestRows(mat: Array<IntArray>, k: Int): IntArray {
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# [1351. Count Negative Numbers in a Sorted Matrix](https://leetcode.com/problems/count-negative-numbers-in-a-sorted-matrix/description/)
2+
3+
## Brute Force
4+
We can count the negative numbers in the matrix by iterating each element.
5+
6+
```python
7+
def countNegatives(self, grid: List[List[int]]) -> int:
8+
m, n = len(grid), len(grid[0])
9+
count = 0
10+
for i in range(m):
11+
for j in range(n):
12+
if grid[i][j] < 0:
13+
count += 1
14+
return count
15+
```
16+
* **Time Complexity**: `O(m * n)`.
17+
* **Space Complexity**: `O(1)`.
18+
19+
## Count in Z-shape
20+
Since each row (column) is sorted, we can count the negative numbers in a Z-shape.
21+
22+
```
23+
++++++
24+
++++--
25+
++++--
26+
+++---
27+
+-----
28+
+-----
29+
```
30+
31+
![](../media/240.search-a-2d-matrix-ii.png)
32+
33+
We start from left-botton corner, and move up to right-top corner, and count the negative numbers in each column.
34+
35+
```python
36+
def countNegatives(self, grid: List[List[int]]) -> int:
37+
m = len(grid)
38+
n = len(grid[0])
39+
40+
r = m - 1
41+
c = 0
42+
count = 0
43+
while c < n:
44+
while 0 <= r and grid[r][c] < 0:
45+
r -= 1
46+
count += m - r - 1
47+
c += 1
48+
49+
return count
50+
51+
## Equivalently, starting from right-top corner, and move down to left-bottom corner.
52+
def countNegatives(self, grid: List[List[int]]) -> int:
53+
count = 0
54+
n = len(grid[0])
55+
negIndex = n - 1
56+
57+
for r in grid:
58+
while negIndex >= 0 and r[negIndex] < 0:
59+
negIndex -= 1
60+
61+
count += (n - negIndex - 1)
62+
63+
return count
64+
```
65+
66+
* **Time Complexity**: `O(m + n)`.
67+
* **Space Complexity**: `O(1)`.
68+
69+
## Binary Search in Each Row (Column)
70+
For each row (column), we can use binary search to find the first negative number, and the count of negative numbers will be `n - index`.
71+
72+
```kotlin
73+
fun countNegatives(grid: Array<IntArray>): Int {
74+
var count = 0
75+
for (i in 0 until grid.size) {
76+
val index = binarySearch(grid[i])
77+
println(index)
78+
count += grid[i].size - index
79+
}
80+
return count
81+
}
82+
83+
private fun binarySearch(array: IntArray): Int {
84+
var left = 0
85+
var right = array.size - 1
86+
while (left <= right) {
87+
val middle = left + (right - left) / 2
88+
if (0 <= array[middle]) {
89+
left = middle + 1
90+
} else {
91+
right = middle - 1
92+
}
93+
}
94+
return left
95+
}
96+
```
97+
98+
* **Time Complexity**: `O(m * lg(n))`.
99+
* **Space Complexity**: `O(1)`.

leetcode/139.word-break.md

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,53 @@
1-
## [139. Word Break](https://leetcode.com/problems/word-break/)
1+
# [139. Word Break](https://leetcode.com/problems/word-break/)
2+
3+
## Observation
4+
```js
5+
// For word "cars" and dictionary ["car", "ca", "rs"]
6+
f(car) =
7+
f(ca) + "r" in dict ||
8+
f(ca) =
9+
f(c) + "a" in dict ||
10+
f() + "ca" in dict
11+
f(c) + "ar" in dict ||
12+
f(c) =
13+
f() + "c" in dict
14+
f() + "car" in dict
15+
```
216

3-
### Observation
417
1. **Optimal substructure**: We can break down the string into smaller substrings, and we can build the substrings from dictionary to build the solution for entire string.
5-
2. **Overlapping subproblems**: When breaking down the string, we might have same substrings, and we can reuse the solution.
18+
2. **Overlapping subproblems**: When breaking down the string, we might have same substrings (`f(c)` from above example), and we can reuse the solution.
619
3. **Memoization**: We keep track of whether substrings can be built from dictionary.
720

821
Suppose we break down the string into `s[0:i]` and `s[i:]`, if we can build `s[0:i]` from dictionary recursively and `s[i:]` exists in dictionary, then we can build `s[0:i] + s[i:]` from dictionary. Then we break down `s[0:i]` into `s[0:j]` and `s[j:i]`, and if we can build `s[0:j]` from dictionary recursively and `s[j:i]` exists in dictionary, then we can build `s[0:j] + s[j:i]` from dictionary. And so on. Therefore, we can solve this problem by dynamic programming.
922

1023
```js
1124
// f(s) = word break function of input string s.
12-
f(s) = f(s[0:i]) && f(s[i:])
13-
= f(s[0:i]) && if s[i:] in dict
25+
f(s) = f(s[0:i]) && if s[i:] in dict for i in 1 to s.length
26+
27+
// Break into smaller substrings
28+
f(s[0:i]) = f(s[0:j]) && if s[j:i] in dict for j in 1 to i
1429

15-
f(s[0:i]) = f(s[0:j]) && f(s[j:i])
16-
= f(s[0:j]) && if s[j:i] in dict
30+
f(s[0:j]) = f(s[0:k]) && if s[k:j] in dict for k in 1 to j
1731

18-
... so on.
32+
// ... so on.
1933
```
2034

2135
Here we don't know the `i` and `j` for breaking down the string, so we will try all possible `i` and `j` (iterate all substrings) to find if we can build the string from dictionary.
2236

23-
```js
37+
For example
38+
```kotlin
2439
f('leetcode') =
25-
(f('leetcod') && 'e' in dict) &&
26-
(f('leetco') && 'de' in dict) &&
27-
(f('leetc') && 'ode' in dict) &&
28-
(f('leet') && 'code' in dict) &&
29-
(f('lee') && 'tcode' in dict) &&
30-
(f('le') && 'etcode' in dict) &&
31-
(f('l') && 'eetcode' in dict) &&
40+
(f('leetcod') && 'e' in dict) ||
41+
(f('leetco') && 'de' in dict) ||
42+
(f('leetc') && 'ode' in dict) ||
43+
(f('leet') && 'code' in dict) ||
44+
(f('lee') && 'tcode' in dict) ||
45+
(f('le') && 'etcode' in dict) ||
46+
(f('l') && 'eetcode' in dict) ||
3247
(f('') && 'leetcode' in dict)
3348
```
3449

35-
### Top-Down DP
36-
50+
## Top-Down DP
3751
```kotlin
3852
fun wordBreak(s: String, wordDict: List<String>): Boolean {
3953
val memo = hashMapOf<String, Boolean>()
@@ -60,9 +74,10 @@ private fun breakdown(s: String, dict: HashSet<String>, memo: HashMap<String, Bo
6074
* **Space Complexity**: `O(n)` for memoization.
6175

6276
## Bottom-Up DP
63-
Let's define `dp[i]` as the state if we can build the substring `s[0:i]` from dictionary, and the state transition will be
77+
Let's define `dp[i]` as the state if we can build the substring `s[0:i]` from dictionary, and the state transition will be:
6478

6579
```js
80+
// s = 0.....j.....i
6681
// The state of s[0:i] = s[0:j] + s[j:i]
6782
dp[i] =
6883
dp[j] // The state of s[0:j],
@@ -110,6 +125,7 @@ fun wordBreak(s: String, wordDict: List<String>): Boolean {
110125
}
111126
```
112127

128+
## Dry Run
113129
```js
114130
println(wordBreak("cars", listOf("car", "rs", "ca")))
115131
0, 1, c

leetcode/264.ugly-number-ii.md

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
## [264. Ugly Number II](https://leetcode.com/problems/ugly-number-ii/)
1+
# [264. Ugly Number II](https://leetcode.com/problems/ugly-number-ii/)
22

3-
What's the next number? It would be the smallest number **all the previously existing numbers multiplied by 2 or 3, 5** that does't not shown before.
3+
## Dynamic Programming
4+
What's the next number? It would be the smallest number **all the previously existing ugly numbers multiplied by 2 or 3, 5** that hasn't shown before.
45

5-
We assume there is one number shown, which is `1`, the next number is `min(2 * 1, 3 * 1, 5 * 1) && not shown before`, that is 2, and you can think that comes from `min(2 * dp[1], 3 * dp[1], 5 * dp[1])`.
6+
We start calculating from `1`, the next number is `min(2 * 1, 3 * 1, 5 * 1)` that hasn't shown, that is 2, and you can think that is `dp[n = 2]` which comes from `min(2 * dp[n = 1], 3 * dp[1], 5 * dp[1])`.
67

7-
Again, what's the next number? Now we only need to consider `min(2 * 2, 3 * 1, 5 * 1)`, that would be `min(2 * dp[2], 3 * dp[1], 5 * dp[1])`, that means we have three pointers (for 2, 3, 5 respectively) points to the previous existing number, then we move the pointer if we use that in the round.
8+
Then, what's the next number? Now we only need to consider `min(2 * 2, 3 * 1, 5 * 1)`, that would be `min(2 * dp[2], 3 * dp[1], 5 * dp[1])`, that means we have three pointers (for 2, 3, 5 respectively) points to the previous existing number, then we move the pointer if we use that in the round.
89

910
```js
1011
n=1, dp[1] = 1 // base case
@@ -20,6 +21,23 @@ n=4, dp[4] = min(dp[2] * 2, dp[2] * 3, dp[1] * 5)
2021
// pointer 3 2 1
2122
```
2223

24+
The wrong way to think this problem is that we just maintain the three pointers and move them to the next when selecting it, and we can get the result. But the correct way is that we need to consider the next number **from the previous existing numbers**, and we need to maintain the three pointers to point to the previous existing number.
25+
26+
```js
27+
n=1, 1
28+
n=2, min(2 * 1, 3 * 1, 5 * 1) = 2
29+
n=3, min(2 * 2, 3 * 1, 5 * 1) = 3
30+
n=4, min(2 * 2, 3 * 2, 5 * 1) = 4
31+
n=5, min(2 * 3, 3 * 2, 5 * 1) = 5
32+
n=6, min(2 * 3, 3 * 2, 5 * 2) = 6
33+
// Which one to select?
34+
n=7, min(2 * 4, 3 * 2, 5 * 2) = 6 // select 2 * 3, wrong answer
35+
^^^^^
36+
37+
n=7, min(2 * 3, 3 * 3, 5 * 2) = 9 // select 3 * 2, wrong answer
38+
^^^^^
39+
```
40+
2341
```kotlin
2442
fun nthUglyNumber(n: Int): Int {
2543
if (n == 1) return n

leetcode/279.perfect-squares.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
11
## [279. Perfect Squares](https://leetcode.com/problems/perfect-squares/)
22

3+
```js
4+
f(12) = min(
5+
9 + f(3)
6+
4 + f(8)
7+
1 + f(11)
8+
)
9+
10+
f(3) = min(
11+
1 + f(2)
12+
)
13+
14+
f(8) = min(
15+
4 + f(4)
16+
1 + f(7)
17+
)
18+
19+
f(11) = min(
20+
9 + f(2)
21+
4 + f(7)
22+
1 + f(10)
23+
)
24+
25+
// So on...
26+
```
27+
28+
We can break down the problem into subproblems: `f(n)` = `min(f(n - i*i) + 1)` where `n - i*i >= 0` and `i` starts from 1.
29+
330
Suppose `dp[i]` represents the minimum perfect squares number to construct `i`.
431

532
```js

leetcode/300.longest-increasing-subsequence.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,14 @@ fun lengthOfLIS(nums: IntArray): Int {
8383
### Time Optimization with Binary Search
8484
We maintain a list for LIS, and iterate every item in the array:
8585
1. If the item is greater than the last item of LIS, we append it to the LIS. This forms a longer LIS.
86-
2. Otherwise, we find the first item in LIS that is greater than or equal to the current item, and replace it with the current item. This keeps the list sorted, and the LIS is still valid.
86+
2. Otherwise, we find the first item in LIS that is greater than or equal to the current item, and replace it with the current item. This keeps the list sorted, and the LIS is still valid. The LIS becomes smaller to form a possible longer LIS.
8787

88+
```js
89+
current = 3
90+
LIS = [1, 2, 7, 8]
91+
```
92+
93+
Then we find the first item in LIS that is greater than or equal to `3`, which is `7`. We replace `7` with `3`, and the LIS becomes `[1, 2, 3, 8]`.
8894
At last, the list size is the LIS.
8995

9096
```kotlin

0 commit comments

Comments
 (0)