Minimum changes so the XOR of every k consecutive elements is 0

Viewed 3363

I believe the online judge for this task has expired. Given my proposed solution below, is it logically sound? Can we do better in terms of time or space complexity? What would a practical brute-force method look like for comparison?

Task:

Given an array of length n, find the minimum number of elements that would need to be changed so that the XOR of every k consecutive elements is 0.

Constraints:

1 ≤ k ≤ n ≤ 10^4
0 ≤ A[i] < 1024

Proposed solution:

Say we have an optimal selection for the first k elements. To update the current window to the next contiguous k elements, we remove the contribution of the first element and XOR with the next proposed element. To remove the contribution of the first element, we XOR with it, which means the only option to make the next window XOR to zero is to XOR with the element we just removed. This means the optimal first k elements must continue to repeat throughout.

e1, e2, e3,...ek, e1, e2, e3,...ek, etc.

Lets call each sequence of elements A[i], A[i+k], A[i+2*k]... that must be equal to one another, seq(i). We can calculate a minimal upper bound on the number of changes needed by noting that if just one seq(i) is allowed to have its elements set to any one arbitrary element, we can incur that cost and make any selection for the remaining seq(i)s a viable solution, including the selection with least cost for each.

To try and do better than the minimal upper bound, we rule out using any arbitrary assignment, so all target options for each seq(i) must come from the set seq(i) itself. Furthermore, as we iterate, we can use the minimal upper bound to rule out any XOR prefix that costs as much or more.

Time complexity O(k * n/k * 1024) = O(n). Space complexity O(n).

Python 3 example:

from collections import defaultdict
from math import ceil

A = [1, 2, 3, 1, 4]
k = 3

n = len(A)

seqs = [None] * k

for i in range(k):
  seqs[i] = defaultdict(lambda: 0)

  for j in range(i, n, k):
    seqs[i][A[j]] += 1

def cost(i, e):
  return ceil((n - i) / k) - seqs[i][e]
  
def min_cost(i):
  return min([cost(i, e) for e in seqs[i]])
  
total_min_cost = sum([min_cost(i) for i in range(k)])

upper_bound = total_min_cost + min([ceil((n - i) / k) - min_cost(i) for i in range(k)])

dp = {0: 0}

for i in range(k):
  new_dp = defaultdict(lambda: float('inf'))

  for e in seqs[i]:
    for xor_pfx in dp:
      new_cost = cost(i, e) + dp[xor_pfx]

      if new_cost < upper_bound:
        new_pfx = xor_pfx ^ e
        new_dp[new_pfx] = min(new_dp[new_pfx], new_cost)

  dp = new_dp
  
result = dp[0] if 0 in dp else upper_bound

print(result)
1 Answers

As mentioned by OP, two conditions must be fulfilled in order to get a valid sequence:

1. xor-sum_(i=0 to K-1) A[i] = 0
2. A[i+K] = A[i] for all i

This implies that there are 'K-1' degrees of freedom to built such a sequence.
Note: this kind of sequence can be interpreted as the channel encoding of an information sequence of size K-1, with the concatenation of a simple parity check encoding (condition 1., a sequence of length K is obtained)) and a repetition encoding (condition 2 -> length N). Then the exercise consists in correcting the errors introduced by a transmission channel. After the channel, the conditions are no longer respected and the most reliable estimation consists in rebuilding a correct sequence, while introducing as few modifications (corrections) as possible.

Let us call S[i] the K sets corresponding to identical values. S[i] = {A[i], A[i+K], A[i+2*K], ...}, with i: 0 -> K-1,
and let us call L[i] the size of each S[i].

The first step consists of an attempt to decode the repetition codes, i.e. deciding which is or which are the best estimation(s) of each sets S[i]. Logically, the best estimation consists in finding, for each set, which value is the most represented. For each set S[i] and each possible value j (j from 0 to to Amax, here Amax = 1023), the reliability of j is equal to the number of times it appears in Set[i]. Practically:

Reliab[i][j]++ each times `j` appears in `S[i]`. 
and then, Cost[i][j] = L[i] - Reliab[i][j]

By maximizing the reliability in each set, we get an estimation B[i] for set E[i].
At this point, if the estimations respects the parity condition:

xor-sum B[i] = 0

Then we have found our estimation, and the number of changes corresponds to a lower bound:

lower_bound = sum(L[i] - reliab[i][B[i]])

However, in the general case, the parity condition is not respected, and we need to find the way to alter as minimum the number of changes. One rather simple possibility consists in modifying only one estimation, the one corresponding to the minimum additional cost. For example, if we accept to modify the estimation B[i], then we have to replace it with

C[i] = xor-sum B[j], for j different of i. 

Then the additional number of changes is equal to

add_cost[i] = Reliab[B[i]] - Reliab[C[i]]]

However, this solution of modifying one former estimation only cannot insure all the time minimisation of the number of changes.

To solve it, one possibility (brute force!) is to calculate all the costs corresponding to all possibilities, iteratively.

For (i: 0 -> K-1) For (j: 0 -> Amax)
    cumul_cost[i][j] = min(k) {cumul_cost[i-1][j^k] + Cost[i][k]} (k = 0 to Amax)

Then, the answer if equal to cumul_cost[K-1][0]

The problem is that this method has a complexity equal to O(N + K*Amax^2) which seems too much.

At least, this solution is simple to implement and should provide a reference to check simpler solutions.

In this method, many intermediate results are considered, which cannot correspond to a viable solution. A practical solution that should be much better consists in implementing backtracking, while prioritizing the more reliable elements.

This can be obtained by sorting the sets E[i], and not exploring further a DFS branch while the current number of modifications is greater than the current obtained best solution.

Related