蜜月日记 Part8

· · 休闲·娱乐

前言

继续蜜月日记。

Day 71

给模拟赛准备的。

2024/12/12 自由(Freedom)

题意

给定一个 n 个点 m 条边的有向图,定义路径的点权为经过点的点权的和,边权为经过边的边权的积。求点权和为 V 的所有路径的边权和。提交答案。

思路

Case 1

发现 n,m,V 都很小,设 dp(i,j) 表示点权为 i 结尾为 j 的路径边权和,转移枚举 j 的出边。时间复杂度 O(V(n+m))

Case 2,3

都是 \texttt{K} 开头,数据里 m=n^2,是完全图。不仅如此,每条边的边权还是一样的。

那就不用考虑边不边的了。题意转化成求所有值域在 [1,n] 和为 V 的序列的权值和,若序列长为 t 则序列的权值为 c^{t-1}。在测试点 2,3c=3,7

看着就很可以 dp 啊!设 dp(i) 表示点权和为 i 的答案乘 c,转移为 dp(t)=\sum\limits_{i=1}^nc\times dp(t-a_i)。这是一个常系数线性齐次递推,用普通方法做是 O(W\log W\log V) 的,其中 Wa_i 的最大值。可以通过这两个点。

Case 4

还是边权相同的完全图,但这次 W 很大,普通的常系数线性齐次递推跑不过。这里 c=7

但是观察点权,你发现只有两个值,因此转移形如 dp(t)=r\times dp(t-1)+s\times dp(t-x),其中 r=42c=294,s=38c=266,x=537653823

考虑组合意义,把 ii+x 连边权为 s 的边,向 i+1 连边权为 r 的边,则要求从 0v 所有路径边权积的和,最后乘上 \dfrac 1 c,因为我们把第一个点也当连边算了,但其实它没连。

发现路径边权积只取决于经过了多少条 i\to i+x 的边。我们枚举经过的边数 t,由于 xt\le V,有 t=O(\dfrac V x)

对于一个 t,我们可以求出经过 i\to i+1 的边数为 V-tx,则路径个数为 \dbinom{V-tx+t}{t},权值为 r^{V-tx}s^t

所以答案为 \dfrac 1 c\sum\limits_{t=0}^{\lfloor\frac V x\rfloor}\dbinom{V-tx+t}{t}r^{V-tx}s^t。暴力计算组合数,复杂度为 O\left(\left(\dfrac V x\right)^2\right),大概是 20000^2,能过。

Case 5

进入 \texttt{MP} 开头的点。发现点权全部都是 1,于是询问的其实是经过 V 个点的路径权值和。

这是一个非常经典的模型。设 dp(i,j) 表示经过 i 个点目前到 j 的路径权值和,转移形如乘一个矩阵,可以矩阵快速幂。

值得注意的是,这个矩阵是邻接矩阵,记为 A

最后答案为所有 dp(V,*) 的和,初始 dp(1,*)=1。故答案为 A^{V-1} 所有元素的和。

这个点 n 很小,直接暴力矩阵乘法 O(n^3\log V) 可过。

Case 6

观察图的性质,发现前 n 条边是 1 连出去的,后 n-1 条边从 i 连向 i+1 且边权为 1

所以说邻接矩阵形如(没写出的元素均为 0):

w_1 & w_2 & \cdots & w_n \\ 1 & & & \\ & 1 & & \\ & & \ddots & \\ & & & 1 \end{bmatrix}

