某模拟赛 - 20200120【组合数学 差分约束 矩阵树定理 拉格朗日插值】

T1

题意

N N N 个学生,每个学生至多有一名室友,至多有一名同桌。求排列 P P P 的数量,使原来第 i i i 个都学生换到 P i P_i Pi 号学生原本所在的寝室/桌子后原本的室友以及同桌关系依旧不变,答案模 1 0 9 + 7 10^9+7 109+7 N ≤ 2 × 1 0 5 N\leq 2\times10^5 N2×105

题解

显然,把两种关系都看成图上的边,每个节点的度数都不大于 2 2 2,也就只有链与环。将所有连通块分为五种情况:

图

  1. 单独的一个点( 1 1 1);
  2. 以室友关系开头,同桌关系结尾的链( 1 1 1);
  3. 以室友关系开头,室友关系结尾的链( 2 2 2 - 可以翻转);
  4. 以同桌关系开头,同桌关系结尾的链( 2 2 2 - 可以翻转);
  5. 大小为 x x x 的环( x x x - 可以两个两个地旋转,可以翻转)。

以上每种情况(再根据链/环的大小区分后)对应的连通块个数数的阶乘相乘,每个连通块再乘以括号后的数字即为答案。

代码:

#include<bits/stdc++.h>
using namespace std;
int getint(){
	int ans=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9'){
		if(c=='-')f=-1;
		c=getchar();
	}
	while(c>='0'&&c<='9'){
		ans=ans*10+c-'0';
		c=getchar();
	}
	return ans*f;
}
const int N=2e5+10,mod=1e9+7;
int room[N],desk[N];
bool vis[N];
int r[N];//chains that begin with room (consider room first)
int d[N];//chains that begin with desk
int single=0;
int c[N];//circles
int frac[N],p2[N];
int qpow(int x,int y){
	int ans=1;
	while(y){
		if(y&1)ans=ans*1ll*x%mod;
		x=x*1ll*x%mod;
		y>>=1;
	}
	return ans;
}

int main(){
	int n=getint(),m1=getint(),m2=getint();
	for(int i=0;i<m1;i++){
		int x=getint(),y=getint();
		room[x]=y;
		room[y]=x;
	}
	for(int i=0;i<m2;i++){
		int x=getint(),y=getint();
		desk[x]=y;
		desk[y]=x;
	}
	for(int i=1;i<=n;i++){
		//chains that begin with room
		//-=-=-=- -=-=-=
		if(!room[i]&&!desk[i]){
			vis[i]=1;
			single++;
		}
		if(!vis[i]&&room[i]&&!desk[i]){
			int now=i,len=0;
			bool t=1;
			while(now){
				vis[now]=1;
				++len;
				now=(t?room[now]:desk[now]);
				t=!t;
			}
			r[len]++;
		}
	}
	for(int i=1;i<=n;i++){
		//chains that begin with desk
		//=-=-=-=
		if(!vis[i]&&desk[i]&&!room[i]){
			int now=i,len=0;
			bool t=0;
			while(now){
				vis[now]=1;
				++len;
				now=(t?room[now]:desk[now]);
				t=!t;
			}
			d[len]++;
		}
	}
	for(int i=1;i<=n;i++){
		//circles
		if(!vis[i]){
			int now=i,len=0;
			bool t=1;
			while(!vis[now]){
				vis[now]=1;
				++len;
				now=(t?room[now]:desk[now]);
				t=!t;
			}
			c[len]++;
		}
	}
	
	frac[0]=1;p2[0]=1;
	for(int i=1;i<=n;i++)frac[i]=frac[i-1]*1ll*i%mod,p2[i]=(p2[i-1]<<1)%mod;
	int ans=frac[single];
	for(int i=1;i<=n;i++){
		ans=ans*1ll*frac[r[i]]%mod;
		ans=ans*1ll*frac[d[i]]%mod;
		ans=ans*1ll*frac[c[i]]%mod;
		if((i&1)^1){
			ans=ans*1ll*qpow(i,c[i])%mod;
			ans=ans*1ll*p2[r[i]]%mod;
			ans=ans*1ll*p2[d[i]]%mod;
		}
	}
	cout<<ans;
	return 0;
}

T2

题意

