传送门
题解:
注意,代码实现里面按照习惯用 k k k表示限制了度数的点数, n n n表示节点个数, m m m表示边数,以下推导过程中变量意义与上述描述一致。
很显然地,我们知道 k k k那么小,我们可以考虑对 k = 0 , 1 , 2 k=0,1,2 k=0,1,2的情况进行分类讨论。
其实也有增加单次询问复杂度来减少分类讨论的做法,这里给出一个预处理之后单次回答 O ( 1 ) O(1) O(1)的做法。
k=0
k = 0 k=0 k=0的情况我们需要计算 n n n个点, m m m条边的带标号无向连通简单图个数。
直觉告诉我们这个可以用DP来求,设 f [ n ] [ m ] f[n][m] f[n][m]表示 n n n个点, m m m条边的带标号无向连通图个数。
发现直接合并只能用背包而且复杂度有点迷,没想清楚的话还会重复计数。
考虑容斥,由于所有图的方案数显然是 ( ( n 2 ) m ) {{n\choose 2} \choose m} (m(2n)),考虑计算所有不连通的图的方案数。
根据套路,我们枚举一号点所在连通块的大小 i i i,边数 j j j,剩下的边可以在其他地方随意放置 ( ( n − i 2 ) m − j ) {{n-i\choose 2}\choose m-j} (m−j(2n−i)),然后分配剩下的标号 ( n − 1 i − 1 ) {n-1\choose i-1} (i−1n−1)
于是我们得到这样一个 O ( n 2 m 2 ) O(n^2m^2) O(n2m2)的DP式子:
f [ n ] [ m ] = ( ( n 2 ) m ) − ∑ i = 1 n ∑ j = i − 1 min ( m , ( i 2 ) ) f [ i ] [ j ] ⋅ ( ( n − i 2 ) m − j ) ⋅ ( n − 1 i − 1 ) f[n][m]={{n\choose2 }\choose m}-\sum_{i=1}^n\sum_{j=i-1}^{\min(m,{i\choose 2})}f[i][j]\cdot{{n-i\choose 2}\choose m-j}\cdot{n-1\choose i-1} f[n][m]=(m(2n))−i=1∑nj=i−1∑min(m,(2i))f[i][j]⋅(m−j(2n−i))⋅(i−1n−1)
现在我们就解决了 k = 0 k=0 k=0的情况。这个DP数组实际上在接下来的分类讨论中也会用到。
k=1
当 k = 1 k=1 k=1的时候,我们先把特殊点拿出来,把没有特殊点的图建立出来,考虑一种构造方式,选择一条边,然后把这条边替换成这个度数为 2 2 2的点,连接原来边的两端。
很遗憾,这个做法显然可能是错的,万一特殊点就在某个三元环里面呢?
所以我们只需要考虑两种情况:
- 删去原来的边,那么在加入新点之后,比原图多了一边一点,答案为 ( m − 1 ) ⋅ f [ n − 1 ] [ m − 1 ] (m-1)\cdot f[n-1][m-1] (m−1)⋅f[n−1][m−1]
- 保留原来的边,那么在加入新点之后,比原图多了两边一点,答案为 ( m − 2 ) ⋅ f [ n − 1 ] [ m − 2 ] (m-2)\cdot f[n-1][m-2] (m−2)⋅f[n−1][m−2]
k=2
我们发现 k = 2 k=2 k=2又凭空多了一种情况:两个特殊点绑在一起的时候。
1.两个特殊点绑在一起
好像和 k = 1 k=1 k=1区别不是很大,这个时候我们发现有一种在 k = 1 k=1 k=1的时候不会出现的讨论:
- 两个端点连在同一个点上,显然这是合法的,保证图仍然是一个简单图,选择一个点,两个端点等价,方案数为 ( n − 2 ) ⋅ f [ n − 2 ] [ m − 3 ] (n-2)\cdot f[n-2][m-3] (n−2)⋅f[n−2][m−3]
- 两个端点连的点没有直接连边,方案数为 2 ( m − 2 ) ⋅ f [ n − 2 ] [ m − 2 ] 2(m-2)\cdot f[n-2][m-2] 2(m−2)⋅f[n−2][m−2]
- 两个端点连的点有直接连边,方案数为 2 ( m − 3 ) ⋅ f [ n − 2 ] [ m − 3 ] 2(m-3)\cdot f[n-2][m-3] 2(m−3)⋅f[n−2][m−3]
我们还需要考虑两个端点分开的情况。
- 两个点分别都在一个三元环中,发现两个点的连边并不冲突, ( m − 4 ) 2 ⋅ f [ n − 2 ] [ m − 4 ] (m-4)^2\cdot f[n-2][m-4] (m−4)2⋅f[n−2][m−4]
- 一个点在某个三元环中,另一个不在,两个点插入时候的选边显然不能选择同一条边, 2 ( m − 3 ) ( m − 4 ) f [ n − 2 ] [ m − 3 ] 2(m-3)(m-4)f[n-2][m-3] 2(m−3)(m−4)f[n−2][m−3]
- 两个点都不在任何三元环中,相当于删去了插入时候选择的边,我们发现这时候需要对它们是否选择同一条边进行讨论,如果选择了同一条边,则方案数为 ( m − 3 ) f [ n − 2 ] [ m − 3 ] (m-3)f[n-2][m-3] (m−3)f[n−2][m−3],否则为 ( m − 2 ) ( m − 3 ) f [ n − 2 ] [ m − 2 ] (m-2)(m-3)f[n-2][m-2] (m−2)(m−3)f[n−2][m−2]
于是可以 O ( 1 ) O(1) O(1)回答每个询问了。
Update:
去看了一圈题解,发现好像我的是所有 O ( 1 ) O(1) O(1)回答询问的方法中,分类讨论总数最少的。
代码:
#include<bits/stdc++.h>
#define ll long long
#define re register
#define cs const
cs int mod=1e9+7;
inline int add(int a,int b){return (a+=b)>=mod?a-mod:a;}
inline int dec(int a,int b){return (a-=b)<0?a+mod:a;}
inline int mul(int a,int b){ll r=(ll)a*b;return r>=mod?r%mod:r;}
inline int power(int a,int b,int res=1){
for(;b;b>>=1,res=mul(res,a))(b&1)&&(res=mul(res,a));
return res;
}
inline void Inc(int &a,int b){(a+=b)>=mod&&(a-=mod);}
class KingdomAndCities{
private:
static cs int N=55,M=N*N;
int f[N][N],c[M][M];
int n,m;
inline int C(int n){return n*(n-1)>>1;}
inline int calc(int n,int m){
int res=0,tmp=0;
for(int re i=1;i<=n-1;++i){
for(int re j=i-1,lim=std::min(C(i),m);j<=lim;++j){
Inc(tmp,mul(f[i][j],c[C(n-i)][m-j]));
}
Inc(res,mul(tmp,c[n-1][i-1]));
tmp=0;
}
return dec(c[C(n)][m],res);
}
inline void init(){
c[0][0]=1;
for(int re i=1,lim=C(n);i<=lim;++i){
c[i][0]=c[i][i]=1;
for(int re j=1;j<i;++j)c[i][j]=add(c[i-1][j],c[i-1][j-1]);
}
for(int re i=1;i<=n;++i)
for(int re j=i-1,lim=std::min(C(i),m);j<=lim;++j)f[i][j]=calc(i,j);
}
inline int g(int n,int m){return (n>0&&m>0)?f[n][m]:0;}
public:
int howMany(int N, int K, int M){
n=N,m=M;
if(n==1)return !m&&!K;
if(n==2)return m==1&&!K;
init();
int res=0,tmp=0;
switch(K){
case 0:return f[n][m];
case 1:
Inc(res,mul(m-1,g(n-1,m-1)));
Inc(res,mul(m-2,g(n-1,m-2)));
return res;
case 2:
Inc(res,mul(n-2,g(n-2,m-3)));
Inc(res,mul(2*(m-2),g(n-2,m-2)));
Inc(res,mul(2*(m-3),g(n-2,m-3)));
Inc(res,mul((m-4)*(m-4),g(n-2,m-4)));
Inc(res,mul(2*(m-3)*(m-4),g(n-2,m-3)));
Inc(res,mul(m-3,g(n-2,m-3)));
Inc(res,mul((m-2)*(m-3),g(n-2,m-2)));
return res;
}
}
};
#ifdef zxyoi
KingdomAndCities Solver;
int n,m,k;
signed main(){
freopen("kingdom.in","r",stdin);
std::cin>>n>>m>>k;
std::cout<<Solver.howMany(n,m,k);
return 0;
}
#endif