IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> [ACNOI2022]数树 -> 正文阅读

[数据结构与算法][ACNOI2022]数树

题目

题目背景
数学能够成为严谨的科学学科,离不开这三条公理的支持:

  1. 若一个命题无法被证伪,则它是正确的。
  2. 若一个命题无法被证明,则它是错误的。
  3. 若这么做的人不是 O U Y E \sf OUYE OUYE,则前两条都是错误的。

题目描述
给出一个简单无向图 G = ( V , E ) G=(V,E) G=(V,E),给出 m m m 个点集 S 1 , S 2 , … , S m S_1,S_2,\dots,S_m S1?,S2?,,Sm?,问有多少个生成树 T T T 满足 S i ?? ( 1 ? i ? m ) S_i\;(1\leqslant i\leqslant m) Si?(1?i?m) 是树 T T T 上的连通块。

数据范围与提示
∣ V ∣ ? 500 |V|\leqslant 500 V?500 m ? 2000 m\leqslant 2000 m?2000 。显然 ∣ E ∣ ? ( ∣ V ∣ 2 ) |E|\leqslant{|V|\choose 2} E?(2V?)

我的思路

从一些简单的情况想起。若 m = 0 m=0 m=0 就是矩阵树定理裸题。

m = 1 m=1 m=1,考虑 ∣ S 1 ∣ = 2 |S_1|=2 S1?=2,即存在一条必选的边,则直接选择它然后缩点。推广到 ∣ S 1 ∣ ? 2 |S_1|\geqslant 2 S1??2,只需要先对该连通块建立生成树(矩阵树定理求生成树个数)然后缩点,再做全局的矩阵树定理。

m = 2 m=2 m=2,考虑 S 1 ∩ S 2 = ? S_1\cap S_2=\varnothing S1?S2?=?,则二者独立,分别缩点。否则,我们注意到树上连通块的交集仍然是连通块,所以 S ′ = S 1 ∩ S 2 S'=S_1\cap S_2 S=S1?S2? 也是连通块。将 S ′ S' S 缩点后则 ∣ S 1 ∩ S 2 ∣ = 1 |S_1\cap S_2|=1 S1?S2?=1,这样的连通块也是独立的,又可以分别缩点了。

推广到任意 m m m,直观来看似乎只需处理每种 S S S 的交集。这是非常感性的;从特殊到一般不要想当然。我在过程中就犯了下面的诸多错误。

其实 m > 2 m>2 m>2 的时候,只需要依次合并信息。先令 T = V T=V T=V 即全集,然后检查 m m m S i S_i Si?,在 ∣ S i ∩ T ∣ > 1 |S_i\cap T|>1 Si?T>1 时令 T = S i ∩ T T=S_i\cap T T=Si?T 。最后对 T T T 运用矩阵树定理。重复该过程即可。

