2926. Maximum Balanced Subsequence Sum
Problem Description
You are given a 0-indexed integer array nums.
A subsequence of nums with length k and consisting of indices i₀ < i₁ < ... < i_{k-1} is called balanced if it satisfies the following condition:
For every j in the range [1, k-1]: nums[i_j] - nums[i_{j-1}] >= i_j - i_{j-1}
In other words, the difference between consecutive values in the subsequence must be at least as large as the difference between their indices.
A subsequence of length 1 is automatically considered balanced.
Your task is to find the maximum possible sum of elements in any balanced subsequence of nums.
Key Points:
- A subsequence maintains the relative order of elements from the original array but doesn't need to be contiguous
- The balance condition requires that the value increase between consecutive elements is at least as much as the index increase
- You need to maximize the sum of the selected elements while maintaining the balance property
Example Understanding: If you select indices 2 and 5 from the array, then:
- The values are
nums[2]andnums[5] - The balance condition requires:
nums[5] - nums[2] >= 5 - 2 = 3 - These two indices can form a balanced subsequence only if the value at index 5 is at least 3 more than the value at index 2
Intuition
Let's start by understanding what the balance condition really means. We have nums[i_j] - nums[i_{j-1}] >= i_j - i_{j-1} for consecutive elements in our subsequence.
If we rearrange this inequality, we get: nums[i_j] - i_j >= nums[i_{j-1}] - i_{j-1}
This is a key insight! If we create a new array arr where arr[i] = nums[i] - i, then the balance condition simply becomes: for any two consecutive indices in our subsequence, the corresponding values in arr must be non-decreasing.
In other words, a balanced subsequence in the original array corresponds to selecting indices where the arr values form a non-decreasing sequence.
Now our problem transforms into: given the array arr, select a subset of indices such that:
- The
arrvalues at these indices are non-decreasing - The sum of the corresponding
numsvalues is maximized
This looks like a dynamic programming problem where for each index i, we need to find the best subsequence ending at i. We can define f[i] as the maximum sum achievable by a balanced subsequence ending at index i.
For each position i, we look at all previous positions j where arr[j] <= arr[i] (meaning we can extend the subsequence from j to i), and choose the one that gives us the maximum sum. The recurrence is: f[i] = max(f[j] for all j < i where arr[j] <= arr[i]) + nums[i]
However, finding all such j values for each i would be O(n²). To optimize this, we need a data structure that can efficiently:
- Query the maximum
f[j]value among alljwherearr[j] <= arr[i] - Update the value at a given position
A Binary Indexed Tree (Fenwick Tree) is perfect for this! We can use coordinate compression to map the arr values to a smaller range, then use the BIT to maintain prefix maximums. This reduces our complexity to O(n log n).
Learn more about Segment Tree, Binary Search and Dynamic Programming patterns.
Solution Implementation
1from typing import List 2from bisect import bisect_left 3 4class BinaryIndexedTree: 5 """ 6 Binary Indexed Tree (Fenwick Tree) for range maximum queries. 7 This implementation supports point updates and prefix maximum queries. 8 """ 9 10 def __init__(self, n: int): 11 """ 12 Initialize the Binary Indexed Tree with size n. 13 14 Args: 15 n: The size of the tree (1-indexed) 16 """ 17 self.size = n 18 # Tree array initialized with negative infinity (1-indexed) 19 self.tree = [float('-inf')] * (n + 1) 20 21 def update(self, index: int, value: int): 22 """ 23 Update the tree at position index with maximum of current and new value. 24 25 Args: 26 index: The position to update (1-indexed) 27 value: The new value to consider for maximum 28 """ 29 while index <= self.size: 30 # Update current position with maximum value 31 self.tree[index] = max(self.tree[index], value) 32 # Move to next position affected by this update 33 # Adding the least significant bit moves to parent in update tree 34 index += index & -index 35 36 def query(self, index: int) -> int: 37 """ 38 Query the maximum value in range [1, index]. 39 40 Args: 41 index: The right boundary of the query range (1-indexed) 42 43 Returns: 44 The maximum value in the range [1, index] 45 """ 46 maximum_value = float('-inf') 47 while index > 0: 48 # Consider current position's value 49 maximum_value = max(maximum_value, self.tree[index]) 50 # Move to previous range by removing least significant bit 51 index -= index & -index 52 return maximum_value 53 54 55class Solution: 56 def maxBalancedSubsequenceSum(self, nums: List[int]) -> int: 57 """ 58 Find the maximum sum of a balanced subsequence. 59 A subsequence is balanced if for any two consecutive elements at positions i and j, 60 nums[j] - nums[i] >= j - i. 61 62 Args: 63 nums: The input array of integers 64 65 Returns: 66 The maximum sum of a balanced subsequence 67 """ 68 # Transform the constraint: nums[j] - nums[i] >= j - i 69 # Rearranging: nums[j] - j >= nums[i] - i 70 # Create array of transformed values 71 transformed_values = [nums[i] - i for i in range(len(nums))] 72 73 # Compress coordinates: map transformed values to ranks 74 # This allows us to use them as indices in the BIT 75 sorted_unique_values = sorted(set(transformed_values)) 76 77 # Initialize Binary Indexed Tree with compressed coordinate space size 78 bit = BinaryIndexedTree(len(sorted_unique_values)) 79 80 # Process each element in the original array 81 for i, current_num in enumerate(nums): 82 # Find the compressed coordinate (rank) of the transformed value 83 # Add 1 because BIT is 1-indexed 84 compressed_index = bisect_left(sorted_unique_values, current_num - i) + 1 85 86 # Query maximum sum ending at or before current transformed value 87 # Take max with 0 to allow starting a new subsequence 88 max_previous_sum = max(bit.query(compressed_index), 0) 89 90 # Calculate new sum including current element 91 new_sum = max_previous_sum + current_num 92 93 # Update the BIT with the new maximum sum at this position 94 bit.update(compressed_index, new_sum) 95 96 # Return the overall maximum sum found 97 return bit.query(len(sorted_unique_values)) 981/** 2 * Binary Indexed Tree (Fenwick Tree) for range maximum queries 3 * This implementation maintains maximum values instead of sums 4 */ 5class BinaryIndexedTree { 6 private int size; 7 private long[] tree; 8 private final long NEGATIVE_INFINITY = 1L << 60; 9 10 /** 11 * Initialize BIT with given size 12 * @param size the size of the tree 13 */ 14 public BinaryIndexedTree(int size) { 15 this.size = size; 16 this.tree = new long[size + 1]; 17 Arrays.fill(this.tree, -NEGATIVE_INFINITY); 18 } 19 20 /** 21 * Update the tree with maximum value at position x 22 * @param position the 1-indexed position to update 23 * @param value the value to potentially update with (takes max) 24 */ 25 public void update(int position, long value) { 26 while (position <= size) { 27 tree[position] = Math.max(tree[position], value); 28 // Move to next position affected by this update 29 position += position & -position; 30 } 31 } 32 33 /** 34 * Query the maximum value from index 1 to x 35 * @param position the 1-indexed position to query up to 36 * @return the maximum value in range [1, position] 37 */ 38 public long query(int position) { 39 long maximum = -NEGATIVE_INFINITY; 40 while (position > 0) { 41 maximum = Math.max(maximum, tree[position]); 42 // Move to parent node 43 position -= position & -position; 44 } 45 return maximum; 46 } 47} 48 49/** 50 * Solution for finding maximum balanced subsequence sum 51 * A subsequence is balanced if nums[i_j] - nums[i_{j-1}] >= i_j - i_{j-1} 52 * This can be rewritten as nums[i_j] - i_j >= nums[i_{j-1}] - i_{j-1} 53 */ 54class Solution { 55 /** 56 * Find the maximum sum of a balanced subsequence 57 * @param nums the input array 58 * @return the maximum sum of any balanced subsequence 59 */ 60 public long maxBalancedSubsequenceSum(int[] nums) { 61 int n = nums.length; 62 63 // Transform array: store nums[i] - i for each position 64 // This transformation helps identify valid balanced subsequences 65 int[] transformedValues = new int[n]; 66 for (int i = 0; i < n; ++i) { 67 transformedValues[i] = nums[i] - i; 68 } 69 70 // Sort and remove duplicates to create coordinate compression 71 Arrays.sort(transformedValues); 72 int uniqueCount = 0; 73 for (int i = 0; i < n; ++i) { 74 if (i == 0 || transformedValues[i] != transformedValues[i - 1]) { 75 transformedValues[uniqueCount++] = transformedValues[i]; 76 } 77 } 78 79 // Initialize BIT with compressed coordinate space 80 BinaryIndexedTree fenwickTree = new BinaryIndexedTree(uniqueCount); 81 82 // Process each element in original order 83 for (int i = 0; i < n; ++i) { 84 // Find compressed coordinate of current transformed value 85 int compressedIndex = binarySearch(transformedValues, nums[i] - i, uniqueCount) + 1; 86 87 // Query maximum sum ending at values <= current transformed value 88 // Add current value to extend the subsequence 89 long maxSum = Math.max(fenwickTree.query(compressedIndex), 0) + nums[i]; 90 91 // Update the tree with new maximum at this position 92 fenwickTree.update(compressedIndex, maxSum); 93 } 94 95 // Return the overall maximum from all possible subsequences 96 return fenwickTree.query(uniqueCount); 97 } 98 99 /** 100 * Binary search to find leftmost position of target value 101 * @param array the sorted array to search 102 * @param target the value to search for 103 * @param right the right boundary (exclusive) 104 * @return the index of first occurrence of target or where it would be inserted 105 */ 106 private int binarySearch(int[] array, int target, int right) { 107 int left = 0; 108 while (left < right) { 109 int mid = (left + right) >> 1; 110 if (array[mid] >= target) { 111 right = mid; 112 } else { 113 left = mid + 1; 114 } 115 } 116 return left; 117 } 118} 1191class BinaryIndexedTree { 2private: 3 int size; 4 vector<long long> tree; 5 const long long NEGATIVE_INFINITY = 1e18; 6 7public: 8 // Initialize BIT with given size, all values set to negative infinity 9 BinaryIndexedTree(int n) { 10 this->size = n; 11 tree.resize(n + 1, -NEGATIVE_INFINITY); 12 } 13 14 // Update the BIT at position x with maximum value v 15 // Uses the BIT update pattern: move to next index by adding lowbit 16 void update(int index, long long value) { 17 while (index <= size) { 18 tree[index] = max(tree[index], value); 19 index += index & -index; // Add lowbit to move to next position 20 } 21 } 22 23 // Query the maximum value from index 1 to x 24 // Uses the BIT query pattern: move to previous index by subtracting lowbit 25 long long query(int index) { 26 long long maxValue = -NEGATIVE_INFINITY; 27 while (index > 0) { 28 maxValue = max(maxValue, tree[index]); 29 index -= index & -index; // Subtract lowbit to move to previous position 30 } 31 return maxValue; 32 } 33}; 34 35class Solution { 36public: 37 long long maxBalancedSubsequenceSum(vector<int>& nums) { 38 int n = nums.size(); 39 40 // Transform nums[i] to nums[i] - i to handle the balanced condition 41 // A balanced subsequence satisfies: nums[i_j] - nums[i_{j-1}] >= i_j - i_{j-1} 42 // Which transforms to: nums[i_j] - i_j >= nums[i_{j-1}] - i_{j-1} 43 vector<int> transformedValues(n); 44 for (int i = 0; i < n; ++i) { 45 transformedValues[i] = nums[i] - i; 46 } 47 48 // Coordinate compression: sort and remove duplicates 49 // This maps the transformed values to a smaller range [1, m] 50 sort(transformedValues.begin(), transformedValues.end()); 51 transformedValues.erase(unique(transformedValues.begin(), transformedValues.end()), 52 transformedValues.end()); 53 int compressedSize = transformedValues.size(); 54 55 // Initialize BIT with compressed size 56 BinaryIndexedTree fenwickTree(compressedSize); 57 58 // Process each element in original order 59 for (int i = 0; i < n; ++i) { 60 // Find the compressed index for current transformed value 61 // Add 1 because BIT uses 1-based indexing 62 int compressedIndex = lower_bound(transformedValues.begin(), 63 transformedValues.end(), 64 nums[i] - i) - transformedValues.begin() + 1; 65 66 // Calculate maximum sum ending at current position 67 // Either start new subsequence (0) or extend previous subsequence 68 long long currentMaxSum = max(fenwickTree.query(compressedIndex), 0LL) + nums[i]; 69 70 // Update BIT with the new maximum sum at this compressed index 71 fenwickTree.update(compressedIndex, currentMaxSum); 72 } 73 74 // Return the overall maximum sum by querying all positions 75 return fenwickTree.query(compressedSize); 76 } 77}; 781// Binary Indexed Tree (Fenwick Tree) for range maximum queries 2let n: number; 3let c: number[]; 4 5// Initialize the BIT with size and fill with negative infinity 6function initBIT(size: number): void { 7 n = size; 8 c = Array(size + 1).fill(-Infinity); 9} 10 11// Update the BIT at position x with maximum value v 12function update(x: number, v: number): void { 13 while (x <= n) { 14 c[x] = Math.max(c[x], v); 15 x += x & -x; // Move to next position using lowbit 16 } 17} 18 19// Query the maximum value from position 1 to x 20function query(x: number): number { 21 let maxValue = -Infinity; 22 while (x > 0) { 23 maxValue = Math.max(maxValue, c[x]); 24 x -= x & -x; // Move to parent position using lowbit 25 } 26 return maxValue; 27} 28 29// Binary search to find the leftmost position where arr[mid] >= target 30function binarySearch(arr: number[], target: number): number { 31 let left = 0; 32 let right = arr.length; 33 34 while (left < right) { 35 const mid = (left + right) >> 1; // Equivalent to Math.floor((left + right) / 2) 36 if (arr[mid] >= target) { 37 right = mid; 38 } else { 39 left = mid + 1; 40 } 41 } 42 return left; 43} 44 45// Find the maximum sum of a balanced subsequence 46// A subsequence is balanced if nums[i_j] - nums[i_{j-1}] >= i_j - i_{j-1} 47// Which can be rewritten as nums[i_j] - i_j >= nums[i_{j-1}] - i_{j-1} 48function maxBalancedSubsequenceSum(nums: number[]): number { 49 const length = nums.length; 50 51 // Create array of (nums[i] - i) values for comparison 52 const differences = Array(length).fill(0); 53 for (let i = 0; i < length; ++i) { 54 differences[i] = nums[i] - i; 55 } 56 57 // Sort differences to prepare for coordinate compression 58 differences.sort((a, b) => a - b); 59 60 // Remove duplicates from sorted array (coordinate compression) 61 let uniqueCount = 0; 62 for (let i = 0; i < length; ++i) { 63 if (i === 0 || differences[i] !== differences[i - 1]) { 64 differences[uniqueCount++] = differences[i]; 65 } 66 } 67 differences.length = uniqueCount; 68 69 // Initialize BIT with compressed size 70 initBIT(uniqueCount); 71 72 // Process each element in original order 73 for (let i = 0; i < length; ++i) { 74 // Find compressed index for current element's (nums[i] - i) value 75 const compressedIndex = binarySearch(differences, nums[i] - i) + 1; // +1 for 1-indexed BIT 76 77 // Query maximum sum ending before current position, add current value 78 const maxSum = Math.max(query(compressedIndex), 0) + nums[i]; 79 80 // Update BIT with new maximum sum at this position 81 update(compressedIndex, maxSum); 82 } 83 84 // Return the overall maximum sum 85 return query(uniqueCount); 86} 87Solution Approach
Based on our intuition, let's implement the solution step by step:
Step 1: Transform the Problem
First, we create the transformed array arr where arr[i] = nums[i] - i:
arr = [x - i for i, x in enumerate(nums)]
Step 2: Coordinate Compression
Since we'll use a Binary Indexed Tree indexed by the values in arr, we need to compress these values to a smaller range [1, m] where m is the number of unique values:
s = sorted(set(arr)) # Get unique values and sort them
This creates a mapping where each unique value in arr gets an index from 1 to len(s).
Step 3: Binary Indexed Tree for Maximum Queries
We implement a BIT that supports:
update(x, v): Update positionxwith valuev(keeping the maximum)query(x): Get the maximum value in the range[1, x]
The BIT is initialized with negative infinity values since we're looking for maximums:
self.c = [-inf] * (n + 1)
Step 4: Dynamic Programming with BIT
For each element in nums, we:
- Find the compressed index of
arr[i] = nums[i] - i:
j = bisect_left(s, x - i) + 1 # +1 because BIT is 1-indexed
- Query the maximum sum of all subsequences whose last element has
arrvalue ≤ currentarr[i]:
v = max(tree.query(j), 0) + x
We take max(..., 0) because we can start a new subsequence from the current element.
- Update the BIT with the maximum sum ending at current position:
tree.update(j, v)
Step 5: Get the Final Answer
After processing all elements, the answer is the maximum value stored in the entire BIT:
return tree.query(len(s))
Time Complexity Analysis:
- Sorting unique values: O(n log n)
- For each element, we do binary search and BIT operations: O(n log n)
- Overall: O(n log n)
Space Complexity: O(n) for the BIT and auxiliary arrays.
The key insight is that by transforming the problem and using coordinate compression with a BIT, we efficiently maintain the maximum sums for all valid prefixes, allowing us to build the optimal balanced subsequence incrementally.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with nums = [3, 3, 5, 6].
Step 1: Transform the array We create arr[i] = nums[i] - i:
arr[0] = 3 - 0 = 3arr[1] = 3 - 1 = 2arr[2] = 5 - 2 = 3arr[3] = 6 - 3 = 3
So arr = [3, 2, 3, 3]
Step 2: Coordinate Compression Get unique sorted values: s = [2, 3] This maps:
- Value 2 → compressed index 1
- Value 3 → compressed index 2
Step 3: Initialize BIT Create a BIT of size 3 (len(s) + 1) initialized with -inf
Step 4: Process each element
Processing index 0 (nums[0] = 3):
arr[0] = 3→ compressed index = 2- Query BIT for max in range [1, 2]: returns
-inf - Calculate:
v = max(-inf, 0) + 3 = 0 + 3 = 3 - Update BIT at index 2 with value 3
- BIT state:
[-inf, -inf, 3]
Processing index 1 (nums[1] = 3):
arr[1] = 2→ compressed index = 1- Query BIT for max in range [1, 1]: returns
-inf - Calculate:
v = max(-inf, 0) + 3 = 0 + 3 = 3 - Update BIT at index 1 with value 3
- BIT state:
[-inf, 3, 3]
Processing index 2 (nums[2] = 5):
arr[2] = 3→ compressed index = 2- Query BIT for max in range [1, 2]: returns
max(3, 3) = 3 - Calculate:
v = max(3, 0) + 5 = 3 + 5 = 8 - Update BIT at index 2 with value 8
- BIT state:
[-inf, 3, 8]
Processing index 3 (nums[3] = 6):
arr[3] = 3→ compressed index = 2- Query BIT for max in range [1, 2]: returns
max(3, 8) = 8 - Calculate:
v = max(8, 0) + 6 = 8 + 6 = 14 - Update BIT at index 2 with value 14
- BIT state:
[-inf, 3, 14]
Step 5: Get the answer Query the entire BIT: tree.query(2) = 14
Verification: The optimal subsequence is indices [1, 2, 3] with values [3, 5, 6]:
- Check balance between indices 1 and 2:
5 - 3 = 2 >= 2 - 1 = 1✓ - Check balance between indices 2 and 3:
6 - 5 = 1 >= 3 - 2 = 1✓ - Sum = 3 + 5 + 6 = 14 ✓
This subsequence works because in the transformed array, we have arr[1] = 2 ≤ arr[2] = 3 ≤ arr[3] = 3, forming a non-decreasing sequence.
Time and Space Complexity
Time Complexity: O(n × log n)
The time complexity is dominated by several operations:
- Creating the array
arrwithx - ifor each element:O(n) - Sorting the unique elements:
O(n × log n)in the worst case when all elements are unique - The main loop iterates
ntimes, and for each iteration:bisect_leftperforms binary search:O(log n)tree.query(j)calls the BIT query operation, which loops at mostO(log n)times (following the path down by removing the least significant bit)tree.update(j, v)calls the BIT update operation, which loops at mostO(log n)times (following the path up by adding the least significant bit)
- Final
tree.query(len(s)):O(log n)
Overall: O(n) + O(n × log n) + O(n × log n) = O(n × log n)
Space Complexity: O(n)
The space complexity consists of:
- Array
arrstoring transformed values:O(n) - Set and sorted array
sstoring unique values:O(n)in the worst case - BinaryIndexedTree with array
cof sizen + 1:O(n) - Additional variables and temporary storage:
O(1)
Overall: O(n) + O(n) + O(n) = O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of Negative Values in BIT Initialization
Pitfall: One common mistake is initializing the BIT with 0 instead of negative infinity. This causes incorrect results when all possible balanced subsequences have negative sums.
Why it happens: When querying bit.query(compressed_index), if no valid subsequence has been found yet, the BIT should return negative infinity, not 0. If initialized with 0, the algorithm might incorrectly assume there's a valid empty subsequence with sum 0.
Example scenario:
nums = [-10, -20, -30] # All negative values
With incorrect initialization (using 0), the algorithm might return 0 instead of the correct answer -10 (taking just the first element).
Solution: Always initialize the BIT with float('-inf'):
self.tree = [float('-inf')] * (n + 1) # Correct # NOT: self.tree = [0] * (n + 1) # Wrong!
2. Off-by-One Error in BIT Indexing
Pitfall: Forgetting that BIT is 1-indexed while coordinate compression returns 0-indexed positions.
Why it happens: bisect_left returns a 0-indexed position, but BIT operations expect 1-indexed positions.
Incorrect code:
compressed_index = bisect_left(sorted_unique_values, current_num - i) # Wrong! bit.update(compressed_index, new_sum) # This will fail for index 0
Solution: Always add 1 when converting from bisect result to BIT index:
compressed_index = bisect_left(sorted_unique_values, current_num - i) + 1 # Correct
3. Misunderstanding the Transformation Logic
Pitfall: Incorrectly transforming the balance condition, such as using nums[i] + i instead of nums[i] - i.
Why it happens: The balance condition nums[j] - nums[i] >= j - i needs careful algebraic manipulation:
- Rearranging:
nums[j] - j >= nums[i] - i - This means we need
arr[i] = nums[i] - i, notnums[i] + i
Incorrect transformation:
transformed_values = [nums[i] + i for i in range(len(nums))] # Wrong!
Solution: Use the correct transformation:
transformed_values = [nums[i] - i for i in range(len(nums))] # Correct
4. Not Handling the Case of Starting a New Subsequence
Pitfall: Forgetting that a single element can form a valid balanced subsequence, leading to missing optimal solutions.
Why it happens: When calculating the maximum sum at position i, we need to consider both:
- Extending a previous subsequence
- Starting a new subsequence from the current element
Incorrect code:
max_previous_sum = bit.query(compressed_index) # Wrong! new_sum = max_previous_sum + current_num # Might use -inf
Solution: Take the maximum with 0 to allow starting fresh:
max_previous_sum = max(bit.query(compressed_index), 0) # Correct new_sum = max_previous_sum + current_num
5. Using Standard BIT for Sum Instead of Maximum
Pitfall: Using a standard BIT implementation that calculates prefix sums instead of prefix maximums.
Why it happens: Most BIT tutorials and implementations focus on range sum queries, but this problem requires range maximum queries.
Solution: Modify the BIT operations to use max instead of addition:
# In update method: self.tree[index] = max(self.tree[index], value) # Use max, not += # In query method: maximum_value = max(maximum_value, self.tree[index]) # Use max, not +=
Which data structure is used to implement priority queue?
Recommended Readings
Segment Tree Faster Range Queries For this article we want to introduce the idea of a Segment Tree Segment Trees allow us to quickly perform range queries as well as range updates Suppose we have an array and we want to know the sum of a particular range of numbers as well
https assets algo monster cover_photos Binary_Search svg Binary Search Intuition Binary search is an efficient array search algorithm It works by narrowing down the search range by half each time If you have looked up a word in a physical dictionary you've already used binary search in real life Let's
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!