Topics › Dynamic Programming › DP on LIS

Maximum Sum Increasing Subsequence

Medium TUF ID 698 SDE

Problem

Given an array arr of n positive integers, find the largest possible sum of a strictly increasing subsequence. A subsequence keeps the original order but may skip elements; “strictly increasing” means every chosen element is larger than the one chosen before it, so equal values cannot both be used.

Constraints in the usual version (GFG, “Maximum sum increasing subsequence”): 1 ≤ n ≤ 10^3, positive values. The last approach below also handles n = 10^5.

Example 1

arr = [1, 101, 2, 3, 100]
answer = 106        // 1 + 2 + 3 + 100

Example 2

arr = [4, 6, 1, 3, 8, 4, 6]
answer = 18         // 4 + 6 + 8

Example 3

arr = [1, 2, 3, 100, 4, 5, 6, 7]
answer = 106        // 1 + 2 + 3 + 100; the longest increasing subsequence 1..7 only sums to 28

Example 3 is the reason this is a separate problem from the Longest Increasing Subsequence: the longest chain and the heaviest chain can be completely different.

Approach 1: Recursion

Intuition. Walk through the array making one decision per element: skip it, or take it. Taking is allowed only if the element is larger than the last element taken, so the recursion needs to remember the index prev of the last taken element (-1 when nothing has been taken yet). The answer from index i onwards is the better of the two choices.

Algorithm.

  1. solve(i, prev): if i == n, return 0.
  2. skip = solve(i + 1, prev).
  3. If prev == -1 or arr[i] > arr[prev]: take = arr[i] + solve(i + 1, i); otherwise take = 0.
  4. Return max(skip, take); the answer is solve(0, -1).
static int recursion(int[] arr) {
    return solve(0, -1, arr);
}

/** Best sum obtainable from arr[i..] when the last chosen index is prev (-1 means nothing chosen yet). */
static int solve(int i, int prev, int[] arr) {
    if (i == arr.length) return 0;
    int skip = solve(i + 1, prev, arr);
    int take = 0;
    if (prev == -1 || arr[i] > arr[prev]) take = arr[i] + solve(i + 1, i, arr);
    return Math.max(skip, take);
}

Complexity. Time O(2^n): each element doubles the number of paths. Space O(n) for the recursion stack.

Approach 2: Memoization

Intuition. The result of solve(i, prev) depends only on the pair, and i ranges over n values while prev ranges over n + 1 values (including -1). So there are only O(n^2) distinct calls. Store each result in memo[i][prev + 1]; the + 1 shift lets prev = -1 use column 0. Since all values are positive, every real result is ≥ 0 and -1 can mark “not computed”.

static int memoization(int[] arr) {
    int n = arr.length;
    int[][] memo = new int[n][n + 1];
    for (int[] row : memo) Arrays.fill(row, -1);
    return solveMemo(0, -1, arr, memo);
}

static int solveMemo(int i, int prev, int[] arr, int[][] memo) {
    if (i == arr.length) return 0;
    if (memo[i][prev + 1] != -1) return memo[i][prev + 1];
    int skip = solveMemo(i + 1, prev, arr, memo);
    int take = 0;
    if (prev == -1 || arr[i] > arr[prev]) take = arr[i] + solveMemo(i + 1, i, arr, memo);
    return memo[i][prev + 1] = Math.max(skip, take);
}

Complexity. Time O(n^2), space O(n^2) for the table plus O(n) stack.

Approach 3: Tabulation, best sum ending at each index

Intuition. A cleaner state uses only one index. Let dp[i] be the largest sum of an increasing subsequence that ends exactly at index i. In such a subsequence, the element before arr[i] (if there is one) is some arr[j] with j < i and arr[j] < arr[i]. The part ending at j should itself be as heavy as possible: replacing it with a heavier increasing subsequence that also ends at arr[j] keeps the whole thing increasing. Hence

dp[i] = arr[i] + max(0, max of dp[j] over j < i with arr[j] < arr[i]),

and the answer is the largest dp[i]. This also cuts the memory from O(n^2) to O(n).

Algorithm.

  1. For each i: start with dp[i] = arr[i] (the subsequence containing only arr[i]).
  2. For every j < i with arr[j] < arr[i]: dp[i] = max(dp[i], dp[j] + arr[i]).
  3. Track the maximum over all dp[i].
static int tabulation(int[] arr) {
    int n = arr.length, best = 0;
    int[] dp = new int[n];
    for (int i = 0; i < n; i++) {
        dp[i] = arr[i];                                    // the subsequence containing only arr[i]
        for (int j = 0; j < i; j++) {
            if (arr[j] < arr[i]) dp[i] = Math.max(dp[i], dp[j] + arr[i]);
        }
        best = Math.max(best, dp[i]);
    }
    return best;
}

Dry run on Example 1:

i arr[i] usable j (arr[j] < arr[i]) dp[i]
0 1 none 1
1 101 0 1 + 101 = 102
2 2 0 1 + 2 = 3
3 3 0, 2 max(1, 3) + 3 = 6
4 100 0, 2, 3 (not 1, since 101 > 100) 6 + 100 = 106

Complexity. Time O(n^2) from the double loop, space O(n).

Approach 4: Optimal, Fenwick tree over values

Intuition. The inner loop answers one question: “what is the largest dp[j] among earlier positions whose value is smaller than arr[i]?” If we index the stored dp values by array value instead of by position, this becomes a prefix-maximum query over all values below arr[i], on a structure that receives one update per step as we move left to right. A Fenwick tree (binary indexed tree) supports both operations in O(log m):

A Fenwick tree usually stores sums, but it also works for maximum as long as stored values only ever go up, which is true here. Values can be large, so we first compress them to ranks 1..m among the distinct values. Querying rank r - 1 instead of r excludes equal values, which keeps the subsequence strictly increasing.

Algorithm.

  1. Sort the distinct values; the rank of x is its position in that list plus one.
  2. For each x in array order: bestBefore = query(rank(x) - 1), cur = bestBefore + x, then update(rank(x), cur) and update the answer.
static int fenwick(int[] arr) {
    int[] sorted = Arrays.stream(arr).distinct().sorted().toArray();
    int[] tree = new int[sorted.length + 1];               // tree over ranks 1..m, storing prefix maxima of dp
    int best = 0;
    for (int x : arr) {
        int rank = Arrays.binarySearch(sorted, x) + 1;     // 1-based rank of x among distinct values
        int bestBefore = 0;
        for (int r = rank - 1; r > 0; r -= r & -r) bestBefore = Math.max(bestBefore, tree[r]);   // values strictly below x
        int cur = bestBefore + x;                          // dp for this position
        for (int r = rank; r < tree.length; r += r & -r) tree[r] = Math.max(tree[r], cur);
        best = Math.max(best, cur);
    }
    return best;
}

Dry run on Example 1. The distinct sorted values are [1, 2, 3, 100, 101], with ranks 1..5.

x rank best dp among smaller values cur
1 1 0 1
101 5 1 102
2 2 1 3
3 3 3 6
100 4 6 106

Note how 101 does not influence 100: it sits at rank 5, outside the query range 1..3.

Complexity. Time O(n log n): the sort, plus one binary search, one query and one update of O(log n) each per element. Space O(n). The Java file runs it on 10^5 elements.

Edge cases and pitfalls