先别算复杂度。我们凭什么能缩点?已经连通的点并不等价。比如三元环, S 1 = { 1 , 2 } S_1=\{1,2\} S1?={1,2} S 2 = { 2 , 3 } S_2=\{2,3\} S2?={2,3},答案为 1 1 1 。然而缩点 S 1 S_1 S1? 会导致新图为 E ′ = { ( 1 ′ , 2 ) , ( 1 ′ , 2 ) } E'=\{(1',2),(1',2)\} E={(1,2),(1,2)},求出答案为 2 2 2

所以,我们实际上是 临时缩点,在求某个点集 T T T 的生成树数量时,将 T T T 中已经连通的点视为一个点。用并查集维护哪些点已经连通。同时,原本的 ∣ S i ∩ T ∣ > 1 |S_i\cap T|>1 Si?T>1 也要改为 S i ∩ T S_i\cap T Si?T 尚未完全连通。缩点的问题就解决了。

无论复杂度如何,你会发现三元环上 m = ( 3 2 ) m={3\choose 2} m=(23?) 判不出无解(即每条边都被 S i S_i Si? 指定了)。因为 ∣ S 1 ∩ S 2 ∣ = 1 |S_1\cap S_2|=1 S1?S2?=1 时二者独立,前提是我们料想它们的连通性互不影响——这在它们确实构成树时,是毋庸置疑的。然而无解时呢?

需要判断无解。其实简单,在并查集合并 A , B A,B A,B 两个点集时,若某个 S i S_i Si? 满足 S i ∩ A ≠ ? S_i\cap A\ne\varnothing Si?A?=? S i ∩ B ≠ ? S_i\cap B\ne\varnothing Si?B?=? 且当前操作点集 T ? S i T\nsubseteq S_i T?Si?,则无解。因为 T ? S i T\nsubseteq S_i T?Si? 说明 ∣ T ∩ S i ∣ ? 1 |T\cap S_i|\leqslant 1 TSi??1,即 “理论上不应影响 S i S_i Si? 中点的连通性”。而现在影响了,肯定无解。

终于,到了优化复杂度的时候。首先是找 T T T 。共 n n n 轮,每轮求 k k k 次大小为 n n n 的集合交,并判断它是否完全连通。用 b i t s e t \tt bitset bitset 求出集合并,然后找到其中任意一个元素——用 _Find_first 函数,虽说似乎不太正规——设为 x x x 。对并查集的每个连通块,用 b i t s e t \tt bitset bitset 维护其中的元素,那么只需要判断 x x x 所在连通块对应的点集是否包含 S i ∩ T S_i\cap T Si?T 。复杂度 O ( k n 2 ω ) \mathcal O({kn^2\over\omega}) O(ωkn2?)

接着是矩阵树定理,总复杂度 O ( n 3 ) \mathcal O(n^3) O(n3) 不解释。然后是判断无解。共判断 O ( n ) \mathcal O(n) O(n) 次,每次 O ( k ) \mathcal O(k) O(k) O ( n ω ) \mathcal O({n\over\omega}) O(ωn?) b i t s e t \tt bitset bitset 判断,也是 O ( k n 2 ω ) \mathcal O({kn^2\over\omega}) O(ωkn2?) 的。

最后是维护并查集信息。只有 O ( n 2 ω ) \mathcal O({n^2\over\omega}) O(ωn2?),不用考虑了。总复杂度 O ( k n 2 ω + n 3 ) \mathcal O({kn^2\over\omega}+n^3) O(ωkn2?+n3) 搞定。

正解思路

constructive \text{constructive} constructive 。但确实具有美感,可以讲讲。

我的做法 为何复杂?因为 ∣ S 1 ∩ S 2 ∣ = 1 |S_1\cap S_2|=1 S1?S2?=1 时,二者也独立。这说明 点不是基本元素。生成树实际上是选择边的过程。于是我们考虑一条边被 S i S_i Si? 包含的次数,记 λ ( u , v ) = ∑ [ u ∈ S i ∧ v ∈ S i ] \lambda(u,v)=\sum[u\in S_i\land v\in S_i] λ(u,v)=[uSi?vSi?] 。结论是 最大生成树即为所求,当然,需要权值和恰好为 ∑ ( ∣ S i ∣ ? 1 ) \sum(|S_i|-1) (Si??1)

何故?显然 S i S_i Si? λ \lambda λ 上的系数最多贡献 ( ∣ S i ∣ ? 1 ) (|S_i|{-}1) (Si??1),而且必须达到。所以只能是最大生成树。求最大生成树个数,只需对每种权值的边分别做一次即可。时间复杂度 O ( n 3 + n 2 k ω ) \mathcal O(n^3+{n^2k\over\omega}) O(n3+ωn2k?),代码实现想必很简单。

代码

正解思路 代码可见最强者的博客。欲知谁最强,重读《题目背景》。

#include <cstdio> // megalomaniac JZM yydJUNK!!!
#include <iostream> // Almighty XJX yyds!!!
#include <algorithm> // decent XYX yydLONELY!!!
#include <cstring> // Casual-Cut DDG yydOLDGOD!!!
#include <cctype> // oracle: ZXY yydBUS!!!
#include <bitset>
#include <vector>
typedef long long llong;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
# define rep0(i,a,b) for(int i=(a); i!=(b); ++i)
inline int readint(){
	int a = 0, c = getchar(), f = 1;
	for(; !isdigit(c); c=getchar()) if(c == '-') f = -f;
	for(; isdigit(c); c=getchar()) a = a*10+(c^48);
	return a*f;
}

const int MOD = 998244353;
inline void modAddUp(int &x, const int &y){
	if((x += y) >= MOD) x -= MOD;
}
inline llong qkpow(llong b, int q){
	llong a = 1;
	for(; q; q>>=1,b=b*b%MOD) if(q&1) a = a*b%MOD;
	return a;
}

const int MAXN = 503;
int mat[MAXN][MAXN];
int getDet(int n){
	int res = 1;
	for(int i=1,id; i<=n; ++i){
		rep(j,id=i,n) if(mat[j][i]){
			id = j; break; // found
		}
		if(mat[id][i] == 0) return 0;
		int* const _end = mat[i]+n+1;
		if(id != i){
			for(int *l=mat[i]+i,*r=mat[id]+i; l!=_end; ++l,++r)
				std::swap(*l,*r); // manual swap
			res = MOD-res; // negate
		}
		res = int(llong(res)*mat[i][i]%MOD);
		llong inv = qkpow(mat[i][i],MOD-2);
		for(int *j=mat[i]+i; j!=_end; ++j)
			*j = int((*j)*inv%MOD);
		rep(j,i+1,n) if(mat[j][i]){
			const llong f = MOD-mat[j][i];
			for(int *l=mat[i]+i,*r=mat[j]+i; l!=_end; ++l,++r)
				*r = int(((*r)+f*(*l))%MOD);
		}
	}
	return res;
}

namespace UFS{
	int fa[MAXN];
	std::bitset<MAXN> cov[MAXN];
	void init(const int &n){
		rep(i,1,n) fa[i] = i, cov[i].set(i);
	}
	inline int find(int a){
		if(fa[a] == a) return a;
		return fa[a] = find(fa[a]);
	}
	void merge(int a, int b){
		cov[fa[b]] |= cov[fa[a]];
		fa[fa[a]] = fa[b];
	}
}

const int MAXK = 2000;
std::bitset<MAXN> bel[MAXK];
bool link(int a, int b, const std::bitset<MAXN> &now, const int &k){
	a = UFS::find(a), b = UFS::find(b);
	if(a == b) return true; // nothing to do
	rep0(i,0,k) if(((~bel[i])&now).any())
		if((UFS::cov[a]&bel[i]).any()) // so complex
		if((UFS::cov[b]&bel[i]).any()) return false;
	UFS::merge(a,b); return true;
}

int haxi[MAXN], xjx[MAXN];
int gra[MAXN][MAXN]; char str[MAXN+10];
int main(){
	int n = readint(), k = readint();
	rep0(i,1,n) rep(j,i+1,n)
		gra[i][j] = gra[j][i] = readint();
	for(int i=0; i!=k; ++i){
		scanf("%s",str+1);
		rep(j,1,n) bel[i].set(j,str[j]^48);
	}
	UFS::init(n); int ans = 1;
	rep0(_,1,n){ // at most n times
		std::bitset<MAXN> now; now.set();
		rep0(i,0,k){ // brutely
			std::bitset<MAXN> jj = now&bel[i];
			if(jj.count() <= 1) continue;
			int rt = int(jj._Find_first());
			if(((~UFS::cov[UFS::find(rt)])&jj).any())
				now = jj; // smaller range
		}
		memset(xjx+1,0,n<<2); int tot = 0, rt = 0;
		rep(i,1,n) if(now.test(i)){
			int x = UFS::find(i);
			if(!haxi[x]) haxi[x] = ++ tot;
			xjx[i] = haxi[x], rt = x;
			rep0(j,1,i) if(xjx[j]) // link edge
				modAddUp(mat[xjx[i]][xjx[j]],gra[i][j]);
		}
		rep0(i,1,tot) mat[i][i] = 0;
		rep0(i,1,tot) rep(j,i+1,tot){
			const int v = (mat[i][j]+mat[j][i])%MOD;
			mat[i][j] = mat[j][i] = MOD-v;
			modAddUp(mat[i][i],v), modAddUp(mat[j][j],v);
		}
		ans = int(llong(ans)*getDet(tot-1)%MOD);
		rep(i,1,n) if(haxi[i]){
			if(link(i,rt,now,k) == false){
				puts("0"); return 0;
			}
			haxi[i] = 0; // clear
		}
		rep(i,1,tot) memset(mat[i]+1,0,tot<<2);
	}
	printf("%d\n",ans);
	return 0;
}
  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-04-06 23:27:37  更:2022-04-06 23:29:48 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 9:50:32-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码