[FJOI2007] 轮状病毒

lolifamily

由于内容过长、公式较多,暂时将内容隐藏,请公式恐惧症们做好心理准备。

题解备份

Problem

Description

轮状病毒有很多变种,所有轮状病毒的变种都是从一个轮状基产生的。

一个N轮状基由圆环上N个不同的基原子和圆心处一个核原子构成的,2个原子之间的边表示这2个原子之间的信息通道。

如下图所示

1.png

N轮状病毒的产生规律是在一个N轮状基中删去若干条边,使得各原子之间有唯一的信息通道,例如共有16个不同的3轮状病毒,如下图所示:

2.png

现给定N(N 100),编程计算有多少个不同的N轮状病毒

Input

第一行有1个正整数N

Output

计算出的不同的N轮状病毒数输出

Sample Input

3

Sample Output

16

法一: 行列式

转载自 vfleaking

对于新手还是建议去看看基尔霍夫矩阵,这一篇论文挺不错的。

用基尔霍夫矩阵使用高斯消元解行列式,时间复杂度O(n3)似乎可以 AC

首先行列式有很多性质:

  • a×k加到第b行上去,行列式的值不变
  • 三角行列式的值等于对角线元素之积
  • a行与第b行互换,行列式的值取反
  • 常数×行列式,可以把常数乘到某一行里去

如果你行列式不是很熟,建议先搜搜行列式~不然下面会看晕~

其实如果你仔细观察矩阵,可以发现它是这样的:(消去了病毒中央)

|310000011310000001310000001300000001000000003100000013100000013110000013|

那么我们现在对行列式进行变换,我们把第1行与第2行交换,再把第2行与第3行交换……,再把第n1行与第n行变换,得到新的行列式:

|131000000131000000130000000100000000310000001310000001311000001331000001|

这个行列式跟一开始的那个行列式的值不一定相等。因为我们是通过n1次交换行的操作得到的,为了说话方便我们称一开始的行列式为A,上面刚写的行列式为B那么由行列式性质得:A=(1)n1B现在就可以正大光明地处理B了~

利用行列式性质,来手算这个行列式。之所以刚才有那么一步,就是为了方便手算。因为观察B矩阵,发现就只剩下左下角的131三个倒霉了。

倒数第二行:10000013用第一行的:13100000乘以1 来消:03100013再用第二行:01310000乘以3 来消:00830013

这样就有了初步感觉了~

现在把这个过程一般化:

第 k 个和第 k+1 个:00F(k)G(k)0013总能找到上面的某一行0013   1000乘以 F(k) 来消:000F(k+1)G(k+1)013

于是得到:

{F(k+1)=G(k)+3F(k)G(k+1)=F(k)

整合一下:F(k+1)=3F(k)F(k1)

从初始的行和消了一次之后的行中取得边界条件:F(1)=1,F(2)=3最终一定会变为下面这种情况:

倒数第二行:0000F(n3)G(n3)13用倒数第四行:00001310乘以 F(n3) 来消:00000F(n2)G(n2)13用倒数第三行:00000131乘以 F(n2) 来消:000000F(n1)1G(n1)+3

好现在搞定了倒数第二行,来看看成果:(f=F(n1)1,g=G(n1)+3)

|13100000013100000013000000010000000031000000131000000131000000fg31000001|

好,现在来搞倒数第一行。和倒数第二行的方法是类似的。

再设函数H(k)I(k),意义与F(k)G(k)类似,得:

{H(k+1)=I(k)+3H(k)I(k+1)=H(k)

其实跟FG的递推式是一样的我会乱说?H(k+1)=3H(k)H(k1)边界条件是:H(1)=3,H(2)=8最后使劲搞一搞,倒数第一行就成了:

0000000H(n1)I(n1)1

再来看成果:(h=H(n1),i=I(n1)1)

|13100000013100000013000000010000000031000000131000000131000000fg000000hi|

用倒数第二行来消倒数第一行,得:

0000000ig(hf)

现在这个行列式已经是三角行列式了,它的值就是对角线元素之积。于是:B=(1)×(1)×(1)××f(ighf)一共有n21

如前文所述:A=(1)n1B

又因为:B=(1)n2(figh)

于是有:A=(1)2n3(figh)=fi+gh

带入fghi的值得:A=(F(n1)1)(I(n1)1)+(G(n1)+3)H(n1)

带入HI的值:A=(F(n1)1)(H(n2)1)+(F(n2)+3)H(n1)

然后再展开…… 回忆下FH的递推式

A=F(n1)H(n2)+F(n1)H(n2)1F(n2)H(n1)+3H(n1)=H(n)+F(n1)+F(n1)H(n2)F(n2)H(n1)1=H(n)+F(n1)+|F(n1)H(n1)F(n2)H(n2)|1

发现不能化简了?没关系!在行列式上动动手脚吧!

FH 定理

对于任意大于2k有:

|F(k1)H(k1)F(k2)H(k2)|=|F(2)H(2)F(1)H(1)|

证明:对于行列式:

|F(k1)H(k1)F(k2)H(k2)|

把行列式最下面的行取反,则行列式的值取反:

|F(k1)H(k1)F(k2)H(k2)|

把行列式的上面的行乘以3加到下面去:

|F(k1)H(k1)3F(k1)F(k2)3H(k1)H(k2)|

特意构造的递推式出现了:

|F(k1)H(k1)F(k)H(k)|

有点眉目了~ 把第一行与第二行调换位置,行列式的值取反:

|F(k)H(k)F(k1)H(k1)|

一目了然,这是 k++ 后的行列式的样子。(Pascal 同学早日转 C++

那么立即推出:

|F(k1)H(k1)F(k2)H(k2)|=|F(2)H(2)F(1)H(1)|

FH 定理得证。

利用 FH 定理,把F(1)=1,F(2)=3,H(1)=3,H(2)=8带入:

|F(n1)H(n1)F(n2)H(n2)|=1

于是就爽了嘛!

A=H(n)+F(n1)+(1)1=H(n)+F(n1)2

进一步我们发现…… 设R(n)=H(n)+F(n1)2

那么立即有:

R(n)=3H(n1)H(n2)+3F(n2)F(n3)2=3(R(n1)+2)(R(n2)+2)2=3R(n1)R(n2)+2

所以,轮状病毒的方案数满足递推式F(n)=3F(n1)F(n2)+2,其中F(1)=1,F(2)=5

然后随手写一个高精度就可以过了~

法二:DP

转载自 boshi

如果用 f[x] 表示加入了x个周围的点后的方案数,我们首先想到的递推式是:f[i]=j=1if[ij]j

解释:最后加入的j个点每个都可能与中心点连边,将所有方案数累加即可。

但是,第一个点永远不会与第n个点连边,因此方案数统计并不准确。

我们再设:g[i]=j=2if[ij]j(j1)

解释:如果有j个周围的点连成一条,且跨越了1n,我们将所有这样的情况累加到答案中去。如果这样的点有j个,剩下的点肯定不与这j个点相连,所以连边方案数就是f[ij],这j个点有(j1)种选法(跨越1n),与中心点连边的方案数是j,根据乘法原理,答案要累加f[ij]j(j1)

这样的f[n]+g[n]就是我们要求的轮状病毒的数量。

下面我们思考如何快速求出fg

多阶差分

首先分析f[i]。如果我们可以求出所有f[ij]j的前缀和,这个问题就变得非常方便了。

问题是对于不同的i,这个前缀和中每一项都会发生变化。

那如果我们知道了变化的量是多少呢?于是我们就对前缀和进行差分。

Δf[i]=j=1if[ij]jj=1i1f[i1j]j=j=0if[ij]jj=0i1f[i1j]j(f[i]0=f[i1]0=0)=j=0if[j](ij)j=0i1f[j](i1j)(交换枚举顺序)=j=0if[i]g[i]=j=2if[ij]j(j1)=j=0if[ij]j(j1)(f[i1]10=0)Δg[i]=j=0if[ij]j(j1)j=0i1f[ij1](j1)(j2)=j=0if[j](ij)(ij+1)j=0i1f[j](ij)(ij1)=j=0if[j](ij)2(ii=0)Δ2g[i]=j=0if[j](ij)2j=0i1f[j](ij1)2=j=0i2f[j](ii=0)Δ3=2f[i]

我们维护f[i]的前缀和,以及f[ij]j的前缀和,每次将f[i]累加进f[i]的前缀和,将f[i]的前缀和累加进f[ij]j的前缀和,g[i]同理。

行列式解法:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define digit 100000000
using namespace std;
struct bigint
{
mutable int a[205];
inline bigint()
{
memset(a,0,sizeof(a));
a[0]=1;a[1]=0;return;
}
inline bigint(int b)
{
memset(a,0,sizeof(a));
a[0]=1;a[1]=b;return;
}
inline int& operator[](size_t pos)const{return a[pos];}
inline bigint operator+(int b)const
{
int i;bigint c=*this;
c[1]+=b;
for(i=1;i<=c[0];++i)
{
if(c[i]>=digit)
{
++c[i+1];c[i]-=digit;
}
}
if(c[c[0]+1])++c[0];
return c;
}
inline bigint operator-(const bigint& b)const
{
int i;bigint c;c[0]=a[0];
for(i=1;i<=c[0];++i)c[i]=a[i]-b[i];
for(i=1;i<=c[0];++i)
{
if(c[i]<0)
{
c[i]+=digit;--c[i+1];
}
}
while(!c[c[0]]&&c[0]>1)--c[0];
return c;
}
inline bigint operator*(int b)const
{
int i;bigint c;c[0]=a[0];
for(i=1;i<=c[0];++i)c[i]=a[i]*b;
for(i=1;i<=c[0]||c[i];++i)
{
if(c[i]>=digit)
{
c[i+1]+=c[i]/digit;c[i]%=digit;
}
}
c[0]=i-1;
return c;
}
friend ostream& operator<<(ostream& os,const bigint& a)
{
printf("%d",a[a[0]]);
for(int i=a[0]-1;i>=1;--i)printf("%08d",a[i]);
return os;
}
}f[3005];
int main(void)
{
int i,n;
scanf("%d",&n);
f[1]=1;f[2]=5;
for(i=3;i<=n;++i)f[i]=f[i-1]*3-f[i-2]+2;
cout<<f[n]<<endl;
return 0;
}

DP 解法:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define digit 1000000000
using namespace std;
struct bigint
{
mutable int a[205];
inline bigint()
{
memset(a,0,sizeof(a));
a[0]=1;a[1]=0;return;
}
inline bigint(int b)
{
memset(a,0,sizeof(a));
a[0]=1;a[1]=b;return;
}
inline int& operator[](size_t pos)const{return a[pos];}
inline bigint& operator+=(const bigint& b)
{
int i;a[0]=max(a[0],b[0]);
for(i=1;i<=a[0];++i)a[i]+=b[i];
for(i=1;i<=a[0];++i)
{
if(a[i]>=digit)
{
++a[i+1];a[i]-=digit;
}
}
if(a[a[0]+1])++a[0];
return *this;
}
friend ostream& operator<<(ostream& os,const bigint& a)
{
printf("%d",a[a[0]]);
for(int i=a[0]-1;i>=1;--i)printf("%09d",a[i]);
return os;
}
}F=1,F1=1,F2=1,G=0,G1=0,G2=0;
int main(void)
{
int i,n;
scanf("%d",&n);
for(i=1;i<=n;++i)
{
if(i)G2+=F,G2+=F;
if(i<n)G1+=G2,G+=G1;
F=F1;F2+=F;F1+=F2;
}
cout<<(F+=G)<<endl;
return 0;
}

完结撒花!★,°:.☆( ̄▽ ̄)/$:.°★