Skip to content

Commit 43f0d1f

Browse files
committed
feat(algorithms, dynamic-programming): longest increasing subsequence
1 parent 15abdc5 commit 43f0d1f

File tree

3 files changed

+134
-53
lines changed

3 files changed

+134
-53
lines changed

algorithms/dynamic_programming/longest_increasing_subsequence/README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,75 @@ Explanation: The longest increasing subsequence is {3, 7, 40, 80}
4141
- Array
4242
- Binary Search
4343
- Dynamic Programming
44+
- Binary Indexed Tree
45+
- Segment Tree
46+
47+
---
48+
# Number of Longest Increasing Subsequence
49+
50+
Given an integer array nums, return the number of longest increasing subsequences.
51+
52+
Notice that the sequence has to be strictly increasing.
53+
54+
## Examples
55+
56+
Example 1:
57+
```text
58+
Input: nums = [1,3,5,4,7]
59+
Output: 2
60+
Explanation: The two longest increasing subsequences are [1, 3, 4, 7] and [1, 3, 5, 7].
61+
```
62+
63+
Example 2:
64+
```text
65+
Input: nums = [2,2,2,2,2]
66+
Output: 5
67+
Explanation: The length of the longest increasing subsequence is 1, and there are 5 increasing subsequences of length 1,
68+
so output 5.
69+
```
70+
71+
## Related Topics
72+
73+
- Array
74+
- Dynamic Programming
75+
- Binary Indexed Tree
76+
- Segment Tree
77+
78+
## Solution
79+
80+
The key intuition behind this algorithm is that for every position in the array, we figure out two things: the length of
81+
the longest increasing subsequence that ends there, and how many such subsequences exist. By looking at all earlier
82+
elements, we extend any subsequence that can grow by placing the current number at the end. Each time we form a longer
83+
subsequence, we update both its length and its count, and if we find another way to reach the same length, we add those
84+
counts as well. In the end, the longest subsequence length across all positions is known, and by summing how many
85+
subsequences achieve that length, we determine the total number of longest increasing subsequences.
86+
87+
Using the intuition above, we implement the algorithm as follows:
88+
89+
1. Start by creating two arrays of the same size as the input: length to track the length of the longest increasing
90+
subsequence ending at each index, and count to track how many such subsequences end at that index. Initialize both
91+
arrays with 1 because every element alone forms an increasing subsequence of length 1
92+
2. Initialize a variable, maxLen, with 1 to track the longest subsequence length found so far.
93+
3. Iterate through each element in the array using index i. For each i:
94+
- Check all previous elements using index j, where j is less than i.
95+
- For each pair, check if nums[j] is less than nums[i]. If so:
96+
- If extending the subsequence from j creates a longer subsequence than what is currently recorded for i:
97+
- Update the length at i to be length[j] + 1.
98+
- Update count[i] to count[j] because all subsequences ending at j now extend to i.
99+
- Otherwise, if extending from j produces a subsequence of the same length as the best known for i:
100+
- Add count[j] to count[i]. This accumulates all valid ways to produce the same LIS length ending at i.
101+
- After processing all j for index i, update maxLen if length[i] is larger.
102+
4. Initialize a variable, result, with 0 to accumulate the final count.
103+
5. Scan through the length array to identify the indexes that achieved the maximum LIS length:
104+
- If length[i] equals maxLen, add count[i] to result.
105+
6. Return result as the total number of longest increasing subsequences.
106+
107+
### Time Complexity
108+
109+
The algorithm’s time complexity is O(n^2), where n is the number of elements in nums. This is because for each element
110+
in the array, we compare it with all previous elements to determine whether it can extend an increasing subsequence.
111+
This results in a nested loop over the input size n.
112+
113+
### Space Complexity
114+
115+
The algorithm’s space complexity is O(n) because we use two additional arrays, length and count, each of size n.

algorithms/dynamic_programming/longest_increasing_subsequence/__init__.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,38 @@ def longest_increasing_subsequence(nums: List[int]) -> int:
2020
piles.append(num)
2121

2222
return len(piles)
23+
24+
25+
def find_number_of_lis(nums: List[int]) -> int:
26+
n = len(nums)
27+
# length[i]: length of the LIS ending at i
28+
# count[i]: number of LIS of that length ending at i
29+
length = [1] * n
30+
count = [1] * n
31+
32+
# Try to build LIS ending at each index i
33+
for i in range(n):
34+
# Check all previous positions j < i
35+
for j in range(i):
36+
# Can nums[i] extend an increasing subsequence from j?
37+
if nums[j] < nums[i]:
38+
# Found a strictly longer subsequence ending at i
39+
if length[j] + 1 > length[i]:
40+
length[i] = length[j] + 1
41+
# inherit all ways ending at j
42+
count[i] = 0
43+
# Found another way to reach the same LIS length at i
44+
if length[j] + 1 == length[i]:
45+
# accumulate all valid paths
46+
count[i] += count[j]
47+
48+
# Update global maximum LIS length
49+
max_length = max(length)
50+
result = 0
51+
52+
# Sum counts of all positions that achieve the LIS length
53+
for i in range(n):
54+
if length[i] == max_length:
55+
result += count[i]
56+
# total number of longest increasing subsequences
57+
return result

