联合省选2020 作业题

作业题

小 W 刚刚在离散数学课学习了生成树的知识:一个无向图 \(G = (V,E)\) 的生成树 \(T\) 为边集 \(E\) 的一个大小为 \(|V|-1\) 的子集,且保证 \(T\) 的生成子图在 \(G\) 中连通。

小 W 在做今天的作业时被这样一道题目难住了:

给定一个 \(n\) 个顶点 \(m\) 条边(点和边都从 \(1\) 开始编号)的无向图 \(G\),保证图中无重边和无自环。每一条边有一个正整数边权 \(w_i\),对于一棵 \(G\) 的生成树 \(T\),定义 \(T\) 的价值为:\(T\) 所包含的边的边权的最大公约数乘以边权之和,即:

\[val(T) = \left(\sum_{i=1}^{n-1} w_{e_i}\right) \times \gcd (w_{e_1}, w_{e_2}, \dots, w_{e_{n-1}}) \]

其中 \(e_1, e_2, \dots, e_{n-1}\)\(T\) 包含的边的编号。

小 W 需要求出 \(G\) 的所有生成树 \(T\) 的价值之和,他做了很久也没做出来,请你帮帮他。由于答案可能很大,你只需要给出答案对 \(998244353\) 取模后的结果。

\(100\%\) 的数据满足:\(2\le n\le 30, 1\le m \le \frac {n(n-1)}2, 1\le w_i \le 152501\)

题解

https://www.cnblogs.com/dysyn1314/p/13209932.html

首先要把这个 \(\gcd\) 给拆掉,才能用我们熟悉的“矩阵树定理”等一系列方法来解题。

众所周知, \(\sum_{d|x}\varphi(d)=x\) 。本题中,我们就可以利用这个 \(\varphi\) 来拆掉 \(\gcd\) 。具体来说,如果我们用 \(T\) 来表示枚举的一棵树, \(e_1,e_2,\dots ,e_{n-1}\) 来表示 \(T\) 包含的边的编号,那么首先,答案可以表示为:

\[\sum_{T}\left(\sum_{i=1}^{n-1}w_{e_i}\right)\times\gcd(w_{e_1},w_{e_2},\dots,w_{e_{n-1}}) \]

我们把 \(\gcd(w_{e_1},w_{e_2},\dots,w_{e_{n-1}})\) 用上述的公式带入,得到:

\[=\sum_{T}\left(\sum_{i=1}^{n-1}w_{e_i}\right)\times\left(\sum_{d|\gcd(w_{e_1},w_{e_2},\dots,w_{e_{n-1}})}\varphi(d)\right) \]

然后就可以把这个 \(d\) ,放到前面来,得到:

\[=\sum_{d=1}^{W}\varphi(d)\times\left(\sum_{\begin{gather*}T\\d|w_{e_1},\dots ,d|w_{e_{n-1}}\end{gather*}}\sum_{i=1}^{n-1}w_{e_i}\right) \]

其中, \(W\) 表示最大的权值。后面括号里的部分,相当于【所有【 \(w_{e_i}\)\(d\) 的倍数的边】组成的子图】的所有生成树的边权和。

普通的矩阵树定理,可以用来求所有生成树的边权积之和。其中最为大家所熟知的应用,就是当所有边权都为 \(1\) 时,就相当于是求生成树的数量,也就是生成树计数问题。它的实现,就是求矩阵行列式,因为需要用到高斯消元,所以时间复杂度是 \(O(n^3)\) 的。

但是本题中,要求的是边权和,而不是“边权积之和”。一种朴素的想法是考虑每一条边的贡献,也就是求【包含这条边的生成树数量】。它又等于【原图的生成树数量】减去【原图去掉这条边后的生成树数量】。对每条边都求一次【原图去掉这条边后的生成树数量】,时间复杂度 \(O(mn^3)\) 。因为外层还要枚举 \(d\) ,所以总时间复杂度 \(O(W\cdot mn^3)\) ,无法通过本题。

求所有生成树的边权和,其实有更好的方法。我们把每条边的边权,看做一个多项式: \((1+w_{e_i}x)\) 。其中, \(x\) 不是任何具体的数,它只是一个符号,表示多项式的“一次项”。那么,一个生成树,它的边权之积的“一次项”前的系数,就是这颗生成树的边权和(这可以根据多项式乘法的定义来理解)。

如此以来,求所有生成树的边权和的问题,当我们把边权换成这样一个多项式后,就转化为了求所有生成树的边权积之和的问题。只不过,现在新的边权,不再是一个数,而是一个多项式。所以我们只需要定义出多项式的四则运算,就可以直接用矩阵树定理求解了。另外,我们只关心多项式的“一次项”系数,所以更高的项可以舍去。换句话说,所有的多项式运算,可以在 \(\bmod x^2\) 意义下进行。那么把多项式定义为一个 \(\texttt{pair}\) 就可以了。

多项式的加、减、乘运算都比较简单。
多项式的除法,因为在 \(\bmod x^2\) 意义下进行,所以只需要求出多项式 \(\bmod x^2\) 意义下的逆即可。用经典的倍增法。设原多项式为 \(A(x)\) ,要求的、它的逆为 \(F(x)=A^{-1}(x)\pmod{x^n}\) 。如果已经求出了 \(F_0(x)=A^{-1}(x)\pmod{x^{\lceil\frac{n}{2}\rceil}}\) ,那么:

