#21 CF830D & CF850D & CF896D
Singer House
题目描述
解法
同时路径计数问题,本题可以和 环 这题对比起来理解。
基本方法都是一样的,首先考虑计数顺序应该是自底向上的树形 \(dp\),但是计数顺序却和我们考虑的状态——有向路径产生了冲突,因为按照这样的计数顺序,有向路径从某个点来看,可能就是若干个分散的有向链。
为了解决这样的冲突,我们在 \(dp\) 的过程中就需要维护一个有向链分散,合并的过程。这个过程的计数可以通过记录有向链的数量来实现,设 \(f_{n,k}\) 表示深度为 \(n\) 的子树内有 \(k\) 条有向链的方案数,转移:
- 不选根节点:\(f_{n,k}\leftarrow f_{n-1,i}\cdot f_{n-1,k-i}\)
- 让根节点成为单独的链:\(f_{n,k}\leftarrow f_{n-1,i}\cdot f_{n-1,k-i-1}\)
- 让根节点拼接一条链,可以选择成为起点或者成为终点:\(f_{n,k}\leftarrow f_{n-1,i}\cdot f_{n-1,k-i}\cdot (2k)\)
- 让根节点拼接两条链,方案是 \(A(k,2)\),因为有顺序:\(f_{n,k}\leftarrow f_{n-1,i}\cdot f_{n-1,k-i+1}\cdot (k+1)\cdot k\)
时间复杂度 \(O(n^3)\)
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int M = 405;
const int MOD = 1e9+7;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,f[M][M];
int F(int n,int k)
{
if(!k) return 1;
if(n==1) return k==1;
if(~f[n][k]) return f[n][k];
int r=0;
for(int i=0;i<=k;i++)
r=(r+F(n-1,i)*F(n-1,k-i))%MOD;
for(int i=0;i<k;i++)
r=(r+F(n-1,i)*F(n-1,k-i-1))%MOD;
for(int i=0;i<=k;i++)
r=(r+2*k*F(n-1,i)%MOD*F(n-1,k-i))%MOD;
for(int i=0;i<=k+1;i++)
r=(r+k*(k+1)*F(n-1,i)%MOD*F(n-1,k-i+1))%MOD;
return f[n][k]=r;
}
signed main()
{
n=read();
memset(f,-1,sizeof f);
printf("%d\n",F(n,1));
}
Tournament Construction
题目描述
解法
以前学了个假的兰道定理,但是发现解决这个题是完全足够的。
真的兰道定理:我们把出度序列从小到大排序 \(d_1,d_2...d_n\),那么这个出度序列对应到竞赛图的充要条件是:
可以类似 \(\tt Hall\) 定理来理解,它的本质就是对于每个子集,出度和不能小于其导出子图的度数和。
回到本题,首先考虑判断有无解,显然可以那个背包来做。设 \(dp[i][j][k]\) 表示考虑了出度集合的前 \(i\) 种,已经选出了 \(j\) 个点,它们出度和是 \(k\) 是否合法,转移时只需要时刻保证 \(j\geq i,k\geq{j\choose 2}\) 即可。
构造答案有一种 \(\tt naive\) 的方法,我们取出当前最小的点 \(u\),然后把较小的 \(d_u\) 个点 \(v\),我们设置边 \((u,v)\),它们的出度不变;对于剩下较大的点 \(v\),我们设置边 \((v,u)\),它们的出度会减少 \(1\)
上述构造方法为什么正确呢?\(n=1\) 时显然正确,操作后会从 \(n\) 阶竞赛图变成 \(n-1\) 阶竞赛图,发现考虑最小点的连边之后仍然满足 \(\sum_{i=1}^kd_i\geq {k\choose 2}\) 的条件,那么我们就归纳到了更小的情况。
时间复杂度 \(O(n^5)\)
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 62;
const int M = 1900;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,a[N],d[N],p[N],e[N][N],f[N][N][M],g[N][N][M];
signed main()
{
m=read();
for(int i=1;i<=m;i++) a[i]=read();
sort(a+1,a+1+m);
f[0][0][0]=1;
for(int i=1;i<=m;i++) for(int j=i;j<N;j++)
for(int k=i-1;k<j;k++) for(int x=(k-1)*k/2;x<M;x++)
{
int y=x+(j-k)*a[i];
if(y>=M) continue;
if(f[i-1][k][x]) f[i][j][y]=1,g[i][j][y]=j-k;
}
for(n=m;n<N;n++)
if(f[m][n][n*(n-1)/2]) break;
if(n==N) {puts("=(");return 0;}
printf("%d\n",n);
for(int i=m,j=n,k=n*(n-1)/2;i;i--)
{
int x=g[i][j][k];
for(int p=0;p<=x;p++) d[j-p]=a[i];
j-=x;k-=x*a[i];
}
for(int i=1;i<=n;i++) p[i]=i;
for(int i=1;i<=n;i++)
{
sort(p+i,p+1+n,[&](int i,int j)
{return d[i]<d[j];});
int u=p[i];
for(int j=i+1;j<=i+d[u];j++)
e[u][p[j]]=1;
for(int j=i+d[u]+1;j<=n;j++)
e[p[j]][u]=1,d[p[j]]--;
}
for(int i=1;i<=n;i++,puts(""))
for(int j=1;j<=n;j++)
printf("%d",e[i][j]);
}
Nephren Runs a Cinema
题目描述
解法
这道 *2900
的题竟然被我手切了,虽然是水题但还是写篇题解纪念一下。
首先枚举 \(\tt VIP\) 用户的个数(哇爆率真的很高),再枚举获得 \(50\) 元钞票的总数 \(x\),就可以转化成这样的问题:从 \((0,0)\) 走到 \((n-x,x)\),不越过 \(y=x\) 的方案数,根据卡特兰数的知识可以知道方案数是 \({n\choose n-x}-{n\choose n-x+1}\)
其中 \(x\) 需要满足 \(l\leq n-2x\leq r\),可以解出 \(x\in [x_{1},x_{2}]\),那么我们把方案数求和看看:
发现只剩下两项组合数了!那么剩下的问题是预处理所有 \({n\choose i}\),可以把模数的所有质数次幂拆分出来,然后分两部分计算即可(模数质数和非模数质数),时间复杂度 \(O(n\log n)\)
有一个容易错的小细节是 \(i\) 的枚举范围应该是 \([0,n-l]\),要不然会出问题。
#include <cstdio>
#define int long long
const int M = 100005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,k,MOD,l,r,ans,fac[M],inv[M],c[M][20],p[20];
int qkpow(int a,int b)
{
int r=1;
while(b>0)
{
if(b&1) r=r*a%MOD;
a=a*a%MOD;
b>>=1;
}
return r;
}
int C(int n,int m)
{
if(m<0 || n<m) return 0;
int x=fac[n]*inv[m]%MOD*inv[n-m]%MOD;
for(int i=1;i<=k;i++)
x=x*qkpow(p[i],c[n][i]-c[m][i]-c[n-m][i])%MOD;
return x;
}
signed main()
{
n=read();MOD=read();l=read();r=read();
//part I : initialize
int x=MOD,phi=x;
for(int i=2;i*i<=x;i++) if(x%i==0)
{
x/=i;p[++k]=i;phi=phi/i*(i-1);
while(x%i==0) x/=i;
}
if(x>1) p[++k]=x,phi=phi/x*(x-1);
inv[0]=fac[0]=1;
for(int i=1;i<=n;i++)
{
x=i;
for(int j=1;j<=k;j++)
while(x%p[j]==0) x/=p[j],c[i][j]++;
fac[i]=x*fac[i-1]%MOD;
inv[i]=qkpow(fac[i],phi-1);
for(int j=1;j<=k;j++) c[i][j]+=c[i-1][j];
}
//part II : Catalan numbers
for(int i=0;i<=n-l;i++)
{
int x1=(n-i-l)/2,x2=(n-i-r+1)/2;
int h=C(n-i,n-i-x1)-C(n-i,n-i-x2+1);
h=(h%MOD+MOD)%MOD;
ans=(ans+h*C(n,i))%MOD;
}
printf("%lld\n",ans);
}