zhylj 的博客 / zh-CN Wed, 21 Jul 2021 11:41:00 +0800 Wed, 21 Jul 2021 11:41:00 +0800 「Codeforces 1110G」Tree-Tac-Toe /index.php/archives/52/ /index.php/archives/52/ Wed, 21 Jul 2021 11:41:00 +0800 zhylj 题目

在一个 $n$ 个点的树上玩井字棋,白先手黑后手,谁先拥有一个相同颜色的长度为三的简单路径则赢。初始有一些点已经被染白。

求是否先手必胜 / 后手必胜 / 必平。

$1\le n\le 5\times 10^5$.

分析

(图片均来自 CF 题解)

我们可以把已染白的节点作如下替换:

那么,若 $A$ 被染白,则后手必然要染 $B$(否则后手就输了),然后先后手都不会去染 $C,D$ 了。

然后此时仍然是白先手,但 $A$ 已经被染白了。

然后我们只需要解决没有染色的问题了,考虑若一个节点 $u$ 满足 $\deg_u \ge 4$,那么先手可以先染这个节点,那么后手是永远堵不上的,所以先手必胜。

同样的,若存在 $u$ 使得 $\deg_u = 3$ 且 $u$ 有超过 $2$ 个非叶节点作为相邻的点,那么先手必胜(先手先染 $u$,再染一个相邻的非叶节点)。

那么,情况就只剩下一条链,只有两端可能出现度数为 $3$ 的节点:

容易发现,若此时度数为 $3$ 的节点个数小于 $2$,则必平,否则考虑链上节点个数的奇偶性:若中间有奇数个节点,那么我们每次可以在距离端点奇数的地方染色,然后最后就剩下如下图二的情况,先手必胜。否则容易证明必平。

然后就做完了,时间复杂度 $\mathcal O(n)$.

]]>
0 /index.php/archives/52/#comments /index.php/feed/archives/52/
「做题记录」FJ 省队集训笔记 /index.php/archives/51/ /index.php/archives/51/ Tue, 13 Jul 2021 19:44:00 +0800 zhylj 前言

其实是讲后补题笔记,所以会有些出入。

Day 1

AGC 039E Pairing Points

考虑和 $1$ 相连的点 $i$,则其他点跨过 $1\to i$ 这条线的必然互不相交。设 $f(l,r,i)$ 表示只考虑 $[l,r]$ 中的点,其中 $i$ 向区间外连边,其余点两两连边的方案数,则转移可以考虑枚举第一条跨过 $1\to i$ 的边 $j\to k$ 和与其相交的区间 $[l,x],[y,r]$,那么有:

$$ f(l, r, i) = \sum_{l\le j\le x < i < y\le k\le r, A_{j,k}=1} f(l, x, j) f(x+1,y-1,i) f(y,r,k) $$

稍微调换一下循环顺序,则可以得到:

$$ \sum_{l\le x < i < y\le r} f(x+1,y-1,i) \sum_{j\le x < y\le k, A_{j,k}=1} f(l, x, j) f(y, r, k) $$

后面那项只和 $l,r,x,y$ 有关,设:

$$ \begin{aligned} g(l,x,y,r) &= \sum_{j\le x < y\le k, A_{j,k}=1} f(l, x, j) f(y, r, k) \\ &= \sum_{j\le x} f(l,x,j)\sum_{y\le k,A_{j,k}=1} f(y,r,k) \end{aligned} $$

设 $h(y,r,j) = \sum_{y\le k,A_{j,k} = 1} f(y,r,k)$.

于是通通暴力算,然后复杂度就变成 $\mathcal O(n^5)$ 啦。

AGC 049D Convex Sequence

注意到相当于差分单调不降,考虑枚举第一个最小值的位置 $p$.

于是存在三个操作:选择一个 $i<p$,令 $A_i,A_{i-1},\cdots,A_1$ 分别加上 $1,2,\cdots,i$(差分前缀减),以及选择一个 $i>p$,令 $A_i, A_{i+1},\cdots,A_n$ 加上 $1,2,\cdots,i$(差分后缀加),以及全局 $+1$.

注意到操作 $1$ 必然对 $p-1$ 位置做一次(否则就不是第一个最小值),故 $p$ 只有 $\mathcal O(\sqrt m)$ 种可能取值,然后之后的操作只有 $\mathcal O(\sqrt m)$ 种,所以可以直接做完全背包。

换最小值的位置时只有 $\mathcal O(1)$ 个物品改变,总的时间复杂度可以做到 $\mathcal O(m\sqrt m)$.

ARC 101F Robots and Exits

显然一个机器人只可能到其左右两边最近的出口(对于两边的只有一个可能出口的机器人可以忽略)。

记它到左边的距离为 $a_i$,到右边的距离为 $b_i$,把机器人抽象成点 $(a_i - 0.5,b_i - 0.5)$,则我们相当于画一条折线代表向左移动距离和向右移动距离的历史最大值的变化,那么若某次折线穿过 $x=x_i$ 或 $y=y_i$ 则代表一个机器人到了一个出口。

那么折线上方的就是到了左边的出口,而折线下方就是到了右边的出口。

考虑若某个点被选取了,则其右下角的所有点显然不会再被选取了,于是按 $a_i$ 排序后实际上就是一个上升子序列计数,容易做到 $\mathcal O(n\log n)$.

AGC 046D Secret Passage

显然,若我们有一个合法的最终串 $T$,使得 $S$ 有一个最长的后缀 $S'$ 是 $T$ 的子序列,则存在一个方案使得我们不需要对 $S'$ 作任何修改。

设 $f(i,j,k)$ 表示 $\operatorname{suf}(S,i)$ 插入 $j$ 个 $0$、$k$ 个 $1$ 能得到的不同的字符串的个数。

为了避免重复计算,我们要求 $S'$ 必须是 $T$ 的第一个子序列,那么 $S_x',S_{x+1}'$ 之间就插入的数就必须与 $S_{x+1}'$ 不同。

同时,我们也不能插入某个数,使得 $T$ 存在一个更长的 $S$ 的后缀作为子序列。

然后我们可以容易写出 $f(i,j,k)$ 的转移方程,再设 $g(i,j,k)$ 表示 $\operatorname{pre}(S,i)$ 是否可以拿出 $j$ 个 $0$、$k$ 个 $1$,暴力转移即可做到 $\mathcal O(n^3)$.

AGC 041D Problem Scores

(参考了洛谷上的题解)

显然条件满足的充要条件是是取尽可能大、且不重合的大小分别为 $k+1$,$k$ 的前后缀满足条件(即 $k = \lfloor (n-1)/2\rfloor$)。

前两个条件相当于:给定一个初始全为 $n$ 的序列 $A_i$,每次选择一个前缀减一。显然若设每个前缀被减一的次数 $k_i$,$\{k_i\}$ 和合法序列一一对应。

再记 $x$ 表示前 $k + 1$ 个数与后 $k$ 个数之差,则每个操作造成的 $\Delta x = -1,-2,\cdots,-k,-k-1,-k,\cdots,-2,-1$。

做个背包就好了,$\mathcal O(n^2)$.

AGC 024F Simple Subsequence Problem

我们将对每个可能的 $t$ 求出其为 $S$ 中多少个串的子序列。

直接暴力是 $\mathcal O(4^nn)$ 的,我们考虑匹配过程中实际上我们只关心剩下还未匹配的串长什么样,而不关心已经匹配的串长什么样,于是设:$f(t_p,s_s)$ 表示已经确定了 $t$ 的前若干位 $t_p$,$s$ 还剩下后缀 $s_s$ 未匹配的 $s$ 的个数。转移时枚举 $t_p$ 的下一个位置是 $0/1$,或者直接停止匹配(把 $s_s$ 变为空串 $\varepsilon$)。

注意到 $|t_p| + |s_s| \le n$,所以总共有 $2^nn$ 个状态(还需压一个分界线)。每次转移是 $\mathcal O(1)$ 的,所以总的时间复杂度是 $\mathcal O(2^nn)$ 的.

AGC 040E Prefix Suffix Addition

容易通过在序列前后加 $0$ 的方式变为区间加单增 / 单减的序列,假如我们能确定最后每个位置加的是 $A_i=b_i+c_i$($b_i$ 为操作 $1$ 加上的,$c_i$ 为操作 $2$ 加上的),那么显然所需要的最少操作次数为(不妨令 $x_0=x_{n+1} = 0$):

$$ \sum_{i=1}^n ([b_i>b_{i+1}]+[c_i<c_{i+1}]) $$

设 $f(i,j)$ 表示考虑了前 $i$ 个数,$b_i=j$ 时,$\sum_{k=1}^{i-1} ([b_k>b_{k+1}]+[c_k<c_{k+1}])$ 的最小值。

我们可以发现两个性质:

  • $i$ 固定时,$f(i,j)$ 随 $j$ 增大单调不升:显然 $b_i$ 增大则 $c_i$ 减小,那么 $[b_{i-1} > b_i]$ 和 $[c_{i-1} < c_i]$ 就更不可能为真(显然 $i-1$ 前面部分的贡献是每个 $j$ 都一样的)。
  • $i$ 固定时,$f(i,j)$ 的极差不超过 $2$。由代价定义显然。

于是可以把 dp 值分三段去维护,具体而言,转移的时候拿每段 $j$ 最小点去转移(这样显然是最优的),先求出 $f(i+1,0)$,然后再找分界线就好了。

然后就做完了,$\mathcal O(n)$.

ARC 108E Random IS

设 $f(i,j)$ 表示区间 $[i,j]$ 已经被标记了,只考虑该区间内部的期望是多少,显然有:

$$ f(i,j) = 1+\dfrac 1{{\rm cnt}_{i,j}} \sum_{k\in(i,j),a_k\in(a_i,a_j)} [f(i,k-1)+f(k+1,j)] $$

其中 ${\rm cnt}_{i,j}$ 表示 $(i,j)$ 中合法的 $k$ 的数量,容易在 $\mathcal O(n^2\log n)$ 时间内求得。

注意到 $\sum f(i,k-1)$ 和 $\sum f(k+1,j)$ 互不影响,所以我们可以分开统计。对于前者,我们考虑建立 $n$ 棵线段树,第 $i$ 棵的下标 $j$ 维护以 $i$ 为左端点,而右端点 $r$ 满足 $a_{r+1} = j$ 的 dp 值之和。转移较为容易,而后者同理。所以总的时间复杂度也是 $\mathcal O(n^2\log n)$.

AGC 033D Complexity

待补。

CF 585F Digits of Number Pi

待补。

AGC 022E Median Replace

待补。

Day 2

FJOI 2018 城市路径问题