其中 w_i1\to i 的边权。这是一个常系数线性齐次递推的矩阵,也就是说问题变成求 x_t=\left\{\begin{matrix} 1 & (t<n)\\ \sum\limits_{i=1}^n w_ix_{t-i}& (t\ge n) \end{matrix}\right. 的第 V-1 项到 n+V-2 项的和。

为了求区间和,我们想求出前缀和。不妨设 x 的生成函数是 F(z),我们来研究前缀和数列的生成函数 G(z)。因为这里有 x 所以形式幂级数用 z

考虑 [z^i]F(z) 的贡献,它会累加到所有 \ge i 的项,相当于这项乘了 1+z+z^2+\cdots=\dfrac 1{1-z}

因此我们有 G(z)=\dfrac{F(z)}{1-z}。由于 F(z) 是有理函数,G(z) 也是有理函数,分子分母次数还是 O(n)。问题转化为求一个有理函数的高次项系数,这可以用 FFT 优化的 Bostan-Mori 算法解决,时间复杂度 O(n\log n\log V)

当然也可以把 G(z) 重新转回常系数线性齐次递推,虽然稍微有些麻烦,不如直接 Bostan-Mori,而且因为 n 很大你是不能 O(n^2) 的 BM 求递推式的。

Case 7

这次的 V 超级大,而且 A^{V-1} 是不能用欧拉定理降幂的。

发现输入中每个点连出去的边很少,再仔细看看发现点 i 只会连向 \ge i 的点,而且必然有一条边权为 i+1 的边连向 i

于是得出这是一个主对角线互不相同的上三角矩阵,而且很稀疏。

再结合我们要把指数从矩阵上变到数上来使用欧拉定理,这启发我们相似对角化。

A=P^{-1}\Lambda P,其中 \Lambda 是把特征值排在主对角线上的对角矩阵,则 A^{V}=P^{-1}\Lambda^V P。发现 PP^{-1} 是不随 V 而改变的,因此答案形如 \sum\limits_{i=1}^n c_i\lambda_i^V,其中 c_i 是常数。根据欧拉定理,V 可以对 998244352 取模。

写出答案的生成函数为 F(x)=\sum\limits_{j=0}^{\infty}x^j\sum\limits_{i=1}^n c_i\lambda_i^j,简单化简得到 F(x)=\sum\limits_{i=1}^n\dfrac{c_i}{1-\lambda_i x}

答案是有理函数,但是带 c,还是不好处理。通分一下,分子变成次数小于 n 的多项式 G(x),那么有 F(x)=\dfrac{G(x)}{\prod\limits_{i=1}^n1-\lambda_i x}

由于 A 稀疏,在不带自环的情况下图的路径数很少,可以暴力求出 F(x) 的前 n 项,然后递推出 G(x)。这样就可以 Bostan-Mori 了。

Case 8

给出的图包括 2i\to 2i-1,2i-1\to 2i,2i-1\to 2i-1,2i-1\to 2i+1 这些边,也就是相邻奇数偶数连双向边,相邻奇数从小向大连,奇数连自环。所有边边权为 1

好像没有什么很好直接算的方法,这个图又很规整,那就递推吧。

[x^V]F_i(x) 表示从 2i-1 出发长为 V 路径个数(即边权和,因为边权为 1)的生成函数,[x^V]G_i(x) 表示从 2i 出发长为 V 路径个数。为了求和,设 H_i(x)=F_i(x)+G_i(x),再设 t=\dfrac n 2

最终答案为 [x^V]\sum\limits_{i=1}^t H_i(x)

先来列递推方程。考虑每个点向外走到哪个点,可以列出:

F_i(x)=x(1+F_{i-1}(x)+F_i(x)+G_i(x)) \\ G_i(x)=x(1+F_{i}(x)) \end{matrix}\right.

解得 F_i(x)=\dfrac{x(F_{i-1}(x)+1+x)}{1-x-x^2}

代入到 H_i(x),得到 H_i(x)=\dfrac{xH_{i-1}(x)+2x}{1-x-x^2},其中 H_0(x)=x

x 看作常数,H 看成数列,则这个递推式是形如 a_i=pa_{i-1}+q 的经典形式,可以化成等比数列求通项。算出来得到:

H_i(x)=\dfrac{x(x^{i}+\dfrac{2(1-x-x^2)^i-2x^i}{1-2x-x^2})}{(1-x-x^2)^i}

接下来要求 \sum\limits_{i=1}^t H_i(x)。把分子拆开,每一项都是等比数列,使用等比数列求和得到:

\sum\limits_{i=1}^t H_i(x)=\dfrac{1+(n-4) x+(1-2 n) x^{2}+(2-n) x^{3}}{(1-2 x-x^{2})^{2}}+\frac{x^{t+2}(1+x)^{2}}{(1-2 x-x^{2})^{2}(1-x-x^{2})^{t}} #### Case $9

是环。设 s 是点权和,那么肯定是先绕环走 \lfloor\dfrac V s\rfloor 圈,然后再走几步。走的步数可以用双指针求,顺便统计贡献。

要用高精除算圈数,时间复杂度 O(n+\log V\log n)。注意圈数在指数上,所以是对 998244352 取模。

Case 10

这个图只有自环。对于点 i,如果 a_i|V,则会做出 w_i^{\frac V{a_i}-1} 的贡献,其中 w_ii\to i 的边权。直接计算复杂度 O(n\log V)

代码

提交答案就不给了。

Day 72

补题。

2024/12/13 [省选联考 2024] 重塑时光

题意

给定 m 个形如“xy 之前”的限制,求有多少种把一个长为 n 的排列分成 k+1 个可空的段的方案,使得存在一种这 k+1 段的排列满足这 m 条限制,以及总方案数。

思路

首先总方案数是 (n+k)!。把 m 条限制建图,肯定是 DAG。

由于排列任意,分割方式也任意,顺序就没那么重要了。可以为空看起来比较烦人,题意转化成把 \{1,2,\cdots,n\} 分成至多 k+1 个不可空的集合。

f'_i 为分割成 i 个集合的方案。它对合法方案数的贡献应该是这 i 个集合重排的 i!、空集合插入的 \dbinom{k+1}{i} 和切 k 刀的顺序 k!。总的贡献是 f'_i i!k!\dbinom{k+1}{i}

问题转化为求 f'_i。发现这个和拓扑序计数很像,而且数据范围不大,考虑状压。设 f(S,i) 表示把集合 S 分成 i 个序列的方案数,注意这里序列内部有序,序列间无序。

首先,由于序列内部有序,可以先预处理出 g(S) 表示把集合 S 重排满足拓扑序的方案数。可以 O(2^n n) 处理。

然后根据 DAG 计数经典技巧,加入一些入度为 0 的点。设 h(S,i) 表示将集合 S 分成 i 个序列的方案数,注意这里序列内部有序,序列间也有序。这是为了方便转移。

然后是 $f$ 的转移。枚举与 $S$ 不交且没有有 $S$ 连向 $T$ 的边的点集 $T$,然后 $\dfrac{f(S,i)h(T,j)(-1)^{j-1}}{j!}\to f(S\cup T,i+j)$。这里分母 $j!$ 是因为 $h$ 有序但 $f$ 无序,分子 $(-1)^{j-1}$ 是因为 $T$ 之外也可能有入度为 $0$ 的点需要容斥掉。 时间复杂度 $O(3^n n^2)$。转移第二维是卷积,维护点值序列可以做到 $O(3^n n)$,但是我不想写。 ### 代码 ```cpp #include<bits/stdc++.h> #define File(a) freopen(#a".in","r",stdin);freopen(#a".out","w",stdout) #define ll long long #define int ll #define F(i,a,b) for(int i(a),i##i##end(b);i<=i##i##end;++i) #define R(i,a,b) for(int i(a),i##i##end(b);i>=i##i##end;--i) using namespace std; const int MAXN=16,MAXS=(1<<15)+10,MOD=1e9+7; int n,m,k,all,pcnt[MAXS]; int in[MAXN],ou[MAXN],In[MAXS],Ou[MAXS]; int h[MAXS][MAXN],f[MAXS][MAXN]; ll g[MAXS],fact[100],inv[100]; inline ll C(int x,int y){ return x>=y?fact[x]*inv[y]%MOD*inv[x-y]%MOD:0; } signed main(){ ios::sync_with_stdio(0); cin.tie(0),cout.tie(0); cin>>n>>m>>k; fact[1]=inv[1]=inv[0]=fact[0]=1; F(i,2,99) fact[i]=fact[i-1]*i%MOD,inv[i]=MOD-MOD/i*inv[MOD%i]%MOD; F(i,2,99) inv[i]=inv[i-1]*inv[i]%MOD; all=(1<<n)-1; F(i,1,all) pcnt[i]=pcnt[i>>1]+(i&1); F(i,1,m){ int u,v; cin>>u>>v; in[v]|=(1<<(u-1)); ou[u]|=(1<<(v-1)); } F(i,1,all) F(j,1,n) if(i>>(j-1)&1) In[i]|=in[j],Ou[i]|=ou[j]; g[0]=1; F(i,1,all) F(j,1,n) if((i>>(j-1)&1)&&!(ou[j]&i)) (g[i]+=g[i^(1<<(j-1))])>=MOD&&(g[i]-=MOD); h[0][0]=1; F(i,1,all) for(int S(i);S;S=(S-1)&i){ int T=i^S; if((Ou[S]&T)||(In[S]&T)) continue; F(j,0,pcnt[T]) h[i][j+1]=(h[i][j+1]+h[T][j]*g[S])%MOD; } f[0][0]=1; F(i,1,all) for(int S(i);S;S=(S-1)&i){ int T=i^S; if(Ou[T]&S) continue; F(j,1,pcnt[i]) F(t,0,j-1){ if((j-t)&1) f[i][j]=(f[i][j]+f[T][t]*h[S][j-t]%MOD*inv[j-t])%MOD; else (f[i][j]=(f[i][j]-f[T][t]*h[S][j-t]%MOD*inv[j-t])%MOD)<0&&(f[i][j]+=MOD); } } int ans=0; F(i,1,k+1) ans=(ans+f[all][i]*fact[i]%MOD*fact[k]%MOD*C(k+1,i))%MOD; cout<<ans*inv[n+k]%MOD; return 0; } ``` ## Day 73 真难吧。 2024/12/19 [[AGC064F] No Permutations](https://atcoder.jp/contests/agc064/tasks/agc064_f) ### 题意 求有多少个长度为 $3n$ 的序列满足以下条件: * 该序列中 $1$ 到 $n$ 恰好各出现了 $3$ 次。 * 该序列中所有长度为 $n$ 的子区间都不是一个 $1\sim n$ 的排列。 答案对 $998244353$ 取模。 ### 思路 搬运一下官方题解做法。 直接做肯定没法做,考虑容斥。 记所有长度为 $n$ 的区间的集合 $R=\{[1,n+1),[2,n+2),\cdots,[2n+1,3n+1)\}$,设 $S\subseteq R$,定义 $f(S)$ 表示 $(-1)^{|S|}$ 乘上满足“$S$ 中任意区间都是 $1\sim n$ 的排列”的方案数。下面如无特殊说明,区间全部左闭右开。 则答案为 $\sum\limits_{S\subseteq R}f(S)$。 当 $S=\varnothing$ 时,$f(S)=\dfrac{(3n)!}{(3!)^n}$,也就是把相同值元素看作不同再除以每种方案重复算的次数。下面只讨论 $S\neq\varnothing$ 的情况。 定义两个区间 $[l,l+n),[r,r+n)$ 相交当且仅当 $|l-r|\le n$,注意 $|l-r|=n$ 的时候也是相交。定义一个连通块是一个区间的集合 $T$,满足对于其中任意两个区间 $a,b$,都存在一个区间的序列 $a=r_1,r_2,\cdots,r_k=b$,其中 $r_i(2\le i\le k)$ 与 $r_{i-1}$ 相交。此时称 $T$ 是连通的,其长度为区间 $\bigcup_{x\in T} x$ 的长度,记为 $\operatorname{len}(T)$。 直接计算所有 $S\subseteq R,S\neq\varnothing$(下记为 $S\in 2^R\setminus\{\varnothing\}$,即 $S$ 是 $R$ 的幂集的元素)的贡献不好算,我们把 $2^R\setminus\{\varnothing\}$ 划分成两个集合分别计算贡献。如无特殊说明,下面默认 $S\in 2^R\setminus\{\varnothing\}$,全集是 $2^R\setminus\{\varnothing\}$。 设 $P\subseteq 2^R$ 是满足以下条件的 $S$ 的集合:存在一个区间 $x\in S$,满足对于所有 $y\in S$,$x,y$ 相交。再设 $Q$ 是 $P$ 的补集。 那么下面就剩下了两个问题:计算 $\sum\limits_{S\in P}f(S)$ 和 $\sum\limits_{S\in Q}f(S)$。 #### 计算 $\sum\limits_{S\in P}f(S)

\operatorname{len}(S)=n+LS 中区间左端点分别为 l_0<l_1<\cdots<l_k,不难发现 l_k=l_0+L,则排列的方案数有以下几部分:

全部乘起来,把容斥系数拆到求积号里面,有 f(S)=-n!\rho(2n-L)\prod\limits_{i=1}^k(-(l_i-l_{i-1})!)

枚举 L 求和得到 \sum\limits_{S\in P}f(S)=-n!\sum\limits_{L=0}^{2n}\rho(2n-L)\sum\limits_{S\in P,\operatorname{len}(S)=n+L}\prod\limits_{i=1}^k(-(l_i-l_{i-1})!)

问题转化为求 \sum\limits_{S\in P,\operatorname{len}(S)=n+L}\prod\limits_{i=1}^k(-(l_i-l_{i-1})!)

观察发现 l_i-l_{i-1} 是一组 L 的拆分,可以用生成函数描述。但是生成函数只是无脑加起来,并不能满足 S\in P 的限制。所以我们考虑放宽限制。

P' 表示连通的 S 的集合。容易得到 P\subseteq P'。因此要求 \sum\limits_{S\in P',\operatorname{len}(S)=n+L}\prod\limits_{i=1}^k(-(l_i-l_{i-1})!)-\sum\limits_{S\in P'\setminus P,\operatorname{len}(S)=n+L}\prod\limits_{i=1}^k(-(l_i-l_{i-1})!)

先来解决式子的第一项。正如刚刚说的,求积号里是一个拆分,可以用生成函数描述。

w(L)=[x^L]\dfrac{1}{1+\sum\limits_{i=1}^n i!x^i},则 \sum\limits_{S\in P',\operatorname{len}(S)=n+L}\prod\limits_{i=1}^k(-(l_i-l_{i-1})!)=(2n-L+1)w(L)。这里 2n-L+1l_0 的选法。

w(1)\sim w(2n) 可以用多项式求逆 O(n\log n) 求出来。

接下来就是本题最难的部分,求 \sum\limits_{S\in P'\setminus P,\operatorname{len}(S)=n+L}\prod\limits_{i=1}^k(-(l_i-l_{i-1})!)

我们先考虑 S\in P'\setminus P 长什么样子。设 S 最左边的区间是 [u,u+n),最右边是 [v,v+n)。你发现,由于每个区间长为 n,因此任意一个 S 中区间都会和 [u,u+n),[v,v+n) 中恰好一个相交。这是因为如果和两个相交就属于 P 了,和两个都不相交长度就不可能到达 n

于是,设和 [u,u+n) 相交的区间的集合是 U,和 [v,v+n) 相交的区间的集合是 V。显然,U,V 都是连通的。U 的长度是 n+x,区间左端点是 u=u_0<u_1<\cdots<u_p=u+xV 的长度是 n+y,区间左端点是 v=v_0<v_1<\cdots<v_q=v+xzU,V 的交的长度,即 z=(u_p+n)-v_0。则,x+y-z+n=L,x\in[0,n),y\in[0,n),z\in(0,\min\{x,y\})。不妨令 (x,y,z) 的三元组的集合是 A,即 A=\{(x,y,z)\in\mathbb{Z}^3|x\in[0,n),y\in[0,n),z\in[0,\min\{x,y\})\}。三元组中 z 是最小的。

所以有 \sum\limits_{S\in P'\setminus P,\operatorname{len}(S)=n+L}\prod\limits_{i=1}^k(-(l_i-l_{i-1})!)=\sum\limits_{S\in P'\setminus P,\operatorname{len}(S)=n+L}(v_0-u_p)!\prod\limits_{i=1}^p(-(u_i-u_{i-1})!)\prod\limits_{i=1}^q(-(v_i-v_{i-1})!)。这里前面没有负号是因为 U,V 的负号消掉了。

类似于上面的讨论,我们有该式等于 \sum\limits_{S\in P'\setminus P,\operatorname{len}(S)=n+L}(n-z)!w(x)w(y)

枚举 S 变为枚举 (x,y,z),得到该式等于 (2n-L+1)\sum\limits_{(x,y,z)\in A,x+y-z+n=L}(n-z)!w(x)w(y)2n-L+1l_0 的选法。

不妨设 B_L=-\sum\limits_{(x,y,z)\in A,x+y-z=L-n}(n-z)!w(x)w(y)。我们需要快速求出 B_0\sim B_{n}。这里前面的负号是为了把之前的式子前面的减变成加。

考虑分治。设 solve(l,r) 表示将满足 x,y,z\in[l,r) 的所有 (x,y,z)\in A 的贡献加进 B_{x+y-z},再设 m=\lfloor\dfrac{l+r}{2}\rfloor。一共有三种贡献:

时间复杂度 O(n\log^2 n)

计算 \sum\limits_{S\in Q}f(S)

类似于上面的讨论,设 S 最左边的区间是 [u,u+n),最右边是 [v,v+n),任意一个 S 中区间都会和 [u,u+n),[v,v+n) 中恰好一个相交。设和 [u,u+n) 相交的区间的集合是 U,和 [v,v+n) 相交的区间的集合是 V。显然,U,V 都是连通的。U 的长度是 n+x,区间左端点是 u=u_0<u_1<\cdots<u_p=u+xV 的长度是 n+y,区间左端点是 v=v_0<v_1<\cdots<v_q=v+xzU,V 的距离,即 z=v-u-n

排列的方案数有以下几部分:

拆一下容斥系数,有 f(S)=n!(n-x)!(n-y)!\prod\limits_{i=1}^p(-(u_i-u_{i-1})!)\prod\limits_{j=1}^q(-(v_j-v_{j-1})!)

枚举 S 变成枚举 (x,y,z),结合 Q 的定义,我们有 \sum\limits_{S\in Q}f(S)=n!\sum\limits_{z=1}^n(n-z+1)\sum\limits_{x=0}^{z-1}\sum\limits_{y=0}^{z-1}w(x)w(y)(n-x)!(n-y)!

化简即为 \sum\limits_{S\in Q}f(S)=n!\sum\limits_{z=1}^n(n-z+1)\left(\sum\limits_{i=0}^{z-1}w(i)(n-i)!\right)^2n-z+1 是选择 u_0

可以前缀和优化做到 O(n) 计算。

综上,我们以 O(n\log^2 n) 的时间复杂度解决了这个问题。

代码

#include<bits/stdc++.h>
#define File(a) freopen(#a".in","r",stdin);freopen(#a".out","w",stdout)
#define ll long long
#define F(i,a,b) for(int i(a),i##i##end(b);i<=i##i##end;++i)
#define R(i,a,b) for(int i(a),i##i##end(b);i>=i##i##end;--i)
#define vi vector<int>
using namespace std;
const int MAXN=1<<20,MOD=998244353,INV2=499122177,INV6=166374059,G=3;
inline ll qpow(ll base,int expo){
    ll res(1);
    while(expo){
        (expo&1)&&(res=res*base%MOD);
        base=base*base%MOD,expo>>=1;
    }
    return res;
}
const int INVG=qpow(G,MOD-2);
int gpow[21],invgpow[21];
inline void calc(){
    F(i,1,20) gpow[i]=qpow(G,(MOD-1)>>i),invgpow[i]=qpow(INVG,(MOD-1)>>i);
    return;
}
inline void meow(int&t){
    t<0&&(t+=MOD);
    t>=MOD&&(t-=MOD);
    return;
}
int rev[MAXN];
inline void NTT(int*poly,int len,bool inv){
    F(i,0,len-1) (i<rev[i])&&(swap(poly[i],poly[rev[i]]),1);
    static ll g[MAXN];
    g[0]=1;
    for(int i(1),expo(1);i<len;i<<=1,++expo){
        ll omega=inv?invgpow[expo]:gpow[expo];
        F(j,1,i-1) g[j]=g[j-1]*omega%MOD;
        for(int j(0);j<len;j+=(i<<1)) F(k,0,i-1){
            int&x(poly[j|k]),&y(poly[i|j|k]);
            ll qwq(g[k]*y%MOD);
            y=x-qwq;
            y<0&&(y+=MOD);
            x+=qwq;
            x>=MOD&&(x-=MOD);
        }
    }
    if(inv){
        ll invl=qpow(len,MOD-2);
        F(i,0,len-1) poly[i]=poly[i]*invl%MOD;
    }
    return;
}
inline void build_rev(int n){
    F(i,0,n-1) rev[i]=(rev[i>>1]>>1)|((i&1)?(n>>1):0);
    return;
}
struct poly{
    int num[MAXN]={};
    int len=0;

    inline void resize(const int a){
        for(;len>a;--len) num[len]=0;
        len=a;
        if(len<0) len=0;
        return;
    }
    inline poly operator+(const poly a)const{
        poly res;
        res.len=max(a.len,len);
        F(i,0,res.len) res.num[i]=num[i]+a.num[i],meow(res.num[i]);
        return res;
    }
    inline poly operator+(const int a)const{
        poly res=*this;
        int&qwq(res.num[0]);
        qwq+=a;
        meow(qwq);
        return res;
    }
    inline poly operator-(const poly a)const{
        return a*(-1)+*this;
    }
    inline poly operator-(const int a)const{
        return *this+(-a);
    }
    inline poly operator*(const poly a)const{
        poly x,y=*this;
        if(a.len*1ll*len<=1e5){
            x.len=a.len+len;
            F(i,0,len) F(j,0,a.len){
                int&qwq(x.num[i+j]);
                qwq=(qwq+a.num[j]*1ll*num[i])%MOD;
            }
            return x;
        }
        x=a;
        int expo=max(__lg(((len+a.len+1)<<1)+1)+1,1),l=1<<expo;
        build_rev(l);
        NTT(x.num,l,0);
        NTT(y.num,l,0);
        F(i,0,l-1) x.num[i]=1ll*x.num[i]*y.num[i]%MOD;
        NTT(x.num,l,1);
        x.resize(len+a.len);
        return x;
    }
    inline poly operator*(const int a)const{
        poly res=*this;
        F(i,0,len){
            int&qwq(res.num[i]);
            qwq=a*1ll*qwq%MOD;
            meow(qwq);
        }
        return res;
    }
    inline poly inv(){
        poly res;
        res.num[0]=qpow(num[0],MOD-2);
        for(int l(2),expo(1);l<(len<<1);l<<=1,++expo){
            int tmp[MAXN]={};
            F(i,0,(l<<1)-1) rev[i]=(rev[i>>1]>>1)|((i&1)<<expo);
            memcpy(tmp,num,sizeof(int)*l);
            NTT(tmp,l<<1,0);
            NTT(res.num,l<<1,0);
            F(i,0,(l<<1)-1){
                int&qwq(res.num[i]),t(2-1ll*qwq*tmp[i]%MOD);
                meow(t);
                qwq=1ll*qwq*t%MOD;
            }
            NTT(res.num,l<<1,1);
            F(i,l,(l<<1)-1) res.num[i]=0;
        }
        res.resize(len);
        return res;
    }
};
int px[MAXN],py[MAXN];
inline vi mul(const vi&x,const vi&y){
    int n=x.size()+y.size()-1;
    n=1<<(__lg(n)+1);
    build_rev(n);
    F(i,0,x.size()-1) px[i]=x[i];
    F(i,x.size(),n-1) px[i]=0;
    F(i,0,y.size()-1) py[i]=y[i];
    F(i,y.size(),n-1) py[i]=0;
    NTT(px,n,0),NTT(py,n,0);
    F(i,0,n-1) px[i]=px[i]*1ll*py[i]%MOD;
    NTT(px,n,1);
    vi res(x.size()+y.size()-1);
    F(i,0,res.size()-1) res[i]=px[i];
    return res;
}
int n;
ll fact[MAXN],B[MAXN];
poly w;
inline ll rho(int L){
    return fact[2*n-L]*qpow(INV2,max(n-L,0))%MOD;
}
inline void m_le_xy(int l,int m,int r){
    vector<int>x(r-l+1),y(r-l+1);
    F(i,m,r-1) (x[i-l]+=w.num[i])>=MOD&&(x[i-l]-=MOD);
    F(i,l,m-1) y[r-i]=fact[n-i];
    x=mul(x,x);
    x=mul(x,y);
    F(i,max(r-2*l,0),x.size()-1){
        int qwq=i-r+l*2;
        if(qwq>n) break;
        (B[qwq]+=x[i])>=MOD&&(B[qwq]-=MOD);
    }
    return;
}
inline void x_le_m_le_y(int l,int m,int r){
    vector<int>x(r-l+1),y(r-l+1);
    F(i,l,m-1) (x[i-l]+=w.num[i])>=MOD&&(x[i-l]-=MOD);
    F(i,l,m-1) y[r-i]=fact[n-i];
    x=mul(x,y);
    F(i,0,min(int(x.size()-1),r-l)) x[i]=0;
    F(i,0,r-l) y[i]=0;
    F(i,m,r-1) y[i-l]=w.num[i];
    x=mul(x,y);
    F(i,max(r-2*l,0),x.size()-1){
        int qwq=i-r+l*2;
        if(qwq>n) break;
        (B[qwq]+=x[i])>=MOD&&(B[qwq]-=MOD);
        (B[qwq]+=x[i])>=MOD&&(B[qwq]-=MOD);
    }
    return;
}
void solve(int l,int r){
    if(r-l<=1) return;
    int m=(l+r)>>1;
    solve(l,m),solve(m,r);
    m_le_xy(l,m,r);
    x_le_m_le_y(l,m,r);
    return;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    calc();
    cin>>n;
    fact[0]=1;
    F(i,1,3*n) fact[i]=fact[i-1]*i%MOD;
    F(i,0,n) w.num[i]=fact[i];
    w.len=2*n+1;
    w=w.inv();
    int ans=fact[3*n]*qpow(INV6,n)%MOD;
    F(L,0,2*n) ans=(ans+rho(L)*(MOD-fact[n])%MOD*(2*n-L+1)%MOD*w.num[L])%MOD;//S in P'
    solve(0,n);
    F(L,n,2*n) ans=(ans+rho(L)*(MOD-fact[n])%MOD*(2*n-L+1)%MOD*B[L-n])%MOD;//S in P'\P
    ll sum=0;
    F(z,1,n){//S in Q
        sum=(sum+w.num[z-1]*fact[n-z+1])%MOD;
        ans=(ans+fact[n]*(n-z+1)%MOD*sum%MOD*sum)%MOD;
    }
    cout<<ans;
    return 0;
}

Day 74

感觉自己非古典概型的部分都很烂。

2024/12/27 [ZJOI2015] 地震后的幻想乡

题意

给定一张 n 个点 m 条边的图,边权是 [0,1] 中均匀随机的实数,求最小生成树最大边边权的期望。

思路

原问题等价于每条边出现的时间是边权,求图第一次连通的期望时间。

设第一次联通的期望时间为随机变量 X,所求为 E(X)

根据经典的拆贡献结论, E(x)=\int_0^1P(X\ge t)\mathrm{d}t,就是把离散情况的求和改成了连续情况的积分。

虽然这里带等号,但其实 P(x\ge t)=P(x>t),也就是要求 t 时刻仍然不连通的概率之和。

由于 n 很小,考虑状压。设 P_S(t) 表示点集 St 时刻不连通的概率,其中 1\in S。由于一个点始终连通,初值为 P_{\{1\}}(t)=0

e(S,T) 表示点集 S,T 之间的边数。转移比较简单,枚举 S 中包含 1 的极大连通块 T,钦定 TS\setminus T 没有连边。其中 T 连通的概率是 1-P_T(t),没有连边的概率是 (1-t)^{e(T,S\setminus T)}。于是有:

P_S(t)=\sum\limits_{T\subsetneq S,1\in T}(1-P_T(t))(1-t)^{e(T,S\setminus T)}

U=\{1,2,\cdots,n\},答案为 \int_0^1 P_U(t)\mathrm d t。这个式子里带积分,所以上面的转移最好也要有积分。

转移式两边同时求 0\sim 1 的定积分,得到:

\int_0^1 P_S(t)\mathrm d t=\int_0^1\sum\limits_{T\subsetneq S,1\in T}(1-P_T(t))(1-t)^{e(T,S\setminus T)}\mathrm d t

根据积分的线性性拆一下得到:

\int_0^1 P_S(t)\mathrm d t=\sum\limits_{T\subseteq S,1\in T}(\int_0^1(1-t)^{e(T,S\setminus T)}\mathrm d t-\int_0^1P_T(t)(1-t)^{e(T,S\setminus T)}\mathrm d t)

第一项定积分直接积出来:

\int_0^1 P_S(t)\mathrm d t=\sum\limits_{T\subsetneq S,1\in T}(\dfrac{1}{1+e(T,S\setminus T)}-\int_0^1P_T(t)(1-t)^{e(T,S\setminus T)}\mathrm d t)

第二项积分没法化简了,我们继续 dp。设 f(S,k)=\int_0^1 P_S(t)(1-t)^k\mathrm d t

代入上面的递推式得:

f(S,k)=\sum_{T\subsetneq S,1\in T}(\dfrac{1}{1+e(T,S\setminus T)+k}-\int_0^1(1-t)^{k+e(T,S\setminus T)} P_{T}(t) \mathrm{d} t)

f(S,k)=\sum_{T\subsetneq S,1\in T}(\dfrac{1}{1+e(T,S\setminus T)+k}-f(T,k+e(T,S\setminus T)))

然后就可以转移了,时间复杂度 O(3^nm)

代码

#include<bits/stdc++.h>
#define File(a) freopen(#a".in","r",stdin);freopen(#a".out","w",stdout)
#define ll long long
#define F(i,a,b) for(int i(a),i##i##end(b);i<=i##i##end;++i)
#define R(i,a,b) for(int i(a),i##i##end(b);i>=i##i##end;--i)
using namespace std;
const int MAXN=11,MAXS=(1<<10)+10;
int n,m,U,con[MAXN],Con[MAXS];
double f[MAXS][MAXN*MAXN];
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;
    U=(1<<n)-1;
    F(i,1,m){
        int u,v;
        cin>>u>>v;
        con[u]|=(1<<(v-1));
        con[v]|=(1<<(u-1));
    }
    F(S,3,U) if(S&1) for(int T=S&(S-1);T;T=(T-1)&S) if(T&1){
        int e=0;
        F(i,0,n-1) if((S>>i&1)&&!(T>>i&1)) e+=__builtin_popcount(con[i+1]&T);
        F(i,0,m-e) f[S][i]+=1/(i+e+1.0)-f[T][i+e];
    }
    cout<<fixed<<setprecision(6)<<f[U][0];
    return 0;
}

Day 75

感觉是一个很典的过程。

2024/12/28 [BJOI2018] 双人猜数游戏

题意

Alice 知道 mn,Bob 知道 m+n,两人都知道 s\le m\le n。从某个人开始,两人轮流说了一共 t 次不知道后均猜出 m,n 的值。求一组合法的 m,n

思路

这种题都是用“不知道”来缩小解的集合,直到只存在一组解。

这种东西看着就很可以递推。毕竟肯定是从“如果 (x,y) 是解,那对方应该早就知道了”推理出“(x,y) 不是解”,再推出“(p,q) 是解”。这种人的推理过程中本身就蕴含了递推。

所以设 dp(i,j,k) 表示如果答案是 (j,k),先知道答案的人在第 i 次回答时是否知道。下面来研究转移。

首先,如果在上一次回答就知道了,那么这一次也肯定知道。即,若 dp(i-2,j,k)=1,则 dp(i,j,k)=1

然后,就是看对方是否只有一组解是不确定的。具体地,若第 i-1 轮是 Alice 回答,满足 dp(i-1,j,k)=0,且对于任意 x,y\ge s,xy=jk,(x,y)\neq(j,k),都有 dp(i-1,x,y)=1,则 dp(i,j,k)=1;若第 i-1 轮是 Bob 回答,满足 dp(i-1,j,k)=0,且对于任意 x,y\ge s,x+y=j+k,(x,y)\neq(j,k),都有 dp(i-1,x,y)=1,则 dp(i,j,k)=1

如此便能确定先知道答案的人的解了。接下来需要注意,一个人知道了,另一个人必须在下一回合也知道,否则就再也无法知道了。根据和上面类似的方式判一下就可以得到答案了。

感觉一下,t 很小,答案大概不会很大。

时间复杂度 O(s^3t),常数小,甚至不需要提交答案。

代码

#include<bits/stdc++.h>
#define File(a) freopen(#a".in","r",stdin);freopen(#a".out","w",stdout)
#define ll long long
#define F(i,a,b) for(int i(a),i##i##end(b);i<=i##i##end;++i)
#define R(i,a,b) for(int i(a),i##i##end(b);i>=i##i##end;--i)
using namespace std;
const int MAXN=501;
bool dp[16][MAXN][MAXN];
int s,t;
char na[10];
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>s>>na>>t;
    bool now=(na[0]=='A');
    F(i,0,t){
        F(j,s,MAXN-1) F(k,j,MAXN-1){
            if(i>=2&&dp[i-2][j][k]) dp[i][j][k]=1;
            else{
                bool qwq=(i==0||!dp[i-1][j][k]);
                if(now) for(int x=s;qwq&&x*x<=j*k;++x) qwq&=((j*k%x)||x==j||(i&&dp[i-1][x][j*k/x]));
                else for(int x=s;qwq&&x+x<=j+k;++x) qwq&=(x==j||(i&&dp[i-1][x][j+k-x]));
                dp[i][j][k]=qwq;
            }
        }
        now^=1;
    }
    F(sum,s+s,MAXN*2) F(m,s,sum-s){
        int n=sum-m;
        if(m>n) break;
        if(!dp[t][m][n]||dp[t-1][m][n]||dp[t-2][m][n]) continue;
        bool qwq=1;
        if(now) for(int x=s;qwq&&x*x<=m*n;++x) qwq&=((m*n%x)||x==m||!dp[t][x][m*n/x]||dp[t-2][x][m*n/x]);
        else for(int x=s;qwq&&x*x<=m*n;++x) qwq&=(x==m||!dp[t][x][m+n-x]||dp[t-2][x][m+n-x]);
        if(qwq) return cout<<m<<" "<<n,0;
    }
    return 0;
}

Day 76

新年第一天,祭祀保平安。

2025/01/01 [CTSC2008] 祭祀

题意

给定一个 n 个点 m 条边的 DAG,求其最长反链大小,构造方案,并求出所有可能在最长反链中的点。

最长反链定义为一个点集满足任意两个点 u,v 都不能从 u 到达 v

思路

问题转化为求最小链覆盖。

可重比较麻烦,所以先用传递闭包求出两点之间的可达性,然后建 n 个点的新图,i,j 连有向边当且仅当 i 可以到达 j

然后新图上的最小不可重路径覆盖就是原图上的最小链覆盖了。

最小不可重路径覆盖是一个经典问题。建二分图,左部 i 向右部 j 连边当且仅当新图中 ij 连边。这个二分图的点数减最大匹配就是答案。

感性理解,初始时每个点都有一条路径,匹配中的一条边就是把两个路径首尾相接变成一个。

然后回答第三问,判断一个点是否能够在最长反链中。这个也简单,删掉能到达这个点的点、这个点本身和这个点能到达的点,强制让这个点在最长反链中,然后求剩下的点构成的图的最长反链大小,如果是第一问的答案减一就可以。

第二问可以用第三问搞。每次选择一个剩下的能够在最长反链中的点,然后把能到达这个点的点、这个点本身和这个点能到达的点删掉,重复这个操作就行。

使用匈牙利算法,时间复杂度 O(n^4),根本跑不满。

代码

#include<bits/stdc++.h>
#define File(a) freopen(#a".in","r",stdin);freopen(#a".out","w",stdout)
#define ll long long
#define F(i,a,b) for(int i(a),i##i##end(b);i<=i##i##end;++i)
#define R(i,a,b) for(int i(a),i##i##end(b);i>=i##i##end;--i)
using namespace std;
const int MAXN=101;
int n,m,match[MAXN];
bool reach[MAXN][MAXN];
vector<int>g[MAXN],gt[MAXN];
bool vis[MAXN],del[MAXN];
inline void bfs(int bg,bool type=1){
    if(type) memset(del,0,sizeof(bool)*(n+1));
    queue<int>q;
    q.push(bg),del[bg]=1;
    while(!q.empty()){
        int now=q.front();
        q.pop();
        for(int i:g[now]) if(!del[i]) del[i]=1,q.push(i);
    }
    q.push(bg);
    while(!q.empty()){
        int now=q.front();
        q.pop();
        for(int i:gt[now]) if(!del[i]) del[i]=1,q.push(i);
    }
    return;
}
bool xyl(int now){
    if(vis[now]) return 0;
    vis[now]=1;
    for(int i:gt[now]) if(!del[i]&&(!match[i]||xyl(match[i]))){
        match[i]=now;
        return 1;
    }
    return 0;
}
int ans;
bool q2[MAXN],q3[MAXN];
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;
    F(i,1,m){
        int u,v;
        cin>>u>>v;
        reach[u][v]=1;
    }
    F(k,1,n) F(i,1,n) F(j,1,n) reach[i][j]|=reach[i][k]&reach[k][j];
    F(i,1,n) F(j,1,n) if(reach[i][j]) g[i].push_back(j),gt[j].push_back(i);
    F(i,1,n){
        memset(vis,0,sizeof(bool)*(n+1));
        ans+=xyl(i);
    }
    cout<<n-ans<<"\n";
    F(i,1,n){
        int all=0,qwq=0;
        bfs(i);
        memset(match,0,sizeof(int)*(n+1));
        F(j,1,n) if(!del[j]){
            memset(vis,0,sizeof(bool)*(n+1));
            ++all,qwq+=xyl(j);
        }
        if(all-qwq==n-ans-1) q3[i]=1;
    }
    memset(del,0,sizeof(bool)*(n+1));
    F(i,1,n) if(q3[i]&&!del[i]) q2[i]=1,bfs(i,0);
    F(i,1,n) cout<<q2[i];
    cout<<"\n";
    F(i,1,n) cout<<q3[i];
    return 0;
}

Day 77

神秘乱搞题。

2024/01/04 [BJOI2014] 想法

题意

初始有 m 个集合,第 i 个包含元素 i。进行 n-m 次操作,每次合并两个集合成第 i+m 个集合并允许一定误差地查询集合大小。

思路

允许误差考虑乱搞。

给第 i 个元素赋随机 [0,1] 实数权,维护每个集合的最小值。

这个最小值的期望是集合大小加 1 分之 1,多跑几次求平均值。

好简洁啊。

代码

#include<bits/stdc++.h>
#define File(a) freopen(#a".in","r",stdin);freopen(#a".out","w",stdout)
#define ll long long
#define F(i,a,b) for(int i(a),i##i##end(b);i<=i##i##end;++i)
#define R(i,a,b) for(int i(a),i##i##end(b);i>=i##i##end;--i)
using namespace std;
const int MAXN=1e6+1,CNT=200;
const ll RANGE=1e18;
int n,m,u[MAXN],v[MAXN];
double val[MAXN],ans[MAXN];
mt19937 gen(time(0)^*new int);
uniform_real_distribution<double>rnd(0,1);
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;
    F(i,m+1,n) cin>>u[i]>>v[i];
    F(T,1,CNT){
        F(i,1,m) val[i]=rnd(gen);
        F(i,m+1,n) val[i]=min(val[u[i]],val[v[i]]),ans[i]+=val[i];
    }
    F(i,m+1,n) ans[i]/=CNT;
    F(i,m+1,n) cout<<int(1/ans[i]-0.5)<<"\n";
    return 0;
}

