说句闲话。今天翻到一篇博客上来给放了个公式:
\[\sum_{i=0}^n\binom{2i}i\binom{2n-2i}{2i}=4^i \]看起来就很不对劲。然后爆算了一波确实是错的。敬请注意。
然后不知道为什么要管 Vizing 定理叫维京定理。感觉不如味精定理。感觉不如胃镜定理。
是不是应该反思一下树上背包都能写挂的问题。或者反思一下是不是太过于信任大样例的问题。
感觉我再不打 rated 就要橙了。
矩阵树定理
oi-wiki 上边的矩阵树定理内容毫不客气地说真的跟个 jb 一样。并不是说讲的不对,只是我觉得大多数人都不想看这一大车定义和公式,而且还是线性代数的。
这玩意拿来计数一张图的生成树个数。直接上结论,证明不会。
以下默认没有自环。
原版
计数无向图生成树个数。
定义无向图的邻接矩阵 \(A\),\(A_{i,j}\) 为 \(1\) 当且仅当 \(i,j\) 间有边。定义度数矩阵 \(D\) ,其中 \(D_{i,i}\) 为节点 \(i\) 度数。定义无向图的基尔霍夫矩阵为 \(K=D-A\)。
那么随便找个 \(k\) 给矩阵 \(K\) 去掉第 \(k\) 行第 \(k\) 列得到 \(K'\),答案就是 \(\det(K')\)。
一道例题:[HEOI2015]小 Z 的房间
建图套板子即可。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <cmath>
using namespace std;
const int mod=1000000000;
int n,m,cnt;
int a[610][610],id[15][15];
char s[15][15];
int gauss(int n){
int ans=1,w=1;
for(int i=1;i<=n;i++){
for(int j=i+1;j<=n;j++){
while(a[i][i]){
int rate=a[j][i]/a[i][i];
for(int k=i;k<=n;k++){
a[j][k]=(a[j][k]-1ll*rate*a[i][k]%mod+mod)%mod;
}
for(int k=1;k<=n;k++)swap(a[i][k],a[j][k]);
w=-w;
}
for(int k=1;k<=n;k++)swap(a[i][k],a[j][k]);
w=-w;
}
}
for(int i=1;i<=n;i++)ans=1ll*ans*a[i][i]%mod;
ans=(1ll*ans*w%mod+mod)%mod;
return ans;
}
void add(int x,int y){
a[x][x]++;a[y][y]++;a[x][y]--;a[y][x]--;
}
signed main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%s",s[i]+1);
for(int j=1;j<=m;j++){
if(s[i][j]=='.')id[i][j]=++cnt;
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(s[i][j]=='.'&&s[i+1][j]=='.')add(id[i][j],id[i+1][j]);
if(s[i][j]=='.'&&s[i][j+1]=='.')add(id[i][j],id[i][j+1]);
}
}
printf("%d\n",gauss(cnt-1));
return 0;
}
边带权
对于边带权的情况,只需要把邻接矩阵的 \(A_{i,j}\) 改为 \(i,j\) 间所有边的边权和,度数矩阵的 \(D_{i,i}\) 改为从 \(i\) 连出去的所有边的边权和,得到的答案就是所有生成树边权乘积的和。
另一道例题:[SDOI2014]重建
一个生成树 \(T\) 的概率显然是
\[\prod_{e\in T}p_e\prod_{e\notin T}(1-p_e) \]稍微转化一下:
\[\begin{aligned} &\prod_{e\in T}p_e\prod_{e\notin T}(1-p_e)\\ =&\prod_{e}(1-p_e)\prod_{e\in T}\frac{p_e}{1-p_e} \end{aligned} \]那么把每条边的权值设为 \(\dfrac {p_e}{1-p_e}\) 就行了。如果是 \(1\) 的话减个 \(eps\) 就行。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <cmath>
using namespace std;
const double eps=1e-8;
int n,m,cnt;
double a[60][60],p[60][60];
double gauss(int n){
double ans=1;
int w=1;
for(int i=1;i<=n;i++){
for(int j=i+1;j<=n;j++){
while(fabs(a[i][i])>eps){
double rate=a[j][i]/a[i][i];
for(int k=i;k<=n;k++){
a[j][k]=a[j][k]-rate*a[i][k];
}
for(int k=1;k<=n;k++)swap(a[i][k],a[j][k]);
w=-w;
}
for(int k=1;k<=n;k++)swap(a[i][k],a[j][k]);
w=-w;
}
}
for(int i=1;i<=n;i++)ans=ans*a[i][i];
ans=ans*w;
return ans;
}
void add(int x,int y){
a[x][x]++;a[y][y]++;a[x][y]--;a[y][x]--;
}
signed main(){
scanf("%d",&n);
double ret=1;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
scanf("%lf",&p[i][j]);
if(i==j)continue;
if(p[i][j]>1-eps)p[i][j]-=eps;
if(i<j)ret*=1-p[i][j];
p[i][j]/=1-p[i][j];
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
a[i][i]+=p[i][j];a[i][j]-=p[i][j];
}
}
printf("%.4lf\n",gauss(n-1)*ret);
return 0;
}
有向图
对于有向图,我们更改一下 \(A\) 和 \(D\)。此处的 \(A\) 变成了有向图的邻接矩阵,而 \(D\) 有两种不同情况:
- \(D_{i,i}\) 为点 \(i\) 入度,此时求的是外向树(从根向外指)。
- \(D_{i,i}\) 为点 \(i\) 出度,此时求的是内向树(从外向根指)。
然后关于去掉一行一列的问题,有向图的问题中去掉哪一行那一列就是以哪个点为根。
当然也可以带权。以下为模板代码。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <cmath>
using namespace std;
const int mod=1000000007;
int n,m,cnt;
int a[610][610],id[15][15];
char s[15][15];
int gauss(int n){
int ans=1,w=1;
for(int i=1;i<=n;i++){
for(int j=i+1;j<=n;j++){
while(a[i][i]){
int rate=a[j][i]/a[i][i];
for(int k=i;k<=n;k++){
a[j][k]=(a[j][k]-1ll*rate*a[i][k]%mod+mod)%mod;
}
for(int k=1;k<=n;k++)swap(a[i][k],a[j][k]);
w=-w;
}
for(int k=1;k<=n;k++)swap(a[i][k],a[j][k]);
w=-w;
}
}
for(int i=1;i<=n;i++)ans=1ll*ans*a[i][i]%mod;
ans=(1ll*ans*w%mod+mod)%mod;
return ans;
}
void add(int x,int y,int w){
(a[x][x]+=w)%=mod;(a[y][y]+=w)%=mod;a[x][y]=(a[x][y]-w+mod)%mod;a[y][x]=(a[y][x]-w+mod)%mod;
}
signed main(){
int t;scanf("%d%d%d",&n,&m,&t);
for(int i=1;i<=m;i++){
int u,v,w;scanf("%d%d%d",&u,&v,&w);
if(t==0)add(u,v,w);
else{
a[v-1][v-1]=(a[v-1][v-1]+w)%mod;;a[u-1][v-1]=(a[u-1][v-1]-w+mod)%mod;
}
}
printf("%d\n",gauss(n-1));
return 0;
}
BEST 定理
内容:有向图欧拉回路数目:
\[ans=T\prod_{i}(out_i-1)! \]其中 \(T\) 是内向生成树个数(容易证明每个节点为根的答案都一样),\(out_i\) 是 \(i\) 的出度。
注意这个没有算上从根开始的路径数目。所以如果从根开始走的路径不同的话需要乘上根节点的出度。
一道例题:「BZOJ3659」WHICH DREAMED IT
裸的 BEST 定理,注意答案乘上根节点出度。
然而这题特判非常之多。需要判是否是欧拉图、图是否联通、是否没有边等等情况。我甚至一度怀疑是不是我行列式求值噶了。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <cmath>
using namespace std;
const int mod=1000003;
int n,m,cnt;
int a[110][110],ind[110],out[110],jc[200010],fa[110];
int find(int x){
return fa[x]==x?fa[x]:fa[x]=find(fa[x]);
}
int gauss(int n){
int ans=1,w=1;
for(int i=1;i<=n;i++){
if(!out[i])continue;
for(int j=i+1;j<=n;j++){
if(!out[j])continue;
while(a[i][i]){
int rate=a[j][i]/a[i][i];
for(int k=i;k<=n;k++){
a[j][k]=(a[j][k]-1ll*rate*a[i][k]%mod+mod)%mod;
}
for(int k=1;k<=n;k++)swap(a[i][k],a[j][k]);
w=-w;
}
for(int k=1;k<=n;k++)swap(a[i][k],a[j][k]);
w=-w;
}
}
for(int i=1;i<=n;i++)if(out[i])ans=1ll*ans*a[i][i]%mod;
ans=(1ll*ans*w%mod+mod)%mod;
return ans;
}
void clear(){
for(int i=1;i<=n;i++)ind[i]=out[i]=0,fa[i]=i;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++)a[i][j]=0;
}
}
void solve(){
scanf("%d",&n);clear();
for(int i=1;i<=n;i++){
scanf("%d",&m);
for(int j=1;j<=m;j++){
int x;scanf("%d",&x);
ind[x]++;out[i]++;
a[i][i]++;a[i][x]--;
fa[find(x)]=find(i);
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
a[i][j]=(a[i][j]+mod)%mod;
}
}
if(n==1){
printf("%d\n",jc[out[1]]);return;
}
for(int i=1;i<=n;i++){
if(ind[i]!=out[i]||(find(i)!=find(1)&&ind[i])){
puts("0");return;
}
}
bool jud=false;
for(int i=1;i<=n;i++)jud=(out[i]);
if(!jud){
puts("1");return;
}
int ans=gauss(n-1);
for(int i=1;i<=n;i++)if(out[i])ans=1ll*ans*jc[out[i]-1]%mod;
ans=1ll*ans*out[1]%mod;
printf("%d\n",ans);
}
int main(){
jc[0]=1;
for(int i=1;i<=200000;i++)jc[i]=1ll*jc[i-1]*i%mod;
int tim;scanf("%d",&tim);
while(tim--)solve();
return 0;
}
标签:prod,15,int,定理,矩阵,include,BEST
From: https://www.cnblogs.com/gtm1514/p/17112485.html