Find the Maximum Sequence Value of Array

HARD

Description

You are given an integer array nums and a positive integer k.

The value of a sequence seq of size 2 * x is defined as:

  • (seq[0] OR seq[1] OR ... OR seq[x - 1]) XOR (seq[x] OR seq[x + 1] OR ... OR seq[2 * x - 1]).

Return the maximum value of any subsequence of nums having size 2 * k.

 

Example 1:

Input: nums = [2,6,7], k = 1

Output: 5

Explanation:

The subsequence [2, 7] has the maximum value of 2 XOR 7 = 5.

Example 2:

Input: nums = [4,2,5,6,7], k = 2

Output: 2

Explanation:

The subsequence [4, 5, 6, 7] has the maximum value of (4 OR 5) XOR (6 OR 7) = 2.

 

Constraints:

  • 2 <= nums.length <= 400
  • 1 <= nums[i] < 27
  • 1 <= k <= nums.length / 2

Approaches

Checkout 2 different approaches to solve Find the Maximum Sequence Value of Array. Click on different approaches to view the approach and algorithm in detail.

Greedy Bitwise Search with Dynamic Programming

A much more efficient approach involves a greedy strategy combined with dynamic programming. We can construct the maximum value bit by bit, from most significant to least significant. For each bit position, we greedily check if we can make that bit a '1' in our final answer, given the choices we've made for the higher-order bits.

Algorithm

  • The core idea is to build the maximum possible result bit by bit, from the most significant bit (MSB) to the least significant bit (LSB).
  • Initialize the result ans = 0.
  • Iterate from bit b = 6 down to 0 (since nums[i] < 2^7).
  • In each iteration, try to set the b-th bit of the answer to 1. Let target = ans | (1 << b).
  • Check if it's possible to find two disjoint k-element subsequences A and B such that their OR-sums orA and orB satisfy (orA XOR orB) having target as a prefix. This means for all bits j >= b, the j-th bit of (orA XOR orB) must match the j-th bit of target.
  • This check is performed by a helper function, isPossible(target, b).
  • If isPossible returns true, it means we can achieve this target prefix, so we update ans = target.
  • If it returns false, we cannot set the b-th bit to 1 (while satisfying higher bits), so we leave ans as is and proceed to the next smaller bit.
  • The isPossible function uses its own DP. Let dp[j1][pA] be the minimum size j2 of the second set (B) required to satisfy the prefix condition, given that the first set (A) has j1 elements and its OR-sum prefix is pA. The prefix is calculated by val & mask, where mask includes all bits from MSB down to b.
  • The DP transition for isPossible considers adding the current number to set A, set B, or neither, and updates the minimum j2 values accordingly.
  • After iterating through all bits, ans holds the maximum possible sequence value.

We iterate from bit 6 down to 0. Let's say our current best-achievable value is ans. For the current bit b, we test if we can achieve a value of ans | (1 << b). This test involves checking if there exist two disjoint k-element subsequences, A and B, such that (OR(A) XOR OR(B)) matches ans | (1 << b) on all bits from 6 down to b.

To perform this check, we use a DP helper function, isPossible(target). This function determines if a given target prefix is achievable. The state for this DP is dp[j1][pA], which stores the minimum number of elements (j2) needed for set B to satisfy the prefix condition, given that set A has j1 elements and its OR-sum, when masked with the current bitmask, results in pA.

The DP table dp has dimensions (k+1) x 128. dp[j1][pA] is initialized to a value larger than k (e.g., k+1), with dp[0][0] = 0.

We iterate through each num in nums. For each num, we create a new DP table new_dp based on the old dp table to represent the states after considering num. For each state (j1, pA) with j2 = dp[j1][pA], we have three choices for num:

  1. Don't use num: The state is carried over, which is implicitly handled by copying dp to new_dp.
  2. Add num to set A: If j1 < k, we can transition to a state with j1+1 elements in A. The new OR-sum prefix for A will be pA | (num & mask). We update new_dp[j1+1][pA | (num & mask)] with min(current_value, j2).
  3. Add num to set B: If j2 < k, we can transition to a state with j2+1 elements in B. The OR-sum prefix for B changes, which in turn changes the required prefix for A to maintain the target XOR relationship. The new required pA is calculated, and we update new_dp accordingly.

After processing all numbers, if there is any pA for which dp[k][pA] <= k, it means we can form two valid sets of size k, and the function returns true.