Day 78

AT 的图论计数题都好厉害。

2025/01/06 [AGC065F] Always Perfect

题意

求出点数为 N 且所有生成树都存在完美匹配的有标号无向连通图的数量,对大质数 M 取模。

思路

这种条件非常复杂的题目,先研究如何判定。

对一个合法的图的每个点双考虑。设 S_u 表示 u 和点双外部挂在 u 上的点的个数。

容易发现,对于所有点双中的点 u,我们有 S_u 的奇偶性相同。否则,我们必然可以找到两个点 u,v,其中 2|S_u,2\nmid S_v,满足 u,v 相邻。此时 u 已经和外面某个挂的点匹配了,v 没有。让生成树里保留边 (u,v)v 就永远匹配不了。

如果所有 S_u 都是偶数,所有点都和外面挂着的某个点匹配,没有限制。

否则,所有点都在点双内匹配。首先点数是偶数,这是容易的。然后每个点度数 \le2,下面使用反证证明。假设 u 连了 v_1,v_2,v_3,考虑这样两棵生成树:连 (u,v_1)v_1,v_2 用不经过 u 的路径与 v_3 连通;然后断开 (u,v_1),连 (u,v_2)。如果这两个都有完美匹配,代表以 v_3 为根时 v_1,v_2 都在自己的子树内部匹配。于是断开 v_1,v_2 连向父亲的边,连 (u,v_1),(u,v_2),(u,v_3),这个生成树没法把 u,v_1,v_2 都匹配掉,矛盾。

