• 【2020省选Day2T3】LOJ3304 「联合省选 2020 A」作业题


    题目链接

    前置知识:(1) 矩阵树定理。(2) 多项式四则运算(加、减、乘、求逆(牛顿迭代))。

    网上的介绍很多。这里就不细讲了。

    首先要把这个(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} ight) imesgcd(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} ight) imesleft(sum_{d|gcd(w_{e_1},w_{e_2},dots,w_{e_{n-1}})}varphi(d) ight) ]

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

    [=sum_{d=1}^{W}varphi(d) imesleft(sum_{substack{T\d|w_{e_1},dots ,d|w_{e_{n-1}}}}sum_{i=1}^{n-1}w_{e_i} ight) ]

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

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

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

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

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

    多项式的加、减、乘运算都比较简单。不了解的可以看如下代码:

    typedef pair<int,int> pii;
    #define fi first
    #define se second
    //为了便于理解,这里就不取模了
    pii operator+(const pii& a,const pii& b){
    	return mk(a.fi+b.fi,a.se+b.se);
    }
    pii operator-(const pii& a,const pii& b){
    	return mk(a.fi-b.fi,a.se-b.se);
    }
    pii operator*(const pii& a,const pii& b){
    	return mk(a.fi*b.fi,a.se*b.fi+b.se*a.fi);
    }
    

    多项式的除法,因为在(mod x^2)意义下进行,所以只需要求出多项式(mod x^2)意义下的逆即可。用经典的倍增法。设原多项式为(A(x)),要求的、它的逆为(F(x)=A^{-1}(x)pmod{x^n})。如果已经求出了(F_0(x)=A^{-1}(x)pmod{x^{lceilfrac{n}{2} ceil}}),那么:

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

    对本题来说,我们就不需要递归了。先求出常数项,也就是求逆元。然后把常数项作为(F_0(x))带入上式即可。具体的代码:

    pii inv(const pii& a){
    	int t=pow_mod(a.fi,MOD-2);
    	return mk(2*t,0)-mk(t,0)*mk(t,0)*a;
    }
    pii operator/(const pii& a,const pii& b){
    	return a*inv(b);
    }
    

    还有一个小细节是,高斯消元求行列式的时候,如果当前行的(要作为被除数)的这个多项式,常数项为(0),上述的求逆的方法就不管用了,因为(0)没有逆元。这种情况下,我们首先考虑,在它下面,找一个常数项不为(0)的行,与它交换(根据行列式的性质,两行交换后行列式要变号,也就是(res:=-res))。如果它下面所有行常数项都为(0),那对这一列,我们就不需要管常数项了,直接拿一次项消就行(就把每一项都看成普通的数而不是多项式就行)。

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

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

    参考代码(在LOJ查看):

    //problem:LOJ3304
    #include <bits/stdc++.h>
    using namespace std;
    
    #define pb push_back
    #define mk make_pair
    #define lob lower_bound
    #define upb upper_bound
    #define fi first
    #define se second
    #define SZ(x) ((int)(x).size())
    
    typedef unsigned int uint;
    typedef long long ll;
    typedef unsigned long long ull;
    typedef pair<int,int> pii;
    
    const int MOD=998244353;
    inline int mod1(int x){return x<MOD?x:x-MOD;}
    inline int mod2(int x){return x<0?x+MOD:x;}
    inline void add(int& x,int y){x=mod1(x+y);}
    inline void sub(int& x,int y){x=mod2(x-y);}
    inline int pow_mod(int x,int i){int y=1;while(i){if(i&1)y=(ll)y*x%MOD;x=(ll)x*x%MOD;i>>=1;}return y;}
    
    pii operator+(const pii& lhs,const pii& rhs){
    	return mk(mod1(lhs.fi+rhs.fi),mod1(lhs.se+rhs.se));
    }
    pii operator-(const pii& lhs,const pii& rhs){
    	return mk(mod2(lhs.fi-rhs.fi),mod2(lhs.se-rhs.se));
    }
    pii operator*(const pii& lhs,const pii& rhs){
    	return mk((ll)lhs.fi*rhs.fi%MOD,((ll)lhs.se*rhs.fi+(ll)rhs.se*lhs.fi)%MOD);
    }
    pii inv(const pii& a){
    	//assert(a.fi!=0);
    	int t=pow_mod(a.fi,MOD-2);
    	return mk(2LL*t%MOD,0)-mk(t,0)*mk(t,0)*a;//牛顿迭代
    }
    pii operator/(const pii& lhs,const pii& rhs){
    	return lhs*inv(rhs);
    }
    pii operator-(const pii& rhs){
    	return mk(mod2(-rhs.fi),mod2(-rhs.se));
    }//负号
    pii& operator+=(pii& lhs,const pii& rhs){
    	lhs=lhs+rhs;
    	return lhs;
    }
    pii& operator-=(pii& lhs,const pii& rhs){
    	lhs=lhs-rhs;
    	return lhs;
    }
    pii& operator*=(pii& lhs,const pii& rhs){
    	lhs=lhs*rhs;
    	return lhs;
    }
    pii& operator/=(pii& lhs,const pii& rhs){
    	lhs=lhs/rhs;
    	return lhs;
    }
    
    const int MAXW=152501;
    int p[MAXW+5],cnt,phi[MAXW+5];
    bool v[MAXW+5];
    void sieve(int lim){
    	phi[1]=1;
    	for(int i=2;i<=lim;++i){
    		if(!v[i]){
    			phi[i]=i-1;
    			p[++cnt]=i;
    		}
    		for(int j=1;j<=cnt && p[j]*i<=lim;++j){
    			v[i*p[j]]=1;
    			if(i%p[j]==0){
    				phi[i*p[j]]=phi[i]*p[j];
    				break;
    			}
    			phi[i*p[j]]=phi[i]*phi[p[j]];
    		}
    	}
    }//线性筛,求phi
    
    const int MAXN=30;
    const int MAXM=MAXN*(MAXN-1)/2;
    int n,m;
    struct Edge{
    	int u,v,w;
    }e[MAXM+5];
    
    struct Matrix{
    	int n;
    	pii a[MAXN+5][MAXN+5];
    	Matrix(){
    		n=0;
    		memset(a,0,sizeof(a));
    	}
    };
    
    pii get_det2(Matrix A,int i){
    	//常数项全部为0
    	pii res=mk(1,0);
    	int line=0;
    	for(int j=i;j<=A.n;++j){
    		if(A.a[j][i].se!=0){
    			line=j;
    			break;
    		}
    	}
    	if(!line)return mk(0,0);
    	if(line!=i){
    		for(int j=1;j<=A.n;++j)swap(A.a[i][j],A.a[line][j]);
    		res=-res;
    	}
    	int _inv=pow_mod(A.a[i][i].se,MOD-2);
    	for(int j=i+1;j<=A.n;++j){
    		int t=(ll)A.a[j][i].se*_inv%MOD;
    		for(int k=1;k<=A.n;++k)
    			sub(A.a[j][k].se,(ll)A.a[i][k].se*t%MOD);
    	}
    	res*=A.a[i][i];
    	return res;
    }
    pii get_det(Matrix A){
    	pii res=mk(1,0);
    	for(int i=1;i<=A.n;++i){
    		int line=0;
    		for(int j=i;j<=A.n;++j){
    			if(A.a[j][i].fi!=0){
    				line=j;
    				break;
    			}
    		}
    		if(line==0){
    			res*=get_det2(A,i);
    			continue;
    		}
    		if(line!=i){
    			for(int j=1;j<=A.n;++j)swap(A.a[i][j],A.a[line][j]);
    			res=-res;
    		}
    		pii _inv=inv(A.a[i][i]);
    		for(int j=i+1;j<=A.n;++j){
    			pii t=A.a[j][i]*_inv;
    			for(int k=1;k<=A.n;++k)
    				A.a[j][k]-=A.a[i][k]*t;
    		}
    		res*=A.a[i][i];
    	}
    	return res;
    }
    
    int main() {
    	cin>>n>>m;
    	int max_w=0;
    	for(int i=1;i<=m;++i){
    		cin>>e[i].u>>e[i].v>>e[i].w;
    		max_w=max(max_w,e[i].w);
    	}
    	sieve(max_w);
    	int ans=0;
    	for(int i=1;i<=max_w;++i){
    		int cnt_e=0;
    		for(int j=1;j<=m;++j)if(e[j].w%i==0)cnt_e++;
    		if(cnt_e<n-1)continue;
    		
    		Matrix mat;
    		for(int j=1;j<=m;++j)if(e[j].w%i==0){
    			mat.a[e[j].u][e[j].u]+=mk(1,e[j].w);
    			mat.a[e[j].v][e[j].v]+=mk(1,e[j].w);
    			mat.a[e[j].u][e[j].v]-=mk(1,e[j].w);
    			mat.a[e[j].v][e[j].u]-=mk(1,e[j].w);
    		}
    		mat.n=n-1;
    		pii det=get_det(mat);
    		ans=((ll)ans+(ll)phi[i]*det.se)%MOD;
    	}
    	cout<<ans<<endl;
    	return 0;
    }
    
  • 相关阅读:
    mysql执行顺序及习题
    多表查询
    PyQt5-03-信号与槽
    PyQt5-01-安装及简单例子
    252.anaconda升级版本
    251.anaconda下载资源包慢
    250.anaconda+vscode
    61.基础语法-函数式编程
    60.基础语法-异常的处理
    59.语法基础-包
  • 原文地址:https://www.cnblogs.com/dysyn1314/p/13209932.html
Copyright © 2020-2023  润新知