5 Best Ways to Find the Sum of Non-Adjacent Elements in a Circular List in Python

πŸ’‘ Problem Formulation: This article addresses the challenge of calculating the sum of non-adjacent elements in a circular list. A circular list can be visualized as a list where the first and last elements are adjacent. Given a circular list like [2, 1, 3, 5, 4], we are interested in finding the maximum sum of non-adjacent elements, which would be 9 (2 + 3 + 4) in this case. The following methods showcase different Pythonic approaches to solve this problem.

Method 1: Dynamic Programming

This method leverages dynamic programming by breaking the problem into simpler subproblems. It calculates the maximum non-adjacent sum for two cases: including the first element and excluding the last element, or excluding the first element and including the last element. The overall solution is the maximum of these two cases, as we account for the circular nature of the list.

Here’s an example:

def max_non_adjacent_sum(nums):
    if len(nums) <= 1:
        return nums[0] if nums else 0

    def helper(nums):
        include, exclude = 0, 0
        for num in nums:
            new_exclude = max(include, exclude)
            include = exclude + num
            exclude = new_exclude
        return max(include, exclude)
    
    return max(helper(nums[:-1]), helper(nums[1:]))

circular_list = [2, 1, 3, 5, 4]
print(max_non_adjacent_sum(circular_list))

Output: 9

This code snippet defines a function max_non_adjacent_sum() that applies a helper function to two different slices of the input list. The helper function helper() computes the max sum by iterating over the sliced list and updating the include and exclude variables according to the dynamic programming approach. The main function then returns the maximum result of the two cases.

Method 2: Recursive Solution

This method uses a recursive strategy to explore all possible combinations of non-adjacent elements. Each recursive call considers whether to include the current element (and skip the next) or exclude it. Due to recursion, this method is less efficient for larger lists but is a clear demonstration of the problem.

Here’s an example:

def rec_max_sum(arr, start, end):
    if end <= start:
        return 0
    return max(arr[start] + rec_max_sum(arr, start + 2, end), rec_max_sum(arr, start + 1, end))
    
def circular_rec_max_sum(nums):
    return max(rec_max_sum(nums, 0, len(nums)-1), rec_max_sum(nums, 1, len(nums)))

circular_list = [2, 1, 3, 5, 4]
print(circular_rec_max_sum(circular_list))

Output: 9

The function circular_rec_max_sum() uses a recursive helper function rec_max_sum() that either includes the current element and skips the adjacent one or excludes the current element. The main function calls this helper twice to handle the circular nature of the array, and the maximum of the two is the result.

Method 3: Iterative with Auxiliary Space

By iteratively calculating the maximum non-adjacent sum and storing the results in an auxiliary array, we can solve the problem efficiently. Similar to dynamic programming, however, it requires extra space proportional to the input list length.

Here’s an example:

def max_sum_non_adjacent(nums):
    n = len(nums)
    if n == 0: return 0
    if n == 1: return nums[0]
    table = [0] * n
    table[0], table[1] = nums[0], max(nums[0], nums[1])
    for i in range(2, n):
        table[i] = max(table[i - 1], table[i - 2] + nums[i])
    return max(table[-2], table[-3] + nums[0])

circular_list = [2, 1, 3, 5, 4]
print(max_sum_non_adjacent(circular_list))

Output: 9

The function max_sum_non_adjacent() initializes a table to keep track of the maximal sums, then it iteratively computes the values with regard to non-adjacent elements. Finally, considering the circular nature, it returns the maximum of either the last element of the table or the sum of the second-to-last element and the first element of the list.

Method 4: Improved Space Complexity

Enhancing Method 3, we can reduce the space complexity by using variables to track the maximum sums instead of an auxiliary table. This becomes a more space-efficient solution while maintaining the same time complexity.

Here’s an example:

def improved_max_sum_non_adjacent(nums):
    n = len(nums)
    if n == 1: return nums[0]
    first, second = nums[0], max(nums[0], nums[1])
    for i in range(2, n):
        current = max(second, first + nums[i])
        first, second = second, current
    return max(first, second - nums[0])

circular_list = [2, 1, 3, 5, 4]
print(improved_max_sum_non_adjacent(circular_list))

Output: 9

By using only two variables first and second, improved_max_sum_non_adjacent() reduces space usage. It iteratively updates these variables with the max non-adjacent sum. The final result accounts for the circularity by comparing the second variable and the sum of the first variable and the element at index 0.

Bonus One-Liner Method 5: Pythonic Approach with Comprehensions

This method showcases the Pythonic style, utilising list comprehensions and built-in functions to solve the problem in a concise manner. However, this approach does not deal with the circular nature of the list directly and is more for demonstration purposes.

Here’s an example:

max_non_adjacent_sum_circular = lambda nums: max(sum(nums[i] for i in range(len(nums)) if i % 2 == 0), sum(nums[i] for i in range(len(nums)) if i % 2 != 0))

circular_list = [2, 1, 3, 5, 4]
print(max_non_adjacent_sum_circular(circular_list))

Output: 6 or 7

This one-liner max_non_adjacent_sum_circular is a lambda function that calculates the sum of elements at even indices and compares it with the sum at odd indices using list comprehensions. This approach is oversimplified for the circular case as it does not consider all non-adjacent combinations.

Summary/Discussion

  • Method 1: Dynamic Programming. It’s an optimal approach for large lists. However, it can be complex to understand at first glance.
  • Method 2: Recursive Solution. The code is simpler and elegant but not suitable for large lists due to the stack overflow risk and high time complexity.
  • Method 3: Iterative with Auxiliary Space. This method strikes a balance between space and time complexity but requires additional memory proportional to the list’s length.
  • Method 4: Improved Space Complexity. It improves upon the previous method by using constant space while maintaining good performance.
  • Bonus Method 5: Pythonic Approach. Syntactically concise, this method is not necessarily practical for the problem due to oversimplification of the circular list’s nature.