综上所述,如果 S_u 都是奇数,这个点双一定是偶环或者两点一边。称这两个结构是一个新点。

根据上面的讨论,可以使用如下方式不重不漏构造出所有合法的图:初始时是若干新点;每次选择 \ge2 个连通块,每个连通块中选出一个点,连成点双;连通时停止。

这个图在新点的意义下是一个有若干点双的有标号连通图。

终于可以开始计数了。先计算 n 个点 m 个点双的连通图数量 f(n,m)n 个点连通图数量 g(n)。注意区分 n,N

先是 g(n) 的转移。可以写暴力多项式求 \ln,也可以容斥。具体地,枚举包含节点 1 的极大连通块,剩下的随意连边,有:

g(n)=2^{\binom{n}2}-\sum\limits_{i=1}^{n-1}\dbinom{n-1}{i-1}2^{\binom{n-i}2}g_i

其中 i 是极大连通块大小,\dbinom{n-1}{i-1} 是给连通块里的点分配标号,2 的幂是内部随意连边的方案数。

$$f(n,1)=g(n)-\sum\limits_{i=2}^{n-1}f(n,i)$$ 接下来是 $f(n,m)$,其中 $m\ge 2$。对圆方树计数,钦定 $1$ 是根,其中 $n$ 个圆点 $m$ 个方点,设第 $i$ 个方点的儿子数是 $d_i$。把方点和其儿子视为一个整体,连成一棵树。如果现不考虑方点的存在,就是典中典 $m+1$ 个大小分别为 $1,d_1,\cdots,d_m$ 的连通块加边连成树,Prüfer 序列得到方案数是 $n^{m-1}\prod_{i=1}^m d_i$。再把方点考虑进来,最后都是方点连向父亲,每个连通块里是哪个点连的都不重要,每种方案被重复算了 $\prod\limits_{i=1}^m d_i$,除掉之后就是 $n^{m-1}$。乘上方点内部的方案数和分配标号的方案数,除掉方点无标号导致多算的系数,有:$f(n,m)=\dfrac{n^{m-1}}{m!}\sum\limits_{d_1+\cdots+d_m=n-1}\dbinom{n-1}{d_1,\cdots,d_m}\prod\limits_{i=1}^m f(d_i+1,1)=\dfrac{n^{m-1}(n-1)!}{m!}\sum\limits_{d_1+\cdots+d_m=n-1}\prod\limits_{i=1}^m\dfrac{f(d_i+1,1)}{d_i!}$。 后面是典型的背包卷积,于是设 $h(n,m)=\sum\limits_{d_1+\cdots+d_m=n-1}\prod\limits_{i=1}^m\dfrac{f(d_i+1,1)}{d_i!}$。注意到 $N\ge 2$,故方点都有儿子,即 $d_i\ge 1$。有转移 $h(n,m)=\sum\limits_{d_m=1}^{n-1}\dfrac{h(n-d_m,m-1)f(d_m+1,1)}{d_m!}$,整理下: $$h(n,m)=\sum\limits_{d_m=1}^{n-1} h(n-d_m,m-1)h(d_m,1)$$ 代回 $f$ 的转移: $$f(n,m)=\dfrac{n^{m-1}(n-1)!h(n,m)}{m!}$$ 最后统计答案。枚举初始放入的新点个数 $x$,连成的点双个数 $y$,第 $i$ 个新点的大小 $s_i$,然后式子和前面计算 $f$ 的差不多,就是总点数变成 $N$ 了。具体地,此时的贡献为 $\dfrac{N^{y-1}(x-1)!h(x,y)\prod_{i=1}^x s_i}{y!}$。 然后再做一个 $\sum\limits_{i=1}^x s_i=N$ 的背包就可以得到结果了。具体地,设 $H(n,s)$ 表示当 $x=n,\sum\limits_{i=1}^x s_i=s$ 的 $\prod_{i=1}^x s_i$,则转移为: $$H(n,s)=\sum\limits_{s_n=2}^{s}[2|s_n]s_n H(n-1,s-s_n)\dbinom{s-1}{s_n-1}G(s_n)$$ 初值 $H(0,0)=1$。 其中 $[2|s_n]$ 是因为新点大小一定是偶数(偶环或两个点)。$G(x)$ 是 $x$ 个点的新点方案数,$x=2$ 是 $1$,否则是 $\dfrac{(x-1)!}2$。组合数是分配标号。 最后答案为: $$G(N)+\sum\limits_{x=1}^N\sum\limits_{y=1}^{x-1}\dfrac{N^{y-1}(x-1)!h(x,y)H(x,N)}{y!}$$ 后半部分是 $\ge 2$ 个新点,前面是一个新点。 看起来或许可以用神秘的在线或半在线卷积做到 $\overset{\sim}{O}(n^2)$,不过暴力做时间复杂度 $O(n^3)$ 已经足够了。 一定一定想好转移顺序再写。 ### 代码 ```cpp #include<bits/stdc++.h> #define File(a) freopen(#a".in","r",stdin);freopen(#a".out","w",stdout) #define ll long long #define F(i,a,b) for(int i(a),i##i##end(b);i<=i##i##end;++i) #define R(i,a,b) for(int i(a),i##i##end(b);i>=i##i##end;--i) using namespace std; const int MAXN=505; int N,MOD; inline ll qpow(ll base,int expo){ ll res(1); while(expo){ (expo&1)&&(res=res*base%MOD); base=base*base%MOD,expo>>=1; } return res; } ll fact[MAXN],inv[MAXN]; inline ll C(int x,int y){ return x>=y?fact[x]*inv[y]%MOD*inv[x-y]%MOD:0; } inline ll G(int x){ return x==2?1:fact[x-1]*inv[2]%MOD; } ll f[MAXN][MAXN],g[MAXN],h[MAXN][MAXN],H[MAXN][MAXN]; signed main(){ ios::sync_with_stdio(0); cin.tie(0),cout.tie(0); cin>>N>>MOD; fact[0]=1; F(i,1,N) fact[i]=fact[i-1]*i%MOD; inv[N]=qpow(fact[N],MOD-2); R(i,N,1) inv[i-1]=inv[i]*i%MOD; g[1]=1; F(n,2,N){ g[n]=qpow(2,n*(n-1)/2); F(i,1,n-1) g[n]=(g[n]-C(n-1,i-1)*qpow(2,(n-i)*(n-i-1)/2)%MOD*g[i]%MOD+MOD)%MOD; } F(n,2,N){ F(m,2,n-1) f[n][m]=qpow(n,m-1)*fact[n-1]%MOD*h[n][m]%MOD*inv[m]%MOD; f[n][1]=g[n]; F(i,2,n) (f[n][1]-=f[n][i])<0&&(f[n][1]+=MOD); h[n][1]=f[n][1]*inv[n-1]%MOD; F(m,2,N) F(dm,1,n-1) h[n+1][m]=(h[n+1][m]+h[n+1-dm][m-1]*h[dm+1][1])%MOD; } H[0][0]=1; F(n,1,N) F(s,1,N) if(!(s&1)) F(sn,2,s) if(!(sn&1)) H[n][s]=(H[n][s]+sn*H[n-1][s-sn]%MOD*C(s-1,sn-1)%MOD*G(sn))%MOD; ll ans=G(N); F(x,1,N) F(y,1,x-1) ans=(ans+qpow(N,y-1)*fact[x-1]%MOD*h[x][y]%MOD*H[x][N]%MOD*inv[y])%MOD; cout<<ans; return 0; } ``` ## Day 79 异常简单的科技飞升做法。 2025/01/07 [[ABC269Ex] Antichain](https://atcoder.jp/contests/abc269/tasks/abc269_h) ### 题意 给定一棵 $n$ 个节点的树,对每个 $k=1,2,\cdots n$ 求出树上大小为 $k$ 的点集个数,满足点集中每个点都不是其他点的祖先。答案对 $998244353$ 取模。 ### 思路 成分非常显然,模数 $998244353$、求 $1\sim n$ 所有答案、$n\le 2\times 10^5$ 时限 8s,肯定是多项式没跑。 如果直接设某个点子树内大小为某个数的点集个数,转移是卷积,复杂度是 $O(n^2)$ 的,应该需要一些神秘的轻重链分治的技巧优化到 $\overset{\sim}{O}(n)$,而且 $\log n$ 的次数可能还有点高。 考虑静态 top tree。设 $dp(i,j,0/1)$ 表示在簇 $i$ 中 $j$ 个点点集的个数,满足簇路径(连接上下界点的路径)上无/有点集中的点。第三维是因为 Compress $u,v$ 两个簇时,$u$ 簇路径上的点是 $v$ 中点的祖先。根据经典 trick,这里钦定上界点不选。 设簇 $y$ 合并到 $x$ 得到 $z$,下面讨论转移。 对于 Rake 节点,转移为: $$dp(z,t,0)=\sum\limits_{i+j=t}dp(x,i,0)(dp(y,j,0)+dp(y,j,1))$$ $$dp(z,t,1)=\sum\limits_{i+j=t}dp(x,i,1)(dp(y,j,0)+dp(y,j,1))$$ 对于 Compress 节点: $$dp(z,t,0)=\sum\limits_{i+j=t}dp(x,i,0)dp(y,j,0)$$ $$dp(z,t,1)=dp(x,t,1)+\sum\limits_{i+j=t}dp(x,i,0)dp(y,j,1)$$ 每层总共要做长度为 $n$ 的卷积,一共 $O(\log n)$ 层,时间复杂度 $O(n\log^2 n)$。 记得在最后把上界点的贡献补进去。 ### 代码 ```cpp #include<bits/stdc++.h> #define File(a) freopen(#a".in","r",stdin);freopen(#a".out","w",stdout) #define ll long long #define F(i,a,b) for(int i(a),i##i##end(b);i<=i##i##end;++i) #define R(i,a,b) for(int i(a),i##i##end(b);i>=i##i##end;--i) using namespace std; const int MAXN=(1<<19)+5,MOD=998244353,G=3; namespace Polynomial{ inline ll qpow(ll base,int expo){ ll res(1); while(expo){ (expo&1)&&(res=res*base%MOD); base=base*base%MOD,expo>>=1; } return res; } const int INVG=qpow(G,MOD-2); int gpow[20][MAXN],invgpow[20][MAXN]; inline void init(){ F(i,1,19){ gpow[i][0]=invgpow[i][0]=1; gpow[i][1]=qpow(G,(MOD-1)>>i),invgpow[i][1]=qpow(INVG,(MOD-1)>>i); ll g=gpow[i][1],invg=invgpow[i][1]; F(j,2,524287) gpow[i][j]=gpow[i][j-1]*g%MOD,invgpow[i][j]=invgpow[i][j-1]*invg%MOD; } } int rev[MAXN]; inline void NTT(int*poly,int len,bool inv){ F(i,0,len-1) (i<rev[i])&&(swap(poly[i],poly[rev[i]]),1); for(int i(1),expo(1);i<len;i<<=1,++expo) for(int j=0;j<len;j+=(i<<1)) F(k,0,i-1){ int&x(poly[j|k]),&y(poly[i|j|k]); ll qwq=(inv?invgpow[expo][k]:gpow[expo][k])*1ll*y%MOD; (y=x-qwq)<0&&(y+=MOD); (x+=qwq)>=MOD&&(x-=MOD); } if(inv){ ll invl=qpow(len,MOD-2); F(i,0,len-1) poly[i]=poly[i]*invl%MOD; } return; } struct Poly{ vector<int>v; Poly operator+(const Poly&x)const{ Poly res; if(v.size()>x.v.size()){ res.v=v; F(i,0,x.v.size()-1) (res.v[i]+=x.v[i])>=MOD&&(res.v[i]-=MOD); }else{ res.v=x.v; F(i,0,v.size()-1) (res.v[i]+=v[i])>=MOD&&(res.v[i]-=MOD); } return res; } Poly operator*(const Poly&x)const{ int n=v.size()+x.v.size()-1; static int px[MAXN],py[MAXN]; n=1<<(__lg(n)+1); F(i,0,n-1) rev[i]=(rev[i>>1]>>1)|((i&1)?(n>>1):0); F(i,0,v.size()-1) px[i]=v[i]; F(i,v.size(),n-1) px[i]=0; F(i,0,x.v.size()-1) py[i]=x.v[i]; F(i,x.v.size(),n-1) py[i]=0; NTT(px,n,0),NTT(py,n,0); F(i,0,n-1) px[i]=px[i]*1ll*py[i]%MOD; NTT(px,n,1); Poly res; res.v.resize(v.size()+x.v.size()-1); F(i,0,res.v.size()-1) res.v[i]=px[i]; return res; } }; } using namespace Polynomial; int n,fa[MAXN]; vector<int>g[MAXN]; namespace TopTree{ int cnt; struct Node{ short type;//-1:unit;0:rake;1:compress int up,dn,siz,lc,rc,fa; Poly dp[2]; Node(int t=-1,int a=0,int b=0,int c=1,int x=0,int y=0):type(t),up(a),dn(b),siz(c),lc(x),rc(y),fa(0){} }tr[MAXN<<2]; inline void upd(int now){ Node&qwq(tr[now]),&l(tr[qwq.lc]),&r(tr[qwq.rc]); if(qwq.type){ qwq.dp[0]=l.dp[0]*r.dp[0]; qwq.dp[1]=l.dp[1]+l.dp[0]*r.dp[1]; }else{ Poly qaq=r.dp[0]+r.dp[1]; qwq.dp[0]=l.dp[0]*qaq; qwq.dp[1]=l.dp[1]*qaq; } return; } inline int merge(int x,int y,bool type){ tr[x].fa=tr[y].fa=++cnt; tr[cnt]=Node(type,tr[x].up,tr[type?y:x].dn,tr[x].siz+tr[y].siz,x,y); return upd(cnt),cnt; } int siz[MAXN],son[MAXN]; void dfs1(int now){ siz[now]=1; for(int i:g[now]){ dfs1(i); tr[i]=Node(-1,now,i); tr[i].dp[0].v={1},tr[i].dp[1].v={0,1}; siz[now]+=siz[i]; siz[son[now]]<siz[i]&&(son[now]=i); } return; } #define Poi vector<int>::iterator int build(Poi l,Poi r,bool type){ if(l==r) return 0; if(l+1==r) return *l; int all=0,sum=0; for(auto it=l;it!=r;++it) all+=tr[*it].siz; Poi mid=l+1; for(auto it=l;it!=r;++it){ sum+=tr[*it].siz; if(sum*2<=all) mid=it+1; else break; } return merge(build(l,mid,type),build(mid,r,type),type); } int rt[MAXN]; void dfs2(int now,bool heavy){ if(son[now]) dfs2(son[now],1); for(int i:g[now]) if(i!=son[now]) dfs2(i,0); if(!heavy){ vector<int>chain; if(now!=1) chain.push_back(now); for(int i=son[now];i;i=son[i]){ vector<int>sub({i}); for(int j:g[fa[i]]) if(j!=i) sub.push_back(rt[j]); chain.push_back(build(sub.begin(),sub.end(),0)); } rt[now]=build(chain.begin(),chain.end(),1); } return; } } using namespace TopTree; int ans[MAXN]; signed main(){ ios::sync_with_stdio(0); cin.tie(0),cout.tie(0); init(); cin>>n; F(i,2,n) cin>>fa[i],g[fa[i]].push_back(i); dfs1(1); cnt=n; dfs2(1,0); tr[rt[1]].dp[0].v.resize(n+1),tr[rt[1]].dp[1].v.resize(n+1); F(i,1,n) (ans[i]=tr[rt[1]].dp[0].v[i]+tr[rt[1]].dp[1].v[i])>=MOD&&(ans[i]-=MOD); (++ans[1])>=MOD&&(ans[1]-=MOD); F(i,1,n) cout<<ans[i]<<"\n"; return 0; } ``` ## Day 80 原定 Part8 最后一题被放到 Day 71 了,所以可能这道题不是很大,但是还挺难的(两处转化想不到啊 😭)。 2025/01/10 [[ARC147F] Again ABC String](https://atcoder.jp/contests/arc147/tasks/arc147_f) ### 题意 $T$ 组询问,给定 $n,x,y,z$,求满足以下条件的字符串数量对 $2$ 取模的值: * 长度为 $n$。 * 仅包含 `A`、`B` 和 `C`。 * 对于长为 $i$ 的前缀,设其中 `A`、`B`、`C` 的个数分别为 $a_i,b_i,c_i$,则 $\forall 1\le i\le n$ 满足: * $a_i-b_i\le x

