题目链接
有n个数,现在要把他们拆分成两个集合,假设S为集合,有如下定义:
f ( S ) = { m i n ( x ⊕ y ) ∣ x , y ∈ S , a n d x ! = y } f(S)=\{min(x\oplus y)|x,y\in S,and\;x!=y\} f(S)={min(x⊕y)∣x,y∈S,andx!=y}
将n个数拆分为两个集合A,B,要求最大化 m i n ( f ( A ) , f ( B ) ) m i n ( f ( A ) , f ( B ) ) min(f(A),f(B)),并输出划分集合的方案,如果有多种,则输出任意一个.
题解思路:
首先要最大化 m i n min min,那么我们先转换成图论问题:异或完全图
就是每个 n n n点,第 i i i个点和第 j j j个点之间边权是 a i ⊕ a j a_i\oplus a_j ai⊕aj,
那么我们要使用我们先按照边权从小到大看:就是不断把点加入集合,就是对于边权最小的这两个点,肯定不能在一个集合里面,然后把这条边加入集合,相当于这两个点要在不同的集合,那么我们一直往这个图里面加边,一旦这个图出现了奇环那么这条边连的两个点一定要在一个集合里面了!!那么这条边就是答案。
出现奇环意味着这个图不是二分图。那么就是不断往里面加边,然后,如果这个边一加进去就不是二分图了,那么这条边权就是答案了。其他边怎么连都会不会比这个边更小。
但是边有 n 2 n^2 n2条,我们不可能暴力去找异或最小的两条边,肯定不行,那么我们该怎么优化?
因为我们要异或最小嘛,那么我们先看看高位,高位相同的在一组,因为这两个高位不同的数在一个集合里面肯定不一定是最小的。还有关键如果当前位分组如果数的个数大过2,那么无论你怎么分这一位在最终答案都是0。
eg:我们先按照第31位划分两个集合,因为这两个集合里面点互相连边,肯定比跨集合连边结果还要小,那么我们就是尽可能去让这两个集合里面的点被划分到1,2里面。但是可能两个集合里面的点数超过3,那么根据鸽巢原理肯定有两个点一定要被分到一个集合里面那么这第31位就结果肯定是0了。那么我们接着对这个集合里面的点按照高30位划分(这是在31位划分好的集合上再划分)。一直划分到第 i i i位的时候,这时候数全部被划分成 x x x个集合,每个集合大小都 ≤ 2 \leq2 ≤2,那么说明同一个集合里面的点可以被划分到不同答案组里面 ( 1 or 2 ) (1\;\text{or}\;2) (1or2),那么这一位就可以 ⊕ = 1 \oplus=1 ⊕=1
但是不是这样就结束了呢?肯定不是我们还要接着看 [ i − 1 , 0 ] [i-1,0] [i−1,0]这些位置的贡献,在这之前我要把第 i i i位的贡献删掉,毕竟 i i i位已经比下面限死了。依次类推就结束了
那么上面按照高位分组是不是很像trie树,按照高位分,不存在大过2的分组就是看trie树下面叶子节点的个数是否大过2?
在划分好集合后,并不是两个点一定是在不同的集合里面了,根据上面的原理,这个图一定要是二分图。
动态判二分图那就是扩展域并查集了。
#include
#define mid ((l + r) >> 1)
#define Lson rt << 1, l , mid
#define Rson rt << 1|1, mid + 1, r
#define ms(a,al) memset(a,al,sizeof(a))
#define log2(a) log(a)/log(2)
#define lowbit(x) ((-x) & x)
#define IOS std::ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define INF 0x3f3f3f3f
#define LLF 0x3f3f3f3f3f3f3f3f
#define f first
#define s second
#define endl '\n'
using namespace std;
const int N = 2e6 + 10, mod = 1e9 + 9;
const int maxn = 500010;
const long double eps = 1e-5;
const int EPS = 500 * 500;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> PII;
typedef pair<ll,ll> PLL;
typedef pair<double,double> PDD;
template<typename T> void read(T &x) {
x = 0;char ch = getchar();ll f = 1;
while(!isdigit(ch)){if(ch == '-')f*=-1;ch=getchar();}
while(isdigit(ch)){x = x*10+ch-48;ch=getchar();}x*=f;
}
template<typename T, typename... Args> void read(T &first, Args& ... args) {
read(first);
read(args...);
}
int T, n;
int arr[maxn], col[maxn];
struct dsu {
int fa[maxn << 1];
inline void init(int n) {
for(int i = 1; i <= n; ++ i) fa[i] = i;
}
inline int find(int x) {
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
inline void merge(int x, int y, int n) {
int fx = find(x);
int fxn = find(x + n);
int fy = find(y);
int fyn = find(y + n);
fa[fx] = fyn;
fa[fxn] = fy;
}
}dsu;
int main() {
IOS;
cin >> T;
while(T --) {
cin >> n;
dsu.init(n<<1|1);
for(int i = 1; i <= n; ++ i) col[i] = col[i + n] = 0;
for(int i = 1; i <= n; ++ i) cin >> arr[i];
ll minans = 0;
for(int i = 29, nouse = 0; i >= 0; -- i) {
nouse |= 1<<i;
unordered_map<int,vector<int>> Set;
for(int j = 1; j <= n; ++ j)
Set[arr[j] & nouse].push_back(j);
bool flag = 0;
for(auto it : Set) {
if((int)it.second.size() > 2 || ((int)it.second.size() == 2 && dsu.find(it.second[0]) == dsu.find(it.second[1])))
flag = 1;//判断当前位划分是否合法
}
if(flag == 1) continue;
for(auto it : Set)
if((int)it.second.size() == 2)
dsu.merge(it.second[0],it.second[1],n);
minans |= 1<<i;
nouse ^= 1<<i; // 把当前位的贡献删掉相当于接着找生成树里面的边
}
cout << minans << endl;
for(int i = 1; i <= n; ++ i) {
if(col[dsu.find(i)]) cout << col[dsu.find(i)]; // 模拟二分图染色过程
else if(col[dsu.find(i+n)]) cout << (col[dsu.find(i)] = 3 - col[dsu.find(i+n)]);
else cout << (col[dsu.find(i)] = 1);
}
cout << endl;
}
return 0;
}