题意简述:$n$ 个城市,第 $i$ 个城市有 $2k$ 个点权 $(a_{i,1},\cdots,a_{i,k},b_{i,1},\cdots,b_{i,k})$,$u$ 到 $v$ 的方案数为 $\sum_{i=1}^k a_{u,i}b_{v,i}$($u$ 可以等于 $v$),$m$ 次询问 $u$ 在 $d$ 步以内到达城市 $v$ 的方案数。($n\le 10^3$,$d<2^{31}$)($k\le 20,m\le 50$ 或 $k=1,m=100$)

设邻接矩阵 $C$,则我们就要求 $I_n+C+C^2+\cdots+C^d$($I_n$ 即 $n$ 阶单位矩阵)第 $u$ 行第 $v$ 列的值,直接倍增则单次询问的复杂度是 $\mathcal O(n^3\log d)$

注意到 $C = AB^{\mathsf T}$,则原式等于:

$$ I_n + A[I_n+B^{\mathsf T}A+\cdots + (B^{\mathsf T}A)^{d-1}] B^{\mathsf T} $$

而 $B^{\mathsf T}A$ 是 $k$ 阶方阵,所以单次询问的复杂度就可以做到 $\mathcal O(nk+k^3\log d)$ 了。

CF 1336E1 Chiori and Doll Picking (easy version)

把 $a_i$ 看成 $\mathbf F_2$ 上的 $m$ 维向量,求出 $V = \mathrm {span}(a_1,\cdots,a_n)$ 的一组基(记 $\dim V = k$),则 $V$ 中的每个向量都有 $2^{n-k}$ 种表示方法,在基上做原问题,然后对答案乘 $2^{n-k}$ 即可。

在 $k$ 较小时直接暴搜就好了,时间复杂度 $\mathcal O(2^k)$.

在 $k$ 较大时,考虑消成行最简形式(即所有自由的位上有且仅有一个基为 $1$),不妨设只有一个 $1$ 的列为主元。

设 $f(i,j,S)$ 表示考虑了前 $i$ 个数,主元选取了 $j$ 个,非主元的状态为 $S$ 的方案数,则为 $1$ 的位数自然是 $j+\operatorname{popcount}(S)$.

大力 dp 就好了,时间复杂度 $\mathcal O(m^22^{m-k})$.

总的时间复杂度大概是 $\mathcal O\left(m2^{\frac m2}\right)$.

LOJ 6247 九个太阳

首先我们有单位根反演:

$$ [k\mid n] = \dfrac 1k\sum_{i=0}^{k-1}\omega_k^{in} $$

所以原式为:

$$ \sum_{i=0}^n [k\mid i]{n\choose i} = \dfrac 1k\sum_{i=0}^n\sum_{j=0}^{k-1}\omega_k^{ji}{n\choose i} = \dfrac 1k\sum_{j=0}^{k-1}(1+\omega_k^j)^n $$

注意到 $k=2^t(t\le 20)$ 且模数为 $998244353$,所以模意义下 $\omega_k$ 总存在,暴力算就好了。时间复杂度 $\mathcal O(k\log n)$.

CF 933D A Creative Cutout

由题意,相当于求:

$$ \begin{aligned} & \sum_{n=1}^m\sum_{x^2+y^2\le n}\sum_{i=x^2+y^2}^ni \\ = & \sum_{x^2+y^2\le m}\sum_{i=x^2+y^2}^mi\sum_{n=i}^m 1 \\ = & \sum_{x^2+y^2\le m}\sum_{i=x^2+y^2}^mi(m-i+1) \end{aligned} $$

由于 $m$ 是常数,所以 $\sum_{i=x^2+y^2}^mi(m-i+1)$ 是关于 $x^2+y^2$ 的三次多项式 $P(x^2+y^2)$,可以通过插值或手算求得系数。

则原式可以看成:

$$ \sum_{x^2+y^2\le m} P(x^2+y^2) = \sum_{|x|\le \lfloor\sqrt m\rfloor} \sum_{|y|\le \lfloor \sqrt {m-x^2}\rfloor} P(x^2+y^2) $$

把 $x$ 看成常数,则 $\sum_{y\le \lfloor \sqrt {m-x^2}\rfloor} P(x^2+y^2)$ 为关于 $\lfloor \sqrt {m-x^2}\rfloor$ 的七次多项式 $Q_x(\lfloor \sqrt {m-x^2}\rfloor)$,于是考虑枚举 $x$,然后插值求出 $Q_x$,再带入求值即可。

时间复杂度 $\mathcal O(\sqrt m)$.

NOI 2019 斗主地

待补。

AGC 020F Arcs on a Circle

待补。

NOI 2020 机器人

待补。

LOJ 508 失控的未来交通工具

待补。

Day 3

待补。

Day 4

IOI 2019 景点划分

不妨设 $a\le b\le c$,则 $a\le \lfloor n/3\rfloor$,$b\le \lfloor n / 2\rfloor$,显然我们应该构造 $A,B$ 连通。

考虑树的情况,我们需要找到一个子树,根为 $u$,满足 $a\le s_u\le n-b$ 或 $b\le s_u\le n-a$($s$ 表示子树大小),然后就可以通过删叶子来得到大小恰为 $a,b$ 的连通块了。如果没有这样的子树,那么无解。

对于图的情况,我们考虑其的一个 DFS 树,对其进行上述的操作,若找不到这样一棵子树,则考虑其重心 $u$,由重心的性质,与其相连的所有子树大小不超过 $n/2$,但又无法通过上述的判断,所以又不满足 $\ge a$,即与其相连的所有子树 $s_v\le a$.

考虑将重心父亲(注意 DFS 树是有根的)的那个子树拿出来,放入集合 $B$,而重心所在子树放入集合 $A$。注意到:

  • 由于重心的所有子树大小都严格小于 $a$,所以重心必然在 $A$ 集合当中。
  • DFS 树是只有返祖边的,所以重心所在的子树中不会互相连边。

于是我们需要利用返祖边不断给 $B$ 加子树,显然,若所有返祖边都加完了,还不满足条件,那么一定无解,否则我们可以证明一定有解:

  • 我们断言,不存在某一时刻 $|A| \ge a,|B| < b$ 在连接一个返祖边后 $|A'|=|A| - s_x< a,|B'| = |B| + s_x \ge b$.
  • 考虑反证法,若存在这样的一个时刻,注意到 $|B|<b$,$|A|-s_x<a$,两式相加得到 $|A|+|B|<a+b+s_x=n$,与 $|A|+|B|=n$ 矛盾,故原命题得证。

于是我们就得到了一个 $\mathcal O(n+m)$ 的做法了。

Day 5~7

咕咕咕

]]>
0 /index.php/archives/51/#comments /index.php/feed/archives/51/
「做题记录」20.3.28~4.7 各省省选题泛做 /index.php/archives/49/ /index.php/archives/49/ Wed, 07 Apr 2021 17:21:00 +0800 zhylj 前言

真就越靠近省选越颓...

ZJOI 2016

*小星星

编号两两不重维护的复杂度很大,而编号两两不重等价于所有编号都被用到,于是考虑子集反演,设 $f(S)$ 表示编号集合为 $S$,且每个编号都被用到的答案,再设:

$$ g(S) = \sum_{T\subseteq S} f(T) $$

即编号集合 $\subseteq S$ 的答案,子集反演得:

$$ f(S) = \sum_{T\subseteq S} (-1)^{|T|-|S|} g(S) $$

而 $g(S)$ 容易用一个 $\mathcal O(n^3)$ 的树形 dp 求得。

时间复杂度 $\mathcal O(2^nn^3)$.

旅行者

考虑分治,使用类似线段树的结构,维护出每一层一个区间内所有点到中线上所有点的最短路,那么查询就是递归直到 $s,t$ 被分到两个不同的区间(到此时 $s\to t$ 的所有路径必然穿过中线),每次枚举所有中线上的点作中转点,路径长度取 $\min$ 就好了。

不妨设 $n\le m$,时间复杂度是 $\mathcal O(n^2m\log^2 m+qn\log m)$,需要卡常,似乎采用更优越的分治方法能够少掉一个 $\log$?

大森林

考虑全局换生长节点,区间加叶子怎么做。把操作离线,按树的下标扫一遍,一开始把所有点都加上,但带 $0$ 的权值,每次若进入某个点生效区间就给它权值加上 $1$,若离开则减去 $1$,那我们可以容易树剖统计路径上有效点数,进而可以统计路径长度。

那么区间换生长节点也就可以使用类似的做法,每次换生长节点我们直接新建一个虚点,权值为 $0$,初始连向它前面一个生长节点新建的虚点,若进入它的生效区间,我们把它改接到操作应该换的节点下面,若离开再把它接回去,可以用 LCT 维护。

需要注意一些细节,例如查询的点 LCA 是生长节点的情况和生长节点换不过去的情况。

时间复杂度 $\mathcal O((n+m)\log n)$.

*线段树

设 $f(x,k,l,r)$ 表示进行了 $k$ 轮,$\forall i\in[l,r],a_i\le x$ 且 $a_{l-1},a_{r+1}>x$ 的方案数。

注意到 $[a_i>x]=1$ 总是在向中间合并的,所以这样设不会出现重复转移。

那么转移就可以分两类,第一类是改变了 $1$ 的位置。

$$ f(x,k+1,i,r) \gets (l-1)\cdot f(x,k+1,l,r) \\ f(x,k+1,l,j) \gets (n-r)\cdot f(x,k+1,l,r) \\ $$

而不变的有:

$$ f(x,k+1,l,r) \gets \frac {(l-1)^{\overline 2}+(n-r)^{\overline 2}+(r-l+1)^{\overline 2}}2\cdot f(x,k+1,l, r) $$

设排完序后的 $a_1,a_2,\cdots,a_n$ 为 $b_1,b_2,\cdots,b_n(b_{n+1}=0)$,于是我们可以统计出位置 $i$ 的答案:

$$ \sum_{j=1}^n\sum_{l\le i\le r} f(b_j,q,l,r)(b_{j+1}-b_j) $$

注意到 $f$ 的转移并没有用到 $x$,于是可以分开来处理,即设:

$$ h(k,l,r) = \sum_{x\in b_i} f(x,k,l,r) $$

容易发现转移是类似的,于是我们就做到了 $\mathcal O(n^2q)$.

ZJOI 2017

树状数组

不难发现实际上是在求后缀和,故询问 $[l,r]$ 正确的概率可以转化成求 $a_{l-1}=a_r$ 的概率(在 $l=0$ 时略有不同,但处理方法类似,不再赘述)。

记 $f_{i,j}$ 表示 $a_i=a_j$ 的概率,则容易发现区间 $[l,r]$ 随机选一个数加一的影响是一个矩形加 / 乘,直接大力上 KD-Tree 维护即可,时间复杂度 $\mathcal O(n\sqrt n)$.