一棵有 n n n 个点的带权无根树,每个点有值 l i , r i l_i,r_i li,ri。现在希望给每个点赋权值 a i a_i ai,使得 a i ∈ [ l i , r i ] a_i\in [l_i,r_i] ai[li,ri] ∣ a i − a j ∣ ≤ d i s ( i , j ) |a_i-a_j|\leq dis(i,j) aiajdis(i,j) T T T 次询问:

  • 是否有合法的赋值方案?
  • 或:最小化正整数 x x x,使得在每个 l i , r i l_i,r_i li,ri 变为 l i − x , r i + x l_i-x,r_i+x lix,ri+x 后有合法的赋值方案。

n ≤ 1 0 6 n\leq 10^6 n106 T ≤ 3 T\leq 3 T3 0 ≤ w , ∣ l ∣ , ∣ r ∣ ≤ 1 0 9 0\leq w,|l|,|r|\leq 10^9 0w,l,r109

题解

首先:

若  ∣ a i − a j ∣ ≤ d i s ( i , j ) , ∣ a j − a k ∣ ≤ d i s ( j , k )  且  j  在  i  到  k  的链上 则  ∣ a i − a k ∣ ≤ ∣ a i − a j ∣ + ∣ a j − a k ∣ ≤ d i s ( i , j ) + d i s ( j , k ) ≤ d i s ( i , k ) \begin{aligned}&\text{若 }|a_i-a_j|\leq dis(i,j),|a_j-a_k|\leq dis(j,k)\text{ 且 $j$ 在 $i$ 到 $k$ 的链上}\end{aligned}\\ \begin{aligned}\text{则 }|a_i-a_k|&\leq |a_i-a_j|+|a_j-a_k|\\ &\leq dis(i,j)+dis(j,k)\\ &\leq dis(i,k)\end{aligned}  aiajdis(i,j),ajakdis(j,k)  j  i  k 的链上 aiakaiaj+ajakdis(i,j)+dis(j,k)dis(i,k)

于是我们只需要关心相邻节点 i , j i,j i,j ∣ a i − a j ∣ ≤ d i s ( i , j ) |a_i-a_j|\leq dis(i,j) aiajdis(i,j) 的限制。

考虑只有两个节点 i , j i,j i,j 时的所有限制(容易拓展到一棵树上):

