EQUXOR - Editorial

PROBLEM LINK:

Practice
Contest: Division 1
Contest: Division 2
Contest: Division 3
Contest: Division 4

Author: jay129
Tester: kingmessi
Editorialist: iceknight1093

DIFFICULTY:

Easy

PREREQUISITES:

Prefix (XOR) sums, dynamic programming

PROBLEM:

You’re given an array A. You can choose any non-negative integer X and then perform the following process:

  • For each i from 1 to N,
    • If A_i = X, add X to your score.
    • Otherwise, set X := X\oplus A_i

Find the maximum possible score.

EXPLANATION:

Let’s call an index i good if its value is added to the answer - meaning the value of X equals A_i when at index i.

Suppose i and j are consecutive good indices.
Then, X must equal A_i when at index i, will be updated at every index between i and j, and then will equal A_j at index j.
This means:

A_i \oplus A_{i+1} \oplus A_{i+2} \oplus\cdots \oplus A_{j-1} = A_j \\ A_i \oplus A_{i+1} \oplus A_{i+2} \oplus\cdots \oplus A_{j-1} \oplus A_j = 0

That is, the subarray XOR from i to j must equal 0.
Since we’re dealing with subarray XORs, let’s try to translate this in terms of prefix XORs instead.
Let P be the prefix XOR array of A, so that
P_i = A_1\oplus A_2\oplus\cdots\oplus A_i

Then, i and j are consecutive good indices if and only if P_{i-1} \oplus P_j = 0, or equivalently P_{i-1} = P_j.
In particular, note that if i is a good index, the next good index is uniquely determined: it’s the smallest j\gt i such that P_j = P_{i-1}.


With this observation, we can build a directed graph with the edge i\to j existing if and only if j is the next good index assuming i is a good index.
Note that this graph is acyclic.
To build the graph quickly, iterate over indices in reverse order and maintain the last seen occurrence of each prefix sum.

If the first good index is fixed, the final score is obtained by simply taking the sum of all values on the path starting from i.
These sums can easily be computed using dynamic programming because the graph is acyclic, by processing indices in descending order:

  • Let dp_i denote the sum of values on the path starting at i.
  • If no outgoing edge i\to j exists, dp_i = A_i.
  • Otherwise, if the edge i\to j exists, dp_i = A_i +dp_j.

Since we’re free to choose the initial value of X, the path can start anywhere, and the answer is thus simply \max(dp).

TIME COMPLEXITY:

\mathcal{O}(N) per testcase.

CODE:

Author's code (C++)
#include<bits/stdc++.h>
typedef long long int ll;
using namespace std;

int main(){
	int t;
	cin>>t;
	while(t--){
		ll n;
		cin>>n;
		vector<ll>a(n),dp(n,0);
		map<ll,ll>m;
		ll ans=0,pfxor=0,tmp;
		// to get x=a[i] for some i 
		// we can get initial value of x using the following recurrence:
			// if there is an index j<i such that a[j]^a[j+1]...^a[i]=0 and j is maximised 
				// Since a[i]!=0, there is no possible k such that a[j]^a[j+1]...^a[k]=0 
				// as this means a[k+1]^a[k+2]...a[i]=0 and k+1<i which contradicts our condition,
					// hence 
					//initial value of x=a[i] = initial value of x=a[j]
			// else x=prefix xor till i
		for(int i=0;i<n;i++){
			cin>>a[i];
			pfxor^=a[i];
			if(m.find(pfxor)!=m.end()){
				dp[i]=m[pfxor]+a[i]; 
			}
			else dp[i]=a[i]; // there was no pfxor present before so we will start with x=pfxor 
							// Hence, there will not be any case for getting j such that a[j]^a[j+1]...^a[i]=0 so only possible way is x=prefix xor till i

			tmp=(pfxor^a[i]);
			m[tmp]=dp[i]; 
			
			ans=max(ans,dp[i]);
		}
		cout<<ans<<'\n';

	}
	return 0;
}
Editorialist's code (PyPy3)
for _ in range(int(input())):
    n = int(input())
    a = list(map(int, input().split()))
    pref = [0]*(n+1)
    for i in range(n): pref[i+1] = pref[i] ^ a[i]
    
    next = {}
    dp = [0]*(n+1)
    for i in reversed(range(1, n+1)):
        if pref[i-1] not in next: dp[i] = a[i-1]
        else: dp[i] = a[i-1] + dp[next[pref[i-1]]]
        next[pref[i]] = i
    
    print(max(dp))
1 Like

This statement is technically wrong: the path cannot start from indices j such that there exists an incoming edge i \to j for some index i. Of course, this doesn’t affect the answer since we are looking for the maximum dp value.