然而可以上二维线段树,被吊打。

仙人掌

由于原图是连通图,所以必须得是仙人掌才能加成仙人掌。

然后注意到我们加的边不可能跨越圆方树的方点(也就是不会跨越在环上的边),所以可以把所有环内的边全部删掉,然后答案就为每个联通块答案之积。

对于每个连通块,注意到选一些点对连成环相当于把整棵树划分成若干条不相交的路径,每条路径对应着一个环(若路径只有一条边,则其在最后的仙人掌上是一个树边)。

于是直接设 $f_u$ 表示以 $u$ 为根的子树的答案,那么有 $f_u = c_u\prod_{v\in\operatorname{son}(u)} f_v$,其中 $c_u$ 为将子树中的路径合并或新建一条路径的方案数,可以用一个 $\mathcal O(n)$ 的 dp 求出,注意根的情况有所不同。

SHOI 2017

期末考试

不放令 $A\gets\min(A,B)$。

考虑枚举最后所有的课程发布时间 $\le t$,那么统计 $s=\sum \max(t-b_i,0)$ 和 $r = \sum \max(b_i-t,0)$,我们尽量多的使用第一个操作,直到没有课程可以用来延迟,那么只能使用第二个操作,这样一定是最优的。

$s$ 和 $r$ 可以用前缀和维护,时间复杂度 $\mathcal O(n)$.

分手是祝愿

列式可以发现操作相当于一个 $n$ 个未知数,$n$ 个方程的异或方程组,所以必然存在唯一解,我们可以通过倒着操作构造解。

设 $f_i$ 表示未知数中还有 $i$ 个 $1$,消成全 $0$ 的期望步数,容易列出 $nf_i = if_{i-1}-(n-i)f_{i+1}+n$(在 $k< i$ 的情况下)于是有:

$$ f_{i+1} = \frac{nf_i-if_{i-1}-n}{n-i} $$

特别的我们有 $f_n = f_{n-1} + 1$,注意到我们没有 $f_{k+1}$ 的式子,所以可以把每个式子表示成 $af_{k+1}+b$ 的形式,然后用最后一个等式 $f_n=f_{n-1} + 1$ 解出 $f_{k+1}$ 即可。

寿司餐厅

选了区间 $[i,j]$ 就必须选区间 $[i',j']\subset [i,j]$,容易发现这是最大权闭合子图的模型。发现 $[i,j]$ 的子区间族为 $[i,j-1]$ 和 $[i+1,j]$ 的子区间族的并,所以我们只需要连 $\mathcal O(n)$ 条边就可以完成建图。

接下来考虑 $mx^2+cx$ 的贡献,注意到 $cx$ 相当于给 $d_{i,i}$ 加上 $a_i$,而 $mx^2$ 相当于选了某一类就附加贡献,所以再新建点表示这类选不选就好了。

九省联考 2018

*IIIDX

考虑对 $i=1,\cdots,n$,依次确定 $d_i$.

于是考虑贪心,假如我们要选取某个 $d_i$,那么就要求选完 $d_i$ 之后剩下的数存在一种选择的方案。对于每个已经确定了权值的点 $j<i$,我们需要在剩下的数中选取 ${\rm siz}_j - 1$ 个不小于 $d_j$ 的数,于是我们可以贪心确定这个方案:每次选最小的合法的数。这样最大的 $d_i$ 就是做完所有贪心之后剩下的最大的数了。

暴力做是 $\mathcal O(n^2\log n)$ 的,考虑如何优化这个过程,记 $f_u$ 表示考虑了 $d_j\ge u$ 的贪心选取后,还剩下多少个 $\ge u$ 的数,那么考虑所有的 $d_j$ 之后,剩下的 $\ge u$ 的数即为 $g_u=\min_{v\le u}f_v$,这样定义的好处在于,每次新选了的数对 $f_u$ 的影响是显而易见的(假如给某个子树大小为 $s$ 的节点选了 $v$,就是对所有 $u\le v$ 有 $f_u\gets f_v-s$),我们可以很容易的用线段树去维护。

然后我们就做到了 $\mathcal O(n\log n)$.

AHOI / HNOI 2018

游戏

可以把门当成房间,这样大概可以少考虑一些细节。

考虑对每个点求出 $l_i,r_i$ 表示能到的最远点。

然后考虑每个起点 $s$、钥匙 $k$,门 $t$ 的位置关系。若 $s<k<t$,那么直接忽略这对钥匙和门,若 $s<t<k$,那么必然开不了这个门,对 $r_s\gets \min(r_s,t-1)$,对 $t,k$ 均在 $s$ 的另一侧的情况同理。

所以只需要考虑跨过 $s$ 的,注意到若 $k_2<t_1<t_2<k_1$,那么相当于区间对 $l,r$ 进行取 $\min/\max$,拆贡献后发现可以对 $s\in(t_1,n]$ 的 $l_s$ 和 $t_1$ 取 $\max$,$s\in[1,t_2)$ 的 $r_s$ 可以和 $t_2$ 取 $\min$,于是我们只需要对每个 $k,t$ 跑一个二维偏序判一下有没有就可以计算贡献了。

然后还有一个限制是 $s\to t$ 的路径上不能有需要走到走不到的地方才能取的要是,这个可以用一个 RMQ 解决。

时间复杂度 $\mathcal O(n\log n+p)$.

正解据说是线性,但是我不会。

排列

原题。容易转化为树上的模型,考虑合并必须相邻的连通块,把每个连通块视作二元组 $(c_i,w_i)$ 分别表示大小和权值和,然后考虑交换 $(c_1,w_1)$(一开始在前面)和 $(c_2,w_2)$ 的代价是 $c_2w_1-c_1w_2$,直接拿这个去贪心地合并就好了,需要用优先队列维护。

时间复杂度 $\mathcal O(n\log n)$.

寻宝游戏

先把输入翻转,方便倒推。

注意到倒推时若使用了或,则所有 $a_{i,j}=1$ 的位置后续就不用管了,反之所有 $a_{i,j}=0$ 的位置都不用管了,所以暴力枚举可以做到 $\mathcal O(nmq)$ 的复杂度。

再注意到交换两个位不影响答案,所以我们可以对所有排序,即使得第一个数为 $00\cdots011\cdots1$ 的形式,第二个数为 $00\cdots011\cdots100\cdots011\cdots1$ 的形式,以此类推。

那么我们每次考虑的就是一段连续的区间的内容了,于是可以预处理前缀和。

乍一看复杂度还是 $\mathcal O(nmq)$ 的,但容易注意到我们只会考察 $\mathcal O(1)$ 条链的状态,故复杂度变成了 $\mathcal O((n+q)m)$.

]]>
0 /index.php/archives/49/#comments /index.php/feed/archives/49/
「APIO 2018」铁人两项 /index.php/archives/48/ /index.php/archives/48/ Mon, 22 Mar 2021 18:06:27 +0800 zhylj 题目

给定一张 $n$ 个点 $m$ 条边的图,求有多少个有序点对 $(a,b,c)$,使得存在一条 $a\to b\to c$ 且不经过重复点的路径。

$1\le n\le 10^5$,$1\le m\le 2\times 10^5$.

分析

这是道不好好分析会分类死的题目。

考虑建出圆方树,则原题条件等价于:求有多少个有序点对 $(a,b,c)$,使得存在一条 $a\to b\to c$,且不重复经过圆点,但可以重复经过方点的路径。

于是不难想象,我们枚举中转点 $b$,我们可以把 $b$ 相连的所有点(显然都是方点)与其合并,然后以 $b$ 为中转点的方案数即为在任意两个不同的子树中取两个点 $a,c$ 的方案数。

然后可以考虑容斥,先求出不考虑在不同子树内的方案,再求出在同一子树内的方案,然后用前者减去后者。

$$ \sum_i\sum_{j\neq i}s_is_j = \left(\sum_i s_i\right)^2 - \sum_is_i^2 $$

于是我们只需要维护,以每个点为根时,所有子树大小的平方和(显然,所有子树大小的和为当前连通块大小 $-1$)。

这可以用简单的换根求出,时间复杂度为 $\mathcal O(n + m)$,然后这道题就做完了。

代码

const int N = 5e5 + 5;

int n, m, b_cnt; ll ans;

int dfv, top, dfn[N], low[N], st[N], rt[N], siz[N];
std::vector <int> E[2][N];
void Add(int u, int v, int p) {
    E[p][u].push_back(v);
    E[p][v].push_back(u);
}
void Tarjan(int u, int r) {
    rt[u] = r;
    dfn[u] = low[u] = ++dfv;
    st[++top] = u;
    for(auto v : E[0][u]) {
        if(!dfn[v]) {
            Tarjan(v, r);
            low[u] = std::min(low[u], low[v]);
            if(low[v] == dfn[u]) {
                rt[++b_cnt] = r;
                for(int x = 0; x != v; --top) {
                    Add(b_cnt, x = st[top], 1);
                    ++siz[b_cnt];
                }
                Add(b_cnt, u, 1);
                ++siz[b_cnt];
            }
        } else low[u] = std::min(low[u], dfn[v]);
    }
}

int t_siz[N]; ll t_sq_s[N];
void Dfs(int u, int p) {
    if(u <= n) t_siz[u] = 1;
    for(auto v : E[1][u])
        if(v != p) {
            Dfs(v, u);
            t_siz[u] += t_siz[v];
            t_sq_s[u] += 1LL * t_siz[v] * t_siz[v];
        }
}
void Dfs2(int u, int p) {
    for(auto v : E[1][u])
        if(v != p) Dfs2(v, u);
    if(u <= n) {
        ll sq_s = 0;
        for(auto v : E[1][u])
            sq_s += t_sq_s[v];
        sq_s -= 1LL * t_siz[u] * t_siz[u];
        ll s_fa = t_siz[rt[u]] - t_siz[p];
        sq_s += s_fa * s_fa;
        ans += 1LL * (t_siz[rt[u]] - 1) * (t_siz[rt[u]] - 1) - sq_s;
    }
}

int main() {
    rd(n, m); b_cnt = n;
    for(int i = 1; i <= m; ++i) {
        int u, v; rd(u, v);
        Add(u, v, 0);
    }
    for(int i = 1; i <= n; ++i)
        if(!dfn[i]) {
            Tarjan(i, i);
            top = 0;
        }
    for(int i = 1; i <= n; ++i)
        if(rt[i] == i) Dfs(i, 0), Dfs2(i, 0);
    printf("%lld\n", ans);
    return 0;
}
]]>
0 /index.php/archives/48/#comments /index.php/feed/archives/48/
「学习笔记」Segment Tree Beats! /index.php/archives/47/ /index.php/archives/47/ Mon, 22 Mar 2021 09:00:47 +0800 zhylj 前言

