[UOJ176]新年的繁荣

122 篇文章 0 订阅
98 篇文章 0 订阅

题目

传送门 to UOJ

题目概要
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 n105 m ≤ 18 m\le 18 m18

思路壹

感谢@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\} {xvx} 之间的边。其次,这个点集内的边的权不小于 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\} {xvx} 这个连通块中,任取的一个代表。事实上,只要随意找到一个点即可,因为形成连通块是必然的。那么可以递推,在 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) 。我试着去优化,然卒未果。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值