However, it would matter if I asked you to count the number of distinct scores one may get. For example, in an array [2, 2, 2, 2] the set of possible scores is \{0, 8\} even though dp has other values too.

4 Likes

Let dp[X] be the max score you can achieve starting with X. You can consider having to xor by A[i] at each index i, and then change the starting X to X^A[i] instead.

from collections import defaultdict

for tc in range(int(input())):
    N = int(input())
    A = list(map(int, input().split()))
    
    s = 0
    dp = defaultdict(int)
    for x in A:
        s ^= x
        dp[s^x] = max(dp[s^x], dp[s] + x)
        dp[s] = 0
    print(max(dp.values()))
4 Likes

Can someone write code for authors explanation by building a graph.

The problem boils down to efficiently computing the maximum score for the given rules using the properties of XOR and prefix sums. Here’s how to implement the solution step-by-step based on the explanation provided:


Steps to Solve:

  1. Understand Prefix XORs:
  • Use a prefix XOR array P such that P[i]=A1⊕A2⊕…⊕AiP[i] = A_1 \oplus A_2 \oplus \ldots \oplus A_iP[i]=A1​⊕A2​⊕…⊕Ai​.
  • For a subarray [l,r][l, r][l,r], its XOR is P[r]⊕P[l−1]P[r] \oplus P[l-1]P[r]⊕P[l−1].
  1. Good Indices and Graph Representation:
  • Good indices iii and jjj satisfy P[i−1]=P[j]P[i-1] = P[j]P[i−1]=P[j], meaning we can “jump” from iii to jjj.
  • Build this jump relation using a hashmap to store the last occurrence of each prefix XOR value.
  1. Dynamic Programming to Compute Scores:
  • Use a dp array where dp[i]dp[i]dp[i] represents the maximum score obtainable starting at index iii.
  • For each index iii, if it has an outgoing edge to jjj, compute dp[i]=A[i]+dp[j]dp[i] = A[i] + dp[j]dp[i]=A[i]+dp[j]; otherwise, dp[i]=A[i]dp[i] = A[i]dp[i]=A[i].
  1. Result:
  • The maximum value in the dp array gives the desired answer, as you can start from any index.

Implementation:

Here is the Python implementation:

python

Copy code

def max_score(A):
    n = len(A)
    prefix_xor = [0] * (n + 1)  # Prefix XOR array
    last_seen = {}  # To store the last seen index of each prefix XOR value
    dp = [0] * n  # DP array to store the maximum scores
    
    # Compute the prefix XOR array
    for i in range(1, n + 1):
        prefix_xor[i] = prefix_xor[i - 1] ^ A[i - 1]
    
    # Traverse the array in reverse to build the graph and compute dp
    max_score = 0
    for i in range(n - 1, -1, -1):
        # Check if there's a good jump to the next index
        if prefix_xor[i] in last_seen:
            j = last_seen[prefix_xor[i]]
            dp[i] = A[i] + dp[j]
        else:
            dp[i] = A[i]
        
        # Update the last seen for the current prefix XOR
        last_seen[prefix_xor[i + 1]] = i
        
        # Update the maximum score
        max_score = max(max_score, dp[i])
    
    return max_score

# Driver code
t = int(input())  # Number of test cases
for _ in range(t):
    n = int(input())
    A = list(map(int, input().split()))
    print(max_score(A))

Explanation of Code:

  1. Prefix XOR Array Construction:
  • Compute prefix_xor in O(N)O(N)O(N) to represent XORs up to each index.
  1. Reverse Traversal:
  • Traverse the array in reverse to:
    • Update the last_seen hashmap with the current prefix XOR.
    • Calculate the dp values efficiently using previously computed values.
  1. Dynamic Programming:
  • Use dp[i]=A[i]+dp[j]dp[i] = A[i] + dp[j]dp[i]=A[i]+dp[j] where jjj is the next good index.
  • If no good index exists, dp[i]=A[i]dp[i] = A[i]dp[i]=A[i].
  1. Maximum Score:
  • Track the maximum score across all possible starting points using the dp array.

Complexity:

  1. Time Complexity:
  • Prefix XOR computation: O(N)O(N)O(N).
  • Reverse traversal and dp computation: O(N)O(N)O(N).
  • Total for one test case: O(N)O(N)O(N).
  1. Space Complexity:
  • Prefix XOR array: O(N)O(N)O(N).
  • Last seen hashmap: O(N)O(N)O(N).
  • DP array: O(N)O(N)O(N).Total: O(N)O(N)O(N).

Example:

Input:

Copy code

1
5
1 2 3 4 5

Output:

Copy code

15

Explanation:

The optimal path is to take all elements as good indices, as the prefix XOR constraints are satisfied.

Why should we need to travel in reverse order ?