线段树维护区间最值操作和区间历史最值。

魔改了论文中的证明,可能会有疏漏。

区间最值操作

区间取 min/max

[scode type="lblue"]

(HDU 5306 Gorgeous Sequence)给定一个长度为 $n$ 的数组 $A$,支持以下操作共 $m$ 个:

  • 给定 $l,r,x$,对 $i\in[l,r]$,令 $A_i\gets \min(A_i,x)$.
  • 给定 $l,r$,求 $\max_{i\in[l,r]} A_i$.
  • 给定 $l,r$,求 $\sum_{i\in[l,r]} A_i$.

$1\le n,m\le 10^6$.

[/scode]

对于线段树的一个区间 $[l,r]$,我们维护 $\operatorname{sum}(l,r)$,$\operatorname{fmax}(l,r)$,$\operatorname{smax}(l,r)$ 和 $\operatorname{cnt}(l,r)$ 分别表示这个区间的和、最大值,严格次大值(不存在则为 $-\infty$)和最大值的个数。

然后每次考虑把一个区间 $[l,r]$ 对 $x$ 取 $\min$ 会发生什么。

$\operatorname{GetMin}(l,r,x)$ 操作:

  • Case 1: $\operatorname{fmax}(l,r)\le x$,则该操作对此区间无影响。
  • Case 2: $\operatorname{smax}(l,r)<x<\operatorname{fmax}(l,r)$,则最大值改变,在此区间打上标记,然后令 $\operatorname{sum}(l,r)\gets \operatorname{sum}(l,r)+(x-\operatorname{fmax}(l,r))\operatorname{cnt}(l,r)$,$\operatorname{fmax}(l,r)\gets x$.
  • Case 3: $x\le \operatorname{smax}(l,r)$,直接递归左儿子右儿子进行修改,然后更新该区间的答案。

这样做的复杂度是多少呢?我们注意到显然由 Case 1 和 Case 2 结束的节点的个数和未结束节点的个数同阶(由于是二叉树,显然它们的个数只相差 $1$),所以只需要考虑需要处理 Case 3 的节点的情况。

可以发现,每次对一个区间进行 Case 3 中的处理时,必然导致该区间值域中的元素个数减少,于是我们考虑设势能函数 $\Phi(A)$ 表示 $A$ 建出的线段树每个区间值域中的元素个数之和。那么单次修改的时间复杂度为 $\mathcal O(-\Delta \Phi(A))$.

于是我们只需要考虑所有修改中 $\Phi (A)$ 的总增加量,我们注意到,在一次区间取 $\min$ 操作中,势能会增加(显然至多增加 $1$)的节点显然只可能是被我们执行了 $\operatorname{GetMin}$ 操作的区间的祖先,显然这样的节点数是 $\mathcal O(\log n)$ 个的。

刚开始的序列可以被视为给初始全为 $\infty$ 的 $A$ 进行了 $n$ 次修改,所以总的时间复杂度为 $\mathcal O((n+m)\log n)$.

区间取最值和区间最值操作

[scode type="lblue"]

(Picks Loves Segment Tree)给定一个长度为 $n$ 的数组 $A$,支持以下操作共 $m$ 个:

  • 给定 $l,r,x$,对 $i\in[l,r]$,令 $A_i\gets \min(A_i,x)$.
  • 给定 $l,r,x$,对 $i\in[l,r]$,令 $A_i\gets A_i+x$($x$ 可能为负数).
  • 给定 $l,r$,求 $\sum_{i\in[l,r]} A_i$.

$1\le n,m\le 3\times 10^5$.

[/scode]

注意到,区间取 $\min$ 的标记(在上题 Case 2 中打上的标记)实际上是针对某个区间的最大值的加减标记,于是考虑把一个节点维护的元素分为两部分:最大值和非最大值,然后下传标记的时候直接传非最大值的加减标记,再判断子树中是否含有当前节点的最大值并下传最大值的加减标记即可。

然后区间加就很好做了:它是同时针对非最大值和最大值的加减标记。

接下来考虑复杂度,发现区间加最坏可能导致上述的势能函数增加 $\mathcal O(n)$,于是考虑换一种势能函数的定义。

考虑设 $\Phi(A)$ 表示 $A$ 建成的线段树中每个节点的势能之和,而定义一个代表区间 $[l,r]$ 节点的势能为满足以下条件中至少一个的权值 $x$ 的数量(${\rm mid}$ 表示区间终点):

  • 存在 $i\in[l,{\rm mid}],j\in({\rm mid}, r]$,$A_i=A_j=x$.
  • 存在 $i\notin [l,r]$,$A_i=x$.

将第 $i$ 次区间加 $x$ 视作区间加 $x + \varepsilon_i(\varepsilon_i\ll x,\varepsilon_i\ll \varepsilon_{i-1})$,其中 $\ll$ 表示远小于,显然并不影响每个位置的大小关系,并且每一次影响到某个节点的区间加操作必然不会使得其势能增加(未被修改和被修改了的节点一定不相等,而修改了的点的内部贡献不变)。

对于一次 $\operatorname{GetMin}(l,r,x)$ 操作。

考虑每个权值 $v$ 的贡献,我们实际上是先从代表 $[l,r]$ 的节点开始,走过一条长度为 $\mathcal O(\log n)$ 链(严格来说,应该是一个毛毛虫类似物,即一条链上挂了一些单点),到了某个节点 $u$,使得 $u$ 的左右子树中都包含 $v$。

此时,考虑 $v$ 的贡献,由于左右子树中都含 $v$,故 $u$ 的子树中权值 $v$ 对某个节点的势能产生贡献当且仅当其在该节点子树中出现。这个定义是与上题中定义等价的。

所以一次 $\operatorname{GetMin}(l,r,x)$ 的操作是 $\mathcal O(-\log n\Delta\Phi(A))$ 的。

于是可以分析得总的复杂度为 $\mathcal O(n\log n + m\log^2 n)$.

事实上,这个东西在实践中的表现是远低于上界的,也没人构造出能卡到严格 $\mathcal O(m\log^2n)$ 的数据。所以可以直接当一个 log 用。

区间历史最值

可以用懒标记处理的历史最值问题

[scode type="lblue"]

给定一个长度为 $n$ 的数组 $A$ 和 $B$,且初始 $B_i=A_i$,支持以下操作共 $m$ 个:

  • 给定 $l,r,x$,对 $i\in[l,r]$,令 $A_i\gets A_i+x$($x$ 可能为负数).
  • 给定 $l,r$,求 $\max_{i\in[l,r]} A_i$.
  • 给定 $l,r$,求 $\max_{i\in[l,r]} B_i$.

每次操作后令 $B_i\gets\max(B_i,A_i)$.

$1\le n,m\le 5\times 10^5$.

[/scode]

首先,我们对每个节点,要维护:$\operatorname{sum}(l,r)$,$\operatorname{max}(l,r)$ 和 $\operatorname{hmax}(l,r)$ 分别表示和,最大值以及历史最大值。

但是,假如我们仅仅维护 $\operatorname{tag}(l,r)$ 表示区间加标记的话,在传给孩子的时候并不知道如何更新孩子的历史最值。

于是,我们维护 $\operatorname{htag}(l,r)$ 表示 $\operatorname{tag}(l,r)$ 在上次下传前的历史最大值,这样在下传的时候,用 $\operatorname{sum}(v) + \operatorname{htag}(u)$ 去更新 $u$ 的孩子 $v$ 的历史最大值 $\operatorname{hmax}(v)$ 就好了。

显然,这样做的时间复杂度是 $\mathcal O((n+m)\log n)$.

[scode type="lblue"]

(洛谷 P4314 CPU 监控)给定一个长度为 $n$ 的数组 $A$ 和 $B$,且初始 $B_i=A_i$,支持以下操作共 $m$ 个:

  • 给定 $l,r,x$,对 $i\in[l,r]$,令 $A_i\gets x$.
  • 给定 $l,r,x$,对 $i\in[l,r]$,令 $A_i\gets A_i + x$.
  • 给定 $l,r$,求 $\max_{i\in[l,r]} A_i$.
  • 给定 $l,r$,求 $\max_{i\in[l,r]} B_i$.

每次操作后令 $B_i\gets\max(B_i,A_i)$.

$1\le n,m\le 10^5$.

[/scode]

在区间加减标记 $\operatorname{add}$ 和区间最大加减标记 $\operatorname{hadd}$ 的基础上,维护区间赋值标记 $\operatorname{asign}$ 和区间历史最大赋值标记 $\operatorname{hasign}$。然后令赋值标记的优先级在加减标记上。

推标记的时候:

  • 先推 $\operatorname{add}$。若推到了有 $\operatorname{asign}$ 标记的节点,则直接加在 $\operatorname{asign}$ 标记上;若推到了没有 $\operatorname{asign}$ 标记的节点,则加在 $\operatorname{add}$ 标记上。
  • 再推 $\operatorname{asign}$。若推到了存在 $\operatorname{add}$ 标记的节点,则清空该标记。但注意保留 $\operatorname{hadd}$ 标记不变。

没有提到的历史版本的标记也是类似的,这里不再展开说明。

时间复杂度 $\mathcal O((n+m)\log n)$.

[scode type="lblue"]

(洛谷 P6242 线段树 3)给定一个长度为 $n$ 的数组 $A$ 和 $B$,且初始 $B_i=A_i$,支持以下操作共 $m$ 个:

  • 给定 $l,r,x$,对 $i\in[l,r]$,令 $A_i\gets A_i+x$.
  • 给定 $l,r,x$,对 $i\in[l,r]$,令 $A_i\gets \min(A_i,x)$.
  • 给定 $l,r$,求 $\sum_{i\in[l,r]} A_i$.
  • 给定 $l,r$,求 $\max_{i\in[l,r]} A_i$.
  • 给定 $l,r$,求 $\max_{i\in[l,r]} B_i$.

每次操作后令 $B_i\gets\max(B_i,A_i)$.

$1\le n,m\le 5\times 10^5$.

[/scode]

在先前的过程中,我们已经得到了一个使用一个 $\log$ 的代价,将区间最值操作问题转化为对每个节点最大值的操作问题的方法。

于是直接套用这个方法,维护每个节点最大值、非最大值的加减标记、历史最大加减标记。

然后我们就做到了 $\mathcal O(n\log n + m\log^2n)$ 的时间复杂度。

代码:

#define cnt(o) T[o].cnt
#define sum(o) T[o].sum
#define fmax(o) T[o].fmax
#define nmax(o) T[o].nmax
#define h_max(o) T[o].h_max
#define tag_fmax(o) T[o].tag_fmax
#define tag_nmax(o) T[o].tag_nmax
#define htag_fmax(o) T[o].htag_fmax
#define htag_nmax(o) T[o].htag_nmax
typedef long long ll;
typedef double ff;
typedef std::pair <int, int> pii;
const int N = 5e5 + 5, Inf = 0x3f3f3f3f;

struct Node {
    ll sum;
    int cnt, fmax, nmax, tag_fmax, tag_nmax,
        h_max, htag_fmax, htag_nmax;
    Node() { fmax = nmax = h_max = -Inf; }
} T[N << 3];
void Update(int o) {
    cnt(o) = 0; sum(o) = sum(o << 1) + sum(o << 1 | 1);
    fmax(o) = std::max(fmax(o << 1), fmax(o << 1 | 1));
    nmax(o) = std::max(nmax(o << 1), nmax(o << 1 | 1));
    if(fmax(o << 1) == fmax(o)) cnt(o) += cnt(o << 1);
    else nmax(o) = std::max(nmax(o), fmax(o << 1));
    if(fmax(o << 1 | 1) == fmax(o)) cnt(o) += cnt(o << 1 | 1);
    else nmax(o) = std::max(nmax(o), fmax(o << 1 | 1));
    h_max(o) = std::max(h_max(o << 1), h_max(o << 1 | 1));
}
void Build(int o, int l, int r, int A[]) {
    if(l < r) {
        int mid = (l + r) >> 1;
        Build(o << 1, l, mid, A);
        Build(o << 1 | 1, mid + 1, r, A);
        Update(o);
    } else {
        sum(o) = fmax(o) = h_max(o) = A[l];
        nmax(o) = -Inf; cnt(o) = 1;
    }
}
void ApplyTag(int o, int l, int r, int add_fmax, int add_nmax, int hadd_fmax, int hadd_nmax) {
    sum(o) += 1LL * cnt(o) * add_fmax + 1LL * (r - l + 1 - cnt(o)) * add_nmax;
    htag_fmax(o) = std::max(htag_fmax(o), tag_fmax(o) + hadd_fmax);
    htag_nmax(o) = std::max(htag_nmax(o), tag_nmax(o) + hadd_nmax);
    h_max(o) = std::max(h_max(o), fmax(o) + hadd_fmax);
    tag_fmax(o) += add_fmax; tag_nmax(o) += add_nmax;
    fmax(o) += add_fmax; if(nmax(o) > -Inf) nmax(o) += add_nmax;
}
void PushTag(int o, int l, int r) {
    int mid = (l + r) >> 1, t_max = std::max(fmax(o << 1), fmax(o << 1 | 1));
    if(fmax(o << 1) == t_max)
        ApplyTag(o << 1, l, mid, tag_fmax(o), tag_nmax(o), htag_fmax(o), htag_nmax(o));
    else ApplyTag(o << 1, l, mid, tag_nmax(o), tag_nmax(o), htag_nmax(o), htag_nmax(o));
    if(fmax(o << 1 | 1) == t_max)
        ApplyTag(o << 1 | 1, mid + 1, r, tag_fmax(o), tag_nmax(o), htag_fmax(o), htag_nmax(o));
    else ApplyTag(o << 1 | 1, mid + 1, r, tag_nmax(o), tag_nmax(o), htag_nmax(o), htag_nmax(o));
    tag_fmax(o) = tag_nmax(o) = htag_fmax(o) = htag_nmax(o) = 0;
}
void GetMin(int o, int l, int r, int v) {
    PushTag(o, l, r);
    if(v >= fmax(o)) return;
    if(v > nmax(o)) {
        ApplyTag(o, l, r, v - fmax(o), 0, v - fmax(o), 0);
        return;
    }
    int mid = (l + r) >> 1;
    GetMin(o << 1, l, mid, v);
    GetMin(o << 1 | 1, mid + 1, r, v);
    Update(o);
}
void Modify(int o, int l, int r, int ql, int qr, int v, int typ) {
    if(ql <= l && r <= qr) {
        if(typ == 1) ApplyTag(o, l, r, v, v, v, v);
        else if(typ == 2) GetMin(o, l, r, v);
    } else {
        int mid = (l + r) >> 1;
        PushTag(o, l, r);
        if(ql <= mid) Modify(o << 1, l, mid, ql, qr, v, typ);
        if(qr > mid) Modify(o << 1 | 1, mid + 1, r, ql, qr, v, typ);
        Update(o);
    }
}
int QueryMax(int o, int l, int r, int ql, int qr, int typ) {
    if(ql <= l && r <= qr) {
        if(typ == 4) return fmax(o);
        else if(typ == 5) return h_max(o);
        else return -Inf;
    } else {
        int mid = (l + r) >> 1, ret = -Inf;
        PushTag(o, l, r);
        if(ql <= mid) ret = std::max(ret, QueryMax(o << 1, l, mid, ql, qr, typ));
        if(qr > mid) ret = std::max(ret, QueryMax(o << 1 | 1, mid + 1, r, ql, qr, typ));
        return ret;
    }
}
ll QuerySum(int o, int l, int r, int ql, int qr) {
    if(ql <= l && r <= qr) return sum(o);
    else {
        int mid = (l + r) >> 1; ll ret = 0;
        PushTag(o, l, r);
        if(ql <= mid) ret += QuerySum(o << 1, l, mid, ql, qr);
        if(qr > mid) ret += QuerySum(o << 1 | 1, mid + 1, r, ql, qr);
        return ret;
    }
}

int A[N];
int main() {
    int n, q;
    rd(n, q);
    for(int i = 1; i <= n; ++i) rd(A[i]);
    Build(1, 1, n, A);
    while(q--) {
        int typ, l, r, v;
        rd(typ);
        if(typ == 1 || typ == 2) {
            rd(l, r, v);
            Modify(1, 1, n, l, r, v, typ);
        } else if(typ == 3) {
            rd(l, r);
            printf("%lld\n", QuerySum(1, 1, n, l, r));
        } else if(typ == 4 || typ == 5) {
            rd(l, r);
            printf("%d\n", QueryMax(1, 1, n, l, r, typ));
        }
    }
    return 0;
}

历史最值的区间问题

[scode type="lblue"]

给定一个长度为 $n$ 的数组 $A$ 和 $B$,且初始 $B_i=A_i$,支持以下操作共 $m$ 个:

  • 给定 $l,r,x$,对 $i\in[l,r]$,令 $A_i\gets A_i+x$.
  • 给定 $l,r$,求 $\sum_{i\in[l,r]} B_i$.

每次操作后令 $B_i\gets\max(B_i,A_i)$.

$1\le n,m\le 5\times 10^5$.

[/scode]

我们设一个辅助数组 $C$,每次令 $C_i\gets A_i-B_i$,则区间加相当于区间最值操作:$C_i\gets \min(C_i+x,0)$。查询时的答案即为 $A$ 的区间和加上 $B$ 的区间和,可以用先前的方法维护。

时间复杂度 $\mathcal O(n\log n + m\log^2n)$.

[scode type="lblue"]

给定一个长度为 $n$ 的数组 $A$ 和 $B$,且初始 $B_i=A_i$,支持以下操作共 $m$ 个:

  • 给定 $l,r,x$,对 $i\in[l,r]$,令 $A_i\gets A_i+x$.
  • 给定 $l,r$,求 $\sum_{i\in[l,r]} B_i$.

每次操作后令 $B_i\gets B_i+A_i$.

$1\le n,m\le 5\times 10^5$.

[/scode]

我们定义辅助数组 $C$,满足 $C_i\gets B_i-tA_i$,其中 $t$ 为当前的操作数。

那么一次区间修改相当于对 $C$ 区间加减 $tx$,可以简单地用线段树维护。

参考资料

]]>
0 /index.php/archives/47/#comments /index.php/feed/archives/47/
「PKUWC 2018」猎人杀 /index.php/archives/46/ /index.php/archives/46/ Fri, 19 Mar 2021 10:51:00 +0800 zhylj 题目

有 $n$ 个人,第 $i$ 个人有权重 $w_i$,每次以正比于权重的概率干掉某个人,求第一个人最后一个被干掉的概率。

$1\le n\le 10^5$.

分析

记 $s = \sum w_i$,$p_i = w_i/s$.

首先考虑概率会变化不好做,于是转化为每次任取一个人,若还没被干掉就干掉他,否则啥也不干,显然新的题目与原先是等价的。

考虑第一个最后一个被干掉的情况,相当于要满足:在第一个人被干掉的时刻之前,所有人都被选取了至少一次,且第一个人没有被选取过。

设 $f_n$ 表示「前 $n$ 个时刻,第一个人没有被选取,且第二个人到第 $n$ 个人都被选取过的概率」,容易发现 $f_n$ 可以被表示为后 $n - 1$ 个人的贡献的二项卷积。

记 $\langle p_1f_n\rangle$ 的 EGF 为 $\widehat F(z)$,于是,我们可以列出式子:

$$ \widehat F(z) = p_1\prod_{2\le i \le n} \left(\sum_{j\ge 1} \frac {(p_iz)^j}{j!}\right) = p_1\prod_{2\le i \le n}(e^{p_iz}-1) $$

然后我们考虑展开这个式子成 $\sum_{i}a_ie^{iz}$ 的形式,于是考虑先换个元:

$$ \begin{aligned} x & = e^{s^{-1}z}\\ p_1\prod_{2\le i \le n}(e^{p_iz}-1) & = p_1\prod_{2\le i \le n} (x^{w_i}-1)\\ \end{aligned} $$

然后我们就要求 $a_k = [x^k]p_1\prod_{2\le i \le n} (x^{w_i}-1)$ 了,根据套路,我们有:

$$ \begin{aligned} p_1\prod_{2\le i\le n} (x^{w_i}-1) & = p_1(-1)^{n-1}\prod_{2\le i \le n} (1-x^{w_i})\\ \prod_{2\le i \le n} (1-x^{w_i}) & = \exp\left(\sum_{2\le i\le n} \ln(1-x^{w_i})\right) \\ & = \exp\left(\sum_{2\le i\le n}\sum_{j\ge 1}\frac{-x^{w_ij}}{j}\right) \end{aligned} $$

于是预处理一下每种 $w_i$ 出现了几次,随便算一下系数再 $\exp$ 就可以在 $\mathcal O(s\log s)$ 的时间复杂度内计算所有 $a_k$ 了。

求出 $a_k$ 有什么用呢?我们注意到如果我们求出 $\widehat F(z)$ 的系数序列的 OGF $F(z)$,那么 $F(1)$ 就为答案,于是我们考虑如何把 $\widehat F(z)$ 写成 OGF 的形式。

注意到 $e^{cz}$ 的系数序列 $\langle c^n\rangle$ 对应的 OGF 为 $\dfrac 1{1-cz}$,于是我们有:

$$ \begin{aligned} \widehat F(z) & = \sum_{i\ge 0} a_ie^{is^{-1}z} \\ \implies F(z) & = \sum_{i\ge 0} \frac {a_i}{1-is^{-1}z} \\ F(1) & = \sum_{i\ge 0} \frac {a_i}{1 - is^{-1}} \end{aligned} $$

然后就做完了,时间复杂度 $\mathcal O(s\log s)$.

然而由于我实现的丑,跑的比其它分治 NTT 的题解慢。

代码

const int N = 5e5 + 5, Mod = 998244353, g = 3, gInv = 332748118;

int QPow(int a, int b) {
    int ret = 1, bas = a;
    for(; b; b >>= 1, bas = 1LL * bas * bas % Mod)
        if(b & 1) ret = 1LL * ret * bas % Mod;
    return ret;
}

int fac[N], fac_inv[N], inv[N];
void Init(int n) {
    fac[0] = 1;
    for(int i = 1; i <= n; ++i)    fac[i] = 1LL * fac[i - 1] * i % Mod;
    fac_inv[n] = QPow(fac[n], Mod - 2);
    for(int i = n - 1; ~i; --i) fac_inv[i] = 1LL * fac_inv[i + 1] * (i + 1) % Mod;
    for(int i = 1; i <= n; ++i) inv[i] = 1LL * fac_inv[i] * fac[i - 1] % Mod;
}

namespace Poly {

int rev[N], ur[N];
int GetRev(int x, int y) {
    int len = 1, h_bit = 0; ur[0] = 1;
    for(x += y; len < x; len <<= 1, ++h_bit);
    for(int i = 1; i < len; ++i)
        rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (h_bit - 1));
    return len;
}
void NTT(int f[], int len, bool flag) {
    for(int i = 0; i < len; ++i)
        if(i < rev[i]) std::swap(f[i], f[rev[i]]);
    for(int i = 1; i < len; i <<= 1) {
        int ur_1 = QPow(flag ? g : gInv, (Mod - 1) / (i << 1));
        for(int j = 1; j < i; ++j)
            ur[j] = 1LL * ur[j - 1] * ur_1 % Mod;
        for(int j = 0; j < len; j += (i << 1))
            for(int k = 0; k < i; ++k) {
                int fl = f[j + k], fr = 1LL * f[i + j + k] * ur[k] % Mod;
                f[j + k] = (fl + fr) % Mod;
                f[i + j + k] = (fl - fr + Mod) % Mod;
            }
    }
    if(!flag) {
        int len_inv = inv[len];
        for(int i = 0; i < len; ++i)
            f[i] = 1LL * f[i] * len_inv % Mod;
    }
}
int tmp_f[N];
void Inv(int f[], int f_inv[], int n) {
    if(n == 1) { f_inv[0] = QPow(f[0], Mod - 2); return; }
    Inv(f, f_inv, (n + 1) / 2);
    int len = GetRev(n, n);
    for(int i = 0; i < len; ++i) tmp_f[i] = f[i] * (i < n);
    for(int i = n; i < len; ++i) f_inv[i] = 0;
    NTT(tmp_f, len, true); NTT(f_inv, len, true);
    for(int i = 0; i < len; ++i)
        f_inv[i] = f_inv[i] * (2 - 1LL * f_inv[i] * tmp_f[i] % Mod + Mod) % Mod;
    NTT(f_inv, len, false);
    for(int i = n; i < len; ++i) f_inv[i] = 0;
}
void Der(int f[], int f_d[], int n) {
    for(int i = 0; i < n - 1; ++i)
        f_d[i] = 1LL * f[i + 1] * (i + 1) % Mod;
    f_d[n] = 0;
}
void Intg(int f[], int f_intg[], int n) {
    for(int i = n - 1; i; --i)
        f_intg[i] = 1LL * f[i - 1] * inv[i] % Mod;
    f_intg[0] = 0;
}
int tmp_f_inv[N];
void Ln(int f[], int f_ln[], int n) {
    Inv(f, tmp_f_inv, n);
    Der(f, f_ln, n);
    int len = GetRev(n, n);
    for(int i = n; i < len; ++i) tmp_f_inv[i] = f_ln[i] = 0;
    NTT(tmp_f_inv, len, true); NTT(f_ln, len, true);
    for(int i = 0; i < len; ++i)
        f_ln[i] = 1LL * tmp_f_inv[i] * f_ln[i] % Mod;
    NTT(f_ln, len, false);
    Intg(f_ln, f_ln, n);
    for(int i = n; i < len; ++i) f_ln[i] = 0;
}
int tmp_f_ln[N];
void Exp(int f[], int f_exp[], int n) {
    if(n == 1) { f_exp[0] = 1; return; }
    Exp(f, f_exp, (n + 1) / 2);
    Ln(f_exp, tmp_f_ln, n);
    int len = GetRev(n, n);
    for(int i = 0; i < n; ++i)
        tmp_f_ln[i] = ((i == 0) - tmp_f_ln[i] + f[i] + Mod) % Mod;
    for(int i = n; i < len; ++i) f_exp[i] = tmp_f_ln[i] = 0;
    NTT(f_exp, len, true); NTT(tmp_f_ln, len, true);
    for(int i = 0; i < len; ++i)
        f_exp[i] = 1LL * f_exp[i] * tmp_f_ln[i] % Mod;
    NTT(f_exp, len, false);
    for(int i = n; i < len; ++i) f_exp[i] = 0;
}

}

int n, s, s_inv, p, w[N], b[N], F[N], G[N];
int main() {
    rd(n); Init(N - 1);
    for(int i = 1; i <= n; ++i) {
        rd(w[i]);
        s += w[i];
        if(i > 1) {
            ++b[w[i]];
        }
    }
    s_inv = QPow(s, Mod - 2);
    p = 1LL * w[1] * s_inv % Mod;
    ++s;
    for(int i = 1; i < s; ++i) {
        for(int j = i; j < s; j += i)
            F[j] = (F[j] - 1LL * inv[j / i] * b[i] % Mod + Mod) % Mod;
    }
    Poly::Exp(F, G, s);
    for(int i = 0; i < s; ++i) {
        if(n % 2 == 0) G[i] = (Mod - G[i]) % Mod;
        G[i] = 1LL * G[i] * p % Mod;
    }
    int ans = 0;
    for(int i = 0; i < s; ++i)
        ans = (ans + 1LL * G[i] * QPow((1 - 1LL * i * s_inv % Mod + Mod) % Mod, Mod - 2)) % Mod;
    printf("%d\n", ans);
    return 0;
}
]]>
0 /index.php/archives/46/#comments /index.php/feed/archives/46/
「Ynoi 2017」由乃打扑克 /index.php/archives/45/ /index.php/archives/45/ Thu, 18 Mar 2021 09:27:53 +0800 zhylj 题目

给定长度为 $n$ 的序列 $a_1,\cdots,a_n$,支持以下两种操作:

  • 区间加 $x$。
  • 区间第 $k$ 大。

$1\le n\le 10^5$,$|a_i|,|x|\le 2\times 10^4$.

分析

好久没有做数据结构题了。

设值域大小为 $c = 4\times 10^9$.

首先有个暴力是先二分然后变成区间加区间 $\le m$ 的元素个数,做法是维护块内有序,然后查询对块外重新排序,每个块单独加。

然后重新排序可以归并,所以上面那个算法可以做到 $\mathcal O(n\sqrt {n\log n})$,再套个二分 $\mathcal O(n\sqrt {n\log n}\log c)$,发现过不去。

设块大小为 $B$,注意到我们可以在二分开始前在 $\mathcal O(B)$ 的时间内提前把排好序的零散点拉出来,所以每次查询可以直接在上面二分。

然后时间复杂度就不平衡了,简单算一下发现单次查询的时间复杂度为:

$$ \mathcal O\left(\frac nB\log n\log c + B\right) $$

于是令 $B = \sqrt {n\log n\log c}$,于是单次查询的时间复杂度变为了 $\mathcal O(\sqrt {n\log n\log c})$.

最后总的时间复杂度就变为了 $\mathcal O(n\log n + q\sqrt {n\log n\log c})$,看上去很大但是玄学调调块长就能过了。

代码

const int N = 1e5 + 405, B = 3000, C = N / B + 5, Inf = 0x3f3f3f3f;

int A[N], tag[N], S[C][B], tmp[N];
void Merge(int a[], int a_len, int b[], int b_len, int c[]) {
    for(int i = 0, j = 0, k = 0; i < a_len || j < b_len; ++k) {
        if(i < a_len && (j >= b_len || A[a[i]] < A[b[j]]))
            tmp[k] = a[i++];
        else tmp[k] = b[j++];
    }
    for(int i = 0; i < a_len + b_len; ++i) c[i] = tmp[i];
}
void PushTag(int x) {
    if(!tag[x]) return;
    for(int i = 0; i < B; ++i)
        A[x * B + i] += tag[x];
    tag[x] = 0;
}
int tmp_b1[B], tmp_b2[B], cnt1, cnt2;
void ModifyInBlock(int l, int r, int pos, int v) {
    PushTag(pos);
    for(int i = l; i <= r; ++i)
        A[i] += v;
    cnt1 = 0, cnt2 = 0;
    for(int i = 0; i < B; ++i) {
        int x = S[pos][i];
        if(x >= l && x <= r) tmp_b1[cnt1++] = x;
        else tmp_b2[cnt2++] = x;
    }
    Merge(tmp_b1, cnt1, tmp_b2, cnt2, S[pos]);
}
void Modify(int l, int r, int v) {
    int pos_l = l / B, pos_r = r / B;
    if(pos_l == pos_r) {
        ModifyInBlock(l, r, pos_l, v);
    } else {
        ModifyInBlock(l, l + B - l % B - 1, pos_l, v);
        ModifyInBlock(r - r % B, r, pos_r, v);
        for(int i = pos_l + 1; i < pos_r; ++i) tag[i] += v;
    }
}
int GetRank(int a[], int a_len, int k) {
    int l = 0, r = a_len;
    while(l < r) {
        int mid = (l + r) >> 1;
        if(A[a[mid]] >= k) r = mid;
        else l = mid + 1;
    }
    return l;
}
int Check(int mid, int pos_l, int pos_r) {
    int rk = 0;
    for(int i = pos_l + 1; i < pos_r; ++i)
        rk += GetRank(S[i], B, mid - tag[i]);
    rk += GetRank(tmp_b1, cnt1, mid - tag[pos_l]);
    rk += GetRank(tmp_b2, cnt2, mid - tag[pos_r]);
    return rk + 1;
}
int Query(int l, int r, int k) {
    int pos_l = l / B, pos_r = r / B;
    cnt1 = 0, cnt2 = 0;
    if(pos_l != pos_r) {
        for(int i = 0; i < B; ++i) {
            int x = S[pos_l][i];
            if(x >= l) tmp_b1[cnt1++] = x;
        }
        for(int i = 0; i < B; ++i) {
            int x = S[pos_r][i];
            if(x <= r) tmp_b2[cnt2++] = x;
        }
    } else {
        for(int i = 0; i < B; ++i) {
            int x = S[pos_l][i];
            if(l <= x && x <= r) tmp_b1[cnt1++] = x;
        }
    }
    int L = -2e9, R = 2e9;
    while(L < R) {
        int mid = (L + R + 1) >> 1;
        if(Check(mid, pos_l, pos_r) > k) R = mid - 1;
        else L = mid;
    }
    return L;
}