class Solution {
    public int maxSequenceValue(int[] nums, int k) {
        int ans = 0;
        for (int b = 6; b >= 0; b--) {
            int target = ans | (1 << b);
            if (isPossible(nums, k, target, b)) {
                ans = target;
            }
        }
        return ans;
    }

    private boolean isPossible(int[] nums, int k, int target, int bit) {
        int mask = 0;
        for (int i = 6; i >= bit; i--) {
            mask |= (1 << i);
        }

        int[][] dp = new int[k + 1][128];
        for (int i = 0; i <= k; i++) {
            for (int j = 0; j < 128; j++) {
                dp[i][j] = k + 1; // Use k+1 as infinity
            }
        }
        dp[0][0] = 0;

        for (int num : nums) {
            int numPrefix = num & mask;
            int[][] newDp = new int[k + 1][128];
            for(int i=0; i<=k; i++) {
                System.arraycopy(dp[i], 0, newDp[i], 0, 128);
            }

            for (int j1 = 0; j1 <= k; j1++) {
                for (int pA = 0; pA < 128; pA++) {
                    if (dp[j1][pA] > k) continue;
                    int j2 = dp[j1][pA];

                    // Option 1: Add num to set A
                    if (j1 < k) {
                        newDp[j1 + 1][pA | numPrefix] = Math.min(newDp[j1 + 1][pA | numPrefix], j2);
                    }

                    // Option 2: Add num to set B
                    if (j2 < k) {
                        int pB = pA ^ target;
                        int newPB = pB | numPrefix;
                        int newPA = newPB ^ target;
                        newDp[j1][newPA] = Math.min(newDp[j1][newPA], j2 + 1);
                    }
                }
            }
            dp = newDp;
        }

        for (int pA = 0; pA < 128; pA++) {
            if (dp[k][pA] <= k) {
                return true;
            }
        }
        return false;
    }
}

Complexity Analysis

Time Complexity: O(B * n * k * C), where B is the number of bits (constant, 7), n is the length of `nums`, k is the subsequence size, and C is the maximum possible OR-sum (128). This simplifies to O(n * k) as B and C are small constants.Space Complexity: O(k * C), where C is the maximum possible OR-sum (128). The DP table inside the `isPossible` function is of size `(k+1) x 128`.

Pros and Cons

Pros:
  • Significantly more efficient than the brute-force DP approach.
  • The time and space complexity are well within the limits for the given constraints.
  • The greedy bit-by-bit strategy is a powerful technique for maximization problems involving bitwise operations.
Cons:
  • The logic, especially the DP state within the greedy check, can be complex to formulate and implement correctly.

Code Solutions

Checking out 3 solutions in different languages for Find the Maximum Sequence Value of Array. Click on different languages to view the code.

class Solution {
public
  int maxValue(int[] nums, int k) {
    int m = 1 << 7;
    int n = nums.length;
    boolean[][][] f = new boolean[n + 1][k + 2][m];
    f[0][0][0] = true;
    for (int i = 0; i < n; i++) {
      for (int j = 0; j <= k; j++) {
        for (int x = 0; x < m; x++) {
          if (f[i][j][x]) {
            f[i + 1][j][x] = true;
            f[i + 1][j + 1][x | nums[i]] = true;
          }
        }
      }
    }
    boolean[][][] g = new boolean[n + 1][k + 2][m];
    g[n][0][0] = true;
    for (int i = n; i > 0; i--) {
      for (int j = 0; j <= k; j++) {
        for (int y = 0; y < m; y++) {
          if (g[i][j][y]) {
            g[i - 1][j][y] = true;
            g[i - 1][j + 1][y | nums[i - 1]] = true;
          }
        }
      }
    }
    int ans = 0;
    for (int i = k; i <= n - k; i++) {
      for (int x = 0; x < m; x++) {
        if (f[i][k][x]) {
          for (int y = 0; y < m; y++) {
            if (g[i][k][y]) {
              ans = Math.max(ans, x ^ y);
            }
          }
        }
      }
    }
    return ans;
  }
}

Video Solution

Watch the video walkthrough for Find the Maximum Sequence Value of Array



Patterns:

Dynamic ProgrammingBit Manipulation

Data Structures:

Array

Subscribe to Scale Engineer newsletter

Learn about System Design, Software Engineering, and interview experiences every week.

No spam, unsubscribe at any time.