思路

首先是一个我不知道怎么想到的转化。设 m=x+y+z+3,原题意等价于有一个长为 m 的环,点顺时针编号 0,1,\cdots,m,初始时有三个人分别在 0,x+1,x+y+2 号点,每一时刻可以选择一个人顺时针走一步,要求任意两个人走的过程中都不重合,求到 n 时刻的合法移动方式的方案数,不同定义为存在一个时刻走的人不同。

还是不好做,那就容斥,总方案数 3^n,计算有重合的方案数。那么容斥系数是什么?

这个容斥系数感觉重合次数感觉有关系,但这样看来更麻烦了。答案模 2 还没有用上,怎么用?

发现第一次两个人重合后,接下来无论方案是什么,我总能交换两个人走的时刻得到另一种方案,除非接下来这两个人不走。也就是说,只要这两个人走了,答案就是偶数,不必(注意不是不能)计算。

因此,只有两个人第一次重合后不走的方案才有贡献。“第一次”有点麻烦,放宽到第 n 时刻重合,反正多出来的方案是偶数不会产生贡献。

减掉两两重合的情况,你发现三个重合的情况被多减了 2,但是模 2 把这一项模没了,所以答案是两两重合的方案数的奇偶性异或起来再异或 1,因为 3^n 是奇数。

