首先,我们从A[0]^A[1]^A[2]...^A[k-1] = A[1]^A[2]...^A[k-1]^A[k]
可以得到A[0]=A[k]
,以此类推,任何元素A[i]=A[i-k]
,也就是说我们要使得整个数组以k为周期循环。我们令Set{i}表示{A[i],A[i+k],A[i+2k]...}
这一组需要改为相同值的元素。总共有k组。
那么对于每一组而言,我们改成什么数值最好呢?贪心的想法是改成该组里出现频次最多的那个元素,这样改动次数就可以最少。但显然,这不一定能达到全局最小的改动次数。事实上,甚至不一定要改成该组里任何曾经出现过的元素,也就是说,全局最优解有可能对应着该组的元素全部都要改动。
想到这里的话,似乎我们只能遍历所有的取值可能;也就是说,对于任何一组元素的改动方案,我们都要把[0,1023]都尝试一遍(题目中给定了每个元素的范围)。另外,因为每个元素的取值都在[0,1023],那么他们的XOR SUM也一定是在这个范围内,所以我们就可以设计DP算法。令dp[i][d]表示使得前i个元素的XOR-SUM等于d的最小改动(注意每次改动要把同组的元素都变一样)。那么突破口就是第i个元素的取值v,我们遍历它的可能:
for (int i=0; i<k; i++)
for (int d=0; d<1024; d++)
for (int v=0; v<1024; v++)
{
dp[i][d] = min(dp[i][d], dp[i-1][d^v] + cost{将第i组元素都改成v});
}
对于上面的表达式有几点说明:
- d^v的本意是找一个元素x,使得前i-1个元素的XOR-SUM等于x,并且x^v之后的结果等于d。根据XOR的性质,我们可以知道这样的x其实就是d^v
- cost{将第i组元素都改成v}可以提前计算并存储起来。它的本质就是第i组元素的总个数,减去第i组元素里数值为v的总个数。前者记为totalCnt[i],后者记为count[i][v],后者的空间大小是kx1024,可以接受.
- 整体的时间复杂度是
o(k*1024*1024)
,注定会TLE。
那么如何改进呢?我们将v的选择分为两部分,第一部分是第i组里面的元素,第二部分是第i组里没有出现的元素。如果只考虑前者,那么之前的代码就是:
for (int i=0; i<k; i++)
for (int d=0; d<1024; d++)
for (int j=i; j<n; j+=k)
{
int v = nums[j];
dp[i][d] = min(dp[i][d], dp[i-1][d^v] + cost{将第i组元素都改成v});
}
这是o(k*n/k*1024)
的复杂度.
那么如果v是Set{i}里没有出现过的元素,我们也需要遍历v才能得到最小的dp[i][d]吗?事实上,我们可以预知哪个v能够得到最小的dp[i][d]。这是因为当v不在Set{i}里出现时,转移方程是
dp[i][d] = dp[i-1][d^v] + totalCnt[i]
其中的cost项是一个与v无关的常数。所以我们只需要找到最小的dp[i-1][d^v],那么得到的dp[i][d]必然也是最小的。那么“最小的dp[i-1][d^v]”是什么呢?只要将dp[i-1][x]里的x从[0,1023]扫一遍就行了。找到了最小dp[i-1][x]对应的x,那么就可以计算v=d^x。
但是这里有一个很迷惑的操作:你这样计算出来的v,不一定真的不在Set{i}里面。也就是说cost不见得真的就是totalCnt[i]。你按照v不在set{i}的方法去计算得到的最优方案v,有可能确实在Set{i}里面。但是这没有关系:因为这种情况下的cost=totalCnt[i]-count[i][v]
,比之前预期的totalCnt[i]
更小,这一定是比v是非Set{i}元素更优的解。
综上所述,完整的算法描述为:
for (int i=0; i<k; i++)
for (int d=0; d<1024; d++)
{
Find x, s.t. dp[i-1][x]最小
v = d^x;
dp[i][d] = dp[i-1][x] + cost{将第i组元素都改成v});
for (int j=i; j<n; j+=k)
{
int v = nums[j];
dp[i][d] = min(dp[i][d], dp[i-1][d^v] + cost{将第i组元素都改成v});
}
}
注意,Find x, s.t. dp[i-1][x]最小
这一行虽然要遍历1024个值,但与d无关,所以可以拿到d循环的外面。于是总的时间复杂度是o(k*( 1024 + 1024 * n/k)
.