int main() {
    int n, q;
    rd(n, q);
    A[0] = 2e9;
    for(int i = 1; i <= n; ++i) {
        rd(A[i]);
        S[i / B][i % B] = i;
    }
    for(int i = 0; i <= n / B; ++i) {
        std::sort(S[i], S[i] + B, [&](const int &x, const int &y) {
            return A[x] < A[y];
        });
    }
    while(q--) {
        int typ, l, r, k; rd(typ, l, r, k);
        if(typ == 1) {
            if(r - l + 1 < k || k < 0) printf("-1\n");
            else printf("%d\n", Query(l, r, k));
        } else if(typ == 2) {
            Modify(l, r, k);
        }
    }
    return 0;
}
]]>
0 /index.php/archives/45/#comments /index.php/feed/archives/45/
「Codeforces 506E」Mr. Kitayuta's Gift /index.php/archives/44/ /index.php/archives/44/ Tue, 16 Mar 2021 15:41:03 +0800 zhylj 题目

给定一个长度为 $n$ 的小写字母串 $s$,求有多少种在其中插入 $m$ 个小写字母的方案使得插入后是回文串,两个方案不同当且仅当得到的回文串不同,对 $10^4 + 7$ 取模。

$1\le n\le 200$,$1\le m\le 10^9$.

分析

相当于求有多少个长度为 $n + m$ 的回文串使得包含子序列 $s$.

先考虑 $n + m$ 是偶数的情况。

考虑一个简单的 dp,设 $f_{i,j,k}$ 表示贪心匹配 $s$ 到长度为 $i$ 的前缀和长度为 $j$ 的后缀(由于是回文串,所以可以从两侧贪心匹配),回文填了长度为 $k$ 的前缀和长度为 $k$ 的后缀的方案数。

那么当 $s_{i+1}=s_{j-1}$ 的时候,显然有如下转移:

$$ \begin{aligned} f_{i,j,k} & \to f_{i+1,j-1,k+1} \\ 25\cdot f_{i,j,k} & \to f_{i,j,k + 1} \end{aligned} $$

当 $s_{i+1} \neq s_{j-1}$ 的时候,有:

$$ \begin{aligned} f_{i,j,k} & \to f_{i+1,j,k+1},f_{i,j-1,k+1} \\ 24\cdot f_{i,j,k} & \to f_{i,j,k + 1} \end{aligned} $$

然后大力矩乘快速幂时间复杂度就做到了 $\mathcal O(n^6\log m)$,显然是过不去的。

考虑矩乘的过程相当于统计某个有限状态自动机上路径的个数,而状态表示为一个二元组 $(i,j)$。

图源 CF 官方题解

(连向结束状态的状态 $(i,j)$ 满足 $|i-j|\le 1$)

这个自动机上的状态可以分为三类:一类连接了 $24$ 条自环,也就是转移的时候满足条件「$s_i=s_j$」的点,不妨称其为红点;一类连接了 $25$ 条自环,也就是转移的时候满足条件「$s_i\neq s_j$」的点,不妨称其为绿点;最后一类连接了 $26$ 条自环,也就是结束状态。

我们考察这个自动机上的每条链,发现其上如果有 $i$ 个红点,则必然有 $\left\lceil\frac {n-i}2\right\rceil$ 个绿点。

于是,事实上只有 $n$ 条本质不同的链,于是我们事先做一遍 $\mathcal O(n^3)$ 的 dp 找到每条链的条数,就可以在 $\mathcal O(n^4\log m)$ 的时间内完成计算。

然后发现还是过不去,注意到我们只做链的过程中,链上点的顺序是无关紧要的,于是考虑先建 $n$ 个红点的链,再建 $\lceil n/2\rceil$ 个绿点的链,一条路径对应着将这两条链之间的某两个点连一条边(当然,这会导致我们多走一条边,所以矩乘的次数要加一)。

于是点数变成 $\mathcal O(n)$ 了,时间复杂度 $\mathcal O(n^3\log m)$,可以通过。

然后是 $n+m$ 是奇数的情况,发现如果按前面的统计我们会多计算终止状态之前一个状态形如 $(i,i+1)$ 的情况,于是以这些状态为终点再矩乘一遍,把不合法的减掉就好了。

时间复杂度不变,仍为 $\mathcal O(n^3\log m)$.

代码

const int N = 2e2 + 5, M = 3e2 + 5, Mod = 1e4 + 7;

char s[N];
int n, m, f[N][N][N], mat[M][M], m_siz, S, T;

void Update(int &x, int y) { x = (x + y) % Mod; }
void Build() {
    f[0][n + 1][0] = 1;
    for(int k = 0; k <= n; ++k)
        for(int len = n + 1; len > 1; --len)
            for(int i = 0; i + len <= n + 1; ++i) {
                int j = i + len;
                if(s[i + 1] == s[j - 1])
                    Update(f[i + 1][j - 1][k], f[i][j][k]);
                else {
                    Update(f[i + 1][j][k + 1], f[i][j][k]);
                    Update(f[i][j - 1][k + 1], f[i][j][k]);
                }
            }
    m_siz = n + (n + 1) / 2 + 2;
    S = 1; T = m_siz;
    for(int i = 1; i <= (n + 1) / 2; ++i) {
        mat[n + i + 1][n + i + 2] = 1;
        mat[n + i + 1][n + i + 1] = 25;
    }
    for(int i = 1; i <= n + 1; ++i) {
        if(i > 1) mat[i][i] = 24;
        if(i <= n) mat[i][i + 1] = 1;
    }
    for(int i = 0; i <= n; ++i) {
        int c = 0;
        for(int j = 0; j <= n; ++j) {
            Update(c, f[j][j + 1][i]);
            if(j) Update(c, f[j][j][i]);
        }
        mat[i + 1][T - (n - i + 1) / 2] = c;
    }
    mat[T][T] = 26;
}

void ReBuild() {
    m_siz = n + (n + 1) / 2 + 1;
    S = 1; T = m_siz;
    for(int i = 1; i <= (n + 1) / 2; ++i) {
        mat[n + i + 1][n + i + 2] = 1;
        mat[n + i + 1][n + i + 1] = 25;
    }
    for(int i = 1; i <= n + 1; ++i) {
        if(i > 1) mat[i][i] = 24;
        if(i <= n) mat[i][i + 1] = 1;
    }
    for(int i = 0; i <= n; ++i) {
        int c = 0;
        for(int j = 0; j <= n; ++j)
            Update(c, f[j][j + 1][i]);
        mat[i + 1][T - (n - i + 1) / 2 + 1] = c;
    }
}

int ret[M][M], bas[M][M], tmp[M][M];
void QPow(int b) {
    memset(ret, 0, sizeof(ret));
    for(int i = 1; i <= m_siz; ++i)
        ret[i][i] = 1;
    memcpy(bas, mat, sizeof(bas));
    for(; b; b >>= 1) {
        if(b & 1) {
            memset(tmp, 0, sizeof(tmp));
            for(int k = 1; k <= m_siz; ++k)
                for(int i = 1; i <= m_siz; ++i) if(ret[i][k])
                    for(int j = 1; j <= m_siz; ++j)
                        Update(tmp[i][j], ret[i][k] * bas[k][j]);
            memcpy(ret, tmp, sizeof(ret));
        }
        memset(tmp, 0, sizeof(tmp));
        for(int k = 1; k <= m_siz; ++k)
            for(int i = 1; i <= m_siz; ++i) if(bas[i][k])
                for(int j = 1; j <= m_siz; ++j)
                    Update(tmp[i][j], bas[i][k] * bas[k][j]);
        memcpy(bas, tmp, sizeof(bas));
    }
}

int main() {
    scanf("%s", s + 1);
    rd(m); n = strlen(s + 1);
    m += n;
    Build();
    QPow((m + 1) / 2 + 1);
    int ans_1 = ret[S][T];
    if(m % 2 == 0) printf("%d\n", ans_1);
    else {
        ReBuild();
        QPow((m + 1) / 2);
        int ans_2 = ret[S][T];
        printf("%d\n", (ans_1 - ans_2 + Mod) % Mod);
    }
    return 0;
}
]]>
0 /index.php/archives/44/#comments /index.php/feed/archives/44/
「学习笔记」拉格朗日反演 /index.php/archives/43/ /index.php/archives/43/ Sun, 14 Mar 2021 20:10:00 +0800 zhylj 前言

做小清新 AtCoder 题做腻了,来点多项式调节一下。

拉格朗日反演

分式环

记形式幂级数环为 $\mathbb R[[z]]$.

众说周知,$\mathbb R[[z]]$ 不是域。因为只有常数项非零的形式幂级数有乘法逆。

考虑定义一个分式环 $\mathbb R((z))$,其中每个元素形如:

$$ F(z) = \sum_{n\in \mathbb Z} a_nz^n $$

也就是存在负数次幂的多项式。

那么我们可以定义 $A(z)/B(z) = C(z)\in \mathbb R[[z]]~(B(z)\neq 0)$(形式除法,这里 $0$ 指加法单位元)为,找到一个 $d$ 使得 $z^{-d}B(z)=B_0(z)$ 可逆,然后就是:

$$ C(z) = A(z)B_0^{-1}(z)z^{-d} $$

然后这显然是个域。

拉格朗日反演

基础形式

对于两个常数项非零的多项式 $G(z),F(z)$,有:

$$ G(F(z)) = z \implies [z^n]F(z) = \frac 1n[z^{-1}]\frac 1{G^n(z)} $$

证明

有:$G(F(z))=z\iff F(G(z)) = z$.

记:

$$ F(z) = \sum_{i\ge 1} a_iz^i $$

那么我们有:

$$ F(G(z)) = \sum_{i\ge 1} a_iG^i(z) = z $$

两边同时求导:

$$ \sum_{i\ge 1} ia_iG^{i-1}(z)G'(z) = 1 $$