algorithms/dynamic_programming/longest_increasing_subsequence/test_longest_increasing_subsequence.py

Lines changed: 27 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,37 @@
11
import unittest
2-
3-
from . import longest_increasing_subsequence
2+
from typing import List
3+
from parameterized import parameterized
4+
from algorithms.dynamic_programming.longest_increasing_subsequence import (
5+
longest_increasing_subsequence,
6+
find_number_of_lis,
7+
)
8+
9+
LONGEST_INCREASING_SUBSEQUENCE_TEST = [
10+
([1, 2, 1, 5], 3),
11+
([0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15], 6),
12+
([3, 10, 2, 1, 20], 3),
13+
([3, 2], 1),
14+
([50, 3, 10, 7, 40, 80], 4),
15+
([10, 9, 2, 5, 3, 7, 101, 18], 4),
16+
([0, 1, 0, 3, 2, 3], 4),
17+
([7, 7, 7, 7, 7, 7, 7], 1),
18+
]
19+
20+
FIND_NUMBER_OF_LONGEST_INCREASING_SUBSEQUENCE_TEST_CASES = [
21+
([1, 3, 5, 4, 7], 2),
22+
([2, 2, 2, 2, 2], 5),
23+
]
424

525

626
class LongestIncreasingSubsequenceTestCase(unittest.TestCase):
7-
def test_1(self):
8-
"""should return 3 from input of [1, 2, 1, 5]"""
9-
nums = [1, 2, 1, 5]
10-
expected = 3
11-
actual = longest_increasing_subsequence(nums)
12-
self.assertEqual(expected, actual)
13-
14-
def test_2(self):
15-
"""should return 6 from input of [0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15]"""
16-
nums = [0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15]
17-
expected = 6
18-
actual = longest_increasing_subsequence(nums)
19-
self.assertEqual(expected, actual)
20-
21-
def test_3(self):
22-
"""should return 3 from input of [3, 10, 2, 1, 20]"""
23-
nums = [3, 10, 2, 1, 20]
24-
expected = 3
25-
actual = longest_increasing_subsequence(nums)
26-
self.assertEqual(expected, actual)
27-
28-
def test_4(self):
29-
"""should return 1 from input of [3, 2]"""
30-
nums = [3, 2]
31-
expected = 1
27+
@parameterized.expand(LONGEST_INCREASING_SUBSEQUENCE_TEST)
28+
def test_longest_increasing_subsequence(self, nums: List[int], expected: int):
3229
actual = longest_increasing_subsequence(nums)
3330
self.assertEqual(expected, actual)
3431

35-
def test_5(self):
36-
"""should return 4 from input of [50, 3, 10, 7, 40, 80]"""
37-
nums = [50, 3, 10, 7, 40, 80]
38-
expected = 4
39-
actual = longest_increasing_subsequence(nums)
40-
self.assertEqual(expected, actual)
41-
42-
def test_6(self):
43-
"""should return 4 from input of [10,9,2,5,3,7,101,18]"""
44-
nums = [10, 9, 2, 5, 3, 7, 101, 18]
45-
expected = 4
46-
actual = longest_increasing_subsequence(nums)
47-
self.assertEqual(expected, actual)
48-
49-
def test_7(self):
50-
"""should return 4 from input of [0,1,0,3,2,3]"""
51-
nums = [0, 1, 0, 3, 2, 3]
52-
expected = 4
53-
actual = longest_increasing_subsequence(nums)
54-
self.assertEqual(expected, actual)
55-
56-
def test_8(self):
57-
"""should return 4 from input of [7,7,7,7,7,7,7]"""
58-
nums = [7, 7, 7, 7, 7, 7, 7]
59-
expected = 1
60-
actual = longest_increasing_subsequence(nums)
32+
@parameterized.expand(FIND_NUMBER_OF_LONGEST_INCREASING_SUBSEQUENCE_TEST_CASES)
33+
def test_find_number_of_lis(self, nums: List[int], expected: int):
34+
actual = find_number_of_lis(nums)
6135
self.assertEqual(expected, actual)
6236

6337

0 commit comments

Comments
 (0)