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 N≤2×105
题解
显然,把两种关系都看成图上的边,每个节点的度数都不大于 2 2 2,也就只有链与环。将所有连通块分为五种情况:
- 单独的一个点( 1 1 1);
- 以室友关系开头,同桌关系结尾的链( 1 1 1);
- 以室友关系开头,室友关系结尾的链( 2 2 2 - 可以翻转);
- 以同桌关系开头,同桌关系结尾的链( 2 2 2 - 可以翻转);
- 大小为 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) ∣ai−aj∣≤dis(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 li−x,ri+x 后有合法的赋值方案。
n ≤ 1 0 6 n\leq 10^6 n≤106, T ≤ 3 T\leq 3 T≤3, 0 ≤ w , ∣ l ∣ , ∣ r ∣ ≤ 1 0 9 0\leq w,|l|,|r|\leq 10^9 0≤w,∣l∣,∣r∣≤109
题解
首先:
若 ∣ 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} 若 ∣ai−aj∣≤dis(i,j),∣aj−ak∣≤dis(j,k) 且 j 在 i 到 k 的链上则 ∣ai−ak∣≤∣ai−aj∣+∣aj−ak∣≤dis(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) ∣ai−aj∣≤dis(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} ⎩⎪⎨⎪⎧li≤ai≤rilj≤aj≤rj∣ai−aj∣≤w=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} ⎩⎪⎪⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎪⎪⎧0ai0ajaiaj≤≤≤≤≤≤ai0aj0ajai−li+ri−lj+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 n≤40, m ≤ 1 0 5 m\leq 10^5 m≤105。
题解
前置知识:
- 矩阵树定理
- 拉格朗日插值
首先考虑没有蓝边、绿边时的做法:直接用矩阵树定理算:求出图的拉普拉斯矩阵,去掉一行一列,剩下的高斯消元直到只剩斜三角,对角线相乘即为答案。
接着考虑没有蓝边的做法:设红边为 1 1 1,绿边为 x x x,通过矩阵树定理可以算出一个多项式, i i i 次项即为 i i i 条绿边的方案数。
求多项式时依次代入 x = 0 … n x=0\dots n x=0…n,通过拉格朗日插值得到原函数。
接着考虑这道题的做法:设红边为 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=0…n,y=0…n,通过二维拉格朗日插值得到原函数。
二维拉格朗日插值公式:
令 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=i∏Xi−Xjx−Xj, 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=i∏Yi−Yjy−Yj, 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 0…n),则原多项式为: ∑ i , j f ( i ) g ( j ) a i , j \sum\limits_{i,j}f(i)g(j)a_{i,j} i,j∑f(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;
}