Max Array Sum Non-Adjacent
The Problem
Given an array of integers, find the subset of non-adjacent elements with the maximum sum and calculate the sum of that subset.
For example, given an array arr = [-2, 1, 3, -4, 5]
we have the following possible subsets:
Subset | Sum |
---|---|
[-2, 3, 5] | 6 |
[-2, 3] | 1 |
[-2, -4] | -6 |
[-2, 5] | 3 |
[1, -4] | -3 |
[1, 5] | 6 |
[3, 5] | 8 |
Some Background
On the surface, this might seem like a simple problem that you could jump in and start coding on. Go ahead and Google “max array sum non-adjacent” and give it a whirl, if you’re game. I did and found enough Techsarcana that I felt compelled to break things down here due to a lot of unexplained depth that should be addressed before attempting this solution naively.
First, this a variation on the Maximum Sub-array problem
, in that we are asked to work with non-adjacent entries in an array. Frankly, Max Sub-array
is hard enough to tackle on its own (with adjacent entries), if you’ve never come across this type of problem before. And to be asked to perform the variant should be considered cruel and unusual punishment.
There is a well-known solution for Max Sub-array
, in fact, and it was proposed decades ago by a man named Joseph Born Kadane. Kadane earned an A.B. in mathematics from Harvard College and a Ph.D. in statistics from Stanford, so don’t feel too bad if you didn’t conjure your own solution straight out of the gates.
Kadane’s Algorithm
function maxSumConsecutive(arr) {
for (let i = 1; i < arr.length; i++) {
if (arr[i - 1] > 0) {
arr[i] += arr[i - 1];
}
}
return arr;
}
Kadane’s algorithm performs the following steps:
- It loops through an array, starting at the second position of the array (
i=1
) through to the end. - It looks at the value of the index prior to the current index in the loop (
arr[i-1]
) and checks if that value is greater than 0. - If
arr[i-1]
is greater than zero, it will overwrite the current index value with the sum of the current index value and the prior index value. - It returns the altered array, whereby each index in the array (starting at
arr[1]
) is theMax Sum
up to that position in the array (left to right).
The result is that the highest value in the returned array is the Max Sum
of the array.
Running the algorithm on the array of [-2, 1, 3, -4, 5]
(from above) results in the following:
[ -2, 1, 4, 0, 5 ]
Here, 5 is the Max Sum
of the array, based on the contiguous subset of [1, 3, -4, 5]
or just [5]
, really. I know, not that exciting, but other arrays return similar (potentially more interesting) results. For example:
Input:
[-2, 1, -3, 4, -1, 2, 1, -5, 4]
Output:
[ -2, 1, -2, 4, 3, 5, 6, 1, 5 ]
Here, 6 is the Max Sum
of the array, based on the contiguous subset of [6, -2, -3, 1, 5]
.
Thank you, Kadane! 🎉
☝️ We’ve got a ways to go. Remember that we haven’t even touched on the idea of non-adjacent values yet. But at least now, we have a basis for deriving the
Max Sum
of a contiguous subset of an array.
Alternative function structure
Another way to write Kadane’s algorithm is like this:
function maxSumConsecutive(arr) {
let maxCurrent = arr[0];
let maxGlobal = arr[0];
for (let i = 1; i < arr.length; i++) {
if (arr[i] > maxCurrent + arr[i]) {
maxCurrent = arr[i];
} else {
maxCurrent = arr[i] + maxCurrent;
}
if (maxCurrent > maxGlobal) {
maxGlobal = maxCurrent;
}
}
return maxGlobal;
}
This function performs the following actions:
- It assigns the first value in the array to two variables:
maxCurrent
andmaxGlobal
. - It loops through array, starting at the second position of the array (
i=1
) through to the end. - On each pass through the loop:
- It compares the value of
arr[i]
with the sum of the valuesarr[1]
andmaxCurrent
and assigns the larger of the two values tomaxCurrent
. - It compares the values of
maxCurrent
andmaxGlobal
and assigns the value ofmaxCurrent
to maxGlobal ifmaxCurrent
is larger.
- It compares the value of
- It returns the value of
maxGlobal
after looping through all values in the array.
While the overall functionality of the algorithm has not changed, this structure–with the introduction of variables–gets us one step closer to being able to handle the non-adjacent part of the originally stated problem.
Using the Math.max() function
The if statements
in the above are kind of clunky, so lets use Math.max()
to clean things up a bit:
function maxSumConsecutive(arr) {
let maxCurrent = arr[0];
let maxGlobal = arr[0];
for (let i = 1; i < arr.length; i++) {
maxCurrent = Math.max(arr[i], maxCurrent + arr[i]);
maxGlobal = Math.max(maxGlobal, maxCurrent);
}
return maxGlobal;
}
☝️ Math.max() is a handy, built-in JavaScript function that returns the largest value from a comma-separated list. Other languages have a built-in version of
Math.max()
, so use the one that’s appropriate for you.
💫 I found this video to be one of the best in explaining Kadane’s algorithm. If your head is spinning at this point, try watching this video it few times. It may help.
I’ve intentionally used the same variable names (albeit with camelCase) and structure from the video examples, to make it easier to compare the video with what we’re working through.
Maximum Sub-Array Sum of Non-adjacent Values
🌶️ Here is where things get a bit spicy, but hang in there we’re nearly there!
To find all of the subsets of non-adjacent values in an array, we’re going to introduce a couple of new variables: maxInclude
and maxExclude
, which shall be arrays
instead of singular values (explained later). And we won’t be using maxCurrent
anymore.
maxInclude
and maxExclude
I found that a lot of folks explaining this problem began using include
and exclude
variables to demonstrate this problem’s solution without really explaining (very well or at all) what purpose each serves.
Case in point is this video. While the dude is obviously a very bright guy, I found the explanation to be full-on Techsarcana.
That said, the terms include
and exclude
are used here to define the scope of consideration for the value we are currently evaluating. In other words, as we loop through the array, we will either include a specific value for consideration because it is non-adjacent to the position in the array that we are currently evaluating, or we will exclude it from consideration because it is adjacent. This is what allows us to identify subsets of non-adjacent values.
The 4-year-old explanation
Okay, you are standing in line with a bunch of other numbers. Ignore the number directly behind you (excluding it from consideration because it is adjacent to you) and look at the number two places behind you (including it for consideration because it is not adjacent to you).
Solution
Here’s one possible solution:
function maxSumNonAdjacent(arr) {
let maxInclude = [];
let maxExclude = [];
maxInclude[0] = arr[0];
maxExclude[0] = 0;
let maxGlobal = arr[0];
for (let i = 1; i < arr.length; i++) {
maxInclude[i] = Math.max(maxExclude[i - 1] + arr[i], arr[i]);
maxExclude[i] = Math.max(maxInclude[i - 1], maxExclude[i - 1]);
maxGlobal = Math.max(maxExclude[i], maxInclude[i]);
}
return maxGlobal;
}
The above returns 8
when given the array, [-2, 1, 3, -4, 5]
.
Breakdown
- Declare
maxInclude
andmaxExclude
as empty arrays. - Assign the first value in
maxInclude
to the first value of the given array. - Assign the first value in
maxExclude
to 0. - Declare
maxGlobal
and set its value to the first value of the given array. - Loop through the array starting at the second position in the array (
[i=1]
). - Assign
maxinclude[i]
to the largest of either:- the sum of
maxExclude[i - 1]
andarr[i]
, or arr[i]
- the sum of
- Assign
maxExclude[i]
to the largest of either:maxInclude[i - 1]
, ormaxExclude[i - 1]
- Assign the larger of
maxExclude[i]
andmaxInclude[i]
tomaxGlobal
- Return
maxGlobal
Walk-through
Before the loop
maxInclude = [-2]
maxExclude = [0]
maxGlobal = -2
After the first the loop with i=1
arr[i] = 1
maxInclude = [-2, 1]
maxExclude = [0, 0]
maxGlobal = 1
After the second the loop with i=2
arr[i] = 3
maxInclude = [-2, 1, 3]
maxExclude = [0, 0, 1]
maxGlobal = 3
What’s happening here?
🛑 This is a good point to stop (in the middle of looping through the array) and explain what exactly is going on.
With the original array of [-2, 1, 3, -4, 5]
when i=2
arr[i] = 3
To derive maxInclude[i]
, we:
maxInclude[i] = Math.max(maxExclude[i - 1] + arr[i], arr[i]);
- look at
maxExclude[i - 1]
, which is 0. - add that to
arr[i]
, which is 3, to get 3. - take the larger value of that and
arr[i]
, which is 3
To derive maxExclude[i]
, we:
maxExclude[i] = Math.max(maxInclude[i - 1], maxExclude[i - 1]);
- compare at
maxInclude[i - 1]
, which is -2, tomaxExclude[i - 1]
, which is 0 - take the larger of those two values, which is 0.
To derive maxGlobal
, we take the larger of maxInclude[i] and maxExclude[i], which is 0.
maxGlobal = Math.max(maxExclude[i], maxInclude[i]);
Continuing on follows the same process…
After the third the loop with i=3
arr[i] = -4
maxInclude = [-2, 1, 3, -3]
maxExclude = [0, 0, 1, 3]
maxGlobal = 3
After the forth the loop with i=4
arr[i] = 5
maxInclude = [-2, 1, 3, -3, 8]
maxExclude = [0, 0, 1, 3, 3]
maxGlobal = 8
If you made it this far, well done! 🎉
I hope you got something out of it!