同除 $G^n(z)$ 并取 $z^{-1}$ 次项系数,则有:

$$ [z^{-1}]\sum_{i\ge 1} ia_iG^{i-n-1}(z)G'(z) = [z^{-1}]\frac 1{G^n(z)} $$

注意到对于所有 $i\neq n$,都有:

$$ G^{i-n-1}(z)G'(z) = \frac 1{i-n}(G^{i-n})'(z) $$

注意到任何的多项式 $G$ 求导完都不带 $z^{-1}$ 项.

于是只差 $i=n$ 的情况了,不妨设 $G(z) = \sum_{i\ge 1} b_iz^i$ 我们有:

$$ \begin{aligned} [z^{-1}]G^{-1}(z)G'(z) & = [z^{-1}]\frac {\sum_{i\ge 1} ib_iz^{i-1}}{\sum_{i\ge 1} b_iz^i} \\ & = [z^0]\frac {\sum_{i\ge 1} ib_iz^{i-1}}{\sum_{i\ge 1} b_iz^{i-1}}\\ & = [z^0]\frac {b_1z^{0}}{b_1z^0} \\ & = 1 \end{aligned} $$

于是就有:

$$ a_n = \frac 1n[z^{-1}]\frac 1{G^n(z)} $$

然后就证完了。

应用

我们注意到这个形式我们根本不会求,于是魔改一下:

$$ G(F(z)) = z \implies [z^n]F(z) = \frac 1n[z^{n-1}]\frac {z^n}{G^n(z)} $$

然后就可以开始做题了。

[scode type="lblue"]

有标号有根树计数:求 $n$ 个点的有标号有根树个数,对 $998244353$ 取模。

$1\le n\le 10^{18}$.

[/scode]

记有标号有根树个数的 EGF 为 $F(z)$,那么 $\exp F(z)$ 则为有标号有根森林的 EGF,再加个点就是有标号有根树的 EGF 了:

$$ F(z) = z\exp F(z)\iff \frac {F(z)}{\exp F(z)} = z $$

记 $G(z) = ze^{-z}$,那么显然有:

$$ [z^n]F(z) = \frac 1n[z^{n-1}]\frac {z^n}{G^n(z)} = \frac 1n[z^{n-1}]e^{nz} = \frac {n^{n-1}}{n!} $$

由于是 EGF,再乘个 $n!$ 就是答案了。

扩展拉格朗日反演

$$ G(F(z)) = H(z) \implies [z^n]G(z) = \frac 1n[z^{-1}]H'(z)\frac 1{F^n(z)} $$

或者:

$$ G(F(z)) = H(z) \implies [z^n]G(z) = \frac 1n[z^{n-1}]H'(z)\frac {z^n}{F^n(z)} $$

证法是一样的,唯一的区别在于原先求导时 $(x)' = 1$,而现在 $(H(x))' = H'(x)$.

图计数相关

边双连通图计数

[scode type="lblue"]

有标号边双连通图计数:求 $n$ 个点的有标号边双连通图个数,对 $998244353$ 取模。

$1\le n\le 10^{5}$.

[/scode]

首先,我们有有标号一般图的 EGF 为:

$$ F(z) = \sum_{i\ge 0} 2^{i\choose 2} \frac {z^i}{i!} $$

然后连通图的 EGF 显然为 $\ln F(z)$.

给第 $i$ 项系数乘上 $i$,就是有根连通图的 EGF,即:

$$ G(z) = z(\ln F(z))' = \frac {zF'(z)}{F(z)} $$

设 $B(z)$ 表示有根边双连通图的 EGF,那么考虑枚举根所在边双连通分量,然后把它删去,就剩下若干有根连通图了(令割边所连到的点为剩下的连通图的根,然后这样的根可以随意枚举),但需要注意的是,连接到根所在的边双的哪个点是没有被确定的,所以我们还需要枚举,设 $b_n = [z^n]B(z)$,则:

$$ \begin{aligned} G(z) & = \sum_{i\ge 0} \frac {b_iz^i}{i!} \sum_{j\ge 0}\frac {i^jG^j(z)}{j!} \\ & = \sum_{i\ge 0} \frac {b_iz^i}{i!}\exp (iG(z)) \\ & = \sum_{i\ge 0} \frac {b_i}{i!}(z\exp G(z))^i \\ & = B(z\exp G(z)) \end{aligned} $$

直接套用扩展拉格朗日反演:

$$ \begin{aligned} [z^n]B(z) &= \frac 1n[z^{n-1}]G'(z) \left(\frac z{z\exp G(z)}\right)^n \\ &= \frac 1n[z^{n-1}]G'(z) \exp (-nG(z)) \end{aligned} $$

直接把多项式板子拍上来就可以求了,时间复杂度 $\mathcal O(n\log n)$.

注意我们求的答案是有根的,并且是 EGF,所以要乘上 $n!/n=(n-1)!$.

点双连通图计数

[scode type="lblue"]

有标号点双连通图计数:求 $n$ 个点的有标号点双连通图个数,对 $998244353$ 取模。

$1\le n\le 10^{5}$.

[/scode]

和做边双连通图计数的时候一样,设有标号一般图的 EGF 为:

$$ F(z) = \sum_{i\ge 0} 2^{i\choose 2} \frac {z^i}{i!} $$

然后连通图的 EGF 为 $\ln F(z)$,而记有根连通图的 EGF 为 $G(z) = z(\ln F(z))'$.

再记无根点双连通图的 EGF 为 $B(z) = \sum_{i\ge 1} b_iz^i/i!$(特别地,$b_1=0$).

考虑寻找一般图和点双连通图 EGF 的关系,于是考虑一般图中圆方树的结构。显然的是,以一个圆点为根的子树的 EGF 是一个普通的有根无向图的 EGF:$G(z)$。

而对于一个以方点为根的子树,我们枚举方点所代表的点双连通分量大小 $i+1$,则这个方点下挂了 $i$ 个圆点(显然除了整张图只有一个点的情况,我们均有任何点双连通分量的大小不小于 $2$),于是以方点为根的子树的 EGF 为:

$$ \sum_{i\ge 1} b_{i+1}\frac {G^i(z)}{i!} = B'(G(z)) $$

于是,以圆点为根的子树的 EGF 也可以写为(注意根的标号在这里已经进行了,所以不需要定义 $B$ 为有根点双连通图):

$$ z\sum_{i\ge 1} \frac{B'^i(G(z))}{i!} = z\exp B'(G(z)) = G(z) $$

把 $z$ 除过去,再两边同时取 $\ln$,则有:

$$ B'(G(z)) = \ln \frac {G(z)}{z} = A(z) $$

套用扩展拉格朗日反演,可得:

$$ \begin{aligned} [z^n]B'(z) & = \frac 1n[z^{n-1}]A'(z) \left(\frac {z}{G(z)}\right)^n \\ & = \frac 1n[z^{n-1}]A'(z) \exp\left(-n\ln \frac {G(z)}{z}\right) \\ & = \frac 1n[z^{n-1}]A'(z) \exp\left(-nA(z)\right) \end{aligned} $$

直接上多项式板子即可,时间复杂度 $\mathcal O(n\log n)$.

参考资料

]]>
0 /index.php/archives/43/#comments /index.php/feed/archives/43/
「AGC 028D」Chords /index.php/archives/42/ /index.php/archives/42/ Sun, 14 Mar 2021 17:12:00 +0800 zhylj 题目

有一个圆,圆上均匀分布着 $2n$ 个点,将点两两匹配,并给匹配的点连接一条线段,定义两个点连通当且仅当它们可以只通过连接的线段和线段之间的交点走到对方。

已经匹配好了 $m$ 对点,求所有将剩下 $2(n - m)$ 个点匹配的方案的连通块个数之和,对 $10^9 + 7$ 取模。

$1\le m\le n\le 300$.

分析

显然在圆上和在一个弧上连出来的效果是一样的。

设 $f_{i,j}$ 表示将 $i,j$ 连到一个连通块中,且 $i,j$ 分别为这个连通块中编号最小和最大的点,只考虑 $[i,j]$ 这部分点的方案数。

再设 $g_i$ 表示将 $i$ 个点两两连边的方案数,显然有:

$$ g_i = \prod_{0\le 2j\le i} (i-2j) $$

再记 $\operatorname{cnt}(i,j)$ 表示 $[i,j]$ 中还未确定连边的点数,则显然答案为:

$$ \sum_{1\le i\le j\le n} f_{i,j}\cdot g_{n-2m+\operatorname{cnt}(i,j)} $$

对于如何求 $f_{i,j}$,考虑显然区间内外不能连边,所以若区间内存在连向区间外的边,则 $f_{i,j} = 0$.

否则考虑先随便连边,这部分的方案数是 $g_{\operatorname{cnt}(i,j)}$,注意到这样 $i,j$ 不一定会被连到一个连通块中,但 $i$ 一定属于某个连通块,考虑枚举这个连通块的右端点为 $k$,则有:

$$ f_{i,j} = g_{\operatorname{cnt}(i,j)} - \sum_{i\le k<j} f_{i,k}\cdot g_{\operatorname{cnt}(k+1,j)} $$

然后就做完了,时间复杂度 $\mathcal O(n^3)$.

代码

const int kN = 6e2 + 5;
const ll kMod = 1e9 + 7;

int n, m, t[kN]; ll f[kN][kN], c[kN][kN], g[kN];
int main() { 
    rd(n, m); n *= 2;
    for(int i = 1; i <= m; ++i) {
        int u, v; rd(u, v);
        t[u] = v; t[v] = u;
    }
    g[0] = 1;
    for(int i = 1; i <= n; ++i) {
        for(int j = i; j <= n; ++j)
            c[i][j] = c[i][j - 1] + (t[j] == 0);
        g[i] = 1;
        for(int j = i - 1; j >= 0; j -= 2)
            g[i] = g[i] * j % kMod;
    }
    for(int len = 1; len <= n; len += 2) {
        for(int i = 1; i + len <= n; ++i) {
            int j = i + len; bool flag = true;
            for(int k = i; k <= j; ++k)
                if(t[k] && (t[k] > j || t[k] < i))
                    flag = false;
            if(flag) {
                f[i][j] = g[c[i][j]];
                for(int k = i; k < j; ++k)
                    f[i][j] = (f[i][j] - f[i][k] * g[c[k + 1][j]] % kMod + kMod) % kMod;
            }
        }
    }
    ll ans = 0;
    for(int i = 1; i <= n; ++i)
        for(int j = i; j <= n; ++j)
            ans = (ans + f[i][j] * g[n - 2 * m - c[i][j]]) % kMod;
    printf("%lld\n", ans);
    return 0;
}
]]>
0 /index.php/archives/42/#comments /index.php/feed/archives/42/