题目
题目概要
n
n
n 个点的完全图,给定整数序列
a
1
,
a
2
,
a
3
,
…
,
a
n
a_1,a_2,a_3,\dots,a_n
a1,a2,a3,…,an 满足边
⟨
x
,
y
⟩
\lang x,y\rang
⟨x,y⟩ 的边权为
(
a
x
and
a
y
)
(a_x\operatorname{and}a_y)
(axanday),其中
and
\text{and}
and 是按位与运算符。求最大生成树。
数据范围与约定
将会给定一个
m
m
m ,满足
∀
x
∈
[
1
,
n
]
,
a
x
∈
[
0
,
2
m
)
\forall x\in[1,n],\;a_x\in[0,2^m)
∀x∈[1,n],ax∈[0,2m) 。
对于所有数据, n ≤ 1 0 5 n\le 10^5 n≤105 且 m ≤ 18 m\le 18 m≤18 。
思路壹
感谢@CzxingcHen 的博客提供了思路与代码。
如果 a a a 值有重复,譬如 a x = a y a_x=a_y ax=ay,那么所有与 a y a_y ay 相连的边,都可以嫁接到 x x x 身上(而边权不变)。毕竟 a x a_x ax 和 a y a_y ay 是一定要连边的(根据相关定理,与 a x a_x ax 相连的所有边中,边权最大的一个一定属于最大生成树)。所以,可以认为 a a a 的值互不相同。
为了方便叙述,认为 a x a_x ax 就是点的编号。也就是说点的编号并不是连续的。
考虑使用 k r u s k a l \tt kruskal kruskal 算法求解。边权的大小 v ∈ [ 0 , 2 m ) v\in[0,2^m) v∈[0,2m),枚举一个边权,问题转化为了:找到有哪些边的边权为 v v v 。
首先边权为 v v v 的边一定是点集 { x ∣ v ⫅ x } \{x\;|\;v\subseteqq x\} {x∣v⫅x} 之间的边。其次,这个点集内的边的权不小于 v v v,所以连接之后它们会成为一个连通块。类似的,点集 { x ∣ ( v or 2 r ) ⫅ x } \{x\;|\;(v\operatorname{or}2^r)\subseteqq x\} {x∣(vor2r)⫅x} 在考虑边权为 v v v 的边之前,就已经成为了一个连通块。一个连通块只需要选取一个点。
同时注意到 r ∈ [ 0 , m ) r\in[0,m) r∈[0,m) 就已经把点集都划分完了。所以我们只需要找这 m m m 个连通块,每个选出一个代表,然后尝试和 v v v 连边。反正两两连接,和直接与 v v v 连接,效果是一样的,本质上是计算了连通块个数。
我们可以用 f ( v ) f(v) f(v) 表示,点集 { x ∣ v ⫅ x } \{x\;|\;v\subseteqq x\} {x∣v⫅x} 这个连通块中,任取的一个代表。事实上,只要随意找到一个点即可,因为形成连通块是必然的。那么可以递推,在 f ( v or 2 r ) f(v\operatorname{or}2^r) f(vor2r) 中找一个就行。
时间复杂度 O ( m 2 m + n ) \mathcal O(m2^m+n) O(m2m+n),相当快。
代码
#include <cstdio>
#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long int_;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
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;
}
inline void writeint(int x){
if(x > 9) writeint(x/10);
putchar((x-x/10*10)^48);
}
const int MaxN = 1<<18;
namespace UFS{
int fa[MaxN], rnk[MaxN];
void init(int n){
rep(i,1,n)
fa[i] = i, rnk[i] = 1;
}
inline int find(int a){
if(fa[a] != a)
fa[a] = find(fa[a]);
return fa[a];
}
bool merge(int a,int b){
a = find(a), b = find(b);
if(a == b) return false;
if(rnk[a] > rnk[b]) swap(a,b);
fa[a] = b, rnk[b] += rnk[a];
return true;
}
}
int f[MaxN];
int main(){
int n = readint(), m = readint();
int_ ans = 0;
for(int a; n; --n){
a = readint();
if(f[a]) ans += a;
else f[a] = a;
}
UFS::init((1<<m)-1);
for(int i=(1<<m)-1; ~i; --i){
for(int j=0; j<m&&(!f[i]); ++j)
f[i] = f[i|(1<<j)];
if(!f[i]) continue; // nothing
for(int j=0; j<m; ++j)
if(!(i>>j&1) && f[i^(1<<j)] &&
UFS::merge(f[i],f[i^(1<<j)]))
ans += i; // one more edge
}
printf("%lld\n",ans);
return 0;
}
思路贰(正确性未知)
这是我的第一思路。似乎和思路一的思考方向差不多。但是一点也不显然。
鉴于 正确性未知,只简单介绍一下。对于每一个二进制位,试图将所有该位上为 1 1 1 的数连接成一个连通块。利用 L C T \tt LCT LCT,当新加入的边形成环的时候,就删去最短的一条边。
思路叁
使用 B开头的算法(我们老师这样称呼它)
B
o
r
u
v
k
a
\tt{Boruvka}
Boruvka 算法。简单来说就是,一个点的邻边中,边权最大的一定在最大生成树上,对于所有点都连接这条边,每次至少会连
n
2
n\over 2
2n 条边,然后缩点。最多连接
O
(
log
n
)
\mathcal O(\log n)
O(logn) 轮。
要求按位与的最大值,就建 0 / 1 t r i e 0/1\;\tt trie 0/1trie 呗。建完之后,自底向上,把 1 1 1 儿子往 0 0 0 儿子里复制一份,这是套路了,因为 and \operatorname{and} and 操作中,当另一个数字是 0 0 0 时,该数字是 0 0 0 或 1 1 1 等效。时间复杂度是 O ( m 2 m ) \mathcal O(m2^m) O(m2m) 的。
接下来的问题是,一个点不能连接自己。需要判断 t r i e \tt trie trie 的子树内是否所有的 a a a 都来自于某个连通块,并且叶子节点需要存两个不同的连通块(如果有的话)。这个好像也挺简单的,在复制的时候也可以维护。
总复杂度 O ( m 2 m log n ) \mathcal O(m2^m\log n) O(m2mlogn) 。我试着去优化,然卒未果。