B.最短路径
题目描述
给定一棵 (n) 个节点的无根树,每条边的边权均为 (1)
树上有 (m) 个互不相同的关键点,从中随机选取 (k) 个点打上标记,问任意起点终点,经过所有被标记点的最短路径长度期望是多少。
(2leq kleq mleq nleq 2000,mleq300)
解法
考虑如果需要走回起点,那么最短路径长度就是虚树的边数( imes2),如果不需要走回起点,那么减去虚树的直径即可。所以问题可以转化成分别求出虚树边数的期望和虚树直径的期望。
求虚树边数的期望可以考虑每条边的贡献,我们用总方案减去所有标记点只出现在子树一侧的方案就可以算出这条边出现在虚树中的方案,组合数随便算算即可。
求虚树直径的期望可以直接枚举直径 ((u,v)),然后统计对应树的方案,我们要保证长度最长和字典序最小就可以不算重,那么我们枚举一个点 (x),不存在下列条件代表合法:
- (d(u,v)<d(x,v)),或 (d(u,v)=d(x,v),x<u)
- (d(u,v)<d(u,x)),或 (d(u,v)=d(u,x),x<v)
这个可以结合树上邻域理论来理解,也就是添加一个点之后考虑会不会存在更优化的直径,这个东西是充要的我就不证了。实现我们暴力枚举点对复杂度就是对的,时间复杂度 (O(m^3))
总结
最小值套期望的题要考虑拆分,做到优秀复杂度很可能会存在一种已知的达到最小值的策略。
#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
#include <cassert>
using namespace std;
const int M = 2005;
const int MOD = 998244353;
const int inf = 0x3f3f3f3f;
#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,m,k,ans,a[M],d[M][M],siz[M],C[M][M];
vector<int> g[M];
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;
}
void init()
{
for(int i=0;i<=m;i++)
{
C[i][0]=1;
for(int j=1;j<=i;j++)
C[i][j]=(C[i-1][j-1]+C[i-1][j])%MOD;
}
}
void dfs(int u,int fa,int rt)
{
for(auto v:g[u])
{
if(v==fa) continue;
d[rt][v]=d[rt][u]+1;
dfs(v,u,rt);
}
}
void cal(int u,int fa)
{
for(auto v:g[u])
{
if(v==fa) continue;
cal(v,u);
siz[u]+=siz[v];
}
if(u!=1)//the contribution of edge(u,fa)
{
int f=(C[m][k]-C[siz[u]][k]-C[m-siz[u]][k])%MOD;
ans=(ans+f+MOD)%MOD;
}
}
signed main()
{
freopen("tree.in","r",stdin);
freopen("tree.out","w",stdout);
n=read();m=read();k=read();init();
for(int i=1;i<=m;i++) a[i]=read();
for(int i=1;i<n;i++)
{
int u=read(),v=read();
g[u].push_back(v);
g[v].push_back(u);
}
//initialize for all distance
for(int i=1;i<=n;i++) dfs(i,0,i);
//first part:edge of trees
for(int i=1;i<=m;i++) siz[a[i]]=1;
cal(1,0);ans=2*ans%MOD;
int tmp=0;
//second part:DD(XYX) of trees
for(int x=1;x<=m;x++)
for(int y=x+1;y<=m;y++)
{
int cnt=0,i=a[x],j=a[y];
for(int z=1;z<=m;z++) if(z!=x && z!=y)
{
int k=a[z];
if(d[i][k]>d[i][j] || d[j][k]>d[i][j])
continue;
if(d[i][k]==d[i][j] && z<y) continue;
if(d[j][k]==d[i][j] && z<x) continue;
cnt++;
}
tmp+=C[cnt][k-2];
ans=(ans-d[i][j]*C[cnt][k-2]%MOD+MOD)%MOD;
}
ans=ans*qkpow(C[m][k],MOD-2)%MOD;
printf("%lld
",ans);
}
C. 仙人掌
题目描述
给一棵 (n) 个点 (m) 条边的仙人掌,求其邻接矩阵的行列式:
(nleq 10^5,n-1leq mleq 2cdot 10^5)
解法
要把行列式的意义对应在图上,我们可以考虑每个排列 (p) 的贡献。
首先考虑树的情况,因为 (p_i) 互不相同又要 ((i,p_i)) 之间有边,只能是原图中的 ((u,v)) 满足 (p_u=v,p_v=u),也就是直接用边去覆盖这两个点,为了方便我们把它看成特殊的环。
环也可以覆盖环上的点,这时候我们考虑环根选哪条边,对应这两种不同的情况。设环的个数为 (c),我们考虑贡献的系数是 ((-1)^{n-c})
环对应着原排列的置换分解,因为一次交换操作会改变逆序对的奇偶性,而通过 (n-c) 次操作就可以变成 (i=p_i) 的排列,所以 (inv(p)=n-cmod 2)
那么直接上圆方树上 (dp) 即可,对于环先考虑整体选取的情况,然后考虑断掉一条边,去做边覆盖,最后再考虑这条边的影响,时间复杂度 (O(n))
总结
有特殊背景的数学题要思考式子的组合意义,环、排列、逆序对三者的关系也很有意思!
#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 100005;
const int MOD = 993244853;
#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,m,k,Ind,s[M],a[M],low[M],dfn[M],dp[M][2];
vector<int> g[M];
void add(int &x,int y) {x=(x+y)%MOD;}
int sub(int x,int y) {return ((x-y)%MOD+MOD)%MOD;}
void dfs(int u,int fa)
{
s[++k]=u;low[u]=dfn[u]=++Ind;
dp[u][0]=0;dp[u][1]=1;
for(auto v:g[u])
{
if(v^fa && dfn[v]) low[u]=min(low[u],dfn[v]);
if(dfn[v]) continue;
//then v is not vistied
dfs(v,u);
low[u]=min(low[u],low[v]);
if(low[v]<dfn[u]) continue;
//then u is the root
int len=1;a[len]=u;
do a[++len]=s[k];
while(s[k--]^v);
reverse(a+2,a+len+1);//attention the order
if(len==2)//just a edge,particularly consider
{
dp[u][0]=sub(dp[u][0]*dp[v][0],dp[u][1]*dp[v][1]);
dp[u][1]=dp[u][1]*dp[v][0]%MOD;
continue;
}
int nw[2]={MOD-2,0};
for(int i=1;i<=len;i++)//choose the whole circle
nw[0]=nw[0]*dp[a[i]][1]%MOD;
int f[2][2]={},g[2][2]={};
f[0][0]=sub(dp[u][0]*dp[a[2]][0],dp[u][1]*dp[a[2]][1]);
f[0][1]=dp[u][0]*dp[a[2]][1]%MOD;
f[1][0]=dp[u][1]*dp[a[2]][0]%MOD;
f[1][1]=dp[u][1]*dp[a[2]][1]%MOD;
for(int i=3;i<=len;i++)
{
swap(f,g);
for(int s=0;s<2;s++)
{
f[s][0]=f[s][1]=0;
add(f[s][1],g[s][0]*dp[a[i]][1]);
add(f[s][0],g[s][0]*dp[a[i]][0]);
add(f[s][0],MOD-g[s][1]*dp[a[i]][1]%MOD);
}
}
add(nw[1],f[1][0]);
add(nw[0],sub(f[0][0],f[1][1]));
dp[u][0]=nw[0];dp[u][1]=nw[1];
}
}
signed main()
{
freopen("cactus.in","r",stdin);
freopen("cactus.out","w",stdout);
n=read();m=read();
for(int i=1;i<=m;i++)
{
int u=read(),v=read();
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1,0);
if(n&1) dp[1][0]=MOD-dp[1][0];
printf("%lld
",dp[1][0]%MOD);
}
D.黑白棋
题目描述
解法
我们记一个 (k/2) 元组:((b_1-a_1-1,b_2-a_2-1...)),转化得形象一点,每个人可以从 (k/2) 堆石子中操作至多 (m) 堆,每堆至多全部取完,至少取一个。
这是一个 ( t nim-k) 问题,当前状态必败,当且仅当所有 (d_i) 转成二进制之后,每一位 (1) 的个数 (mod (m+1)=0)
证明(其实思路和证明 ( t sg) 函数差不多的):
1、全为 (0) 的局面一定是必败态。
2、对于任意一个必败状态,只能到达必胜状态。因为操作 (m) 个数每个二进制位至多改变 (m) 个,那么后继状态 (mod (m+1) ot=0)
3、对于任意一个必胜状态,存在到达必败状态。可以从高到低位考虑,假设让前面的位合法之后已经改变了 (k) 堆,那么这 (k) 堆可以任取 (0/1),设 (sum) 为剩下堆 (1) 的个数 (mod (m+1)=0)
- 如果 (sumleq m-k),那么直接操作这些堆,把 (1) 拿到,把前面操作过的堆置为 (0)
- 如果 (sum>m-k),那么从 (k) 堆中选取 (m+1-sum) 堆置为 (1),其他置为 (0) 就可以合法。
现在考虑如何计数,直接按二进制位规划,记录用了原序列的多少个格子即可,然后再对起点的方案数计数。具体来说设 (f(i,j)) 为考虑 (i) 个二进制位使用格子数为 (j),每一位 (1) 的个数 (mod(m+1)=0) 的方案数,转移时需要把为 (1) 的二进制位分配到 (k/2) 个距离中。
时间复杂度 (T(n)=sum_{i=0}^{log_2 n}frac{nk}{2^i}=O(nk))
#include <cstdio>
const int MOD = 1e9+7;
const int M = 10005;
#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,k,m,ans,fac[M],inv[M],dp[M];
void init()
{
fac[0]=inv[0]=inv[1]=1;
for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%MOD;
for(int i=2;i<=n;i++) inv[i]=inv[MOD%i]*(MOD-MOD/i)%MOD;
for(int i=2;i<=n;i++) inv[i]=inv[i-1]*inv[i]%MOD;
}
int C(int n,int m)
{
if(n<0 || n<m) return 0;
return fac[n]*inv[m]%MOD*inv[n-m]%MOD;
}
void add(int &x,int y) {x=(x+y)%MOD;}
void sub(int &x,int y) {x=((x-y)%MOD+MOD)%MOD;}
signed main()
{
freopen("chess.in","r",stdin);
freopen("chess.out","w",stdout);
n=read();k=read();m=read();init();
dp[0]=1;ans=C(n,k);n-=k;k/=2;
for(int w=0;w<14;w++)
for(int i=n;i>0;i--)
for(int j=m+1;(1<<w)*j<=i && j<=k;j+=m+1)
add(dp[i],dp[i-(1<<w)*j]*C(k,j));
for(int i=0;i<=n;i++)
sub(ans,dp[i]*C(n-i+k,k));
printf("%lld
",ans);
}