专栏文章

蜜月日记 Part8

休闲·娱乐参与者 1已保存评论 0

文章操作

快速查看文章及其快照的属性,并进行相关操作。

当前评论
0 条
当前快照
1 份
快照标识符
@miqtsscu
此快照首次捕获于
2025/12/04 10:37
3 个月前
此快照最后确认于
2025/12/04 10:37
3 个月前
查看原文

前言

继续蜜月日记。

Day 71

给模拟赛准备的。

题意

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

思路

Case 11

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

Case 2,32,3

都是 K\texttt{K} 开头,数据里 m=n2m=n^2,是完全图。不仅如此,每条边的边权还是一样的。
那就不用考虑边不边的了。题意转化成求所有值域在 [1,n][1,n] 和为 VV 的序列的权值和,若序列长为 tt 则序列的权值为 ct1c^{t-1}。在测试点 2,32,3c=3,7c=3,7
看着就很可以 dp 啊!设 dp(i)dp(i) 表示点权和为 ii 的答案乘 cc,转移为 dp(t)=i=1nc×dp(tai)dp(t)=\sum\limits_{i=1}^nc\times dp(t-a_i)。这是一个常系数线性齐次递推,用普通方法做是 O(WlogWlogV)O(W\log W\log V) 的,其中 WWaia_i 的最大值。可以通过这两个点。

Case 44

还是边权相同的完全图,但这次 WW 很大,普通的常系数线性齐次递推跑不过。这里 c=7c=7
但是观察点权,你发现只有两个值,因此转移形如 dp(t)=r×dp(t1)+s×dp(tx)dp(t)=r\times dp(t-1)+s\times dp(t-x),其中 r=42c=294,s=38c=266,x=537653823r=42c=294,s=38c=266,x=537653823
考虑组合意义,把 iii+xi+x 连边权为 ss 的边,向 i+1i+1 连边权为 rr 的边,则要求从 00vv 所有路径边权积的和,最后乘上 1c\dfrac 1 c,因为我们把第一个点也当连边算了,但其实它没连。
发现路径边权积只取决于经过了多少条 ii+xi\to i+x 的边。我们枚举经过的边数 tt,由于 xtVxt\le V,有 t=O(Vx)t=O(\dfrac V x)
对于一个 tt,我们可以求出经过 ii+1i\to i+1 的边数为 VtxV-tx,则路径个数为 (Vtx+tt)\dbinom{V-tx+t}{t},权值为 rVtxstr^{V-tx}s^t
所以答案为 1ct=0Vx(Vtx+tt)rVtxst\dfrac 1 c\sum\limits_{t=0}^{\lfloor\frac V x\rfloor}\dbinom{V-tx+t}{t}r^{V-tx}s^t。暴力计算组合数,复杂度为 O((Vx)2)O\left(\left(\dfrac V x\right)^2\right),大概是 20000220000^2,能过。

Case 55

进入 MP\texttt{MP} 开头的点。发现点权全部都是 11,于是询问的其实是经过 VV 个点的路径权值和。
这是一个非常经典的模型。设 dp(i,j)dp(i,j) 表示经过 ii 个点目前到 jj 的路径权值和,转移形如乘一个矩阵,可以矩阵快速幂。
值得注意的是,这个矩阵是邻接矩阵,记为 AA
最后答案为所有 dp(V,)dp(V,*) 的和,初始 dp(1,)=1dp(1,*)=1。故答案为 AV1A^{V-1} 所有元素的和。
这个点 nn 很小,直接暴力矩阵乘法 O(n3logV)O(n^3\log V) 可过。

Case 66

