博客目录
链接:https://www.nowcoder.com/acm/contest/139/B
来源:牛客网
时间限制:C/C++ 1秒,其他语言2秒
空间限制:C/C++ 524288K,其他语言1048576K
64bit IO Format: %lld
题目描述
Count the number of n x n matrices A satisfying the following condition modulo m.
* Ai, j ∈ {0, 1, 2} for all 1 ≤ i, j ≤ n.
* Ai, j = Aj, i for all 1 ≤ i, j ≤ n.
* Ai, 1 + Ai, 2 + ... + Ai, n = 2 for all 1 ≤ i ≤ n.
* A1, 1 = A2, 2 = ... = An, n = 0.
输入描述:
The input consists of several test cases and is terminated by end-of-file. Each test case contains two integers n and m.
输出描述:
For each test case, print an integer which denotes the result.
示例1
输入
复制
3 1000000000 100000 1000000000
输出
复制
1 507109376
备注:
* 1 ≤ n ≤ 105 * 1 ≤ m ≤ 109 * The sum of n does not exceed 107.
题目大意
给出一个n,向n * n的矩阵中填数字{0,1,2},要求矩阵是对称阵且每行元素之和恰好为2,且主对角线元素为0,求满足要求的矩阵个数。
分析过程
对称方阵可以看做某个图的邻接矩阵。其中aij=0表示点i和j不直接相连,=1表示i和j之间有一条直接相连的无向边,我们在这里称单边,=2表示i和j之间有两条直接相连的无向边,我们在这里称双边。当然,因为有2的存在导致这个图不是简单图。(可是我不会做图论题)
将矩阵看成图之后,每行之和为2,由对称阵得每列之和也为2,图是无向图且每个点的度恰好为2。那么很容易想到以下结论:
1.如果一个点有一个双边(也就是长度为2的环),那么双边一定连着另一个点,这两个点组成一个连通分量。也就是不与第三个点相连
2.任意一个环,图论中很容易知道环中每个点度大于等于2,而本题要求度恰好为2,所以环一定没有分叉且也是一个连通分量,也就是环是纯粹一个环且不跟其他点相连,且删掉任意一个边之后环即退化成一条链。
以上两个结论我们可以知道,这个图可以分成若干连通分量,连通分量可以分为两类:1.双边(长度为2的环) 2.长度大于2的环
虽然看上去双边也是环,但是这个环比较特殊算起来有些不同,下面会提到。
动态规划
设f(n)表示有n个点的满足上述条件的图的个数。考虑第n个点,则有
枚举n点所属的连通分量环的长度k,则有2<=k<=n
设A(m,n)=m!/(m-n)!为排列数,表示从m个点中拿出n个并排列。
- 环中必须有点n,故把n拿出来还剩下k-1个点,这些点可以任意排列,但是正反两种属于同一种环(所以总数除以2),所以组成一个环的方法数为:A(n-1,k-1)/2=(n-1)!/(n-k)!/2 ,剩余的点不与上述 环/双边 相连,故是剩余的点的情况是独立的,可以随意组合只要满足条件即可,则方法数为f(n-k),两者是独立的,所以方法数相乘:(n-1)!/(n-k)!*f(n-k)/2 ,3<=k<=n
- 但是对于双边来讲,除了点x就只有一个点,直接A(n-1,1)=n-1就可以了,不需要除以2,这里体现了双边跟环的区别。而剩余的点有n-2个,也是独立的,所以要乘以f(n-2)。这个情况总数为:(n-1)f(n-2)
上述两个是并列的分类,所以相加:
f(n)=(n-1)f(n-2)+ ∑(3<=k<=n) { (n-1)!/(n-k)!*f(n-k)/2 }
当然,这跟出题者给出的答案有些出入,原因是k的含义不同,我们只需要对k做一个简单处理就可以得到标准答案:
将k=n-k代入上式,得:
f(n)=(n-1)*f(n-2) + ∑(3<=n-k<=n) { (n-1)!/(k)!*f(k)/2 }
=(n-1)*f(n-2) + ∑(0<=k<=n-3) { (n-1)!/(k)!*f(k)/2 }
显然:f(0)=f(1)=0 (f(0)的解释看代码注释),且k<=n-2等价于k<n-3所以
f(n)=(n-1)*f(n-2) + ∑(2<=k<n-2) { (n-1)!/k!*f(k)/2 } ,n>=3
复杂度优化
上述式子是二次的复杂度,考虑化为递推式
我们令g(n)= ∑(2<=k<=n-3) { (n-1)! / k!*f(k)/2 },n>=5
我们发现,式子中跟n有关的有两部分:
1.求和符号上限,我们可以提出最后一项把n变为n-1
2.大括号里面阶乘有n,我们可以将阶乘最后一项n-1提出来,将n变为n-1
所以可以改写成递推形式:
g(n)=把k=n-3的一项提出来 + ∑ { (n-1)*(n-1-1)! / k!*f(k)/2 }
g(n)=(n-1)!/(n-3)!*f(n-3)/2 + (n-1)*∑(2<=k<=(n-1)-3) { ((n-1)-1)! / k!*f(k)/2 }
=(n-1)!/(n-3)!*f(n-3)/2 + (n-1)*g(n-1)
AC代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define regi ll
ll mod;
int const maxn=1e5+10;
ll f[maxn],g[maxn];
int main(){
int n;
f[0]=0;
/*网上和其他人提交的代码令f(0)=1,但是讲道理应
该是0,因为没有行就没办法让行求和等于2,
博客中的推导也间接证明了这一点*/
f[1]=0;
f[2]=f[3]=1;
g[0]=g[1]=g[2]=0 ;
g[3]=1;
while(cin>>n>>mod){
for(regi i=4;i<=n;i++){
f[i]=(i-1)*f[i-2]%mod;
g[i]=(i-1)*(i-2)/2%mod*f[i-3]%mod; //注意这里除以2不需要求逆元
g[i]+=(i-1)*g[i-1]%mod;
g[i]%=mod;
f[i]+=g[i];
f[i]%=mod;
}
printf("%lld\n",f[n]);
}
}
/*
f(n)=(n-1)*f(n-2) + g(n)
g(n)=(n-1)(n-2)*f(n-3)/2 + (n-1)*g(n-1)
*/