A very common problem in computer programming is finding the longest increasing (decreasing) subsequence in a sequence of numbers (usually integers). Actually this is a typical dynamic programming problem.
We need to find maximum increasing subsequence length. In the brute-force approach, we can model this problem as -
If the current element is greater than the previous element, then we can either pick it or dont pick it because we may get a smaller element somewhere ahead which is greater than previous and picking that would be optimal. So we try both options.
If the current element is smaller or equal to previous element, it can’t be picked.
publicclassSolutio {
publicintlengthOfLIS(int[] nums) {
return findLIS(nums, 0, Integer.MIN_VALUE);
}
privateintdfs(int[] nums, int idx, int prev) {
if (idx == nums.length) {
return 0;
}
int pick = 0;
if (nums[idx]> prev) {
// we pick curr element as it is more than prev for now pick = 1 + dfs(nums, idx + 1, nums[idx]);
}
// this time we dont pick the element// 0 indicates length will not be updatedint dontPick = 0 + dfs(nums, idx + 1, prev);
return Math.max(pick, dontPick);
}
}
There are many unnecessary repeated calculations in the brute-force approach. We can observe that the length of increasing subsequence starting atĀ ith element with previously picked elementĀ prevĀ will always be the same. So we can useĀ dynamic programmingĀ to store the results for this state and reuse again in the future.
publicintlengthOfLIS(int[] nums) {
solve(nums, 0, Integer.MIN_VALUE, new HashMap<int[], Integer>());
}
privateintsolve(int[] nums, int idx, int prev, Map<int[], Integer> cache) {
if(idx == size(nums)) {return 0;}
int[] key =newint[idx, prev];
if(cache.containsKey(key)) {
return cache.get(key);
}
int pick = 0;
if (nums[idx]> prev) {
// we pick curr element as it is more than prev for now pick = 1 + solve(nums , idx + 1 , arr[idx], cache);
}
// this time we dont pick the element// 0 indicates length will not be updatedint dontPick = 0 + solve(nums , idx + 1 , prev, cache);
int ans = Math.max(pick , dontPick);
cache.put(key, ans);
return ans;
}
public Solution {
publicintlengthOfLIS(int[] nums) {
int n = nums.length;
solve(nums, 0, -1, new Integer[n][n + 1]);
}
privateintsolve(int[] nums, int idx, int prevIdx, Integer[][] memo) {
if (idx == nums.length) {
return 0;
}
if (memo[idx][prevIdx + 1]!=null) {
return memo[idx][prevIndex + 1];
}
int pick = 0;
if (prevIdx ==-1 || nums[idx]> nums[prevIdx]) {
// we pick curr element as it is more than prev for now pick = 1 + solve(nums, idx + 1, idx, memo);
}
// this time we dont pick the element// 0 indicates length will not be updatedint dontPick = 0 + solve(nums, idx + 1, prevIdx, memo);
int ans = Math.max(pick, dontPick);
memo[idx, prevIdx + 1]= ans;
return ans;
}
}
We can do better and further reduce the state stored using DP. It’s redundant to store states for allĀ iĀ havingĀ prevĀ as its previous element index. The length will always be greatest for the stateĀ (prev, prev)Ā since no more elements beforeĀ prevĀ can be taken. So we can just use a linear DP whereĀ dp[i]Ā denotes the LIS starting at indexĀ i
publicintlengthOfLIS(int[] nums) {
int n = nums.length;
solve(nums, 0, -1, new Integer[n+1]);
}
privateintsolve(int[] nums, int idx, int prevIdx, int[] memo) {
if(idx == nums.length) {return 0;}
if (memo[prevIdx+1]!=null ) {
return dp[idx];
}
int pick = 0;
if (prevIdx ==-1 || nums[idx]> nums[prevIdx]) {
// we pick curr element as it is more than prev for now pick = 1 + solve(nums , idx + 1 , idx, memo);
}
// this time we dont pick the element// 0 indicates length will not be updatedint dontPick = 0 + solve(nums , idx + 1 , prevIdx, memo);
int ans = Math.max(pick , dontPick);
memo[prevIdx+1]= ans;
return ans;
}
Let max[i] represent the length of the longest increasing subsequence so far.
If any element before i is smaller than nums[i], then max[i] = max(max[i], max[j]+1).
Here is an example:
publicintlengthOfLIS(int[] nums) {
if (nums ==null|| nums.length== 0)
return 0;
int n = nums.length;
int[] max =newint[n];
for (int i = n - 1; i >= 0; i--) {
max[i]= 1; // initially LIS is 1for (int j = i+1; j < n; j++) {
if (nums[i]< nums[j]) {
max[i]= Math.max(max[i], max[j]+ 1);
}
}
}
int result = 0;
for (int i = 0; i < max.length; i++) {
result = Math.max(result, max[i]);
}
return result;
}
In the brute-force approach, we were not sure if an element should be included or not to form the longest increasing subsequence and thus we explored both options. The problem lies in knowing if an element must be included in the sequence formed till now. Let’s instead try an approach where we include element whenever possible to maximize the length and if it’s not possible, then create a new subsequence and include it.
Traverse from 0 to n-1, the DP array keep the longest sequence.
if the val is bigger than largest in the dp array, add it to the end;
if it is among the sequence, return the idx that bigger than pres, update the array with this position if val is smaller than dp[idx];
This is to keep the sequence element with the smallest number.
You can solve the Longest Increasing Subsequence (LIS) problem using graph theory by constructing a directed acyclic graph (DAG) where each node represents an element of the array, and there’s a directed edge from node (i) to node (j) if (nums[i] < nums[j]) and (i < j).
The task then becomes finding the longest path in this DAG, which corresponds to the LIS.
This is how the graph look for:
1
nums =[9,1,3,7,5,6,20]
graph LR;
I(9) ---> S(20)
A(1) ---> C(3)
A(1) ---> G(7)
A(1) ---> E(5)
A(1) ---> F(6)
A(1) ---> S
C ---> G & E & F & S
G ---> S
E ---> F & S
F ---> S
style A fill:#FF9933
style C fill:#FF9933
style E fill:#FF9933
style F fill:#FF9933
style S fill:#FF9933
linkStyle 1 stroke:#6f6
linkStyle 7 stroke:#6f6
linkStyle 11 stroke:#6f6
linkStyle 13 stroke:#6f6
The path marked is longest increasing subsequence or longest path in the DAG.
Here are the steps:
Graph Construction:
Represent the array elements as nodes in a graph.
For each pair of elements ((nums[i], nums[j])) where (i < j) and (nums[i] < nums[j]), add a directed edge from node (i) to node (j).
Longest Path in DAG using Dynamic Programming:
Initialize aĀ dpĀ array of the same length as the input array, with each element initially set to 1 (dp[i] = 1). The valueĀ dp[i]Ā represents the length of the longest increasing subsequence ending at indexĀ i.
For each node (i) (from (0) to (n-1)):
For each node (j) (from (0) to (i-1)):
If there’s an edge from (j) to (i) (nums[j] < nums[i]), updateĀ dp[i]: [dp[i] = \max(dp[i], dp[j] + 1)]
The length of the longest increasing subsequence is the maximum value in theĀ dpĀ array.
classSolution {
publicintlengthOfLIS(int[] nums) {
int n = nums.length;
List<Integer>[] graph =new List[n];
for (int i = 0; i < n; i++) {
graph[i]=new ArrayList<Integer>();
}
for (int i = 0; i < n - 1; i++) {
for (int j = i + 1; j < n; j++) {
if (nums[i]<nums[j]) {
graph[i].add(j);
}
}
}
int dp[]=newint[n];
Arrays.fill(dp, 1);
for (int i = 0; i < n; i++) {
for (int j: graph[i]) {
dp[j]= Math.max(dp[j], dp[i]+ 1);
}
}
int ans = 1;
for (int i = 0; i < n; i++) {
ans = Math.max(ans, dp[i]);
}
return ans;
}
}