题目
题目描述
给定长度为
n
n
n 的非负整数序列
{
a
}
\{a\}
{a} 。构造长度为
n
n
n 的序列
{
b
}
\{b\}
{b} ,满足
{
b
}
\{b\}
{b} 的异或和为
0
0
0 。最小化
∑
i
=
1
n
∣
a
i
−
b
i
∣
\sum_{i=1}^{n}|a_i-b_i|
i=1∑n∣ai−bi∣
数据范围与约定
n
≤
15
,
a
i
<
2
30
n\le 15,a_i< 2^{30}
n≤15,ai<230
思路
耿直的暴力
用 f ( i , s ) f(i,s) f(i,s) 表示前 i i i 个数的异或和为 s s s ,代价最小是多少。枚举第 i + 1 i+1 i+1 个数即可。
时间复杂度 O ( n V 2 ) \mathcal O(nV^2) O(nV2) ( V V V 是 a a a 的值域)。
状压的 D P \tt{DP} DP
用 f ( k , S , i , 0 / 1 ) f(k,S,i,0/1) f(k,S,i,0/1) 表示考虑到第 k k k 高的位、前 i i i 个数,当前位的异或值是 0 / 1 0/1 0/1 。 S S S 是一个 3 3 3 进制状态压缩,存储每一个数字的变化情况。
- 在 S S S 中为 0 0 0 :没有发生过变动。
- 为 1 1 1 :曾经变小过(在更高的某一位,从 1 1 1 变成了 0 0 0 )。
- 为
2
2
2 :与
为1
相对。
复杂度 O ( 3 n n log V ) \mathcal O(3^nn\log V) O(3nnlogV) 。
正解的 D P \tt{DP} DP
状态数量太多。问题主要出在 3 n 3^n 3n 这一项。怎样处理这一点?
假设一个数字在某一位变大了,最优情况下,后面的每一位都要成为 0 0 0 ;假设一个数字在某一位变小了,最优情况下,后面的每一位都要成为 1 1 1 。这里的“最优”即代价最小。
然后呢?如果我想对它们进行操作,它们的代价,一定是
2
x
2^x
2x (假设处理到了第
x
x
x 位)!因为此时后面是满的。
最优的情况是红色的情况——全 1 1 1 。后面的操作,一定是满打满算的花费 2 x 2^x 2x 。
既然花费相同,何必开成三进制状态?
用 f ( k , i , S , x , y ) f(k,i,S,x,y) f(k,i,S,x,y) 表示第 k k k高位、第 i i i 个数字, S S S 是状压,表示 数字是否发生过改变。 x x x 代表着在这一位的异或和; y y y 用来 记录更高的位的修改操作带来的异或和改变。
这个 y y y 究竟是什么意思呢?还是刚才那个图:我得将后面整体赋值,才能得到“最优情况”。这个值由谁来记录呢?就是 y y y 了。
T h a t i s t o s a y \tt{That\; is\; to\; say} Thatistosay ,对于 S S S 中已经被改变了的值,单独拿出来做一次异或和。然后剩下的工作就是 x x x 的。
于是我们就可以转移了。时间复杂度 O ( 2 n n log V ) \mathcal O(2^n n \log V) O(2nnlogV)(有常数 4 4 4 ,因 x , y x,y x,y 的枚举)。
代码
k k k 和 i i i 可以一起滚动,降低空间复杂度。
#include <cstdio>
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
inline int readint(){
int a = 0; char c = getchar(), f = 1;
for(; c<'0'||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
template < class T >
void getMax(T&a,const T&b){if(a<b)a=b;}
template < class T >
void getMin(T&a,const T&b){if(b<a)a=b;}
const int infty = 0x3f3f3f3f;
const int MaxN = 15;
int dp[2][1<<MaxN][2][2];
int a[MaxN], n;
void Solve() {
n = readint();
for(int i=0; i<n; ++i)
a[i] = readint();
memset(dp,0x3f,sizeof(dp));
dp[0][0][0][0] = 0;
int fr = 0, to = 1; // 用来进行滚动数组
for(int s=30; ~s; --s) { // 当前处理第s位
for(int i=0; i<n; ++i) { // 考虑第i个数字
int v = a[i]>>s&1; // a[i]的第s位
// cost:第一次操作的花费
int cost = a[i]&((1<<s)-1);
cost = !v ? (1<<s)-cost : cost+1;
memset(dp[to],0x3f,sizeof(dp[to]));
for(int j=0; j<(1<<n); ++j) // 枚举“是否改变过”
for(int l=0; l<2; ++l) // "x"
for(int o=0; o<2; ++o){ // "y"
int x = dp[fr][j][l][o];
if (j>>i&1) { // 已经被改变过了
/* 这个数字什么也不做,异或值承载在y上 */
getMin(dp[to][j][l][o],x);
/* 将其更改(这一位的异或值会改,后面的不会,在x上修改!) */
getMin(dp[to][j][l^1][o],x+(1<<s));
/* 请再看一次图:后面是全1,再次进行修改,后面仍然是全1 */
} else { /* 没有被修改过 */
/* 不修改,在x上记录异或值 */
getMin(dp[to][j][l^v][o], x);
/* 如果这一位是1,修改意味着后面是全1,将异或值放在y上 */
/* 注意:这里的x加入了这个异或值,因为y只影响后面 */
getMin(dp[to][j^(1<<i)][l^v^1][o^v],x+cost);
}
}
swap(fr,to);
}
/* 将要到达下一位 */
/* 在每一位的初始化时,先将y放到x上 */
/* 这样一来,x=0就是valid的充要条件 */
for(int j=0; j<(1<<n); ++j){
/* 将y放到x上 */
dp[fr][j][1][1] = dp[fr][j][0][1];
/* 处理掉不合法情况 */
dp[fr][j][1][0] = infty;
/* 它是一个不存在的情况。 */
/* 高位给了你一个1,而你没有东西,你却拿到0? */
dp[fr][j][0][1] = infty;
/* dp[0][0] 完全不会变动 */
}
}
int ans = infty;
for(int i=0; i<(1<<n); ++i){
getMin(ans,dp[fr][i][1][1]);
getMin(ans,dp[fr][i][0][0]);
}
printf("%d\n", ans);
}
int main() {
for(int T=readint(); T; --T)
Solve();
return 0;
}