3117. Minimum Sum of Values by Dividing Array
Problem Description
You are given two arrays: nums
of length n
and andValues
of length m
.
The value of an array is defined as its last element.
Your task is to divide nums
into exactly m
disjoint contiguous subarrays with the following constraint:
- For the
i-th
subarray (covering indices froml_i
tor_i
), the bitwise AND of all elements in that subarray must equalandValues[i]
- In other words:
nums[l_i] & nums[l_i + 1] & ... & nums[r_i] == andValues[i]
for all1 <= i <= m
The goal is to find the minimum possible sum of the values (last elements) of these m
subarrays.
Example breakdown:
- You must create exactly
m
contiguous, non-overlapping subarrays fromnums
- Each subarray's bitwise AND result must match the corresponding value in
andValues
- The "value" of each subarray is its last element
- You want to minimize the sum of these last elements
Return the minimum possible sum if such a division exists, otherwise return -1
.
Key points:
- The subarrays must be contiguous (elements next to each other)
- The subarrays must be disjoint (no overlap)
- All elements of
nums
must be used exactly once - The bitwise AND operation
&
is applied to all elements within each subarray - If no valid division exists that satisfies all conditions, return
-1
Intuition
To solve this problem, we need to think about how to explore all possible ways to divide the array while keeping track of constraints.
First, let's understand what happens with the bitwise AND operation. When we AND multiple numbers together, the result can only stay the same or decrease (never increase). This is because AND operation can only turn bits from 1 to 0, never from 0 to 1. This property is crucial - once our running AND becomes less than the target andValues[j]
, we can never reach that target by adding more elements to the current subarray.
The problem asks us to minimize the sum of the last elements of each subarray. This suggests we need to try different cutting points and choose the ones that give us the minimum sum.
We can think of this as a decision problem: at each position in nums
, we have two choices:
- Continue the current subarray - extend it by including the current element
- End the current subarray - use the current element as the last element of the current subarray and start a new one
This naturally leads to a recursive approach where we explore both options at each position.
The state we need to track includes:
- Current position
i
in thenums
array - Number of subarrays
j
we've already completed - Current AND value
a
of the subarray we're building
At each state (i, j, a)
:
- We calculate the new AND value by doing
a & nums[i]
- If this new AND is less than
andValues[j]
, we can't possibly reach our target, so this path is invalid - If the new AND equals
andValues[j]
, we have the option to end the current subarray here - We can always try to extend the current subarray (unless it makes the AND too small)
The base cases are:
- If we've successfully divided into
m
subarrays and used all elements, return 0 - If we've run out of elements but haven't created enough subarrays, or vice versa, return infinity (invalid)
Since we'll encounter the same states multiple times through different paths, we use memoization to avoid recalculating the same subproblems. The initial AND value is set to -1
(all bits set to 1) because -1
in binary is all 1s, and ANDing with any number gives that number itself.
Learn more about Segment Tree, Queue, Binary Search and Dynamic Programming patterns.
Solution Approach
The solution uses memoization search (dynamic programming with recursion) to explore all valid ways to divide the array.
Function Design:
We define dfs(i, j, a)
where:
i
= current position innums
arrayj
= number of subarrays already completeda
= current bitwise AND value of the subarray being built- Returns: minimum sum of subarray values starting from position
i
Implementation Details:
-
Pruning Check:
if n - i < m - j: return inf
If remaining elements
(n - i)
are less than remaining subarrays needed(m - j)
, it's impossible to complete the division. -
Base Case:
if j == m: return 0 if i == n else inf
When we've created
m
subarrays, check if we've used all elements. Return 0 if successful, infinity otherwise. -
Update AND Value:
a &= nums[i]
Perform bitwise AND with current element. Initially
a = -1
(all bits set), so first AND givesnums[i]
itself. -
Feasibility Check:
if a < andValues[j]: return inf
Since AND can only decrease or stay same, if current AND is already less than target, we can never reach it.
-
Recursive Choices:
- Extend current subarray:
dfs(i + 1, j, a)
- continue with same subarray, updated AND value - End current subarray (only if AND matches target):
Addif a == andValues[j]: ans = min(ans, dfs(i + 1, j + 1, -1) + nums[i])
nums[i]
(the last element) to the sum and start new subarray witha = -1
- Extend current subarray:
-
Memoization: The
@cache
decorator stores results ofdfs(i, j, a)
to avoid recomputation of identical states.
Main Function:
n, m = len(nums), len(andValues)
ans = dfs(0, 0, -1)
return ans if ans < inf else -1
Start from position 0, with 0 completed subarrays, and initial AND value of -1. Return the result if valid, otherwise -1.
Time Complexity: O(n * m * k)
where k
is the number of distinct AND values possible (bounded by the range of values in nums
)
Space Complexity: O(n * m * k)
for the memoization cache
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 small example to illustrate the solution approach.
Input: nums = [2, 3, 5, 7, 7, 7, 5]
, andValues = [0, 7, 5]
We need to divide nums
into exactly 3 subarrays where:
- First subarray's AND must equal 0
- Second subarray's AND must equal 7
- Third subarray's AND must equal 5
Let's trace through the recursive calls starting from dfs(0, 0, -1)
:
Step 1: dfs(0, 0, -1)
- Start at index 0, 0 subarrays completed, AND = -1
- Update AND:
-1 & 2 = 2
- Since
2 > 0
(our target), we can extend the subarray - Call
dfs(1, 0, 2)
Step 2: dfs(1, 0, 2)
- At index 1, still building first subarray
- Update AND:
2 & 3 = 2
(binary: 10 & 11 = 10) - Still
2 > 0
, continue extending - Call
dfs(2, 0, 2)
Step 3: dfs(2, 0, 2)
- At index 2
- Update AND:
2 & 5 = 0
(binary: 010 & 101 = 000) - Now AND equals target
andValues[0] = 0
! - We have two choices:
- Choice A: End subarray here:
dfs(3, 1, -1) + 5
(add value 5) - Choice B: Extend:
dfs(3, 0, 0)
- Choice A: End subarray here:
Following Choice A: dfs(3, 1, -1)
- Start second subarray at index 3
- Update AND:
-1 & 7 = 7
- AND equals target
andValues[1] = 7
! - We can end here:
dfs(4, 2, -1) + 7
(add value 7)
Continuing: dfs(4, 2, -1)
- Start third subarray at index 4
- Update AND:
-1 & 7 = 7
- Need AND = 5, so extend:
dfs(5, 2, 7)
Step: dfs(5, 2, 7)
- Update AND:
7 & 7 = 7
- Still need 5, extend:
dfs(6, 2, 7)
Step: dfs(6, 2, 7)
- Update AND:
7 & 5 = 5
(binary: 111 & 101 = 101) - AND equals target
andValues[2] = 5
! - End subarray:
dfs(7, 3, -1) + 5
Final: dfs(7, 3, -1)
j == m
(3 subarrays completed) andi == n
(all elements used)- Return 0
Backtracking the sum:
- Third subarray contributes: 5
- Second subarray contributes: 7
- First subarray contributes: 5
- Total sum = 5 + 7 + 5 = 17
The algorithm explores other paths too (like extending the first subarray further), but this path gives us a valid division with sum 17. The memoization ensures we don't recalculate states we've already visited.
Key insights from this example:
- The AND operation progressively reduces values (2 → 2 → 0)
- We make decisions when AND equals target values
- Multiple valid divisions may exist; we choose the one with minimum sum
- The recursive nature allows us to explore all possibilities efficiently
Solution Implementation
1from typing import List
2from functools import cache
3from math import inf
4
5
6class Solution:
7 def minimumValueSum(self, nums: List[int], andValues: List[int]) -> int:
8 """
9 Find the minimum sum of the last elements of each partition where
10 the bitwise AND of each partition equals the corresponding value in andValues.
11
12 Args:
13 nums: List of integers to partition
14 andValues: List of target AND values for each partition
15
16 Returns:
17 Minimum sum of last elements, or -1 if impossible
18 """
19
20 @cache
21 def dfs(current_index: int, partition_index: int, current_and: int) -> int:
22 """
23 Dynamic programming function to find minimum sum.
24
25 Args:
26 current_index: Current position in nums array
27 partition_index: Current position in andValues array (which partition we're building)
28 current_and: Current AND value of the ongoing partition (-1 means new partition)
29
30 Returns:
31 Minimum sum from this state, or inf if impossible
32 """
33
34 # Check if we have enough elements left to form remaining partitions
35 remaining_nums = n - current_index
36 remaining_partitions = m - partition_index
37 if remaining_nums < remaining_partitions:
38 return inf
39
40 # Base case: all partitions formed
41 if partition_index == m:
42 # Valid only if we've used all numbers
43 return 0 if current_index == n else inf
44
45 # Update current AND value with current number
46 current_and &= nums[current_index]
47
48 # If current AND is less than target, this path is invalid
49 if current_and < andValues[partition_index]:
50 return inf
51
52 # Option 1: Continue current partition (add current number to it)
53 result = dfs(current_index + 1, partition_index, current_and)
54
55 # Option 2: End current partition if AND matches target
56 if current_and == andValues[partition_index]:
57 # End partition here, nums[current_index] is the last element
58 next_partition_result = dfs(current_index + 1, partition_index + 1, -1)
59 result = min(result, next_partition_result + nums[current_index])
60
61 return result
62
63 # Initialize variables
64 n = len(nums)
65 m = len(andValues)
66
67 # Start DFS from beginning with no partition formed yet
68 answer = dfs(0, 0, -1)
69
70 # Return result or -1 if impossible
71 return answer if answer < inf else -1
72
1class Solution {
2 private int[] nums;
3 private int[] andValues;
4 private static final int INF = 1 << 29; // Large value representing infinity
5 private Map<Long, Integer> memo = new HashMap<>(); // Memoization cache
6
7 public int minimumValueSum(int[] nums, int[] andValues) {
8 this.nums = nums;
9 this.andValues = andValues;
10
11 // Start DFS from index 0 of nums, index 0 of andValues, with initial AND value of -1 (all bits set)
12 int result = dfs(0, 0, -1);
13
14 // If result is greater than or equal to INF, no valid partition exists
15 return result >= INF ? -1 : result;
16 }
17
18 private int dfs(int numsIndex, int andValuesIndex, int currentAndValue) {
19 // Pruning: Not enough elements left in nums to satisfy remaining andValues
20 if (nums.length - numsIndex < andValues.length - andValuesIndex) {
21 return INF;
22 }
23
24 // Base case: All andValues have been matched
25 if (andValuesIndex == andValues.length) {
26 // Valid only if we've used all elements in nums
27 return numsIndex == nums.length ? 0 : INF;
28 }
29
30 // Update current AND value with current element
31 currentAndValue &= nums[numsIndex];
32
33 // Pruning: Current AND value is already less than target, can't recover
34 if (currentAndValue < andValues[andValuesIndex]) {
35 return INF;
36 }
37
38 // Create unique key for memoization: combine indices and current AND value
39 // numsIndex uses bits 36-63, andValuesIndex uses bits 32-35, currentAndValue uses bits 0-31
40 long memoKey = ((long) numsIndex << 36) | ((long) andValuesIndex << 32) | (currentAndValue & 0xFFFFFFFFL);
41
42 // Check if this state has been computed before
43 if (memo.containsKey(memoKey)) {
44 return memo.get(memoKey);
45 }
46
47 // Option 1: Continue current subarray (extend it to include next element)
48 int minValue = dfs(numsIndex + 1, andValuesIndex, currentAndValue);
49
50 // Option 2: If current AND equals target, can end current subarray here
51 if (currentAndValue == andValues[andValuesIndex]) {
52 // End current subarray at numsIndex, add last element value to sum
53 // Start new subarray from numsIndex + 1 with fresh AND value (-1)
54 int endSubarrayValue = dfs(numsIndex + 1, andValuesIndex + 1, -1) + nums[numsIndex];
55 minValue = Math.min(minValue, endSubarrayValue);
56 }
57
58 // Store result in memoization cache
59 memo.put(memoKey, minValue);
60 return minValue;
61 }
62}
63
1class Solution {
2public:
3 int minimumValueSum(vector<int>& nums, vector<int>& andValues) {
4 // Store input arrays and their sizes
5 this->nums = nums;
6 this->andValues = andValues;
7 numsSize = nums.size();
8 targetSize = andValues.size();
9
10 // Start DFS from position 0, targeting andValues[0], with initial AND value of -1 (all bits set)
11 int result = dfs(0, 0, -1);
12
13 // Return -1 if no valid partition exists, otherwise return the minimum sum
14 return result >= INF ? -1 : result;
15 }
16
17private:
18 vector<int> nums;
19 vector<int> andValues;
20 int numsSize;
21 int targetSize;
22 const int INF = 1 << 29; // Large value representing impossible state
23 unordered_map<long long, int> memo; // Memoization map for DP states
24
25 /**
26 * DFS with memoization to find minimum sum
27 * @param numsIndex: current index in nums array
28 * @param targetIndex: current index in andValues array (which segment we're building)
29 * @param currentAnd: accumulated AND value of current segment
30 * @return minimum sum of last elements in each segment, or INF if impossible
31 */
32 int dfs(int numsIndex, int targetIndex, int currentAnd) {
33 // Pruning: not enough elements left to form remaining segments
34 if (numsSize - numsIndex < targetSize - targetIndex) {
35 return INF;
36 }
37
38 // Base case: all segments formed
39 if (targetIndex == targetSize) {
40 // Valid only if all nums elements are used
41 return numsIndex == numsSize ? 0 : INF;
42 }
43
44 // Update current segment's AND value with current element
45 currentAnd &= nums[numsIndex];
46
47 // Pruning: current AND value is already less than target (AND only decreases)
48 if (currentAnd < andValues[targetIndex]) {
49 return INF;
50 }
51
52 // Create unique key for memoization: pack three values into one long long
53 // numsIndex uses bits 36-63, targetIndex uses bits 32-35, currentAnd uses bits 0-31
54 long long stateKey = (long long)numsIndex << 36 | (long long)targetIndex << 32 | currentAnd;
55
56 // Check if this state has been computed before
57 if (memo.contains(stateKey)) {
58 return memo[stateKey];
59 }
60
61 // Option 1: Continue current segment (add current element to current segment)
62 int minSum = dfs(numsIndex + 1, targetIndex, currentAnd);
63
64 // Option 2: End current segment if AND value matches target
65 if (currentAnd == andValues[targetIndex]) {
66 // End current segment at numsIndex, add nums[numsIndex] to sum
67 // Start new segment at numsIndex + 1 with fresh AND value (-1)
68 minSum = min(minSum, dfs(numsIndex + 1, targetIndex + 1, -1) + nums[numsIndex]);
69 }
70
71 // Memoize and return result
72 return memo[stateKey] = minSum;
73 }
74};
75
1/**
2 * Finds the minimum sum of values when partitioning nums array into m subarrays
3 * where each subarray's bitwise AND equals the corresponding value in andValues
4 * @param nums - The input array to be partitioned
5 * @param andValues - Target AND values for each partition
6 * @returns Minimum sum of last elements of each partition, or -1 if impossible
7 */
8function minimumValueSum(nums: number[], andValues: number[]): number {
9 const numsLength: number = nums.length;
10 const andValuesLength: number = andValues.length;
11
12 // Memoization cache using bigint as key to encode state (index, partition, currentAND)
13 const memoCache: Map<bigint, number> = new Map();
14
15 /**
16 * Dynamic programming with memoization to find minimum sum
17 * @param currentIndex - Current position in nums array
18 * @param partitionIndex - Current partition being processed
19 * @param currentAndValue - Running bitwise AND of current partition
20 * @returns Minimum sum starting from current state
21 */
22 const findMinimumSum = (currentIndex: number, partitionIndex: number, currentAndValue: number): number => {
23 // Check if there are enough elements left to form remaining partitions
24 if (numsLength - currentIndex < andValuesLength - partitionIndex) {
25 return Infinity;
26 }
27
28 // Base case: all partitions formed
29 if (partitionIndex === andValuesLength) {
30 // Valid only if all elements are used
31 return currentIndex === numsLength ? 0 : Infinity;
32 }
33
34 // Update current AND value with current element
35 currentAndValue &= nums[currentIndex];
36
37 // Pruning: if current AND is less than target, no valid partition possible
38 if (currentAndValue < andValues[partitionIndex]) {
39 return Infinity;
40 }
41
42 // Create unique key for memoization (pack three values into one bigint)
43 // Bits 36+: currentIndex, Bits 32-35: partitionIndex, Bits 0-31: currentAndValue
44 const stateKey: bigint = (BigInt(currentIndex) << 36n) |
45 (BigInt(partitionIndex) << 32n) |
46 BigInt(currentAndValue);
47
48 // Return cached result if exists
49 if (memoCache.has(stateKey)) {
50 return memoCache.get(stateKey)!;
51 }
52
53 // Option 1: Continue current partition with next element
54 let minSum: number = findMinimumSum(currentIndex + 1, partitionIndex, currentAndValue);
55
56 // Option 2: End current partition if AND value matches target
57 if (currentAndValue === andValues[partitionIndex]) {
58 // Add current element as last element of partition and start new partition
59 // Reset AND value to -1 (all bits set) for next partition
60 minSum = Math.min(minSum, findMinimumSum(currentIndex + 1, partitionIndex + 1, -1) + nums[currentIndex]);
61 }
62
63 // Cache and return result
64 memoCache.set(stateKey, minSum);
65 return minSum;
66 };
67
68 // Start recursion with initial state: index 0, partition 0, AND value -1 (all bits set)
69 const result: number = findMinimumSum(0, 0, -1);
70
71 // Return -1 if no valid partition exists, otherwise return minimum sum
72 return result >= Infinity ? -1 : result;
73}
74
Time and Space Complexity
The time complexity is O(n × m × log M)
, where:
n
is the length of thenums
arraym
is the length of theandValues
arrayM
is the maximum value in thenums
array (in this problemM ≤ 10^5
)
The log M
factor comes from the number of distinct values that the AND operation parameter a
can take. Since AND operations can only decrease or maintain the value (never increase), and bits can only go from 1 to 0, there are at most O(log M)
distinct values that a
can have as we progress through the array.
The space complexity is O(n × m × log M)
due to the @cache
decorator memoizing the dfs
function. The cache stores results for each unique combination of:
- Position
i
innums
:n
possible values - Position
j
inandValues
:m
possible values - Current AND accumulator
a
:O(log M)
possible distinct values
Therefore, the maximum number of cached states is O(n × m × log M)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Initial AND Value
Pitfall: Using 0
as the initial AND value instead of -1
.
When starting a new subarray, using 0
as the initial value would make all subsequent AND operations result in 0
(since x & 0 = 0
for any x
). This would incorrectly calculate the AND values.
Why -1
works: In binary, -1
is represented as all 1s in two's complement notation. When you AND any number with all 1s, you get the number itself: x & (-1) = x
. This effectively makes the first element of each subarray the starting AND value.
Wrong approach:
# This would always result in 0 for any subarray dfs(i + 1, j + 1, 0) # WRONG!
Correct approach:
# Start with -1 (all bits set to 1) dfs(i + 1, j + 1, -1) # CORRECT
2. Forgetting to Add the Last Element's Value
Pitfall: When ending a subarray, forgetting to add nums[i]
(the last element) to the sum.
The problem defines the "value" of a subarray as its last element. When we decide to end a subarray at index i
, we must add nums[i]
to our total sum.
Wrong approach:
if current_and == andValues[partition_index]:
result = min(result, dfs(current_index + 1, partition_index + 1, -1)) # WRONG!
Correct approach:
if current_and == andValues[partition_index]:
result = min(result, dfs(current_index + 1, partition_index + 1, -1) + nums[current_index]) # CORRECT
3. Off-by-One Error in Index Management
Pitfall: Confusion about whether to end the subarray at current_index
or current_index - 1
.
When we're at current_index
, we've already included nums[current_index]
in the AND calculation. If we decide to end the subarray here, nums[current_index]
is the last element of this subarray. The next recursive call should start from current_index + 1
, not current_index
.
Wrong approach:
# Double-counting the current element
result = min(result, dfs(current_index, partition_index + 1, -1) + nums[current_index]) # WRONG!
Correct approach:
# Move to the next index after ending current subarray
result = min(result, dfs(current_index + 1, partition_index + 1, -1) + nums[current_index]) # CORRECT
4. Missing Early Termination Optimization
Pitfall: Not checking if current_and < andValues[partition_index]
early.
Since the AND operation can only decrease or maintain the value (never increase), once the current AND becomes less than the target, it's impossible to reach the target value. This check prevents unnecessary recursive calls.
Inefficient approach:
# Continue recursion even when it's impossible to reach target
def dfs(current_index, partition_index, current_and):
# ... base cases ...
current_and &= nums[current_index]
# Missing the early termination check
result = dfs(current_index + 1, partition_index, current_and)
# ...
Optimized approach:
def dfs(current_index, partition_index, current_and):
# ... base cases ...
current_and &= nums[current_index]
# Early termination when target is unreachable
if current_and < andValues[partition_index]:
return inf
result = dfs(current_index + 1, partition_index, current_and)
# ...
Which of the following uses divide and conquer strategy?
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
Queue Intro Think of the last time you stood in line to buy movie tickets The first person in line gets their ticket first and any newcomers join the end of the line This system where the first person to arrive is the first to be served is a queue in real
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Want a Structured Path to Master System Design Too? Don’t Miss This!