Facebook Pixel

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 from l_i to r_i), the bitwise AND of all elements in that subarray must equal andValues[i]
  • In other words: nums[l_i] & nums[l_i + 1] & ... & nums[r_i] == andValues[i] for all 1 <= 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 from nums
  • 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
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Continue the current subarray - extend it by including the current element
  2. 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 the nums 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 in nums array
  • j = number of subarrays already completed
  • a = current bitwise AND value of the subarray being built
  • Returns: minimum sum of subarray values starting from position i

Implementation Details:

  1. 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.

  2. 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.

  3. Update AND Value:

    a &= nums[i]

    Perform bitwise AND with current element. Initially a = -1 (all bits set), so first AND gives nums[i] itself.

  4. 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.

  5. 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):
      if a == andValues[j]:
          ans = min(ans, dfs(i + 1, j + 1, -1) + nums[i])
      Add nums[i] (the last element) to the sum and start new subarray with a = -1
  6. Memoization: The @cache decorator stores results of dfs(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 Evaluator

Example 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)

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) and i == 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:

  1. The AND operation progressively reduces values (2 → 2 → 0)
  2. We make decisions when AND equals target values
  3. Multiple valid divisions may exist; we choose the one with minimum sum
  4. 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 the nums array
  • m is the length of the andValues array
  • M is the maximum value in the nums array (in this problem M ≤ 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 in nums: n possible values
  • Position j in andValues: 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)
    # ...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following uses divide and conquer strategy?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More