接下来就是要计算两两重合的方案了。不妨讨论是第一个人和第二个人,其他情况把讨论的 x 换成 y,z 即可。

这两个人具体在哪个点我们不关心,我们只关心差。发现每一时刻差都可以 +1,-1 或不变,初始差为 x+1,最终差为 0,能用生成函数描述,即方案数为 [t^{x+1}]((t^{-1}+1+t)^n\bmod (t^m-1))。由于用过 x,z 了所以形式幂级数用 t 表示,模 (t^m-1) 是因为这是个环所以是循环卷积。

接下来是第二个我不会的转化。我们有经典结论(真的经典吗我没见过啊):(t^{-1}+1+t)^{2^k}\equiv t^{-2^k}+1+t^{2^k}\pmod 2

使用归纳证明。k=0 时显然满足,假设 k=K 满足,则 (t^{-1}+1+t)^{2^{K+1}}=\left((t^{-1}+1+t)^{2^K}\right)^2\equiv(t^{-2^K}+1+t^{2^K})^2\pmod2。根据三项式定理 (a+b+c)^2=a^2+b^2+c^2+2(ab+ac+bc),后面的交叉项全部被模 2 消掉了,只剩下平方项,因此有 (t^{-1}+1+t)^{2^{K+1}}\equiv t^{-2^{K+1}}+1+t^{2^{K+1}}\pmod2,即 k=K+1 时也成立。

