1
$\begingroup$

There is a well-known problem in CS of finding the maximum sum of non-consecutive integers in a list. There is even an SO post about how to solve it:

https://stackoverflow.com/questions/4487438/maximum-sum-of-non-consecutive-elements

That said, I have read this post many times, and many other posts online about how to solve this problem, and I still do not understand why the solution is correct.

Following another post about the problem (that shows the solution, but again lacks in an explanation as to why the solution is correct), I created the following snippit to run in JShell:

class MaxSubset {
    public static Integer maxSum(final List<Integer> ints) {
        final class MaxSubsetR {
            private Integer maxSumR(final List<Integer> ints, final Integer i, final String str) {
                if(i == 0) {
                    System.out.println("in i == 0: " + str);
                    System.out.println("in i == 0: get i " + ints.get(i) + " " + str);
                    System.out.println("in i == 0: get 0 " + ints.get(0) + " " + str);
                    System.out.println("returning: " + ints.get(0)); 
                    return ints.get(0); 
                }

                if(i == 1) {
                    System.out.println("in i == 1: " + str);
                    System.out.println("in i == 1: get i " + ints.get(i) + " " + str);
                    System.out.println("in i == 1: get 0 " + ints.get(0) + " " + str);
                    System.out.println("in i == 1: get 1 " + ints.get(1) + " " + str);
                    System.out.println("returning: " + Math.max(ints.get(0), ints.get(1)));
                    return Math.max(ints.get(0), ints.get(1));
                }
                System.out.println("no match " + i + " " + str);
                return Math.max(maxSumR(ints, (i - 1), "left"), ints.get(i) + maxSumR(ints, (i - 2), "right"));
            }
        }
        return new MaxSubsetR().maxSumR(ints, ints.size() - 1, "init");
    }
}

but after running the algorithm and watching the output, I'm still just as confused as I was when I began.

Can someone explain this algorithm to me? In particular:

1) How is the alternation of choosing (i - 1) or (i - 2) is even determined in the recursion in the first place?

2) How does simply subtracting 1 or 2 from the size of the array create the effect of selecting the optimal subset of non-adjacent items?

3) And just generally, how is it going about picking the right items from the list to achieve the stated goal?

New Question

4) We note that the List<Integer> never mutates during the recursion, so for the two if statements, i == 0 will always return the first item in the list, regardless of list size, and the i == 1 case will always return the greater of the first or second item in the list, regardless of list size. So, how is it that every item in the list is even being considered, if the return statements literally only care about arr[0] and arr[1]?

EDIT NOTES

I have re-written my second question to, hopefully, make it more clear what I mean.

For ease of reading, here is the algorithm in pseudo-code, without print statements:

func maxSum(Int[] arr, Int i)
  if(i == 0) return arr[0]
  if(i == 1) return Math.max(arr[0], arr[1])
  return Math.max(maxSum(arr, (i - 1)), maxSum(arr, (i - 2)) + arr[i])

and it would be called with

maxSum(arr, arr.size - 1)
$\endgroup$
1
$\begingroup$

Let me try to explain how to solve the problem. You are looking for a set of non-consecutive array elements with a maximum sum.

Let's say you have an array of 100 elements. Which ones of elements #99 and #100 can be in the optimal solution? They cannot both be present, because they would be consecutive. They cannot both be absent, because then adding #100 would create a larger sum without violating the rules. So one of #99 and #100 is present, and the other is not.

If #99 is present, then the solution is also the optimal solution for 99 elements. If #100 is present, then the items without #100 would be the optimal solution for 98 elements. So there are two possible ways to get an optimal solution for 100 elements: You take the optimal solution for 98 elements and add #100, or you take the optimal solution for 99 elements.

So f (A, n) = max (f (A, n-1), f (A, n-2) + A[n]) if n ≥ 2; f (A, 1) = A[1] and f (A, 0) = 0.

Note that implementing this using recursion is horribly inefficient. It's best to calculate f (A, 1), f (A, 2), f (A, 3) in sequence in linear time.

$\endgroup$
  • $\begingroup$ But if I had an array of 100 elements, and the solution has to contain any subset of non-adjacent elements that yields the maximum sum, why am I guaranteed that #99 or #100 must be in that subset? What if both #99 and #100 are negative? Then I would not want to include them in the subset at all since it reduces the from the maximum sum. $\endgroup$ – resu Aug 4 '18 at 16:27
  • $\begingroup$ @resu, the question in your link specifies that they are positive integers. $\endgroup$ – D.W. Aug 4 '18 at 23:16
  • $\begingroup$ Interestingly, the algorithm (but not the explanation) still works if the numbers are allowed to be negative, except f (A, 1) should be max (A[1], 0). $\endgroup$ – gnasher729 Aug 5 '18 at 14:53
0
$\begingroup$

Say we have an array Arr[0..i] and M(i) being the optimal solution of (maximum sum of non-adjacent elements) from 0 to i. We define M(0)=Arr[0] and M(-1)=0.

Then we build our recurrence relation as follows;

M(i) = Max(M(i-1),M(i-2)+Arr[i]) for 0..i

M(n) being the solution we want. I will try to explain your questions using examples.

  1. How is the alternation of choosing (i - 1) or (i - 2) is even determined in the recursion in the first place?

Here we choose the max of M(i-1) and M(i-2)+Arr[i], where M(i-1) alone means we take the solution before without adding Arr[i] to it since it would be two adjacent elements added. Next option is to take M(i-2)+Arr[i] this would be okay since (i-2)th solution wont be adjacent to the Arr[i] we are considering at the moment. Take note that since we set M(0)=0 before and if any i-1 or i-2 results in -1it will be M(-1)=0 thus it's like a recurrence exit condition. Also take note that M(i) here is the representation of maximum sum of non adjacent elements from 0 to i it's not a separate array. So if i=3, M(0)=0 and M(1) will be Max(M(0),M(-1)+Arr[1]) which is M(0)=0 and M(-1)=0 + Arr[1]'s value and so on for other values of i. You can use a Dynamic Programming approach and store the values of M() to not calculate repeating sub-problems over and over again. I believe these should answer all your bullet-points.

Just to clear any misunderstandings, M(i) is the maximum value of non-adjacent elements summed until element i(inclusive). That is why we choose the max of M(i-1) and M(i-2)+Arr[i], because if we add i to the sum until i-1 we would break the rule of non-adjacency that's why we consider the value of M(i-1) with Arr[i] not added and M(i-2) with Arr[i] added

Hope this helps!

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.