\[F(x)=2F_0(x)-F_0^2(x)A(x)\pmod{x^{n}} \]

对本题来说,我们就不需要递归了。先求出常数项,也就是求逆元。然后把常数项作为 \(F_0(x)\) 带入上式即可。
还有一个小细节是,高斯消元求行列式的时候,如果当前行的(要作为被除数)的这个多项式,常数项为 \(0\) ,上述的求逆的方法就不管用了,因为 \(0\) 没有逆元。这种情况下,我们首先考虑,在它下面,找一个常数项不为 \(0\) 的行,与它交换。如果它下面所有行常数项都为 \(0\) ,那对这一列,我们就不需要管常数项了,直接拿一次项消就行(就把每一项都看成普通的数而不是多项式就行)。

至此,所有四则运算都可以 \(O(1)\) 实现,所以做一次高斯消元,求出行列式的复杂度就是 \(O(n^3)\) 的。外层还要枚举 \(d\) ,所以总时间复杂度 \(O(Wn^3)\) 。这个复杂度仍然无法通过本题,我们还需要再对 \(W\) 这部分做一些优化。

发现对于一个 \(d\) 。如果“是它的倍数”的边,数量小于 \(n-1\) 条,那必不可能有任何生成树。特判这一情况后,我们惊喜地发现,复杂度降为: \(O(\frac{\sum\sigma_0(w_{e_i})}{n-1}\cdot n^3)=O(n^2\sum\sigma_0(w_{e_i}))\) 。其中 \(\sigma_0(x)\) 表示 \(x\) 的约数数量。这个复杂度很好理解,因为每个 \(d\) ,要作为约数出现 \(n-1\) 次才被计算,所以被计算到的 \(d\) 最多只有 \(\frac{\sum\sigma_0(w_{e_i})}{n-1}\) 个。再看这个复杂度,一个小于等于 \(W\) 的数,约数个数是 \(O(\sqrt{W})\) 级别的,又因为边数是 \(O(n^2)\) 级别的,所以总复杂度就是 \(O(n^4\sqrt{W})\) 。事实上,这个 \(\sqrt{W}\) 只是一个理论上限,在本题的数据范围下,打表发现, \(w\) 的约数个数最多为 \(144\) 。足以通过本题。

typedef array poly;

IN poly operator+(CO poly&a,CO poly&b){
	return {add(a[0],b[0]),add(a[1],b[1])};
}
IN poly operator-(CO poly&a){
	return {mod-a[0],mod-a[1]};
}
IN poly operator-(CO poly&a,CO poly&b){
	return {add(a[0],mod-b[0]),add(a[1],mod-b[1])};
}
IN poly operator*(CO poly&a,CO poly&b){
	return {mul(a[0],b[0]),add(mul(a[0],b[1]),mul(a[1],b[0]))};
}
IN poly operator~(CO poly&a){
	int x=fpow(a[0],mod-2);
	return poly{mul(2,x),0}-poly{mul(x,x),0}*a;
}
IN poly operator/(CO poly&a,CO poly&b){
	return a*~b;
}

CO int N=31,M=2e5;
struct edge {int x,y,w;};
vector edges;
int prm[M],num,phi[M];
poly a[N][N];

poly solve(int n){
	poly ans{1,0};
	for(int i=1;i<=n;++i){
		int p=i;
		for(int j=i;j<=n;++j)if(a[j][i][0]) {p=j; break;}
		if(a[p][i][0]==0){
			int p=i;
			for(int j=i;j<=n;++j)if(a[j][i][1]!=0) {p=j; break;}
			if(!a[p][i][1]) return {0,0};
			if(p!=i){
				swap(a[p],a[i]);
				ans=-ans;
			}
			ans=ans*a[i][i];
			int inv=fpow(a[i][i][1],mod-2);
			for(int j=i+1;j<=n;++j){
				int coef=mul(a[j][i][1],inv);
				for(int k=i;k<=n;++k) a[j][k][1]=add(a[j][1][1],mod-mul(a[i][k][1],coef));
			}
			continue;
		}
		if(p!=i){
			swap(a[p],a[i]);
			ans=-ans;
		}
		ans=ans*a[i][i];
		poly inv=~a[i][i];
		for(int j=i+1;j<=n;++j){
			poly coef=a[j][i]*inv;
			for(int k=i;k<=n;++k) a[j][k]=a[j][k]-a[i][k]*coef;
		}
	}
	return ans;
}
int main(){
	int n=read(),lim=0;
	for(int m=read();m--;){
		int x=read(),y=read(),w=read();
		edges.push_back({x,y,w}),lim=max(lim,w);
	}
	phi[1]=1;
	for(int i=2;i<=lim;++i){
		if(!prm[i]) prm[++num]=i,phi[i]=i-1;
		for(int j=1;j<=num and i*prm[j]<=lim;++j){
			prm[i*prm[j]]=1;
			if(i%prm[j]==0) {phi[i*prm[j]]=phi[i]*prm[j]; break;}
			phi[i*prm[j]]=phi[i]*phi[prm[j]];
		}
	}
	int ans=0;
	for(int i=1;i<=lim;++i){
		if(count_if(edges.begin(),edges.end(),[&](CO edge&e)->bool{
			return e.w%i==0;
		})

你可能感兴趣的:(联合省选2020 作业题)