n 二进制下第 i 位是 1 记为 i\in n,则答案为 [t^{x+1}](\prod\limits_{i\in n}(t^{-2^i}+1+t^{2^i})\bmod (t^m-1))。这是 O(\log n)O(1) 项的多项式循环卷积,直接暴力复杂度是 O(m\log n) 的。

发现把循环卷积去掉,对答案有贡献的项只有 O(\dfrac n m) 个。我们希望能 O(\log n) 求出第 e\equiv x+1\pmod m 项的系数。

为了去掉负指数,把多项式乘上 t^n,则每个多项式形如 1+x^{2^i}+x^{2^{i+1}}。这相当于背包问题。所以把 e 二进制拆分做数位 dp,设 dp(i,0/1) 表示考虑选取物品大小之和从小往大第 i 位,第 i 位选择/没有选择大小为 2^{i+1} 的物品。转移就是看第 i 位选什么能凑到和 e 的第 i 位相同。这样的时间复杂度是 O(\dfrac n m\log n)

根号分治,m\le \sqrt n 时跑 O(m\log n) 的做法,否则跑 O(\dfrac n m\log n) 的做法,总的时间复杂度为 O(T\sqrt n\log n)

代码

#include<bits/stdc++.h>
#define File(a) freopen(#a".in","r",stdin);freopen(#a".out","w",stdout)
#define ll long long
#define F(i,a,b) for(int i(a),i##i##end(b);i<=i##i##end;++i)
#define R(i,a,b) for(int i(a),i##i##end(b);i>=i##i##end;--i)
using namespace std;
const int MAXN=31,MAXM=50000;
ll n,m;
bool ans;
namespace Small{
    bool poly[MAXM],qwq[MAXM];
    inline void main(ll x){
        memset(poly,0,sizeof(bool)*m);
        poly[0]=1;
        F(i,0,MAXN-1) if(n>>i&1){
            ll e=(1<<i)%m;
            memcpy(qwq,poly,sizeof(bool)*m);
            F(j,0,m-1) if(poly[j]) qwq[(j+m-e)%m]^=1,qwq[(j+e)%m]^=1;
            swap(qwq,poly);
        } 
        ans^=poly[(x+1)%m];
        return;
    }
}
namespace Large{
    bool dp[MAXN][2]={};
    inline void getdp(ll e){
        if(n&1){
            if(e&1) dp[0][0]=1,dp[0][1]=0;
            else dp[0][0]=1,dp[0][1]=1;
        }else{
            if(e&1) dp[0][0]=0;
            else dp[0][0]=1;
            dp[0][1]=0;
        }
        F(i,1,MAXN-1){
            if(n>>i&1){
                if(e>>i&1){
                    dp[i][0]=dp[i-1][0]^dp[i-1][1];
                    dp[i][1]=dp[i-1][1];
                }else{
                    dp[i][0]=dp[i-1][0];
                    dp[i][1]=dp[i-1][0]^dp[i-1][1];
                }
            }else{
                if(e>>i&1) dp[i][0]=dp[i-1][1];
                else dp[i][0]=dp[i-1][0];
                dp[i][1]=0;
            }
        }
        ans^=dp[MAXN-1][0];
        return;
    }
    inline void main(ll x){
        ll rem=(x+1+n)%m;
        for(ll e=rem;e<=2*n;e+=m) getdp(e);
        return;
    }
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    int T;
    for(cin>>T;T;--T){
        ll x,y,z;
        cin>>n>>x>>y>>z;
        m=x+y+z+3;
        ans=1;
        if(m*m<=n) Small::main(x),Small::main(y),Small::main(z);
        else Large::main(x),Large::main(y),Large::main(z);
        cout<<ans<<"\n";
    }
    return 0;
}

后记

在集训快走的时候完成了蜜月日记 Part8。我还挺希望写到 Day 100 的,希望自己能坚持到那个时候。