观察图的性质,发现前 nn 条边是 11 连出去的,后 n1n-1 条边从 ii 连向 i+1i+1 且边权为 11
所以说邻接矩阵形如(没写出的元素均为 00):
w_1 & w_2 & \cdots & w_n \\ 1 & & & \\ & 1 & & \\ & & \ddots & \\ & & & 1 \end{bmatrix}$$ 其中 $w_i$ 是 $1\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$。发现 $P$ 和 $P^{-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)$。 先来列递推方程。考虑每个点向外走到哪个点,可以列出: $$\left\{\begin{matrix} 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}}$$ $V$ 很大,肯定还是找循环节。但是我不想找循环节,一般循环节都是 $p,p-1,p+1$ 这种东西乘上一个系数,所以我猜循环节是 $5!\times (p-1)p(p+1)$,然后 Bostan-Mori 就行,复杂度 $O(n\log n\log p)$。 #### 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_i$ 是 $i\to i$ 的边权。直接计算复杂度 $O(n\log V)$。 ### 代码 提交答案就不给了。 ## Day 72 补题。 2024/12/13 [[省选联考 2024] 重塑时光](https://www.luogu.com.cn/problem/P10221) ### 题意 给定 $m$ 个形如“$x$ 在 $y$ 之前”的限制,求有多少种把一个长为 $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$ 个序列的方案数,注意这里序列内部有序,序列间也有序。这是为了方便转移。 $h$ 的转移非常简单,枚举一个与 $S$ 不交且与 $S$ 没有连边的点集 $T$,然后 $h(S,i)g(T)\to h(S\cup T,i+1)$。使用子集枚举,时间复杂度 $O(3^n n)$。 然后是 $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+L$,$S$ 中区间左端点分别为 $l_0<l_1<\cdots<l_k$,不难发现 $l_k=l_0+L$,则排列的方案数有以下几部分: * $[l_0,l_0+n)$ 是 $1\sim n$ 的排列,方案数 $n!$。 * $[l_0+n,l_1+n)$ 是区间 $[l_0,l_1)$ 的排列,方案数 $(l_1-l_0)!$。 * $[l_1+n,l_2+n)$ 是区间 $[l_1,l_2)$ 的排列,方案数 $(l_2-l_1)!$。 * 依此类推,$[l_{k-1}+n,l_k+n)$ 是区间 $[l_{k-1},l_k)$ 的排列,方案数 $(l_k-l_{k-1})!$。 * 不被任何区间包含的 $2n-L$ 个元素,方案数 $\rho(2n-L)=\dfrac{(2n-L)!}{2^{\max(n-L,0)}}$。这是因为总共有 $(2n-L)!$ 种方案,其中有 $\max(n-L,0)$ 对相同元素会算重。 全部乘起来,把容斥系数拆到求积号里面,有 $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+1$ 是 $l_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+x$;$V$ 的长度是 $n+y$,区间左端点是 $v=v_0<v_1<\cdots<v_q=v+x$;$z$ 是 $U,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+1$ 是 $l_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$。一共有三种贡献: * $x,y,z<m$ 或 $x,y,z\ge m$ 时,可以递归下去。 * $z<m\le\min\{x,y\}$ 时,$x,y$ 可以任取 $[m,r)$ 的值,与 $z$ 无关。我们要对于每个 $L$ 求出 $\sum\limits_{x+y-z=L-n}[z\in[l,m)][x\in[m,r)][y\in[m,r)](n-z)!w(x)w(y)$。可以先把 $x,y$ 做和卷积,再把结果和 $z$ 做差卷积,用 NTT 做到 $O((r-l)\log(r-l))$。 * $z\le\min\{x,y\}<m\le\max\{x,y\}$ 时,由于 $x,y$ 是对称的,不妨令 $x<y$ ,答案乘 $2$ 即可,这是因为 $x=y$ 必不可能满足 $\min\{x,y\}<m\le\max\{x,y\}$。因此有 $z\le x<m\le y$,先把 $x,z$ 做差卷积,再和 $y$ 做和卷积即可 $O((r-l)\log(r-l))$。 时间复杂度 $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+x$;$V$ 的长度是 $n+y$,区间左端点是 $v=v_0<v_1<\cdots<v_q=v+x$;$z$ 是 $U,V$ 的距离,即 $z=v-u-n$。 排列的方案数有以下几部分: * 不被 $[u,u+n),[v,v+n)$ 包含的部分,一共 $n!$ 种方案。 * $[u,u+n)$ 包含的部分: * $[u_p,u+n)$ 是不在 $[u+n,u+n+x)$ 元素的排列,方案数 $(n-x)!$。 * $[u_{p-1},u_p)$ 是 $[u_{p-1}+n,u_p+n)$ 元素的排列,方案数 $(u_p-u_{p-1})!$。 * 依此类推,$[u_0,u_1)$ 是区间 $[u_0+n,u_1+n)$ 的排列,方案数 $(u_1-u_0)!$。 * $[v,v+n)$ 包含的部分,类似地,方案数 $(n-y)!\prod\limits_{i=1}^q(v_i-v_{i-1})!$。 拆一下容斥系数,有 $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)^2$。$n-z+1$ 是选择 $u_0$。 可以前缀和优化做到 $O(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) #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] 地震后的幻想乡](https://www.luogu.com.cn/problem/P3343) ### 题意 给定一张 $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)$ 表示点集 $S$ 在 $t$ 时刻不连通的概率,其中 $1\in S$。由于一个点始终连通,初值为 $P_{\{1\}}(t)=0$。 设 $e(S,T)$ 表示点集 $S,T$ 之间的边数。转移比较简单,枚举 $S$ 中包含 $1$ 的极大连通块 $T$,钦定 $T$ 和 $S\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)$。 ### 代码 ```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=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] 双人猜数游戏](https://www.luogu.com.cn/problem/P4459) ### 题意 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)$,常数小,甚至不需要提交答案。 ### 代码 ```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=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] 祭祀](https://www.luogu.com.cn/problem/P4298) ### 题意 给定一个 $n$ 个点 $m$ 条边的 DAG,求其最长反链大小,构造方案,并求出所有可能在最长反链中的点。 最长反链定义为一个点集满足任意两个点 $u,v$ 都不能从 $u$ 到达 $v$。 ### 思路 问题转化为求最小链覆盖。 可重比较麻烦,所以先用传递闭包求出两点之间的可达性,然后建 $n$ 个点的新图,$i,j$ 连有向边当且仅当 $i$ 可以到达 $j$。 然后新图上的最小不可重路径覆盖就是原图上的最小链覆盖了。 最小不可重路径覆盖是一个经典问题。建二分图,左部 $i$ 向右部 $j$ 连边当且仅当新图中 $i$ 向 $j$ 连边。这个二分图的点数减最大匹配就是答案。 感性理解,初始时每个点都有一条路径,匹配中的一条边就是把两个路径首尾相接变成一个。 然后回答第三问,判断一个点是否能够在最长反链中。这个也简单,删掉能到达这个点的点、这个点本身和这个点能到达的点,强制让这个点在最长反链中,然后求剩下的点构成的图的最长反链大小,如果是第一问的答案减一就可以。 第二问可以用第三问搞。每次选择一个剩下的能够在最长反链中的点,然后把能到达这个点的点、这个点本身和这个点能到达的点删掉,重复这个操作就行。 使用匈牙利算法,时间复杂度 $O(n^4)$,根本跑不满。 ### 代码 ```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=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] 想法](https://www.luogu.com.cn/problem/P4581) ### 题意 初始有 $m$ 个集合,第 $i$ 个包含元素 $i$。进行 $n-m$ 次操作,每次合并两个集合成第 $i+m$ 个集合并允许一定误差地查询集合大小。 ### 思路 允许误差考虑乱搞。 给第 $i$ 个元素赋随机 $[0,1]$ 实数权,维护每个集合的最小值。 这个最小值的期望是集合大小加 $1$ 分之 $1$,多跑几次求平均值。 好简洁啊。 ### 代码 ```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=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](https://atcoder.jp/contests/agc065/tasks/agc065_f) ### 题意 求出点数为 $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)$ 也可以容斥出来,即: $$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$ * $b_i-c_i\le y$ * $c_i-a_i\le z$ ### 思路 首先是一个我不知道怎么想到的转化。设 $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)$。 ### 代码 ```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=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 的,希望自己能坚持到那个时候。

评论

0 条评论,欢迎与作者交流。

正在加载评论...