LeetCode 3630 - Partition Array for Maximum XOR and AND
The problem asks us to partition an integer array nums into three subsequences A, B, and C such that every element belongs to exactly one subsequence.
Difficulty: 🔴 Hard
Topics: Array, Math, Greedy, Bit Manipulation, Enumeration
Solution
Problem Understanding
The problem asks us to partition an integer array nums into three subsequences A, B, and C such that every element belongs to exactly one subsequence. The goal is to maximize the sum XOR(A) + AND(B) + XOR(C), where XOR and AND represent the bitwise XOR and AND of the elements in the subsequences. If a subsequence is empty, its contribution is defined as 0.
The input nums is an array of integers with length between 1 and 19, and each integer is between 1 and 10^9. The constraints are tight enough that we cannot rely on simple brute-force enumeration of all partitions for larger arrays without some clever optimization, but small enough that bit manipulation or memoization strategies are feasible. Edge cases include arrays of length 1, arrays with repeated elements, and arrays where one or more subsequences should be empty in the optimal solution.
In essence, the problem combines partitioning, bitwise operations, and combinatorial optimization. Correct solutions must handle all three subsequences and consider the impact of bitwise XOR and AND when combining elements.
Approaches
The brute-force approach considers generating all possible ways to assign each element to one of the three subsequences. For n elements, there are 3^n possible partitions. For each partition, we compute XOR(A) + AND(B) + XOR(C) and track the maximum. This approach is correct because it exhaustively evaluates every partition, but it becomes infeasible for n near 19 because 3^19 is approximately 1.16 billion combinations.
The optimal approach relies on memoization with bitmasking. We can represent subsets with a bitmask and recursively try adding the current element to A, B, or C, caching results for each combination of index and current XOR/AND state. Since XOR can be combined incrementally and AND can also be computed progressively, memoization drastically reduces the number of redundant computations, making the problem feasible even for the maximum constraints.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(3^n * n) | O(n) | Enumerates all partitions; too slow for n ≈ 19 |
| Optimal | O(n * 2^n * 2^k) | O(n * 2^n * 2^k) | Uses DP/memoization and bitmasking; feasible due to small n |
Here, k represents the number of distinct bits in elements, which is bounded by 30 since nums[i] <= 10^9.
Algorithm Walkthrough
- Define a recursive function
dfs(index, xorA, andB, xorC)whereindexis the current position innums, andxorA,andB, andxorCare the current XOR and AND values for the subsequences. - If
indexequals the length ofnums, return the sumxorA + andB + xorCas the result for this partition path. - For the current element
nums[index], recursively calldfsthree times: once adding the element toA(updatexorA), once adding it toB(updateandB), and once adding it toC(updatexorC). - Track the maximum of the three recursive results and return it.
- Use memoization to store results for each combination of
indexand XOR/AND states to avoid redundant computations. - Start the recursion with
dfs(0, 0, 0, 0)and return the result.
Why it works: The algorithm systematically explores all partitions while caching overlapping subproblems. Since XOR and AND can be computed incrementally, the memoized DFS ensures all valid partitions are considered efficiently without recomputation.
Python Solution
from typing import List
from functools import lru_cache
class Solution:
def maximizeXorAndXor(self, nums: List[int]) -> int:
n = len(nums)
@lru_cache(maxsize=None)
def dfs(index: int, xorA: int, andB: int, xorC: int) -> int:
if index == n:
return xorA + andB + xorC
num = nums[index]
# Add num to A
optionA = dfs(index + 1, xorA ^ num, andB, xorC)
# Add num to B
newAndB = num if andB == 0 else andB & num
optionB = dfs(index + 1, xorA, newAndB, xorC)
# Add num to C
optionC = dfs(index + 1, xorA, andB, xorC ^ num)
return max(optionA, optionB, optionC)
return dfs(0, 0, 0, 0)
The implementation defines a memoized recursive function using lru_cache. For each element, it considers three placement options and updates XOR or AND values accordingly. The andB handling ensures that if B is empty, the first number initializes it; otherwise, it uses bitwise AND. Memoization prevents recomputation of overlapping states.
Go Solution
package main
func maximizeXorAndXor(nums []int) int64 {
n := len(nums)
memo := make(map[[4]int]int64)
var dfs func(index int, xorA int, andB int, xorC int) int64
dfs = func(index int, xorA int, andB int, xorC int) int64 {
if index == n {
return int64(xorA + andB + xorC)
}
key := [4]int{index, xorA, andB, xorC}
if val, ok := memo[key]; ok {
return val
}
num := nums[index]
// Add num to A
optionA := dfs(index+1, xorA^num, andB, xorC)
// Add num to B
newAndB := num
if andB != 0 {
newAndB = andB & num
}
optionB := dfs(index+1, xorA, newAndB, xorC)
// Add num to C
optionC := dfs(index+1, xorA, andB, xorC^num)
res := max(optionA, max(optionB, optionC))
memo[key] = res
return res
}
return dfs(0, 0, 0, 0)
}
func max(a, b int64) int64 {
if a > b {
return a
}
return b
}
In Go, we use a map to memoize the recursive calls because Go does not have a built-in lru_cache. The AND initialization and XOR handling follow the same logic as Python. We use int64 for results to prevent overflow from large sums.
Worked Examples
Example 1: nums = [2,3]
Step through DFS:
| Index | xorA | andB | xorC | Decision | Result |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | add 2 to B | andB=2 |
| 1 | 0 | 2 | 0 | add 3 to A | xorA=3 |
Optimal partition: A=[3], B=[2], C=[] → result 5.
Example 2: nums = [1,3,2]
Optimal partition: A=[1], B=[2], C=[3] → result 1 + 2 + 3 = 6.
Example 3: nums = [2,3,6,7]
Optimal partition: A=[7], B=[2,3], C=[6] → result 7 + 2 + 6 = 15.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(3^n) | Each element has 3 choices, memoization reduces redundant recomputation, but worst-case still exponential in n |
| Space | O(n * 3^n) | Stack depth is O(n), memoization table stores up to O(3^n) states |
The recursive branching gives 3 options per element, which leads to an exponential number of potential states. Memoization ensures that overlapping states are computed only once, which makes the solution feasible for n <= 19.
Test Cases
# Provided examples
assert Solution().maximizeXorAndXor([2,3]) == 5 # small array, one empty subsequence
assert Solution().maximizeXorAndXor([1,3,2]) == 6 # optimal split into 3 subsequences
assert Solution().maximizeXorAndXor([2,3,6,7]) == 15 # larger array
# Boundary cases
assert Solution().maximizeXorAndXor([1]) == 1 # single element
assert Solution().maximizeXorAndXor([1,1,1]) == 3 # repeated elements
assert Solution().maximizeXorAndXor([1,2,4,8,16,32,64,128,256,512,1024])
You are given an array nums, no overflow when combining XOR and AND contributions.