{ l i ≤ a i ≤ r i l j ≤ a j ≤ r j ∣ a i − a j ∣ ≤ w = d i s ( i , j ) \begin{cases} l_i\leq a_i\leq r_i\\ l_j\leq a_j\leq r_j\\ |a_i-a_j|\leq w=dis(i,j) \end{cases} liairiljajrjaiajw=dis(i,j)

变形一下得:

{ 0 ≤ a i − l i a i ≤ 0 + r i 0 ≤ a j − l j a j ≤ 0 + r j a i ≤ a j + w a j ≤ a i + w \begin{cases} 0&\leq &a_i&-l_i\\ a_i&\leq &0&+r_i\\ 0&\leq &a_j&-l_j\\ a_j&\leq &0&+r_j\\ a_i&\leq &a_j&+w\\ a_j&\leq &a_i&+w \end{cases} 0ai0ajaiajai0aj0ajaili+rilj+rj+w+w

显然这可以用差分约束来做,建出来的图像是这样的:

差分约束 图

0 0 0 开始跑最短路。假如没有负环,那么就有赋值方式。否则 x x x 会被加到每一个红边与蓝边上,最小简单负环又会恰好从 0 0 0 出发经过蓝边、树边、红边回到 0 0 0,故找到最小简单负环,其权值除以二向上取整即为答案。

寻找最小简单负环可以跑一边 SPFA(重复到达 0 点时即找到一个负环,不再从 0 点继续增广以保证负环是简单的),也可以树形 DP O ( n ) O(n) O(n) 解决。

注意:该题的输入可能多达 130+ MB,需要 fread 读优。

代码:

#include<bits/stdc++.h>
using namespace std;
const int SIZE=1e6;
char buf[SIZE],*ed=buf+SIZE,*pointer=buf;
char gc(){
	return (++pointer==ed)?fread(buf,1,SIZE,stdin),*(pointer=buf):*pointer;
}
int getint(){
	int ans=0,f=1;
	char c=gc();
	while(c<'0'||c>'9'){
		if(c=='-')f=-1;
		c=gc();
	}
	while(c>='0'&&c<='9'){
		ans=ans*10+c-'0';
		c=gc();
	}
	return ans*f;
}
const int N=1e6+10;
#define ll long long

struct bian{
	int l,e,n;
};
bian b[N<<2];
int s[N],tot=0;
void add(int x,int y,int z){
	tot++;
	b[tot].l=z;
	b[tot].e=y;
	b[tot].n=s[x];
	s[x]=tot;
}
int l[N],r[N];

long long f[N];
void ss(int x,int fa){
	f[x]=-l[x];
	for(int i=s[x];i;i=b[i].n){
		if(b[i].e==fa)continue;
		ss(b[i].e,x);
		f[x]=min(f[x],f[b[i].e]+b[i].l);
	}
}
void ss2(int x,int fa){
	for(int i=s[x];i;i=b[i].n){
		if(b[i].e==fa)continue;
		f[b[i].e]=min(f[b[i].e],f[x]+b[i].l);
		ss2(b[i].e,x);
	}
}
long long calc(int n){
	ss(1,0);
	ss2(1,0);
	long long ans=0;
	for(int i=1;i<=n;i++){
		ans=min(ans,f[i]+r[i]);
	}
	return ans;
}

int main(){
	int T=getint(),tp=getint();
	if(tp==0){
		while(T--){
			memset(b,0,sizeof(b));
			memset(s,0,sizeof(s));
			tot=0;
			int n=getint();
			int q=n+2;
			for(int i=1;i<=n;i++){
				l[i]=getint();
				//add(i,q,-l[i]);
			}
			for(int i=1;i<=n;i++){
				r[i]=getint();
				//add(q,i,r[i]);
			}
			for(int i=1;i<n;i++){
				int x=getint(),y=getint(),w=getint();
				add(x,y,w);
				add(y,x,w);
			}
			bool c=calc(n);
			puts(c?"1":"0");
		}
		return 0;
	}else{
		while(T--){
			memset(b,0,sizeof(b));
			memset(s,0,sizeof(s));
			tot=0;
			//cerr<<"begin "<<endl;
			int n=getint();
			int q=n+2;
			for(int i=1;i<=n;i++){
				l[i]=getint();
				//add(i,q,-l[i]);
			}
			for(int i=1;i<=n;i++){
				r[i]=getint();
				//add(q,i,r[i]);
			}
			for(int i=1;i<n;i++){
				int x=getint(),y=getint(),z=getint();
				add(x,y,z);
				add(y,x,z);
			}
			//cerr<<"read "<<endl;
			long long ans=calc(n);
			printf("%lld\n",(-ans+1)/2);
			//cerr<<"over "<<endl;
		}
		return 0;
	}
	return 0;
}

T3

题面

有一个 n n n 个点, m m m 条边的无向图,每条边有红、绿、蓝三种颜色之一。求有多少个生成树满足蓝边不超过 x x x 条,绿边不超过 y y y 条。答案模 1 0 9 + 7 10^9+7 109+7 n ≤ 40 n\leq 40 n40 m ≤ 1 0 5 m\leq 10^5 m105

题解

前置知识:

  • 矩阵树定理
  • 拉格朗日插值

首先考虑没有蓝边、绿边时的做法:直接用矩阵树定理算:求出图的拉普拉斯矩阵,去掉一行一列,剩下的高斯消元直到只剩斜三角,对角线相乘即为答案。

接着考虑没有蓝边的做法:设红边为 1 1 1,绿边为 x x x,通过矩阵树定理可以算出一个多项式, i i i 次项即为 i i i 条绿边的方案数。

求多项式时依次代入 x = 0 … n x=0\dots n x=0n,通过拉格朗日插值得到原函数。

接着考虑这道题的做法:设红边为 1 1 1,绿边为 x x x,蓝边为 y y y,通过矩阵树定理可以算出一个二元多项式, x i y j x^iy^j xiyj 的系数即为 i i i 条绿边、 j j j 条蓝边的方案数。

求多项式时依次代入 x = 0 … n , y = 0 … n x=0\dots n,y=0\dots n x=0n,y=0n,通过二维拉格朗日插值得到原函数。

二维拉格朗日插值公式:

f ( i ) = ∏ j ≠ i x − X j X i − X j f(i)=\prod\limits_{j\neq i}\frac{x-X_j}{X_i-X_j} f(i)=j=iXiXjxXj g ( i ) = ∏ j ≠ i y − Y j Y i − Y j g(i)=\prod\limits_{j\neq i}\frac{y-Y_j}{Y_i-Y_j} g(i)=j=iYiYjyYj f ( X i , Y j ) = a i , j f(X_i,Y_j)=a_i,j f(Xi,Yj)=ai,j(其中 X , Y X,Y X,Y 为每次代入的 x x x 值与 y y y 值,此处就是 0 … n 0\dots n 0n),则原多项式为: ∑ i , j f ( i ) g ( j ) a i , j \sum\limits_{i,j}f(i)g(j)a_{i,j} i,jf(i)g(j)ai,j

实际计算时先预处理出 f , g f,g f,g 的系数,再求出原多项式。

时间复杂度: O ( n 5 ) O(n^5) O(n5),瓶颈在于 O ( n 2 ) O(n^2) O(n2) 次高斯消元求行列式。

代码:

#include<bits/stdc++.h>
using namespace std;
int getint(){
	int ans=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9'){
		c=getchar();
	}
	while(c>='0'&&c<='9'){
		ans=ans*10+c-'0';
		c=getchar();
	}
	return ans;
}
const int N=45,mod=1e9+7;
int qpow(int x,int y){
	int ans=1;
	while(y){
		if(y&1)ans=ans*1ll*x%mod;
		x=x*1ll*x%mod;
		y>>=1;
	}
	return ans;
}
int getinv(int x){
	return qpow(x,mod-2);
}

int n;
int b[N][N][4];
int mat[N][N];
bool vis[N];
int calc(int u,int v){
	//x=u, y=v
	memset(mat,0,sizeof(mat));
	memset(vis,0,sizeof(vis));
	int ans=1;
	for(int i=0;i<n;i++){
		for(int j=0;j<n;j++){
			if(i==j)continue;
			mat[i][j]-=b[i][j][1]+b[i][j][2]*u+b[i][j][3]*v;
			mat[i][i]+=b[i][j][1]+b[i][j][2]*u+b[i][j][3]*v;
		}
	}
	for(int i=0;i<n;i++)for(int j=0;j<n;j++)mat[i][j]=(mat[i][j]+mod)%mod;
	for(int i=1;i<n;i++){
		int r=0;
		for(int j=1;j<n;j++){
			if(mat[j][i]&&!vis[j]){
				r=j;
				break;
			}
		}
		vis[r]=1;
		ans=ans*1ll*mat[r][i]%mod;
		int inv=getinv(mat[r][i]);
		for(int j=1;j<n;j++){
			mat[r][j]=mat[r][j]*1ll*inv%mod;
		}
		for(int j=1;j<n;j++){
			if(vis[j])continue;
			int t=mat[j][i];
			for(int k=1;k<n;k++){
				mat[j][k]=(mat[j][k]+mat[r][k]*1ll*(mod-t))%mod;
			}
		}
	}
	return ans;
}
int a[N][N];//f(i,j)
int e[N][N];//e_x(i)=\prod_\limits{k\neq i}(x-X_k)/(X_i-X_k)
            //e_y(i)=\prod_\limits{k\neq i}(y-Y_k)/(Y_i-Y_k)
int f[N][N];//f(x,y)=\sum e_x(i)e_y(j)a(i,j)

int main(){
	n=getint();
	int m=getint(),x1=getint(),x2=getint();
	for(int i=0;i<m;i++){
		int x=getint()-1,y=getint()-1,c=getint();
		b[x][y][c]++;
		b[y][x][c]++;
	}
	for(int i=0;i<=n;i++){
		for(int j=0;j<=n;j++){
			a[i][j]=calc(i,j);
		}
	}
	for(int i=0;i<=n;i++){
		e[i][0]=1;
		int inv=1;
		for(int j=0;j<n;j++){
			if(i==j)continue;
			//e(i)*=(x-X_j)
			for(int k=j;k>=0;--k){
				e[i][k+1]=(e[i][k+1]+e[i][k])%mod;
				e[i][k]=e[i][k]*1ll*(mod-j)%mod;
			}
			inv=inv*1ll*(i-j+mod)%mod;
		}
		inv=getinv(inv);
		for(int j=0;j<=n;j++){
			e[i][j]=e[i][j]*1ll*inv%mod;
		}
	}
	for(int i=0;i<n;i++){
		for(int j=0;j<n;j++){
			for(int k=0;k<n;k++){
				for(int l=0;l<n;l++){
					f[k][l]=(f[k][l]+e[i][k]*1ll*e[j][l]%mod*a[i][j]%mod)%mod;
				}
			}
		}
	}
	int ans=0;
	for(int i=0;i<=x1;i++){
		for(int j=0;j<=x2;j++){
			ans=(ans+f[i][j])%mod;
		}
	}
	cout<<ans;
	return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值