2023年5月做题简要记录1

P3761 [TJOI2017]城市

给定一棵带权树,你可以选择断掉任意一条边,然后加上一条边权相同的新边,让城市之间距离的最大值最小。

边数 n5000n\le 5000 ,时间限制 3000 ms3000\ \text{ms}

看数据范围,可以用 O(n2)\Omicron(n^2) 算法。

先枚举断掉哪条边,然后最大值可能是下面这些情况之一:

  1. 在同一个树中:求直径即可。
  2. 经过新建的边,即一个在第一个树,另一个在第二个树。

考虑情况2,即在两个树中各选一个点,使得距离树中任意点的最大值最小。

dfs ,考虑点 uu 到树中任意点的最大值,有两种情况,1是经过父亲走下来,2是在自己的子树中。

1的话从传入一个参数 dd ,表示从父亲传下来的最大值,2的话已经在求直径时候求出了。

uu 转移到 vv ,假如 uu 的最长链是从 vv 接上来的,那么就不能把 uu 的最长链计入 vvdd 中,否则就把 uu 的最长链计入 vvdd 中,即 d=w+max(d,f[u])d’=w + \max(d, f[u]) 或者 d=w+max(d,g[u])d'= w + \max(d,g[u])

提交记录

P5283 [十二省联考 2019] 异或粽子

给定一个序列 aa ,要选出 kk(l,r)(l,r) ,使得 (l,r)alal+1ar\sum_{(l, r)} a_l \oplus a_{l+1} \oplus \cdots \oplus a_r 最大。

不难发现可以用前缀和,令 si=j=0iais_i = \sum_{j=0}^i a_i ,那么 alal+1ar=srsl1a_l \oplus a_{l+1} \oplus \cdots \oplus a_r = s_r \oplus s_{l-1}

使用可持久化trie在 O(logn)\Omicron(\log n) 求出 lprl\le p\le r ,使得 spxs_p \oplus x 最大。

我们可以这么做(虽然这么做是对的,但我不理解为什么要这么做):

可以用一个大根堆维护,(x,j,i,l,r)(x, j,i,l,r) 表示 x=sjxix=s_j\oplus x_i 所有可以选出的 j,ij, i,还有 jj 可以选的范围 [l,r][l, r]

每次取出最大的 xx ,计入答案,然后把 [l,r][l, r] 分成两个区间 [l,j1],[j+1,r][l, j-1], [j+1, r] ,在这两个区间中分别选出 j1,j2j_1, j_2 使得 sj1sis_{j_1} \oplus s_isj2sis_{j_2} \oplus s_i 最大,然后重新插入大根堆中。

提交记录

P4064 [JXOI2017]加法

给定一个序列 AAmm 个区间 [li,ri][l_i, r_i] ,要选择 kk 个区间,对于每个区间执行一次区间加 aa (每个区间只能选择一次),最大化序列最小值。

最大化序列最小值,考虑用二分最小值,对于比这个值要小的 AiA_i ,要选择若干个区间加 aa ,使之不比 limlim 小,那么用哪些区间呢?贪心地用右端点最远的区间覆盖,为什么?一个区间还没有被使用,说明在这个点左边并不需要这个区间,而用右端点最远的区间,有可能覆盖更多需要的 AiA_i

提交记录

P4454 [CQOI2018]破解D-H协议

给定 g,p,A,Bg, p, A, B ,满足 A=ga(modp)A = g^a (\bmod p), B=gb(modp)B = g^b (\bmod p) ,求 gab(modp)g^{ab} (\bmod p)

显然求出 a,ba, b 之一即可。

考虑使用 BSGS 算法:

bx=n(modp)b^x = n (\bmod p) ,已知 b,n,pb, n, p ,求 xx

x=itjx = it-j ,其中 t=p,i[1,t],j[0,t)t = \lceil\sqrt p\rceil, i \in [1, t], j \in [0, t)

那么

bx=n(modp)bitj=n(modp)(bt)i=nbj(modp)\begin{align*} b^x &=& n (\bmod p) \\ b^{it-j} &=& n (\bmod p) \\ (b^t)^i &=& n\cdot b^j (\bmod p) \\ \end{align*}

显然预处理出所有的 nbjn \cdot b^j ,存起来,然后枚举 ii 就行了。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ll bsgs(ll b, int n)
{
map<ll, int> mp;
n %= P;
ll t = sqrt(P) + 1;
for (int i = 0; i < t; i++) mp[n * qpow(b, i) % P] = i;
b = qpow(b, t);
if (!b) return !n ? 1 : -1;
for (int i = 1; i <= t; i++) {
ll x = qpow(b, i);
int j = mp.count(x) ? mp[x] : -1;
if (~j && i * t - j >= 0) return i * t - j;
}
return -1;
}

提交记录

P4065 [JXOI2017]颜色

给定序列 aia_i ,问有多少种方案数使得删去若干种数字后,最后剩下来的序列非空且不断开(例如,对于 1,2,3,4,51, 2, 3, 4, 5 ,删去 2,32, 3 之后,变成了 1,_,_,4,51, \_, \_, 4, 5 不合法,若删去 11 ,变成 2,3,4,52, 3, 4, 5 是合法方案)。

显然要在原序列中选择非空的一段,使得这段中的所有数字仅在这段中出现过。有两种解法。

解法一:

对于每个位置,赋一个随机的值 bib_i,并且使得所有同个数字位置上的值的和为 00ai=xbi=0\sum_{a_i=x} b_i = 0) ,然后对于 bib_i 做前缀和 sis_i,到每个位置 ii 统计之前有多少个 jjsi=sjs_i = s_j ,加起来即可。

一种可能的赋值方案是:设 cx=1in:ai=xc_x = \langle 1 \le i \le n: a_i=x \rangle ,那么 1j<cx, bi=rand(),bcx=1j<cxbj1 \le j \lt |c_x|,\ b_i = \operatorname{rand}(), b_{|c_x|} = -\sum_{1\le j \lt |c_x|} b_j

解法二:

先枚举右端点,然后考虑有多少个合法的左端点。设颜色 xx 最小的位置为 mnxmn_x ,最大的位置为 mxxmx_x 。先考虑段中的所有颜色不能在左端点之左:枚举到颜色 xxmxxmx_x 时,把线段树上 mnx+1mxxmn_x+1 \cdots mx_x 设为 11 ,表示这些点不能成为左端点。再考虑段中的颜色不能再右端点之右,即若 mxx>rmx_x \gt r ,那么 xx 一定不能被选中,用栈维护所有的 mxx>rmx_x > r 的点,从左至右把每个的ii 压入栈,然后弹出所有 mxairmx_{a_i} \le rii ,然后此时的栈顶就是不能取到的左端点,即此时的左端点为 l=top()+1l=\operatorname{top()} + 1 ,答案为 rl+1seg.query(lr)r-l+1-seg.\operatorname{query}(l\cdots r)

提交记录

P4782 【模板】2-SAT 问题

要解决一个 2-SAT 问题:有 nn 个布尔变量,x1xnx_1 \cdots x_n ,和 mm 个条件,每个条件要求 xi=axj=bx_i=a \lor x_j = b ,问是否存在一种取值使得满足所有条件,求一种可能取值,或报告无解。

一个 k-SAT 问题就是有若干个布尔变量 xix_i ,和若干个条件,每个条件形如 xa1=b1xak=bkx_{a_1}=b_1 \lor \cdots \lor x_{a_k}=b_k ,问是否存在一组解满足所有条件。

k>2k \gt 2 ,这问题已经被证明是 NP 完全的了,不存在多项式时间内的算法,但若 k=2k=2 ,则有一种特殊的算法可以在 O(n+m)\Omicron(n+m) 内解决。

考虑建出一个无向图,每条边都代表这一个「蕴含」限制,即若存在一条边从 xx 连向 yy ,那么就表示 xyx\rarr y ,其的真值表如下:

xx yy xyx\rarr y
0 0 1
0 1 1
1 0 0
1 1 1

不难发现,就是要求当 xx 为 1 时,yy 也要为 1 。

然后就是对这张图填 0 或 1,问是否存在方案,但你可能会说了:全填 0 或者全填 1 不久可以了吗,不要着急,接下来会介绍一些条件,有若干对点必须有且仅有一个点填 1。

我们把一个变量 xx 拆开来,变成 ¬x\lnot xxx ,对于限制 aba \lor b ,可以被拆成 (¬ab)(¬ba)(\lnot a \rarr b )\land (\lnot b \rarr a) 。(正确性不难用真值表证明,此处略去)。

建成这张图后,进行缩点,显然在一个强连通分量内的点必须同选 0 或者 1,若 ¬x\lnot xxx 在同一个强连通分量内,就无解。

否则,一定有解,考虑对缩点后的图进行拓扑排序,若 ¬x\lnot x 所在的强连通分量的拓扑序小于 xx 所在的强连通分量的拓扑序,则 x=0x=0 ,否则 x=1x=1

你可能会问,假如出现这样的情况:a¬ab¬ba \rarr \lnot a \rarr b \rarr \lnot b ,上面的方案不久不合法了吗?

实际上这种情况不会出现,¬ab\lnot a \rarr b 原来是 aba \lor b ,还有一条边 ¬ba\lnot b \rarr a ,形成了一个强连通分量,不合法,类似情况同理可以证明不会出现。

实现细节:可以不用跑拓扑序,直接用 Tarjan 跑出来的强连通分量编号,其大的,拓扑序反而小。

提交记录

P4151 [WC2011]最大XOR和路径

给定一张无向图,有边权,求一条从 11nn 的路径,使得其边权异或和最大。

由于异或操作的良好性质,我们先钦定一条从 11nn 的路径,然后考虑往路径上面加环,显然我们从路径上某个点,走到环上的某个点,绕完整个环,再沿着同样的路走回来,只有那个环上的路径异或和会被计入答案,任意一条可能的路径都可以被表示为这样的形式。然后用线性基处理所有环异或起来的最大值即可。正确性可能不显然,感性理解一下。

提交记录

P3991 [BJOI2017]喷式水战改

有一个序列,初始为空,有若干个操作,会在序列中第 pip_i 个元素个元素后插入 xix_i 个第 ii 种元素,第 ii 种元素有属性 ai,bi,cia_i, b_i, c_i ,每次求这个序列的最大价值。

定义这种序列的价值是,你要这个序列分成四段子序列,每个子序列可能为空,第 1 个子序列和第 4 个子序列的价值是所有元素的 aia_i 之和,第 2 个子序列是 bib_i 之和,第 3 个子序列是 cic_i 之和,然后求和。

见到插入操作,考虑用平衡树维护,每个节点维护一段连续的相同种类的元素。

显然,对于每段连续的元素,因为每段可以为空,所以划分到一个子序列中是不劣的。

fu[i][j]f_u[i][j] 为以 uu 为根的节点,整棵子树代表的区间,左分到了第 ii030\cdots 3) 个子序列,右分到了第 jj 个子序列的最大值,转移方程可以参照代码,每次 splitmerge ,左右儿子变更时要更新一下。

另外,插入一个新的段的时候,有可能把原来的一段拆成了两端,具体实现较为繁琐。

提交记录

P4577 [FJOI2018] 领导集团问题

给定一棵树,求树上满足如下条件的大小最大的点集:

对于任意点集中的点 i,jS,iji, j \in S, i \not= j ,若在原树中 uuvv 的祖先,则 wuwvw_u \le w_v

考虑序列上的LIS的做法,我们也可以叫这道题“树上LIS”。

对于每个节点 uu 维护一个序列 f[u]f[u] ,其中 f[u][i]f[u][i] 代表着在 uu 为根的子树内,所有选取 ii 个节点的方案 TT 中,minjTwj\min_{j\in T} w_j 的最大值。

节点 uu 的答案就是最大 ii ,使得 f[u][i]f[u][i] 合法。

考虑合并所有的 vsonuv \in son_u ,显然子树中的答案互相不影响,对于一个 ii ,可以贪心地选择出最大 ii 个。

考虑加入 wuw_u ,仿照LIS的做法,从 f[u]f[u] 中找出最小的 ii 使得 wiwuw_i \ge w_u ,显然把 wiw_i 换成 wuw_u 不会使得答案更劣,即 f[u][i]=wuf[u][i] = w_u (正确性不太好理解)。

由于 f[u]f[u] 有单调型,用 multiset 维护 f[u]f[u] ,合并 uu 的儿子时启发式合并即可,用 lower_bound 找到 wiwuw_i \ge w_u ,答案就是 f[1]|f[1]|

提交记录

P4591 [TJOI2018]碱基序列

给定一个字符串 ss ,和若干组字符串 tijt_{ij} ,问有多少组方案,从每组字符串中选择一个,首位拼接,能形成了 ss 的子串。

f[i][j]f[i][j] 表示获得了匹配到 sis_i ,用了前 jj 组字符串的方案数。

f[i][j]=1kaj,lf[l1][j1](s[li]=tjk)f[i][j] = \sum_{1 \le k \le a_j, l} f[l-1][j-1]\cdot (s[l \cdots i] = t_{jk})

可以用哈希判断 s[li]=tjks[l\cdots i] = t_{jk}

提交记录

P3762 [TJOI2017]龙舟

给定 bib_iaija_{ij} ,多次询问,给定 x,Mx, M ,求 b1××bmax1××axmmodM\frac {b_1\times \cdots \times b_m} {a_{x1}\times \cdots \times a_{xm}} \bmod M ,不保证 MPM \in \mathbb{P}

M,aij,bi<2×1018,m10000,询问数50M, a_{ij}, b_i \lt 2\times 10^{18}, m \le 10000, 询问数 \le 50

先介绍两个科技算法:

一:Miller Rabin\mathcal{Miller\ Rabin} 素数判定。

对于一个很大的数 n<2×1018n \lt 2\times 10^{18} ,判断是否是质数。

先筛掉唯一一个偶素数 n=2n = 2

首先会想到费马小定理,若 nPn \in P ,对于任意的 a[1,n1]a \in [1, n-1] ,有:

an1=1(modn)a^{n-1} = 1 (\bmod n)

但是也有一些合数满足这个等式,我们需要更强的限制。

考虑二次探测,若 nP,x<nn\in P, x < n

x2=1(modp)x{1,n1}x^2 = 1 (\bmod p) \rArr x\in \{1,n-1\}

结合费马小定理:

an1=(an12)2=1(modn)an12{1,n1}a^{n-1} = (a^{\frac{n-1}2})^2 = 1(\bmod n) \Rarr a^{\frac{n-1}2}\in\{1,n-1\}

an12=1a^{\frac{n-1}2}=1 ,继续考虑 an14,an18,a^{\frac{n-1}4},a^{\frac{n-1}8},\ldots 直到 n12k\frac {n-1} {2^k} 为奇数。

若有一个判断未通过,就肯定不是质数了。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef __int128_t lll;
lll Miller_Rabin(lll n)
{
if (n == 2) return true;
static const int TEST_TIME = 10;
uniform_int_distribution<uint64_t> d(2, n - 1);
for (int _ = 1; _ <= TEST_TIME; _++) {
lll a = d(eng);
if (qpow(a, n - 1, n) != 1) return false;
lll p = n - 1;
for (; !(p & 1); p /= 2) {
lll x = qpow(a, p, n);
if (x * x % n == 1 && x != 1 && x != n - 1) return false;
}
}
return true;
}

二:Pollar Rho\mathcal{Pollar\ Rho} 算法:

Pollar Rho 算法就是快速找到大整数的一个非 1,非自己的因数算法。

先介绍生日悖论:若有23个人,他们的出生日期均匀分布,那么存在两个人生日相同的概率超过一半(这不难证明 365365×364365×36522365<1/2\frac{365}{365} \times \frac{364}{365} \times \cdots \frac{365-22}{365} \lt 1/2 )。

推广一下,若在 [1,N][1, N] 之间均匀生成数,那么期望 O(n)\Omicron(\sqrt n) 次后会生成重复的数。

考虑 nn 的最小因子 1<pn1 \lt p \le \sqrt n ,期望随机生成 O(n1/4)\Omicron(n^{1/4}) 个数后会出现模 pp 意义下相同的数,那么这两个数的差的绝对值 dd 满足 gcd(d,n)>1\gcd(d, n) > 1 ,可以直接返回 gcd(d,n)\gcd(d, n)

但假如暴力判断的话,复杂度又回到 O(n1/4 logn)\Omicron(n^{1/4}\ \log n) 的了。所以我们设计一种特殊的伪随机数生成器:

0,f(0),f(f(0))),f3(0),, let f(x)=(x2+c)modNc=randint(1n1)\langle 0, f(0), f(f(0))), f^3(0), \ldots\rangle,\ \text{let } f(x) = (x^2+c)\bmod N, c = \operatorname{randint}(1 \cdots n-1)

首先这个生成器会有一个问题,就是可能跳入一个循环出不来了,这时候我们需要换一个 cc

这里采用 Floyd 判环算法,伪代码如下:

1  x=f(0), y=f(f(0))2  while xy3          (Do something.)4          x=f(x), y=f(f(y))5  endwhile\begin{align*} 1\ \ & x = f(0),\ y = f(f(0)) \\ 2\ \ & \operatorname{while}\ x \not= y \\ 3\ \ & \ \ \ \ \ \ \ \ \text{(Do something.)} \\ 4\ \ & \ \ \ \ \ \ \ \ x = f(x),\ y = f(f(y)) \\ 5\ \ & \operatorname{endwhile} \end{align*}

假如走到了一个环,yyxx 更快,肯定会在环上某处相遇。

这样有什么好处呢?

ij=0(modp)f(i)f(j)=i2j2=iji+j=0(modp)|i - j| = 0 (\bmod p) \rArr |f(i)-f(j)| = |i^2-j^2| = |i-j|\cdot |i+j| = 0 (\bmod p)

说明

i, j=fd(i)ij=0(modp)i, j=fd(i)ij=0(modp)\exist i,\ j = f^d(i) \rarr |i-j| = 0 (\bmod p) \\ \Darr \\ \forall i,\ j = f^d(i) \rarr |i-j| = 0 (\bmod p) \\

我们每次可以验证所有「距离」为 dd 的点对。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
lll Pollard_Rho(lll n)
{
if (Miller_Rabin(n)) return -1;
if (n == 4) return 2;
uniform_int_distribution<uint64_t> d(1, n - 1);
while (true) {
lll c = d(eng);
auto f = [c, n](lll y) { return (y * y % n + c) % n; };
lll x = f(0), y = f(f(0));
while (x != y) {
lll z = gcd(abs(x - y), n);
if (z > 1) return z;
x = f(x), y = f(f(y));
}
}
}

为了表示方便,令 ci=axic_i = a_{xi}

由于一些取模的神秘原因(我不会,但不这么做不对),对于要处理的 bib_icic_i ,先消去共同的 MM 的质因数。

为了方便,对于 x=tykx = t\cdot y^k ,定义 cnt(x,y)=k,left(x,y)=t\operatorname{cnt}(x,y)=k, \operatorname{left}(x,y)=t

xPxM, fx=icnt(bi,x)icnt(ai,x)xPxM, bileft(bi,x)xPxM, cileft(ci,x)x \in \mathbb{P} \land x|M,\ f_x = \sum_i \operatorname{cnt}(b_i, x) - \sum_i \operatorname{cnt}(a'_i, x) \\ \forall x \in \mathbb{P} \land x|M,\ b_i \larr \operatorname{left}(b_i, x) \\ \forall x \in \mathbb{P} \land x|M,\ c_i \larr \operatorname{left}(c_i, x) \\

若有 fx<0f_x \lt 0 ,那么无解。

然后答案就是

ibiiciixfx\frac {\prod_i b_i} {\prod_i c_i} \prod_i x^{f_x}

求逆元需要用欧拉定理算 φ(M)\varphi(M)

提交记录

2023年4月做题简要记录

P5304 [GXOI/GZOI2019]旅行者

给定一张有向图图,有若干个关键点,求所有关键点中距离最短的一对的距离。

n,m105n, m \le 10^5

每次选取两个点集 AABB ,以 AA 为起点,跑一边最短路,则 minbBdis[B]\min_{b \in B} dis[B]minaA,bBdis(a,b)\min_{a \in A, b \in B} \operatorname{dis}(a, b)

考虑目标点对 aba \not= baa 中的一位二进制和 bb 中的一位二进制不相同,所以每次通过二进制上的某一位划分 AABB ,跑 logn\log n 次最短路即可。

注意有向图,每次要从 AA 跑一次,从 BB 跑一次

提交记录

P3248 [HNOI2016]树

有一棵「模板树」,下称 树A 。

有一棵「大树」,下称 树B 。

最初,树B和树A是一样的。

每次操作,把树A中以某个点为根的子树复制到树B的某个点下(树A中的那个点成为新的树B的儿子),然后按在原树中的编号大小顺序重新标号。

然后若干次询问,每次询问两个点之间的距离。

询问树上的距离,常用的操作是lca+深度,这里也这么做。

考虑每次操作,形成一个新的「块」,所有的块构成了一棵树,我们维护这棵树上的深度和lca。

重新标号用主席树处理(以 uu 为根的子树中编号第 ii 小的节点,或者以 uu 为根的子树中 xx 第几小),在 dfndfn 序列上做区间第 kk 小即可。

树A中的编号与树B中的编号极容易弄混,需要认真对待。

提交记录 (loj Ofast+少爷机就是好)

n,m105n, m \le 10^5

P3267 [JLOI2016/SHOI2016]侦察守卫

给定一棵树和若干个树上的点,每次可以在 uuwuw_u 的代价覆盖距离 uu 小于等于 dd 的点,求覆盖所有的关键点需要的最小代价。

对于每个点,设 f[u][i]f[u][i] 表示还要满足还要向下覆盖 ii 层最小代价, g[u][i]g[u][i] 表示可以向上覆盖 ii 层最小代价。

那么有,初始化

g[u][0]=w[u]Important?(u)g[u][0] = w[u]\cdot \operatorname{Important?}(u)

i>1,g[u][i]=w[u]i\gt 1, g[u][i]=w[u]

转移

f[u][i]=f[u][i]+f[v][i1]g[u][i]=min(g[u][i]+f[v][i],f[u][i+1]+g[v][i+1])g[u][i]=minj>i(g[u][i],g[u][j]f[u][i]=minj<i(f[u][i],f[u][j]f[0]=g[0]f[u][i] = f'[u][i] + f[v][i - 1]\\ g[u][i] = \min(g[u][i] + f[v][i], f'[u][i + 1] + g[v][i+1])\\ g[u][i] = \min_{j>i}(g[u][i], g[u][j]\\ f[u][i] = \min_{j<i}(f[u][i], f[u][j]\\ f[0] = g[0]

(关于 g[u][i]f[u][i+1]+g[v][i+1]g[u][i] \leftarrow f'[u][i+1] + g[v][i+1] 的转移解释)

1
2
3
4
5
6
7
8
9
10
U 还能向上  D 还能向下

[u] (U i D i)
+--------------------+ --- (Total: i+1)
| | |
[v] (U i+1) [w] (D i-1) |
. |
. |
. |
---

提交记录

P3687 [ZJOI2017]仙人掌 题解

有一张图,问有多少种加边方案,使得加边后的图是一个仙人掌(一条边至多在一个简单环中)。

若最初的图不是仙人掌,返回 0 。

否则删去所有的环边,得到一个森林,考虑每棵树的情况。

对于每棵子树,强制选择其中的一个点可以向上连边(不连的情况就是其向它的父亲连一条重边),设为 f[u]f[u]

对于点 uu 考虑儿子 vv 中的所有向上连边的点,他们有两种方案:

  1. 连向 uu
  2. 连向其他的 vv 中的向上连边的点。

我们用 h[i]h[i] 表示 ii 个点按以上方案连边的的方案数:

h[i]=h[i1]  这个点连向u+(i1)h[i2]  从剩下i1个点中选一个\begin{align*} h[i] &=& h[i-1] &\ \ \text{这个点连向} u \\ &+& (i-1)h[i-2] &\ \ \text{从剩下} i-1 \text{个点中选一个} \end{align*}

所以有

f[u]=h[deg[u]]vson[u]f[v]  选择u作为向上连边的点+vh[deg[u]1] f[v]wson[u],wvf[w]  选择v作为向上连边的点=h[deg[u]+1]vson[u]f[v]\begin{align*} f[u] &=& h[deg[u]] \prod_{v \in son[u]} f[v] & \ \ \text{选择} u \text{作为向上连边的点} \\ &+& \sum_{v} h[deg[u] - 1]\ f[v] \prod_{w \in son[u], w \not= v} f[w] & \ \ \text{选择} v \text{作为向上连边的点} \\ &=& h[deg[u] + 1] \prod_{v \in son[u]} f[v] \\ \end{align*}

但是对于根,由于不能选择其作为向上连边的点(为什么?仔细想一下,其不能向上连边,向下的情况都已经计算过了)。

所以

f[root]=h[deg[root]]vson[root]f[v]f[root] = h[deg[root]] \prod_{v \in son[root]} f[v]

提交记录

P3643 [APIO2016] 划艇

有若干个篮子,每个篮子要么不放东西,要么放 [ai,bi][a_i, b_i] 个东西,且要大于之前所有篮子放东西的个数。

n500,aibi109n \le 500, a_i \le b_i \le 10^9

由于 ai,bia_i, b_i 非常大,考虑离散化,形成若干个区间 [ai,bi)[a'_i, b'_i)

考虑 ii 在区间 jj 内选择,kk 是第一个在区间 jj 之前选择的, k+1ik+1 \cdots i 都在区间 jj 内选择,1k1 \cdots k 都在区间 1j1 \cdots j 内选择

令其的方案数为 f[i][j]f[i][j] (强制 ii 选择, k<ik \lt i 随意)。

那么有 f[i][j]=k=0i1l<jf[k][j] sum(k+1,i)f[i][j] = \sum_{k=0}^{i-1} \sum_{l\lt j} f[k][j]\ sum(k + 1, i)

其中 sum(k+1,i)sum(k + 1, i) 为从 k+1k+1ii 中,在区间 jj 内选择的方案数(如果无法满足就不选, ii 必须选)。

c=cnt(j,k+1i)c = cnt(j, k+1 \cdots i) ,即 k+1ik + 1\cdots i 中可以取到区间 jj 的方案数,那么 sum(k+1,i)=(b[j]a[j]+c1c)sum(k + 1, i) = {b'[j] - a'[j] + c - 1 \choose c}

提交记录

P4139 上帝与集合的正确用法

222modp2^{2^{2^\cdots}} \bmod p

有扩展欧拉定理,对于任意 bφ(p)b \ge \varphi(p)

ab=a(b modφ(p))+φ(p)(modp)a^b = a^{(b\ \bmod \varphi(p)) + \varphi(p)} (\bmod p)

显然 222>>φ(p)2^{2^{2^\cdots}} >> \varphi(p)

f(p)={0p=12f(φ(p))+φ(p) mod potherwisef(p) = \begin{cases} 0 & p=1 \\ 2^{f(\varphi(p)) + \varphi(p)}\ \bmod\ p & \text{otherwise} \end{cases}

P3750 [六省联考 2017] 分手是祝愿

给定一个 01 串,每次操作,给定一个 ii 翻转所有 1ji,ji1 \le j \le i, j | i ,要让全为 00

若当前能在 kk 步之内完成操作,则进行这个操作,否则随机进行一个操作,求期望。

n105n \le 10^5

首先考虑初始状态的最优策略,由于一个 ii 不会被 j<ij \lt i 修改,所以可以从大到小进行贪心操作,设这时的最少操作次数为 cc ,操作的顺序无关紧要。

考虑 dp ,令 f[i]f[i] 表示当前可以最少恰好 ii 步完成操作的期望。

那么对于 iki \le k ,有 f[i]=if[i] = i

否则 f[i]=inf[i1]+ninf[i+1]+1f[i] = \frac i n f[i-1] + \frac {n-i} n f[i + 1] + 1 ,有 in\frac i n 的概率进行正确的操作,剩下 nin\frac {n-i} n 的概率进行错误的操作,可以证明,至少需要再操作一次(按回来)。

对于 f[i]f[i] 的转移,推式子,令 f[i]=f[i1]+b[i]f[i] = f[i-1]+b[i] :

f[i]=in(f[i]b[i])+nin(f[i]+b[i+1])+10=inb[i]+ninb[i+1]+1b[i]=(ni)b[i+1]+ni\begin{align*} f[i] &=& \frac i n (f[i] - b[i]) + \frac {n-i} n (f[i] + b[i+1]) + 1 \\ 0 &=& -\frac i n b[i] + \frac {n-i} n b[i+1] + 1 \\ b[i] &=& \frac {(n-i)b[i+1] + n} i \\ \end{align*}

提交记录

P3270 [JLOI2016] 成绩比较

nn 个人,mm 门课 ,和一个人 B\mathcal{B 神} ,有 kk 个人的每门课的成绩都比B神低(小于等于),同时每门课有最高分 UiU_i 最低分 11,B神的排名是 RiR_i (有且仅有 Ri1R_i - 1 个人分数大于B神),求满足以上情形的方案数。

考虑 dp 。

f[i][j]f[i][j] 为考虑前 ii 个人,有 jj 个人每门课的分数都比 B 神低,那么有如下转移

f[i][j]=k=jn1f[i1][k](kkj)(nk1Ri1(kj))g[i]f[i][j] = \sum_{k = j}^{n-1} f[i-1][k] {k \choose k-j} {n-k-1 \choose R_{i}-1-(k-j)} g[i]

其中 g[i]g[i] 为对于第 ii 门课,选定有谁分数大于B神的情况下的方案数。

考虑这门课有谁的分数比 B神高,那么他就被从“比B神低”中除名了。

考虑上一次有 kk 个人,那么 (kkj)k \choose k-j 为从其中选出有多少人分数第一次比B神高,由于一共需要 Ri1R_i - 1 个人,所以还要从已经比B神高的 nk1n-k-1 个人中选 Ri1(kj)R_i - 1 - (k-j) 个人。

考虑怎么求 g[i]g[i] ,枚举B神的分数 jjRi1R_i -1 个人分数在 j+1Uij+1 \cdots U_i 中,剩下 nRin - R_i 个人分数在 1j1 \cdots j 中:

g[i]=j=1Ui(Uij)Ri1 jnRig[i] = \sum_{j=1}^{U_i} (U_i - j)^{R_i - 1}\ j^{n - R_i}

因为 UiU_i 很大,不能暴力求,发现 g[i]g[i] 是一个关于 UiU_inn 次( xn\sum x^nn+1n+1 次多项式)多项式,即:

G(x)=j=1x(Uij)Ri1 jnRiG(x) = \sum_{j = 1}^x (U_i - j)^{R_i - 1}\ j^{n - R_i}

用拉格朗日插值即可。

提交记录

P3746 [六省联考 2017] 组合数问题

i=0(nkik+r)\sum_{i = 0}^\infin {nk \choose ik + r} ,其中 n109,r<k50n \le 10^9, r \lt k \le 50

也就是求 i mod k=r(nki)\sum_{i\ \bmod\ k = r} {nk \choose i}

考虑二项式定理 (nki)=[xi](i(nki)xi)=[xi](1+x)nk{nk \choose i} = [x^i](\sum_{i} {nk \choose i} x^i) = [x^i](1 + x)^{nk}

也就是要求 i mod k=r[xi](1+x)nk\sum_{i\ \bmod\ k = r} [x^i](1 + x)^{nk} ,考虑使用循环卷积,把超过 kk 的项卷回去,即 H(x)=i=0k1j=0k1[xi]F(x)[xj]G(x)xi+j mod kH(x) = \sum_{i = 0}^{k-1} \sum_{j = 0}^{k-1} [x^i]F(x)\cdot [x^j]G(x)\cdot x^{i+j \ \bmod \ k}

(1+x)nk(1+x)^{nk} 使用循环卷积快速幂即可,最后答案就是求出来的 xrx^r 的系数,时间复杂度 O(k2(lognk))\Omicron(k^2(\log nk))

(注意当 k=1k = 1 时,初值是 22 ,不是 1+x1+x

提交记录

P4099 [HEOI2013]SAO

nn 个点,n1n-1 个限制,对于每个限制,要求第 ii 个点要排在第 jj 个点之前或者之后,保证不存在两个点不相关(不能划分为两个点集 AA, BB, aA,bB,无限制(a,b)\forall a \in A, b \in B, \text{无限制} (a, b) ),求排列方案数。

不考虑限制的顺序的话,这就是一棵树,我们考虑用树形 dp 来求。

f[u][i]f[u][i]uu 在已经处理的 uu 的子树内,排名为 ii 的方案数,每次就是要把 f[v][k]f[v][k] 合并到 f[u][i]f[u][i] 上。

先考虑 vv 要在 uu 之前求。

考虑 vv 中有 jj 个点要被放在 uu 之前,有 jkj \ge k ,即必须让 vv 放在 uu 之前。(有一些问题,稍后讨论),

i=1siz[u],j=0siz[v]f[u][i+j]f[u][i](i+j1j)(siz[u]i+siz[v]jsiz[v]j)(k=1jf[v][k])i = 1 \cdots siz[u], j = 0 \cdots siz[v] \\ f[u][i+j] \larr f'[u][i] {i + j - 1\choose j} {siz[u]-i + siz[v]-j \choose siz[v]-j} (\sum_{k=1}^j f[v][k])

其中 siz[u]siz[u]uu 已经处理过的点数,siz[v]siz[v]vv 的点数,(i+j1j){i + j - 1\choose j} 就是选前 jj 个要被放在哪些地方,(siz[u]i+siz[v]jsiz[v]j)siz[u]-i + siz[v]-j \choose siz[v]-j 就是选后 siz[v]jsiz[v] - j 个要被放在哪些地方。

最后要 siz[u]+=siz[v]siz[u] += siz[v]

假如 vv 要放在 uu 之后,也是同理:

f[u][i+j]f[u][i](i+j1j)(siz[u]i+siz[v]jsiz[v]j)(k=j+1siz[v]f[v][k])f[u][i+j] \larr f'[u][i] {i + j - 1\choose j} {siz[u]-i + siz[v]-j \choose siz[v]-j} (\sum_{k=j+1}^{siz[v]} f[v][k])

现在有一个问题,为什么这么讨论是正确的:

每次插入时,uu 原先的排列和 vv 原先的排列中的相对顺序并没有改变,所以可以保证符合题目要求,同时也可以枚举到所有的相对位置的可能性(???我也不太懂,感性理解一下)。

(说一个可能会疑惑的点,考虑如下的:

1
2
3
v -> u
w -> v
w -> t

考虑 wtvu ,这种情况是不能转移到 wvut 的,但是 wvut 是可以从 wvt 转移过去的)

提交记录

P1975[国家集训队]排队

给定一个数字串,每次交换两个位置,然后求逆序对个数。

gt(l,r,x)gt(l, r, x)lrl \cdots r 中大于 xx 的数的个数,lt(l,r,x)lt(l,r,x) 同理。

考虑交换 l<rl \lt r ,那么对于在 ll 之前或者 rr 之后的位置没有影响,而中间的贡献是 gt(l+1,r1,ar)+gt(l+1,r1,al)lt(l+1,r1,a[l])+gt(l+1,r1,a[r])[al>ar]+[ar>al]-gt(l+1,r-1, a_r) + gt(l+1,r-1,a_l) - lt(l + 1, r - 1, a[l]) + gt(l + 1, r-1, a[r]) - [a_l > a_r] + [a_r > a_l]

考虑维护 gtgtltlt ,用线段树套平衡树,即线段树每个节点维护一个 FHQTreap 即可。

提交记录

P3702[SDOI2017]序列计数

求长为 nn ,每个数在 1m1 \cdots m ,和为 pp 的倍数,且有一个数为质数的数列的方案数。

n109,m2×107,p100n \le 10^9, m \le 2\times10^7,p\le100

考虑使用模 pp 下的多项式,和容斥。

[xi]Fl(x)[x^i]F_l(x) 为用 1m1 \cdots m 组成长度为 ll ,和模 ppii 的方案数,[xi]Gl(x)[x^i]G_l(x) 为用 1m1 \cdots m 中的合数组成长度为 ll ,和模 ppii 的方案数。

那么答案就是 [x0]Fn(x)[x0]Gn(x)[x^0]F_n(x) - [x^0]G_n(x)

考虑如何求 Fl(x)F_l(x)Gl(x)G_l(x)

l=1l=1 :

i[0,p1],[xi]Fl(x)=j[1,m],j mod p=i1i[0,p1],[xi]Gl(x)=j[1,m],j mod p=i,j∉P1i\in [0, p-1], [x^i]F_l(x) = \sum_{j \in [1, m], j\ \bmod\ p = i} 1 \\ i\in [0, p-1], [x^i]G_l(x) = \sum_{j \in [1, m], j\ \bmod\ p = i, j \not \in \mathbb P} 1

否则,合并两个长度为 aabb 的数列,显然可以从两边选择任意一个数列,然后和相加即可。

考虑循环卷积,令 l=a+b,a,b<ll = a + b, a, b \lt l

Fl(x)=i[0,p1]j[0,p1][xi]Fa(x) [xj]Fb(x) x(i+j) mod pF_l(x) = \sum_{i \in [0, p-1]} \sum_{j \in [0, p-1]} [x^i]F_a(x)\ [x^j]F_b(x)\ x^{(i + j)\ \bmod\ p}

然后用快速幂即可。

提交记录

P3760 [TJOI2017] 异或和

有一个序列 aia_i ,求

i,j[1,n],ij, xor(a[ij])i, j \in [1, n], i \le j,\ \operatorname{xor} (\sum a[i \cdots j])

考虑计算所有可能的 a[ij]a[i \cdots j] 的个数,然后答案就是所有出现奇数次的数的异或和。

fxf_x 为所有可能的 a[ij]a[i \cdots j] 中,xx 的出现次数。

gxg_x 为前缀和 a[1i]a[1 \cdots i]xx 的出现次数。

由于有 a[ij]=a[1j]a[1(j1)]a[i \cdots j] = a[1 \cdots j] - a[1 \cdots (j - 1)] ,所以有 fi=j=0migjgi+jf_i = \sum_{j=0}^{m-i} g_jg_{i+j} 。( m=a[1n]m = a[1 \cdots n] )

考虑进行变换

fi=j=0migjgi+jfmi=j=0igjgj+miimifmi=j=0igjgijgigmifi=j=0igjgijfifmi\begin{align*} f_i &=& \sum_{j=0}^{m-i} g_jg_{i+j} & \\ f_{m-i'} &=& \sum_{j=0}^{i'} g_jg_{j+m-i'} & i \larr m - i'\\ f_{m-i'} &=& \sum_{j=0}^{i'} g_jg'_{i'-j} & g'_i \larr g_{m-i}\\ f_{i'} &=& \sum_{j=0}^{i'} g_jg'_{i'-j} & f'_i \larr f_{m-i}\\ \end{align*}

就可以卷积了。

提交记录

P3763[TJOI2017] DNA

有两个字符串 SSTT ,问 SS 有多少个子串更改不超过 33 个字符(长度不变)能和 TT 相同,其中 SSTT 都由 A, T, C, G 四个字符组成。

分别考虑四个字符:

令上文中的 SSS(0)S^{(0)}TTT(0)T^{(0)}

设当前考虑的字符是 chch ,那么 Si=[Si(0)=ch],Ti=[Ti(0)=ch]S_i = [S^{(0)}_i = ch], T_i = [T^{(0)}_i = ch]

PiP_iSSii 结尾,长度 T|T| 的子串只考虑这个字符的情况下的匹配个数:

Pi=j=0m1Tj Si(m1)+jPi=j=0m1Tm1j Si(m1)+jTj=Tm1jPi=k+l=iTl Skl=m1j, k=i+(m1)+j, 并且不用考虑范围\begin{align*} P_i &=& \sum_{j=0}^{m-1}T_j\ S_{i-(m-1)+j} &\\ P_i &=& \sum_{j=0}^{m-1}T'_{m-1-j}\ S_{i-(m-1)+j} & T_j = T'_{m-1-j}\\ P_i &=& \sum_{k+l=i} T'_l\ S_k & l = m-1-j,\ k = i+(m-1)+j,\ 并且不用考虑范围\\ \end{align*}

卷积即可。

提交记录

P3734 [HAOI2017]方案数

有一个定义在非负整数之间的 \subeab=aaba \land b = a \lrArr a \sube b

考虑在一个无限大的空间中:

xx(x,y,z)(x,y,z)x \sube x' \rArr (x, y, z) \rarr (x', y, z)

yy(x,y,z)(x,y,z)y \sube y' \rArr (x, y, z) \rarr (x, y', z)

zz(x,y,z)(x,y,z)z \sube z' \rArr (x, y, z) \rarr (x, y, z')

其中有若干个点不能经过,求到某个点的方案数,不能经过的点数 n10000n \le 10000

一个套路,求不经过某些点的方案数,可以用如下的 dp 解决:

考虑一条非法的路径,枚举它第一次经过的不能经过的点。

f[i]f[i] 为不经过不能经过的点到 ii 的方案数,其中 g(j,i)g(j, i) 为从点 jj 出发,不考虑不能经过的点的方案数。

f[i]=g(0,i)jif[j] g(j,i)f[i] = g(0, i) - \sum_{j \not= i} f[j]\ g(j, i)

现在就要求 g(j,i)g(j, i) 怎么求。

每一次移动,都是在某些位上置 11 ,可以发现,路径数目只与要添加的 11 的个数有关。

h[i][j][k]h[i][j][k]x,y,zx, y, z 上分别要置 i,j,ki, j, k11 的方案数,分别考虑从三个方向转移,那么有:

h[i][j][k]=i=0i1h[i][j][k](ii)+j=0j1h[i][j][k](jj)+k=0k1h[i][j][k](kk)\begin{align*} h[i][j][k] &= \sum_{i'=0}^{i-1} h[i'][j][k] {i \choose i'} \\ &+ \sum_{j'=0}^{j-1} h[i][j'][k] {j \choose j'} \\ &+ \sum_{k'=0}^{k-1} h[i][j][k'] {k \choose k'} \\ \end{align*}

提交记录

P3705[SDOI2017] 新生舞会

给定 n×nn \times nai,ja_{i, j}bi,jb_{i, j} ,选 nn 个位置,每行每列有且只有一个,使得

k=1naxk,ykk=1nbxk,yk\frac {\sum_{k=1}^n a_{x_k, y_k}} {\sum_{k=1}^n b_{x_k, y_k}}

最大 。

分数规划的常用套路:

k=1naxk,ykk=1nbxk,yk=cmidk=1naxk,ykk=1nbxk,yk=(mid+Δ)k=1naxk,yk(mid+Δ)k=1nbxk,yk=0k=1n(axk,yk(mid+Δ)bxk,yk)=0k=1n(axk,ykmidbxk,yk)=Δk=1nbxk,yk\begin{align*} \frac {\sum_{k=1}^n a_{x_k, y_k}} {\sum_{k=1}^n b_{x_k, y_k}} &=& c \ge mid \\ \frac {\sum_{k=1}^n a_{x_k, y_k}} {\sum_{k=1}^n b_{x_k, y_k}} &=& (mid + \Delta) \\ \sum_{k=1}^n a_{x_k, y_k} - (mid + \Delta)\sum_{k=1}^n b_{x_k, y_k} &=& 0 \\ \sum_{k=1}^n (a_{x_k, y_k} - (mid + \Delta)b_{x_k, y_k}) &=& 0 \\ \sum_{k=1}^n (a_{x_k, y_k} - mid\cdot b_{x_k, y_k}) &=& \Delta\sum_{k=1}^n b_{x_k, y_k} \end{align*}

若存在一种方案使得 k=1n(axk,ykmidbxk,yk)0\sum_{k=1}^n (a_{x_k, y_k} - mid\cdot b_{x_k, y_k}) \ge 0 ,那么 Δ0\Delta \ge 0 ,所以能取到 cmidc \ge mid

考虑使用最大费用流,源点向每一行连边,每一列向汇点连边,第 ii 行向第 jj 列连费用为 ai,jmidbi,ja_{i,j} - mid\cdot b_{i, j} 的边 ,然后判断最大费用是否大于等于 00 即可(容量为 11 )。

提交记录

P3755 [CQOI2017]老C的任务

给定两个序列 xi,yix_i, y_i ,要求支持如下操作:

  1. 输入 l,rl, r ,求

    a=lir(xixˉ)(yiyˉ)lir(xixˉ)2a = \frac {\sum_{l\le i\le r} (x_i - \bar x)(y_i - \bar y)} {\sum_{l\le i\le r} (x_i - \bar x)^2} \\

    其中 xˉ\bar xxlxrx_l \cdots x_r 的平均数,定义为 lirxirl+1\frac {\sum_{l\le i\le r}x_i}{r-l+1}yˉ\bar yylyry_l \cdots y_r 的平均数,定义为 liryirl+1\frac {\sum_{l\le i\le r}y_i}{r-l+1}

  2. 输入 l,r,s,tl, r, s, t ,令  lir, xixi+s, yiyi+t\forall\ l\le i\le r,\ x_i \larr x_i + s,\ y_i \larr y_i + t

  3. 输入 l,r,s,tl, r, s, t ,令  lir, xii+s, yii+t\forall\ l\le i\le r,\ x_i \larr i + s,\ y_i \larr i + t

序列问题,考虑用线段树解决,先考虑操作 1,推式子:

a=i(xixˉ)(yiyˉ)i(xixˉ)2=i(xixˉ)(yiyˉ)i(xixˉ)2=ixiyiixˉyiiyˉxi+ixˉyˉi(xixˉ)2=ixiyixˉiyiyˉixi+xˉyˉ(rl+1)i(xixˉ)2=ixiyixˉiyiyˉixixˉyˉ(rl+1)ixi22xˉixi+xˉ2(rl+1)\begin{align*} a &=& \frac {\sum_i (x_i - \bar x)(y_i - \bar y)} {\sum_i (x_i - \bar x)^2} \\ &=& \frac {\sum_i (x_i - \bar x)(y_i - \bar y)} {\sum_i (x_i - \bar x)^2} \\ &=& \frac {\sum_i x_iy_i - \sum_i \bar xy_i -\sum_i \bar yx_i + \sum_i \bar x\bar y} {\sum_i (x_i - \bar x)^2} \\ &=& \frac {\sum_i x_iy_i - \bar x\sum_iy_i - \bar y\sum_ix_i + \bar x\bar y(r-l+1)} {\sum_i (x_i - \bar x)^2} \\ &=& \frac {\sum_i x_iy_i-\bar x\sum_iy_i-\bar y\sum_ix_i\bar x\bar y(r-l+1)} {\sum_i x_i^2 - 2\bar x\sum_i x_i + \bar x^2(r-l+1)} \\ \end{align*}

发现,我们需要用线段树维护这些东西:xiyi,xi,yi,xi2\sum x_iy_i, \sum x_i, \sum y_i, \sum x_i^2

同时我们还需要支持两个修改操作,分别是 xi,yi=ix_i, y_i = ixi+s,yi+tx_i + s, y_i + t ,第 3 个操作可以由这两个操作组合而成。

  1.  lir:xi=yi=i\forall\ l\le i\le r: x_i=y_i=i

    ixiyi=ixixii[l,r]i2=(i[1,r]i2)(i[1,l1]i2)=r(r+1)(2r+1)l(l1)(2l1)6ixi=iyir(1+r)l(l1)2\begin{align*} \sum_i {x_iy_i} = \sum_i {x_ix_i} &\larr& \sum_{i\in[l,r]} i^2 = (\sum_{i\in[1,r]} i^2) - (\sum_{i\in[1,l-1]} i^2) = \frac {r(r+1)(2r+1)-l(l-1)(2l-1)} 6 \\ \sum_i x_i = \sum_i y_i &\larr& \frac {r(1+r) - l(l-1)} 2 \end{align*}

  2.  lir:xixi+S,yiyi+T\forall\ l\le i \le r: x_i \larr x_i + S, y_i \larr y_i + T

    ixii(xi+S)=(ixi)+(rl+1)Siyii(yi+T)=(iyi)+(rl+1)Tixi2i(xi+S)2=xi2+2Sxi+(rl+1)S2ixi yii(xi+S)(yi+T)=xiyi+Syi+Txi+(rl+1)ST\begin{align*} \sum_i{x'}_i &\larr& \sum_i (x_i+S) &=& (\sum_i x_i) + (r-l+1)S \\ \sum_i{y'}_i &\larr& \sum_i (y_i+T) &=& (\sum_i y_i) + (r-l+1)T \\ \sum_i{x'}_i^2 &\larr& \sum_i (x_i+S)^2 &=& \sum x_i^2 + 2S \sum x_i + (r-l+1)S^2 \\ \sum_i{x'}_i\ {y'}_i &\larr& \sum_i (x_i+S)(y_i+T) &=& \sum x_iy_i + S \sum y_i + T \sum x_i + (r-l+1)ST \\ \end{align*}

提交记录

P3773 [CTSC2017]吉夫特

考虑一个长度为 nn 的序列互不相同 aia_i ,要求有多少个 aia_i 的不降子序列 ab1,ab2abk,k2a_{b_1}, a_{b_2} \cdots a_{b_k}, k \ge 2 满足:

i=2k(abi1abi)mod2>0\prod_{i=2}^k {a_{b_{i-1}} \choose a_{b_i}} \bmod 2 > 0

数据范围:n211986,ai233333n \le 211986, a_i \le 233333

原式就是要求每个 (abi1abi)mod2=1{a_{b_{i-1}} \choose a_{b_i}} \bmod 2 = 1

因为有 mod2\bmod 2 这个条件,考虑使用卢卡斯定理。

(ab)modp=(a/pb/p)(amodpbmodp)modp{a \choose b} \bmod p = {a/p \choose b/p} {a \bmod p \choose b \bmod p} \bmod p

可以发现,当 p=2p = 2 的时候每一次进行这个操作,就是把最低一位拆出来。

所以令 a=xt,xt1,,x02,b=yt,yt1,,y02a = \langle x_t, x_{t - 1}, \ldots, x_0\rangle_2, b = \langle y_t, y_{t - 1}, \ldots, y_0\rangle_2 ,那么

(ab)mod2=(xtyt)(xt1yt1)(x0y0)mod2=1(xtyt)=(xt1yt1)==(x0y0)=1{a \choose b}\bmod 2 = {x_t \choose y_t} {x_{t-1} \choose y_{t-1}} \cdots {x_0 \choose y_0}\bmod 2 = 1 \\ \dArr\\ {x_t \choose y_t} = {x_{t-1} \choose y_{t-1}} = \cdots = {x_0 \choose y_0} = 1

因为 xi,yi{0,1}x_i, y_i \in \{0, 1\} ,而

(00)=(10)=(11)=1(01)=1{0 \choose 0} = {1 \choose 0} = {1 \choose 1} = 1 \\ {0 \choose 1} = 1 \\

不难发现,这就要求在二进制下 bb 要是 aa 的子集。

回到题目中, 2ik,abiabi1\forall\ 2\le i \le k, a_{b_i}\sub a_{b_{i-1}}

fif_i 表示以 ii 结尾的,长度大于 22 的满足如上条件的的方案数,那么:

ji, fjfj+fi+1j \sub i,\ f_j \larr f_j + f_i + 1

要加 11 是因为可以 ii 开始,注意可能不存在某个 at=ja_t = j

每次统计答案 ansans+aians \larr ans + a_i

提交记录

P3813 [FJOI2017]矩阵填数

给定一个 h×wh \times w 的矩阵,每个格子能填上一个 1m1\cdots m 的数,同时有 nn 个限制,每个限制给出一个子矩阵,要求子矩阵的最大值为 vv ,求方案数。

h,w104,n10h, w \le 10^4, n \le 10

最大值为 vv 较难处理,因为要求至少有一个位置取到 vv ,考虑容斥:1v的方案数1v1的方案数用 1\cdots v 的方案数 - 用 1\cdots v-1 的方案数

h,wh, w 较大,而 nn 有较小,考虑对其进行离散化,得到若干个子矩阵(左上开,右下闭),每个子矩阵有最大值限制,并用状压记录下,若取到这个最大值,有哪些限制(有取到最大值)能被满足。

然后枚举子矩阵 ii 和已经满足的限制 jj ,令 xx 为不取最大值的方案数, yy 为取到(至少一个)最大值的方案数,能满足 ss 的限制,那么:

fi,jfi1,j+xfi,jsfi1,j+yf_{i,j} \larr f_{i-1,j} + x \\ f_{i,j\cup s} \larr f_{i-1,j} + y \\

并且 x=(v1)area,y=vareavx = (v-1)^{area}, y = v^{area} - v ,其中 areaarea 为其的面积。

提交记录

P4735 最大异或和

给定非负整数序列 aa ,初始长度为 nn ,有若干个操作:

  1. 给定 xx ,在序列末尾添加 xxnn+1n \larr n+1

  2. 给定 l,r,xl, r, x

    maxlprapap+1anx\max_{l\le p\le r} a_p \oplus a_{p+1} \oplus \cdots \oplus a_n \oplus x

考虑操作2,可以用前缀和 sis_i,变为:

maxlprsp1snx=maxl1pr1spsnx\max_{l\le p\le r} s_{p-1} \oplus s_n \oplus x = \max_{l-1\le p\le r-1} s_p \oplus s_n \oplus x

snxs_n \oplus x 知道,要求 sps_p 异或上它最大,这是一个传统的问题,可以用 trie 解决。

考虑怎么满足 l1pr1l-1\le p\le r-1 的限制:

先考虑 pr1p \le r-1 ,可以采用主席树的思想,即可持久化 trie ,每次插入一个新的数,都新开一棵 trie ,询问时就在第 r1r-1 棵 trie 上贪心即可。

再考虑 l1pl-1\le p ,对 trie 的每个节点记录最新的节点编号 newestnewest ,若 newest<l1newest\lt l-1 ,那么显然不满足要求。

提交记录

P5787 二分图 /【模板】线段树分治

给定一张 nn 个节点的图,有 kk 个时间,有 mm 会在某个时间出现,在某个时间消失,求每个时间内这个图是否是二分图。

先介绍一下如何判断一个图是二分图,使用并查集:

把每个点拆成两个:uuu+nu+n

每次连边 u,vu, v ,先检查若 uuvv 在同一个并查集内,则不是二分图,否则合并 (u,v+n)(u, v+n)(v,u+n)(v, u+n)

线段树分治可以处理这样的一个问题:有若干个操作,每个操作只在 lrl \cdots r 的时间内有效,同时还有一些询问,询问某个时间点内操作的贡献。

线段树分治将时间轴放到线段树上,每进入一个节点,处理在这个节点的 l,rl, r 内会生效的操作,离开这个节点时,撤销 这些操作(你很难 删除 这些操作带来的贡献,比如对于这题的并查集),若到了叶节点,询问即可。

回到这道题,我们建出一棵线段树,然后用类似区间操作的方法,把一条边拆成若干份,挂在若干个线段树节点(这条边要在这个线段树节点对应的时间内完全生效)上。

然后询问,到每个节点把这个节点上所有的边加入并查集(因为要撤销,所以不能路径压缩,只能按秩合并),并判断是否符合要求,若否,这个节点的子树中也不可能符合要求了。否则继续递归,到叶节点时设置答案,然后每个节点离开时,按倒序撤销边的贡献即可。

提交记录

P5227 [AHOI2013]连通图

给定一个无向连通图和若干个小集合(集合大小 c4c \le 4 ),每个小集合包含一些边,对于每个集合,你需要确定将集合中的边删掉后改图是否保持联通。集合间的询问相互独立。

用线段树分治,处理出每个边不被删掉的时间段,放到线段树上。

然后dfs线段树,用并查集维护连通性,进去的时候加入,离开的时候撤销,到每个叶节点判断是否联通( dsu.size[dsu.get(1)] == n )。

提交记录

P3703 [SDOI2017]树点涂色

一棵 nn 个点的有根树,其中 11 号点是根节点。并且每个点上的颜色不同。

定义一条路径的权值是:这条路径上的点(包括起点和终点)共有多少种不同的颜色。

进行这几种操作:

  • 1 x 表示把点 xx 到根节点的路径上所有的点染上一种没有用过的新颜色。
  • 2 x yxxyy 的路径的权值。
  • 3 x 在以 xx 为根的子树中选择一个点,使得这个点到根节点的路径权值最大,求最大权值。

不难发现,每种颜色都是一条从上到下的链,令 val[x]val[x]xx 到根的这条路径的权值,那么操作2就是 val[x]+val[y]2val[lca(x,y)]+1val[x] + val[y] - 2\cdot val[\operatorname{lca}(x, y)] + 1 (需要 +1+1 是因为 val[lca(x,y)]- val[lca(x,y)] 会把 lcalca 所在的那段颜色扣掉),操作三就是 uu 子树中做 maxval[v]\max val[v] ,可以用 dfn + 线段树维护。

考虑用 lct 维护操作1 。lct 用一棵 splay 维护一段链,这里强制用一棵 splay 维护一段颜色,发现一个节点的 valval 就是这个节点到根的虚边数量 +1+1 ,并且 access 操作强制把一段链到根的链变成一棵树,那么我们就可以用其的 access 操作来实现操作1 。

具体做法:

1
2
3
4
5
6
7
8
9
void access(int u)
{
for (int v = 0; u; v = u, u = tr[u].fa) {
splay(u);
if (tr[u].ch[1]) seg.modify_tr(findrt(tr[u].ch[1]), 1); // u->tr[u].ch[1] 变为虚边 +1
if (v) seg.modify_tr(findrt(v), -1); // u->v 变为实边 -1
conn(u, v, 1);
}
}

时间复杂度不会证,是均摊 O(log n)O(log\ n)

提交记录

P4219 [BJOI2014]大融合

给定 nn 个点,有若干个操作,一种是添加一条边,另一种是问经过某条边的路径数目有多少。保证这个图任意时刻都是森林。

森林,加边,lct 没跑了。

询问操作就是两个点的虚儿子个数 +1+1 乘起来。

注意 access 操作的时候维护虚儿子个数即可。

提交记录

P3688 [ZJOI2017] 树状数组

有一个错误的树状数组操作,单点加 11,区间查询(前缀和)(对 22 取模),只不过代码中把修改和查询操作的下标更新的正负号写反了。有两种操作,若干次,第一种是在 [l,r][l,r] 中等概率选取一个 xx ,单点修改,第二种是询问某个区间得到正确结果的概率是多少。

不难发现,对 22 取模单点加 11 就是异或,这个错误的树状数组其实实现的是查询后缀和。

那么要求的:alal+1ara_l \oplus a_{l+1} \oplus \cdots \oplus a_r ,实际求的: al1alanarar+1an=al1ar1a_{l-1}\oplus a_l \oplus \cdots \oplus a_n \oplus a_r \oplus a_{r+1} \oplus \cdots \oplus a_n = a_{l-1}\oplus \cdots \oplus a_{r-1}

二者要相等,及 alaral1ar1=al1ar1=0a_l \oplus \cdots \oplus a_r \oplus a_{l-1}\oplus\cdots\oplus a_{r-1} = a_{l-1}\oplus a_{r-1} = 0

即所求的是 al1=ar1a_{l-1} = a_{r - 1} 的概率。

考虑用树套树来维护 al=ara_l = a_r 的概率。

对于第一种操作 (L,R)(L, R) :

len=RL+1len = R-L+1

对于所有 l<LrRl \lt L \le r \le R ,有 1len\frac 1 {len} 概率翻转。

对于所有 LlR<rL\le l\le R\lt r ,有 1len\frac 1 {len} 概率翻转。

对于所有 LlrRL\le l \le r \le R ,有 2len\frac 2 {len} 概率翻转。

同时需要考虑一种特殊情况,若查询时 l=1l = 1 (原操作求”前缀和“时,若 x=0x=0 返回 00 ):

(a1ar)(0aran)(a_1 \oplus \cdots \oplus a_r) \oplus (0 \oplus a_r \oplus \cdots \oplus a_n)

即前缀和和后缀和相不相等。

对于所有 r<LRr \lt L \le R ,有 11 的概率翻转。

对于所有 LR<rL \le R\lt r ,有 11 的概率翻转。

对于所有 LrRL\le r \le R ,有 11len1 - \frac 1 {len} 的概率翻转(有 1len\frac 1 {len} 的几率恰巧碰到 rr ) 。

现在思考一下实现细节:

树套树对于每个点,用 l,rl, r 维护 al=ara_l = a_r 的概率。

有一棵线段树,对于线段树的每一个区间,都开一个线段树。对于区间修改操作,对每个完全覆盖的区间,在对应的线段树上继续操作。对于单点查询,要注意累加所有覆盖这个点的区间的线段树上的贡献。

合并两个概率的函数:merge(x,y)=xy+(1x)(1y)\operatorname{merge}(x, y) = xy + (1-x)(1-y) (有可能都翻转,也有可能都不翻转)。

提交记录

网络流中相关习题总结1

基础概念在 上一篇

个人整理的题单:https://www.luogu.com.cn/training/273755

后续内容在 下一篇

记号:

  1. 在提及两端点情况下,用 (c,w)(c, w) 表示一条容量为 cc ,费用为 ww 的边,下同。

  2. uv,(c,w)u \rarr v, (c, w) 来表示从 uuvv 连一条容量为 cc ,费用为 ww 的边,下同, cc 有时会省略。

类型 1:基本

这个类型建图不需要太多的转换,只需要按照题意建即可。

重要 trick 拆点:可以将一个点 uu 拆成入点 uinu_{in} 和出点 uoutu_{out} ,所有连向 uu(v,u)(v, u) 连向 uinu_{in}(v,uin)(v, u_in) ,所有从 uu 连的 (u,w)(u, w) 改为从 uoutu_{out} 连:(uout,w)(u_{out} , w) ,再在 uinu_{in}uoutu_{out} 之间连边,以此限制路径经过这个点的次数和价值。

重要 trick 只取一次:若一个边(点用拆点)上有价值,但只能只取一次,可以这么连边:两个点之间,先连一条 (1,w)(1, w)ww 为价值,保证只有一次,再连一条 (,0)(\infty, 0) ,让后面可以通过。

P4013 数字梯形问题

https://www.luogu.com.cn/problem/P4013

题意:

一个梯形最上面一行有 mm 个数字,每一行比上面一行多 11 个数字,交错排列,从顶部开始,在每个数字可以向左下或者右下走,形成 mm 条路径,求以下限制下的路径上所有数字的最大和:

  1. 每条路径不能在点或者边相交;
  2. 每条路径不能在边上相交;
  3. 每条路径可以在点上或者边上相交。

解法:

拆点,源点向最上面的连边 (,0)(\infty, 0) ,最下面的向汇点连边 (,0)(\infty, 0),中间的点向左下和右下的点连边,情况 1 2 时为 (1,0)(1, 0) ,情况 3 时为 (,0)(\infty, 0),同时每个数字 ii 拆成的出点和入点之间,情况 1 连 (1,i)(1, i) ,否则为 (,i)(\infty, i)

跑最大费用最大流即可。

P1042 深海机器人问题

https://www.luogu.com.cn/problem/P4012

题意:

有一个网格,和一些机器人,机器人可在格点上向右或向上运动,一些格边上有一些有价值的物品,会被第一个机器人取走,给出若干个起点和终点,每个起点和终点可以让若干个机器人出或入。

要让所有机器人回到终点,求物品价值最大。

解法:

和上一题一样,不过这次由于物品在格边上,故不用拆点,不过注意到一个物品只能被取一次,可以这样操作:一条边从 uuvv (有方向限制),物品价值 ww,先建一条 (1,w)(1, w) ,再建一条边 (,0)(\infty, 0) ,可以保证只取一次。然后从 ss 向所有的起点连边 (x,0)(x, 0) ,其中 xx 为起点可以让多少个机器人出发,终点同理。

跑最大费用最大流即可。

P3356 火星探险问题

https://www.luogu.com.cn/problem/P3356

题意:

一个网格图,若干个机器人,机器人在格子中,从左上走到右下,有障碍,也有价值为 11 的物品,注意物品只能被取一次。

问在到达终点的机器人个数最大的情况下,被取的物品最大价值是多少,注意没有到终点的机器人取的物品价值不会被计算。

输出方案:开始时机器人都在左上,每次选择一个机器人和它所做的操作,即输出 机器人编号 操作(0 为下, 1 为右)

和上一题几乎一样,用上面介绍的 trick 建图,但这题毒瘤的地方在于要输出方案。

结合网络流的性质可以发现,如果一条边它的边权每减小了 11 ,说明有一个机器人走过。考虑到机器人并没有差别,即一个物品不在乎是那个机器人取走的,我们可以一个一个走,从起点开始,寻找一个边权改变了的边(反向边边权 >0\gt 0 ),然后走过它,把边权改回去,根据网络流的性质,不可能存在走着走着走不下去的情况,故这么做是正确的。

输出方案的参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
vector<pair<int, int>> edges_new[MAXN];
void export_paths(int lim) // lim 为机器人走到终点的个数,即最大流
{
// 先建成一个新图,方便处理
for (int i = 1; i <= n * m * 2; i++) {
if (i % 2 == 1) continue;
for (int j = head[i]; j; j = edges[j].nxt) {
int v = edges[j].v;
if (edges[j ^ 1].flw && v != i - 1 && v != s && v != t) {
edges_new[i == s ? s : i / 2].push_back({v == t ? t : (v + 1) / 2, edges[j ^ 1].flw});
}
}
}
for (int i = 1; i <= lim; i++) {
int u = 1;
while (u != n * m) {
for (auto& p : edges_new[u]) {
// p.second 为这条边最多经过机器人次数
if (p.second) {
if (p.first == u + 1) {
printf("%d %d\n", i, 1);
} else {
assert(p.first == u + m);
printf("%d %d\n", i, 0);
}
p.second--; // 改边权
u = p.first;
break;
}
}
}
}
}

P4066 吃豆豆

https://www.luogu.com.cn/problem/P4066

两个 PACMAN 吃豆豆。一开始的时候,PACMAN 在所有豆豆的左下方,PACMAN 走到豆豆处就会吃掉它。PACMAN 只能向右走或者向上走,行走的路线不可以相交。问最多能吃多少个豆豆。

点数 n2000n \le 2000

两条路线不能相交的限制并没有什么用处,只要不在有豆豆的地方相交,就可以这样:

1

那么就像上一题一样了,可以从任意的点开始和结束(也是为了方便),从 ss 向所有的点连边,tt 同理。考虑到 nn 很大,不能暴力连边,如果有 uv,vwu \rarr v, v \rarr w 显然就没有必要 uwu \rarr w 了。

建图详见代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (int i = 1; i <= n; i++) scanf("%d%d", &a[i].first, &a[i].second);
sort(a + 1, a + n + 1);

for (int i = 1; i <= n; i++) {
int minx = INF;
G.add_edge(ss, i * 2 - 1, 1, 0), G.add_edge(i * 2, tt, 1, 0);
G.add_edge(i * 2 - 1, i * 2, 1, -1), G.add_edge(i * 2 - 1, i * 2, 1, 0);
for (int j = i + 1; j <= n; j++) {
if (a[j].second >= a[i].second && a[j].second < minx) {
G.add_edge(i * 2, j * 2 - 1, INF, 0);
minx = a[j].second;
}
}
}

类型 2:进阶——不明显的图

有些网络流题目的图不会那么明显。

P2770 航空路线问题

给定若干个城市,从西到东排列,城市之间有一些航线,判断是否存在一条最长航线满足以下要求,若满足,输出最多访问的城市个数和方案:

  1. 从最西端的城市出发,从西向东到最东端的城市,在从东向西回到最西端的城市。

  2. 除了最东端和最西端的城市,每个城市只能访问一次。

显然可以建图,把城市之间的航线看作边,要求的航线看作路径。

一条折回来的航线不好考虑,改为两条从起点出发、不相交的自西向东的航线。

每个点只访问一次的限制可以拆点解决,最西边、最东边的点可以访问两次,其他的点只能访问一次。

故综上,建图方案为:

s1in,(2,0)s \rarr 1_{in}, (2, 0)1in1out,(2,1)1_{in} \rarr 1_{out}, (2, 1)nint,(2,0)n_{in} \rarr t, (2, 0)nin1out,(2,1)n_{in} \rarr 1_{out}, (2, 1)

2in1,iiniout,(1,1)2 \le i \le n-1, i_{in} \rarr i_{out}, (1, 1)

航线 (x,y)(x, y) 连边 xoutyin,(1,0)x_{out} \rarr y_{in}, (1, 0)

求最大费用最大流。

注意:只有两个点时,最大流为 11 ,但有可行方案( 1211 \rarr 2 \rarr 1 ),注意特判。

输出方案在之前有提及,就是跳被修改过的边就可以了。

P2766 最长不下降子序列问题

https://www.luogu.com.cn/problem/P2766

给定一个序列,

  1. 计算其最长不下降子序列的长度 ss

  2. 如果每个元素只允许使用一次,计算从给定的序列中最多可取出多少个长度为 ss 的不下降子序列。

  3. 如果允许在取出的序列中多次使用 x1x_1xnx_n ,计算序列中最多能取出多少个不同的长度为 ss 的不下降子序列。

对于一个序列 a=a1,a2,ana = a_1, a_2, \cdots a_n ,如果有一个序列 pp 满足 1p1<p2<<pkn1 \le p_1 \lt p_2 \lt \cdots \lt p_k \le n ,那么序列 b=ap1,ap2,apkb = a_{p_1}, a_{p_2}, \cdots a_{p_k} 为序列 aa 的子序列。

对于序列 aa 的子序列 bbcc ,分别是由 ppqq 构造的,称 bbcc 不同,当且仅当 pp 不等于 qq ,即 pq1ip,piqi|p| \not= |q| 或 \exists 1\le i \le |p|, p_i \not= q_i

求最长不下降子序列的长度可以由 dp 完成,设 fif_i 为最后一个数是 aia_i 的最长不降子序列长度,那么 fi=maxj=1i1fj+1,aiajf_i = \max_{j=1}^{i-1} f_j + 1, a_i \ge a_js=maxfis = \max f_i

我们把选出一条长度为 ss 的不降子序列看作选出一条路径。

把一个点 ii 拆点,从入点向出点连一条边,容量为 11 ,保证只有一条路径,即一个序列选择这个点。

然后从源点向所有 fi=1f_i = 1ii 的入点连边,从所有 fi=sf_i = sii 的出点向汇点连边,然后对于所有 ii 的出点向 fj=fi+1,j>if_j = f_i + 1, j \gt i 的入点连边。以上所有的边权都是 11

以样例为例

跑一次最大流,就是第二问的答案。

对于第三问,只要把从 s1in,1in1out,ninnout,noutts \rarr 1_{in}, 1_{in} \rarr 1_{out}, n_{in} \rarr n_{out}, n_{out} \rarr t 的边的容量改为 \infty 即可。

第三问中的「不同」的限制只是限制位置不同,而这已经被 iji \rarr j 之间的容量为 11 的条件限制住了,故不用额外考虑。

P3358 最长k可重区间集问题

https://www.luogu.com.cn/problem/P3358

有若干个开区间,给定他们的左右端点,要求从其中选出一些,使得对于任意的 x0x_0 ,至多 kk 个开区间包含 x0x_0

求最长的区间长度之和。

考虑要如何处理这两个限制:最多 kk 个区间,和如果选了这一个区间,就会影响从左到右的若干个点。

考虑这么建图:

对于区间 (l,r)(l, r) ,从 llrr 连一条容量为 11 ,费用为 (l,r)|(l, r)| 的边,在相邻的点从左向右连一条容量为 kk ,费用为 00 的边。然后从源点向最左边的点连一条容量为 kk ,费用为 00 的边,从最右边的点向汇点连一条容量为 kk ,费用为 00 的边,然后跑最大费用最大流。

以样例为例

为什么是对的呢?考虑将选择的区间分成 kk 组,每组之间两两不交,就相当于从源点向汇点找 kk 条路径,每条路径不同时经过两个区间,每个区间只用一次,恰好,我们的建图就能满足这个条件,同时显然满足条件的情况下,多加入一个区间会使答案更优,所以最大费用最大流是正确的。

P3357 最长k可重线段集问题

https://www.luogu.com.cn/problem/P3357

有若干的开线段,给定两端点的坐标,要求从其中选出一些,使得对于任意的 x0x_0 ,至多 kk 个线段和 x=x0x = x_0 相交。

求最长的线段长度(一条线段的距离定义为两端点的欧几里得距离下取整)。

显然和上一题很像,对于每一条线段,显然可以将他们看作在 xx 轴上的投影,但注意有特殊情况,即垂直于 xx 轴的线段,它们按照上一题的建图方式建出来是不能限制只取 kk 个的。

考虑拆点:

假如一条线段的两端点 xx 坐标不同,从左端点的 xx 坐标对应的出点连到右端点的 xx 坐标对应的入点。如果相同,就从 xx 坐标对应的入点连到出点,然后把剩下的串起来就可以了:iiniout(i+1)in(i+1)outi_{in} \rarr i_{out} \rarr (i+1)_{in} \rarr (i+1)_{out} \rarr \cdots

P3980 志愿者招募

有一个工作共 nn 天,第 ii 天的工作需要 aia_i 人,同时可以有 mm 种志愿者,第 ii 种志愿者可以从 sis_i 天工作到 tit_i 天(每天都来,不能拆开来),每人共需要 cic_i 的工钱。

问最小花费。

这题和上两题类似,不过上两题是至多,这题至少。

考虑用流量限制人数,对于每个 ii ,从 sis_iti+1t_i + 1 连一条 (,ci)(\infty, c_i) 的边,同时取一个较大的数 MAXMAX ,从 iii+1i + 1 连一条 (MAXai,0)(MAX - a_i, 0) 的边,然后连 s1,(MAX,0)s \rarr 1, (MAX, 0)n+1t,(MAX,0)n + 1 \rarr t, (MAX, 0) ,然后跑最小费用最大流。

为了让网络跑到最大流 MAXMAX ,对于 ii+1i \rarr i + 1 ,其容量为 MAXaiMAX - a_i ,就要在经过这段的志愿这的边至少流过 aia_i ,这就满足了每天要有 aia_i 个人的限制了。

解释

类型 3:分层图

把分层图的思想引入网络流,可以记录更多的信息。

P4099 汽车加油行驶问题

https://www.luogu.com.cn/problem/P4099

有一个网格,一辆汽车在格点和边上行驶。汽车从起点 (1,1)(1, 1) 出发驶向右下角终点 (n,n)(n, n)

在若干个格点处,设置了油库,可供汽车在行驶途中加油。汽车在行驶过程中应遵守如下规则:

  1. 出发时汽车已装满油,装满油的汽车能够行驶 kk 条网格边。

  2. 汽车若向左或向上行驶,需付费用 BB

  3. 汽车在行驶过程中遇油库则应加满油并付加油费用 AA

  4. 在需要时可在网格点处增设油库,并付增设油库费用 CC (不含加油费用AA )。

问从起点行驶到终点的最小费用。

n100,k10n \le 100, k \le 10

不能用边的容量等条件限制汽车的油亮,考虑使用分层图。

连边如下,点 (x,y,p)(x, y, p) 汽车在 (x,y)(x, y) ,还有 pp 的油。

起点和终点:s(1,1,k),(1,0); (n,n,p)t,(1,0)s \rarr (1, 1, k), (1, 0);\ (n, n, p) \rarr t, (1, 0)

中间的点:1xn,1yn1 \le x \le n, 1 \le y \le n(x,y)(x', y')(x,y)(x, y) 相邻,需要费用 wwwwBB00 )。

如果 (x,y)(x, y) 有油库,则 (x,y,p)(x,y,k1),(,w+A)(x, y, p) \rarr (x', y', k - 1), (\infin, w + A)

否则,若不设油库, (x,y,p)(x,y,p1),(,w)(x, y, p) \rarr (x', y', p - 1), (\infin, w) ,若设油库 (x,y,p)(x,y,p1),(,w+C)(x, y, p) \rarr (x', y', p - 1), (\infty, w + C) (绕一圈回到原来的地方显然是不优的,所以不必担心设油库的费用 CC 被算两次)。

P2754 家园 / 星际转移问题

https://www.luogu.com.cn/problem/P2754

把若干个人从地球转移到月球,有 nn 个太空站, mm 艘太空船在地球,月球,太空站之间运送旅客,第 ii 个太空站编号 ii ,为方便,地球编号 00 ,月球编号 1-1

设第 ii 艘太空船,若它的路线为 (x1,x2,xn)(x_1, x_2, \cdots x_n) ,那么它会依次停靠 x1,x2,,xn,x1,x2,,xn,x_1, x_2, \cdots, x_n, x_1, x_2, \cdots, x_n, \cdots 号太空站(或地球或月球),它可以容纳 hih_i 个人。

问至少多久可以将所有人从地球运到月球。

使用分层图的思路,设 (i,k)(i, k) 为编号为 ii 的地方在第 kk 时刻的节点。

从第 00 个时刻开始,每个时刻 kk ,对于每个地方,新建一个节点,(i,k1)(i,k),(0)(i, k-1) \rarr (i, k), (0) (月球除外, (1,k)(1,k1)(-1, k) \rarr (-1, k-1) ),然后对于每个飞船 ii ,设其上个时刻所在的位置为 pi,k1p_{i, k-1} ,这个位置在 pi,kp_{i, k} ,那么 (pi,k1,k1)(pi,k,k),(hi)(p_{i, k-1}, k-1) \rarr (p_{i, k}, k), (h_i)

不断重复这个操作,知道这个图的最大流大于等于人数即可(添加新的边,最大流肯定是不降的,所以不用清空,直接跑就行了)。

P2053 修车

https://www.luogu.com.cn/problem/P2053

nn 辆车,分给 mm 个人修,第 ii 个人修第 jj 辆车需要 ti,jt_{i,j} 的时间,第 ii 辆车的等待时间是修它的时间加上选择修它那个人修之前的车的等待时间之和,求最小等待时间。

形式化地说:

ii 个人修 kik_i 辆车,分别为 ai,1,ai,2,ai,kia_{i, 1}, a_{i, 2}, \cdots a_{i, k_i} ,其中 ki=n\sum k_i = na1,1,a1,2a1,k1,ai,jam,kma_{1, 1}, a_{1, 2} \cdots a_{1, k_1}, \cdots a_{i, j} \cdots a_{m, k_m} 互不相同,且在 11nn 之间。

mini=1mj=1kil=1jti,ai,ln\min \frac {\sum_{i=1}^m \sum_{j=1}^{k_i} \sum_{l=1}^j t_{i, a_{i, l}}} n

肯定先求总时间最少。

考虑使用贡献,和从后往前安排顺序,如果第 ii 辆车,找 jj 修,从后往前第 kk 个,那么它对答案的贡献就是 tj,ikt_{j, i} k

解释

二分图,左边是 nn 个人,右边是 mnm * n 个点,右边的点 (j,k)(j, k) 表示找 jj ,从后往前第 kk 个修,然后左边的 ii 向右边的 jj 连边 (1,tj,ik)(1, t_{j, i}k) ,然后从源点向左边连 (1,0)(1, 0) ,从右边向汇点连 (1,0)(1, 0)

跑最小费用最大流就可以了。

P2050 美食节

这一题和上一题类似,不过数据范围较大。

我们发现不是所有的 (j,k)(j, k) 都是有用的,所以我们最开始只建 (j,1)(j, 1) ,然后假如 (j,k)(j, k) 被用了,再建 (j,k+1)(j, k + 1)

可以参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
inline pair<int, int> dinic()
{
int fsum = 0, csum = 0;
while (spfa()) {
while (true) {
pair<int, int> p = dfs(S, INF);
int x = p.first, y = p.second;
fsum += x, csum += y;
if (!x) break;
}
for (register int j = 1; j <= m; j++) {
if (vis1[n + id(j, top[j])] && top[j] < sum) {
// (j, k) 被用了
top[j]++;
add_edge(n + id(j, top[j]), T, 1, 0);
for (register int i = 1; i <= n; i++)
add_edge(i, n + id(j, top[j]), 1, a[i][j] * top[j]);
// 连接 i
}
}
}
return make_pair(fsum, csum);
}

网络流中相关习题总结2

上上篇 基础知识

上一篇 习题总结1

记号:

  1. 在提及两端点情况下,用 (c,w)(c, w) 表示一条容量为 cc ,费用为 ww 的边,下同。

  2. uv,(c,w)u \rarr v, (c, w) 来表示从 uuvv 连一条容量为 cc ,费用为 ww 的边,下同, cc 有时会省略。

类型4:最小割

最小割主要解决的是这样的问题:求满足某种条件(要转化为源点和汇点不联通)的最小代价。

一个小模型

这里有个能在部分题中用上的模型:

有若干个东西,每个东西都有一些价值。

还有若干个限制,对于每个限制,有若干个物品不能同时选(即必须有至少一个不选,通常为两到三个)。

那么把每个物品拆点,连边容量为价值。

然后对于每个限制,用边从源点到汇点串起来(边权 \infty ,可能要安排顺序,形成环的话很麻烦),求最小割,答案就是总价值减去最小割。

例子

解释:每个限制体现在图中都是一些路径,而这中的每条路径都要割断一些点,让源点和汇点不联通,用网络流完成最优的决策,即删除的点代价最小。

P2774 方格取数问题

https://www.luogu.com.cn/problem/P2774

给定一个网格图,从网格中选取若干个数,使选取的格子没有公共边,求取出的数最大的和。

相邻的两个格子不能同时取,显然就是上面的模型了,黑白染色一下,然后源点连黑的,容量为格子中的数字,然后黑点连相邻的白的,容量 \infty ,白的连汇点,容量为格子中的数字。

P3355 骑士共存问题

https://www.luogu.com.cn/problem/P3355

在一张 n×nn\times n 的方格国际象棋棋盘上,马可以攻击的范围如下图所示:

S 为马,x 为可以攻击的格子

有些格子设置了障碍,不能进入,求最多放置多少匹马,使它们互相不攻击。

黑白染色后,一匹马能攻击到的格子的颜色恰好是和它在的格子颜色相反,并且假如 AA 能攻击到 BBBB 也能攻击到 AA

假如一条点有障碍,在加边的时候忽略它即可。

和上一题差不多。

源点连黑点,黑点连能够攻击到的白点,白点连汇点。

P2762 太空飞行计划问题

https://www.luogu.com.cn/problem/P2762

mm 个实验,每个实验 ii 收益 pip_i ,有 nn 个仪器,买仪器 ii 花费 cic_i ,每个实验 ii 需要某些仪器 RiR_i ,仪器可以重复使用,问最大收益,并输出方案。

这题是最大权闭合子图问题,关于这个问题,不过多介绍,考虑用上面提及的模型解决。

发现这题的限制是:选实验 ii选仪器 jRij \in R_i

发现「不选」的条件有点特殊,考虑怎么处理。

在模型中,先选上所有的物品,然后减去最小的代价,一个物品的代价就是选它减不选它对答案贡献的差。

实验 ii ,不选它贡献为 00 ,选它贡献为 pip_i

不选仪器 jj ,不选它贡献为 cj-c_j ,选它贡献为 00

答案为 pi最小割\sum p_i - 最小割

按照上面模型的方法建图,不过由于只有两个限制,不用拆点。

从源点向实验节点 iipip_i 的边 ,从仪器节点 jj 向汇点连 cjc_j 的边,从实验节点 ii 向所有 jRij \in R_i\infty 的边,跑最小割,然后用 pi\sum p_i 减去最小割就是答案。

还要输出方案数:做实验 ii ,说明源点到 ii 的边没被割,买仪器 jj ,说明点 jj 到汇点的边被割了。

来自 @Ajwallet 题解的图片

P4174 最大获利

https://www.luogu.com.cn/problem/P4174

nn 个基站,每个基站需要 PiP_i 的成本,有 mm 个用户,需要基站 AiA_iBiB_i ,可以获利 CiC_i

求最大获利。

和上一题差不多,源点连用户,容量 CiC_i ,用户 ii 连基站 AiA_iBiB_i ,边权 \infty ,基站 ii 连汇点,容量 PiP_i 。答案为 Ci最小割\sum C_i - 最小割

P5458 水晶

https://www.luogu.com.cn/problem/P5458

地图由若干个密铺的六边形单元组成,如下图所示,为了方便,用 (x,y,z)(x, y, z) 表示一个格子,表示从原点开始,按所示的 x,y,zx, y, z 轴走了 x,y,zx, y, z 步,注意一个格子可以由多个坐标表示。

地图示意

nn 块水晶在地图内,第 ii 块的坐标为 (xi,yi,zi)(x_i, y_i, z_i) ,每块有 cic_i 的价值,注意一格内可能有多块水晶。

满足 x+y+z=0(mod3)x + y + z = 0 (\bmod 3) 的格子内有能量源,有能量源的格子内的水晶的价值多 10%10\%

绿色格子内有能量源

会形成两种共振:

  1. aa 共振,如果三块水晶所在的格子排列成一个三角形,会有 aa 共振。
    红色线标出

  2. bb 共振,如果三块格子所在单元连成一条直线,并且中间的格子有能量源,会有 bb 共振。
    红色线标出

你要破坏一些水晶,使不会有共振,并且留下的水晶价值最大。

由于在平面上,只需要两个坐标就够了,要把格子的坐标转换。

发现往 zz 轴走 11 格,可以转化为往 yy 轴走 1-1 格,往 xx 轴走一格。

示意

所以 (x,y,z)(x, y, z) 被转化为 (xz,yz)(x - z, y - z),并且

x+y+zmod3=x+y+3z2zmod3=x+y2zmod3=xz+yzmod3x + y + z \bmod 3 = x + y + 3z - 2z \bmod 3 = x + y - 2z \bmod3 = x - z + y - z \bmod 3

故新旧坐标加起来模 33 不变。

而题目中 x+y+zmod3=0x + y + z \bmod 3 = 0 的格子有能量源也提醒了我们可以按 mod3\bmod 3 对格子分类。

考虑什么样的有水晶的格子会形成共振。

a 共振,注意模 3 余数的改变

b 共振,注意模 3 余数的改变

不难发现,假如一个模 3300 的有水晶的格子旁边有一个余 11 和余 22 的有水晶的格子,那么会引发共振。

根据上面的模型,我们可以这么建图:

拆点,每个有水晶的格子拆成入点和出点,之间连边容量为水晶的价值。

源点向所有模 3311 的点连边,余 11 向相邻的余 00 的点连边,余 00 向相邻的余 22 的点连边,余 22 向汇点连边,这些边边权均为 \infty

然后价值和减去最小割就可以了。

另一个小模型

这个模型可以用在一些需要二选一的问题上。

需要作出 nn 个一些二选一的抉择,设为 AABB ,每个选 AA 或者选 BB 都能有一定价值,同时若干个同选 AA 或者同选 BB 也能有一定价值,问最大价值。

形式化来说:

nn 个数,记为 {xn}\{x_n\}xix_i 可以为 1100

同时有若干个条件,记为 (Sj,yj,vj)(S_j, y_j, v_j) ,假如对于 iSji \in S_j 都有 xi=yjx_i = y_j ,那么可以有 vjv_j 的价值。

问最大价值。

我们考虑用联通性来表示一个选择。

iiss 联通,那么选 AA ,否则要与 tt 联通,选 BB

我们考虑加上所有的价值,然后删去尽可能小的,满足「二选一」,可以试着用最小割。

对于每个条件,新建一个节点 YjY_j ,若 yj=0y_j = 0 ,从源点向 YjY_jvjv_j 的边,然后对于 iSji\in S_j ,从 jjii\infty 的边;若 yi=1y_i = 1 ,反过来即可,即 ijti \rarr j \rarr t

答案就是价值和减最小割。

为什么是正确的呢?

考虑一个点选 AA ,那么就要割断所有与 tt 的路径。

这样所有要这个点选 BB 的条件都不会对答案有贡献了。

虚线为省略,绿线保留,红线割掉

P4313 文理分科

https://www.luogu.com.cn/problem/P4313

有一个班级,要文理分科,这个班级可以用 n×mn \times m 的矩阵表示。

对于 (i,j)(i, j)

  • 选文科,可以获得 arti,jart_{i, j} 的满意值,假如和他相邻的人都选了文科,额外获得 same_arti,jsame\_art{i, j} 的满意值。
  • 选理科,可以获得 sciencei,jscience_{i, j} 的满意值,假如和他相邻的人都选了理科,额外获得 same_sciencei,jsame\_science_{i, j} 的满意值。

问所有人最大满意值之和。

显然就是上面的模型:

对于自己:

s(i,j),arti,js \rarr (i, j), art_{i, j}(i,j)t,sciencei,j(i, j) \rarr t, science_{i, j}

对于周围的人,令 Xi,j,Yi,jX_{i, j}, Y_{i, j} 是两个为处理 same_artsame\_artsame_sciencesame\_science 的节点,设 (i,j)(i', j') 为和 (i,j)(i, j) 相邻的点。

sXi,j,same_arti,js \rarr X_{i, j}, same\_art_{i, j}Xi,j(i,j),X_{i, j} \rarr (i', j'), \inftyXi,j(i,j),X_{i, j} \rarr (i, j), \infty

Yi,jt,same_sciencei,jY_{i, j} \rarr t, same\_science_{i, j}(i,j)Yi,j,(i', j') \rarr Y_{i, j}, \infty(i,j)Yi,j,(i, j) \rarr Y_{i, j}, \infty

P1646 happiness

https://www.luogu.com.cn/problem/P1646

有一个班级,要文理分科,每个人选文科,选理科,相邻的两个人选文科或理科会获得喜悦值,求最大喜悦值之和。

和上面那题几乎一模一样。

源点向每个人连容量为选文科可以获得的喜悦值的边。

对于每对相邻的人,新建一个点,源点向那个点连容量为那对人同时选文科可以获得的喜悦值,然后那个点向那两个人连边。

理科同理,不过反过来。

CF311E Biologist

https://www.luogu.com.cn/problem/CF311E

有一个长度为 nn0/10/1aa ,将第 ii 个位置变为另一个数字的代价为 viv_i

然后有 mm 个要求,每个要求给定 kik_i 个位置(设为 RiR_i ),要求这些位置同为 pip_i ,假如满足,有 WiW_i 的利润,否则,部分要求要倒给 gg

求最大收益。

这一题比较不同的地方就是要部分条件要倒给 gg 或者 viv_i,考虑使用 P2762 的方式解决,使得割边时能够把它们剪掉,留着时不计入答案。

建边方式:

ai=0a_i = 0si,0;it,vis \rarr i, 0; i \rarr t, v_i ;否则 , si,vi;it,0s \rarr i, v_i; i \rarr t, 0

对于每一个限制 jj ,新建点 LjL_j ,若 pi=0p_i = 0sLj,Wi(+g,假如有要求);iRj,Lji,s \rarr L_j, W_i (+ g, \text{假如有要求}); \forall i \in R_j, L_j \rarr i, \infty ,若 pi=1p_i = 1Ljt,Wi(+g,假如有要求);iRj,iLj,L_j \rarr t, W_i (+ g, \text{假如有要求}); \forall i \in R_j, i \rarr L_j, \infty

答案为 Wi最小割\sum W_i - 最小割

类型5

这种类型要解决的问题大多可以转化为这样的一个模型:

有一个有向图,求一个子图,满足点 ii 的入度不大于 IiI_i ,出度不大于 OiO_i ,边数尽可能多,求最小费用或最大收益。

建图方法如下:

对于每个点 ii ,拆成两个点 AiA_iBiB_i ,然后 sAi,(Oi,0);Bit,(Ii,0)s \rarr A_i, (O_i, 0); B_i \rarr t, (I_i, 0) 。如果原图中有一条边 (u,v,w)(u, v, w) ,那么连边 AuBv,(1,w)A_u \rarr B_v, (1, w)

然后跑最小费用最大流。

为什么?

考虑一条边 (u,v,w)(u, v, w) 如果被选了,即在网络流中 (Au,Bv)(A_u, B_v) 流量为 11 ,那么 (S,Au)(S, A_u) 的流量会多 11(Bv,t)(B_v, t) 的流量会多 11 。因为 (S,Au)(S, A_u) 的流量限制为 OuO_u ,故这里只能连 OuO_u 条边,满足出度小于等于 OuO_u 的限制,入度限制同理。

最大流可以保证选至多的边,在很多情况下保证跑满入度或出度,而最小费用保证费用最小。

CF277E Binary Tree on Plane

https://www.luogu.com.cn/problem/CF277E

给定平面上的若干个点,要求组成一棵二叉树,若 iijj 的父亲,必须要有 yi>yjy_i > y_j ,两个点相连的边权定义为两个点之间的欧几里得距离。

求最小二叉树边权和,或报告这样的二叉树不存在。

是上面提及的这个模型,每个点入度小于等于 11 ,出度小于等于 22 ,每个点向所有 yy 比自己小的点连边。

连边方式:

sAi,(0,2);Bit,(1,0)s \rarr A_i, (0, 2); B_i \rarr t, (1, 0)AiBj,yj<yi,(1,dis(i,j))A_i \rarr B_j, y_j \lt y_i, (1, \text{dis}(i, j))

跑最小费用最大流,假如最大流小于 n1n - 1 则无解。

P2764 最小路径覆盖问题

https://www.luogu.com.cn/problem/P2764

给定有向图 G=(V,E)G = (V, E) ,设 PPGG 的一个简单路(顶点不相交,特别的,路径长度即所含路径条数可以为 00 )的集合,如果 VV 中的每个顶点恰好出现在 PP 中的一条路上一次,那么称 PPGG 的路径覆盖,求最小路径覆盖,即 P|P| 最小的 PP

每个点在一条路径上,说明入度小于等于 11 ,出度小于等于 11

按照上面的模型建边,得到最多有 xx 条边,那么最少要 nxn - x 条路径,因为 xx 个点的入度为 11 ,剩下都为 00 ,而每个入度为 00 的点都是一条路径的开头。

输出方案也很简单,如果 AuBvA_u \rarr B_v 跑满了,说明选 uvu \rarr v ,得到原图的一个子图,就容易输出了。

P2765 魔术球问题

https://www.luogu.com.cn/problem/P2765

nn 个柱子,依次向柱子上面放 1,2,3,1, 2, 3, \cdots 的球,要求同一个柱子两个相连的球加起来是一个完全平方数。

把球看成点,把能放在一起的球连起来,需要多少个柱子,就是这个图的最小路径覆盖。

11 开始,不断向图中加点,假如加到 ii ,找出图中所有 ii 能放在上面的点 jj ,即 i+j=x2,xNi + j = x^2, x \in \mathbb{N}jjii 连边,直到这个图的最小路径覆盖大于 nn

P1251 餐巾计划问题

https://www.luogu.com.cn/problem/P1251

一个餐厅在 nn 天中,第 ii 天需要 rir_i 块餐巾,每天,餐厅可以购买餐巾,每块 pp 分,在第 ii 天晚上,餐厅可以把旧餐巾送到快洗部,每块 ff 分,能在第 i+mi + m 天早上收到餐巾,也可以送到慢洗部,每块 ss 分,在第 i+ni + n 天早上收到餐巾。

和我们的模型大抵相符,不过有需要改动的地方,介绍一种采用这种模型思想建图的另一种解释。

把每一天拆成早上和晚上,分别为 MiM_iEiE_i

sEi,(ri,0)s\rarr E_i, (r_i, 0) 表示每天晚上收到 rir_i 块脏餐巾。

Mit,(ri,0)M_i \rarr t, (r_i, 0) 表示每天早上需要 rir_i 块干净餐巾。

sMi,(,p)s \rarr M_i, (\infin, p) 每天早上可以购买餐巾。

EiEi+1,(ri,0)E_i \rarr E_{i+1}, (r_i, 0) 晚上不洗可以留到第二天。

EiEi+m,(,f)E_i \rarr E_{i + m}, (\infin, f) 送到快洗部。

EiEi+n,(,s)E_i \rarr E_{i + n}, (\infin, s) 送到慢洗部。

跑最小费用最大流即可。

P4553 80人环游世界

NN 个国家,按从东向西的顺序给出,某些国家之间有航线, MM 个人旅游,每个人可从任意国家开始,在任意国家结束,从东向西游历若干个国家,注意路线上相邻的两个国家必须由航线直接相连。

同时第 ii 个国家有且仅有 ViV_i 个人游历。

问最少机票钱。

考虑先建出模型中的原图,在根据模型建出跑网络流的图。

可以从任意地方出发,所以设一个出发点 ssss ,出度 MM ,结束点 tttt ,入度 MM ,然后每个国家 ii ,出度 ViV_i ,入度 ViV_i

ssi,0;itt,0ss \rarr i, 0; i \rarr tt, 0 可以从任意地方开始或结束。

iijj 之间有航线,机票一人 ww ,且 iji \le jij,wi \rarr j, w 自东向西,要航线相连。

根据模型,建出跑网络流的图,然后跑最小费用最大流即可。

网络流中的基本概念

介绍一些网络流中的基本概念,实现方法。

对于题目的总结在下一篇

什么是网络流

有一个有向图,其中有两个特殊的点,源点和汇点,你可以通过图中的点和边,从源点向汇点流流量,边有流量限制,然后解决这个图在一定情况下的流量问题(稍后会提及)。

你也可以把它理解为一个供水网络,源点就是水厂,汇点就是你家,每个点就是中转站,有管道连接一些点,管道有粗细,能流过的最大流量不同。

形式化来说:

在一个有向图 G=(V,E)G = (V, E) 中,每条边 (u,v)E(u, v) \in E 都有一个权值 c(u,v)c(u, v) ,表示这条边的容量,同时为了方便,通常认为,对于 (u,v)∉E(u, v) \not\in Ec(u,v)=0c(u, v) = 0

同时,每条边 (u,v)(u, v) 还有个值 f(u,v)f(u, v) ,其为这条边上的流量,它需要满足:

  1. f(u,v)c(u,v)f(u, v) \le c(u, v) 不得超出最大流量限制;
  2. 对于所有的 us,tu \not= s, t 满足 (v,u)Ef(v,u)=(u,w)f(u,w)\sum_{(v, u)\in E} f(v, u) = \sum_{(u, w)} f(u, w) 输入和输出的流量相等,流量守恒;
  3. (s,u)Ef(s,u)=(v,t)Ef(v,t)\sum_{(s, u) \in E} f(s, u) = \sum_{(v, t) \in E} f(v, t) 从源点流出的流量等于汇点流入的流量;

接下来,就是网络流中一个非常精妙的地方,对于一条边 (u,v)(u, v) ,我们建一条反向边 (v,u)(v, u) ,满足 f(v,u)=f(u,v)f(v, u) = -f(u, v) ,即一条边的反向边的流量是他的相反数。

这有什么用呢?它给了我们一个反悔的机会,可以让我们撤销一条边的选择,这在最大流的实现中会提及。

显然加上这个条件后,性质 2. 仍满足。

最大流

对于一个网络流,让它的流量,即 (s,u)Ef(s,u)\sum_{(s, u) \in E} f(s, u) 最大,这就是最大流。

第一反应是可以用一个贪心的做法,不断找到一条从源点到汇点的可行路径(流量比容量小),然后修改路径上的边。

即对于路径 P={(s,u1),(u1,u2),,(up,t)}P = \{(s, u_1), (u_1, u_2), \cdots, (u_p, t)\} ,设其的流量 fP=min(u,v)Pf(u,v)f_P = min_{(u, v) \in P} f(u, v) ,然后 (u,v)P,f(u,v)f(u,v)+fP(u, v) \in P, f(u, v) \larr f(u, v) + f_P

但这样有个问题,例如对于下面的图

1

显然最大流应该是 44 ,但假如找到了这样一条路径:

2

就没有办法继续了,此时的最大流 22 显然是错误的。

现在就要反向边出场了:(只展示了中间的边的反向边)

3

然后又可以有新的路径了:

4

现在最大流就是 44

可以看出反向边的作用就是提供一个撤销错误的贪心决定的机会,以实现全局最优解。

用 bfs 寻找增广路,然后进行增广,就是 Edmonds-Karp 算法,复杂度 O(nm2)O(nm^2) ,其中 nn 为点数, mm 为边数,下同。

Dinic 算法

但一条一条路径(可以称为增广路)找实在太慢了,可不可以一次找多条增广路呢?

这就是 Dinic 算法,先用 bfs 分层,然后用 dfs ,每次只能转移到下一层,向下递归时,寻找增广路,回来时,完成对增广路上边容量的修改,不断重复直到不联通(不存在一条合法路径)。

代码参考:

模板题:「洛谷」P3376

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
struct graph_t {
struct edge_t { // 链式前向星
int v, nxt, flw;
// 实现上为了方便,直接修改流量
} edges[MAXM];
int head[MAXN], e_cnt = 1;
// u^1 得到 u 的反向边

void _add(int u, int v, int f) { edges[++e_cnt] = {v, head[u], f}, head[u] = e_cnt; }
void add(int u, int v, int f) { _add(u, v, f), _add(v, u, 0); }

int dis[MAXN], now[MAXN]; // 当前弧优化,从未流尽的边开始考虑
bool bfs()
{
memset(dis, 0x3f, sizeof(dis));
memcpy(now, head, sizeof(now));
queue<int> q;
q.push(s), dis[s] = 0;
while (q.size()) {
int u = q.front();
q.pop();
for (int i = head[u]; i; i = edges[i].nxt) {
if (edges[i].flw && dis[edges[i].v] > dis[u] + 1) {
// 分层
dis[edges[i].v] = dis[u] + 1;
q.push(edges[i].v);
}
}
}
return dis[t] < INF;
}

int dfs(int u, int flow)
{
if (u == t || !flow) return flow;
int maxflow = 0;
for (int i = now[u]; i && flow; now[u] = i = edges[i].nxt) {
int v = edges[i].v;
if (edges[i].flw && dis[v] == dis[u] + 1) {
// 一层一层来
int k = dfs(v, min(flow, edges[i].flw));
if (!k) dis[v] = INF;
// 删去流尽的边
maxflow += k, flow -= k, edges[i].flw -= k, edges[i ^ 1].flw += k;
// 应用修改
}
}
return maxflow;
}

int dinic()
{
int maxflow = 0, x = 0;
while (bfs()) {
while ((x = dfs(s, INF))) maxflow += x;
}
return maxflow;
}
} G;

时间复杂度证明

最坏时间复杂度 O(n2m)O(n^2m) ,大多数情况远远跑不满 。

考虑每次增广,在使用当前弧优化的情况下,对于每个点,显然当前弧 now 至多变化 mm 次,共 nn 个点,故单次时间复杂度 O(nm)O(nm)

对于汇点 tt ,其的层数 dis[t] 在一次增广后一定变大至少 11 (假如未变大,说明有一条应被考虑的增广路未被考虑),显然层数最大 n1n - 1 ,故 Dinic 算法的时间复杂度 O(n2m)O(n^2m)

HLPP 算法(不重点介绍)

Highest-Label-Preflow-Push 最高标号预流推进算法,可以在 O(n2m)O(n^2\sqrt m) 时间复杂度计算出最大流。

首先来看一下预流推进算法的思想。

这个算法的不同之处就是每个点可以存一些流量(即输入流量减去输出流量),称作超额流,同时每个节点都有高度,可以把存储的超额流推送到有边连接的,高度比他小点(push 操作),但假如没有这样的点,就要更改这个节点的高度,让它的超额流可以流出去(relabel 重贴标签操作),最后所有除了源、汇点之外的点都没有超额流了(每个点无法流到汇点的超额流会被退回到源点),此时进入汇点的流量就是这个网络的最大流。

例子(来自 oi-wiki):

来自 oi-wiki

具体实现结合代码注释理解:

模板题 「洛谷」P4722

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
int n, m, s, t;

struct graph_t {
struct edge_t {
int v, nxt, flw;
} edges[MAXM];
int head[MAXN], e_cnt = 1;

void _add_edge(int u, int v, int f) { edges[++e_cnt] = {v, head[u], f}, head[u] = e_cnt; }
void add_edge(int u, int v, int f) { _add_edge(u, v, f), _add_edge(v, u, 0); }

int h[MAXN], gap[MAXN + 100], val[MAXN];
bool in_q[MAXN];
priority_queue<pair<int, int>> q;

// 从汇点开始标记高度
bool bfs()
{
memset(h, 0, sizeof(h));
queue<int> q;
q.push(t);
h[t] = 0;
while (q.size()) {
int u = q.front();
q.pop();
for (int i = head[u]; i; i = edges[i].nxt) {
int v = edges[i].v;
if (edges[i ^ 1].flw && h[v] > h[u] + 1) {
h[v] = h[u] + 1;
q.push(v);
}
}
}
if (h[s] < 1e9) {
for (int i = 1; i <= n; i++) {
if (h[i] >= 1e9) h[i] = n + 100;
}
return h[s] = n, true;
} else return false;
}

// 向高度正好小 1 的点流超额流
void push(int u)
{
for (int i = head[u]; i && val[u]; i = edges[i].nxt) {
int v = edges[i].v;
if (edges[i].flw && h[v] == h[u] - 1) {
int k = min(edges[i].flw, val[u]);
edges[i].flw -= k, edges[i ^ 1].flw += k;
val[u] -= k, val[v] += k;
if (v != s && v != t && !in_q[v]) q.push({h[v], v}), in_q[v] = true;
}
}
}

// 重贴标签,让 h 刚好比相连的 h 最小的大 1
void relabel(int u)
{
h[u] = 1e9;
for (int i = head[u]; i; i = edges[i].nxt) {
int v = edges[i].v;
if (edges[i].flw && h[u] > h[v] + 1) h[u] = h[v] + 1;
}
if (h[u] >= 1e9) h[u] = n + 100;
}

int hlpp()
{
if (!bfs()) return 0;
// 高度为 h 的有多少个
for (int i = 1; i <= n; i++) gap[h[i]]++;
// 从源点推出,忽略高度
for (int i = head[s]; i; i = edges[i].nxt) {
int v = edges[i].v;
if (edges[i].flw) {
int k = edges[i].flw;
edges[i].flw -= k, edges[i ^ 1].flw += k;
val[v] += k;
if (v != s && v != t && !in_q[v]) q.push({h[v], v}), in_q[v] = true;
}
}

while (q.size()) {
int u = q.top().second;
// 取出高度最高的点
q.pop();
in_q[u] = false;
push(u);
if (val[u]) { // 没流完
gap[h[u]]--;
if (!gap[h[u]]) {
// gap 优化
// 比它高的点不能把流量推送到 t 了
// 高度设为 n + 1 尽快推送回 s
for (int i = 1; i <= n; i++) {
if (i != s && i != t && h[i] > h[u] && h[i] < n + 1) h[i] = n + 1;
}
}
relabel(u);
// 重贴标签
gap[h[u]]++;
q.push({h[u], u});
in_q[u] = true;
}
}
return val[t];
}
} G;

最小割

一个有向图,有边权,它的最小割就是切断一些边,把这个图切成两半,让源点 ss 和汇点 tt 不存在一条路径,同时割断的边边权和最小。

形式化来说:

对于图 G=(V,E)G = (V,E) ,其的割为将点划分为两部分,SST=VST = V - S ,有 sSs \in S, tTt \in T

一个割 (S,T)(S, T) 的容量为 c(S,T)=uS,vTc(u,v)c(S, T) = \sum_{u \in S, v \in T} c(u, v)

GG 的最小割就是求一个割,使其容量最小。

一个图的最小割等于其的最大流,感性理解,不作证明。

最小费用最大流

一个图 G=(V,E)G = (V, E) ,每条边上还有一个单位流量的费用 w(u,v)w(u, v) ,在满足流量最大的情况下,同时使费用最小。

即在满足 (s,u)Ef(s,u)\sum_{(s, u) \in E} f(s, u) 的情况下,让 (u,v)Ef(u,v)w(u,v)\sum_{(u, v) \in E} f(u, v) w(u, v) 最小(只考虑原图的边,不考虑反向边)。

对于边 (u,v)(u, v) 的反向边 (v,u)(v, u) ,令 w(v,u)=w(u,v)w(v, u) = -w(u, v) ,使得流经反向边时能够撤销贡献。

只要把 Dinic 算法的 bfs 改成 spfa ,每次寻找最短的路径( ww 最小)增广。

参考代码:

模板题 「洛谷」P3381

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
struct graph_t {
struct edge_t {
int v, nxt, flw, cst;
} edges[MAXM];
int head[MAXN], e_cnt = 1;
void _add_edge(int u, int v, int f, int c) { edges[++e_cnt] = {v, head[u], f, c}, head[u] = e_cnt; }
void add_edge(int u, int v, int f, int c) { _add_edge(u, v, f, c), _add_edge(v, u, 0, -c); }

int dis[MAXN], now[MAXN];
bool vis[MAXN];
bool spfa() // 最短路
{
memset(dis, 0x3f, sizeof(dis)), memcpy(now, head, sizeof(now));
queue<int> q;
q.push(s), dis[s] = 0;
while (q.size()) {
int u = q.front();
q.pop();
vis[u] = false;
for (int i = head[u]; i; i = edges[i].nxt) {
int v = edges[i].v;
if (edges[i].flw && dis[v] > dis[u] + edges[i].cst) {
dis[v] = dis[u] + edges[i].cst;
if (!vis[v]) vis[v] = true, q.push(v);
}
}
}
return dis[t] < INF;
}

pair<int, int> dfs(int u, int flow) // 和最大流差不多
{
if (u == t || !flow) return {flow, 0};
int maxflow = 0, mincost = 0;
vis[u] = true;
for (int i = now[u]; i && flow; now[u] = i = edges[i].nxt) {
int v = edges[i].v;
if (edges[i].flw && dis[v] == dis[u] + edges[i].cst && !vis[v]) {
auto p = dfs(v, min(flow, edges[i].flw));
int x = p.first, y = p.second;
if (!x) dis[v] = INF;
flow -= x, maxflow += x, edges[i].flw -= x, edges[i ^ 1].flw += x;
mincost += y + x * edges[i].cst; // 更新贡献
}
}
vis[u] = false;
return {maxflow, mincost};
}

pair<int, int> dinic()
{
int maxflow = 0, mincost = 0;
while (spfa()) {
while (true) {
auto p = dfs(s, INF);
maxflow += p.first, mincost += p.second;
if (!p.first) break;
}
}
return {maxflow, mincost};
}
} G;

最大费用最大流只要把最短路改为最长路即可。

上下界网络流

上下界网络流就是在普通的网络流上,再对每条边加上一个限制 l(u,v)l(u, v) ,流量需满足 l(u,v)f(u,v)c(u,v)l(u, v)\le f(u, v) \le c(u, v)

无源汇上下界可行流

没有源点汇点,即所有的点都满足输入流量等于输出流量,且每条边的流量都满足上下界限制,问是否存在这样的方案。

考虑每个边已经流满了下界,同时下文中的源点和汇点,指在新的图中新建的超级源点 ss' 和超级汇点 tt'

我们构造一个只有流量上限的新图 GG ,对于原图中每条边 (u,v)(u, v) ,在新图中连接 (u,v)(u, v) ,同时其的流量限制为 f(u,v)=f(u,v)l(u,v)f'(u, v) = f(u, v) - l(u, v)

接下来考虑点 uu ,设在图 GG 中连向 uu 的所有边的下界和 in(u)=(v,u)Gl(u,v)in(u) = \sum_{(v, u) \in G} l(u, v), out(u)out(u)uu 连向其他点的下界和 out(u)=(u,w)Gl(u,w)out(u) = \sum_{(u, w)\in G} l(u, w)

in(u)out(u)>0in(u) - out(u) > 0 ,则从源点向 uu 连容量为 in(u)out(u)in(u) - out(u) 的边。

in(u)out(u)<0in(u) - out(u) < 0 ,则从 uu 向汇点连权值为 out(u)in(u)out(u) - in(u) 的边。

然后跑最大流即可,所有连向源点和汇点的边跑满,则可行。

为什么是正确的呢?

GG' 满足流量守恒,我们要让图 GG 也满足流量守恒。

设源点向 uu 连边的边权为 a(u)a(u)uu 向汇点连边的边权为 b(u)b(u)

vtf(v,u)+a(u)=wtf(u,w)+b(u)f(v,u)l(v,u)+a(u)=f(u,w)l(u,w)+b(u)a(u)b(u)+l(u,w)l(v,u)=f(u,w)f(v,u)=0a(u)b(u)=l(v,u)l(u,w)=in(u)out(u)\begin{aligned} \sum_{v \not= t'} f'(v, u) + a(u) &= \sum_{w \not= t'} f'(u, w) + b(u) \\ \sum f(v, u) - \sum l(v, u) + a(u) &= \sum f(u, w) - \sum l(u, w) + b(u) \\ a(u) - b(u) + \sum l(u, w) - \sum l(v, u) &= \sum f(u, w) - \sum f(v, u) = 0 \\ a(u) - b(u) &= \sum l(v, u) - \sum l(u, w) = in(u) - out(u) \\ \end{aligned}

in(u)out(u)>0in(u) - out(u) > 0b(u)=0,a(u)=in(u)out(u)b(u) = 0, a(u) = in(u) - out(u)

in(u)out(u)<0in(u) - out(u) < 0 同理。

有源汇上下界可行流

从原图源点向汇点连一条,下界 00 ,上界 \infty 的边,转化为无源汇上下界可行流。

此时可行流为新图中原来的汇点和原来的源点之间连边的流量。

证明:

考虑源点 ss ,其连向超级汇点的边 b(s)=out(s)=l(s,u)b(s) = out(s) = \sum l(s, u)

utf(s,u)+b(s)=f(v,u)f(s,u)l(s,u)+l(s,u)=f(v,u)f(s,u)=f(v,u)\begin{aligned} \sum_{u\not=t} f'(s, u) + b(s) &= f'(v, u) \\ \sum f(s, u) - \sum l(s, u) + \sum l(s, u) &= f'(v, u) \\ \sum f(s, u) &= f'(v, u) \\ \end{aligned}

有源汇上下界最大流

先对图 GG 跑一边可行流(得到新图 GG' ),然后删去所有的 (s,u)(s', u)(u,t)(u, t') 还有 (t,s)(t, s) ,以 ss 为源点, tt 为汇点,跑一遍最大流,加起来,即为答案。

跑可行流时,就满足了流量限制和流量守恒,再跑最大流,不破坏流量限制,同时也能保证流量守恒,和流量最大。

有源汇上下界最小费用流

注意:不一定满足最大流。

对原图 GG 按照有源汇上下界可行流的方式建出图 GG' ,其中图 GG' 中的 w(u,v)=w(u,v)w'(u, v) = w(u, v) ,即和原来的图的费用一样,tt 连向 ss ,连向 tt' 和 从 ss' 连出的其他点费用为 00 ,然后对这个图跑最小费用最大流,在加上图 GG 中的每条边下界乘上边权即可。

例题:「洛谷」P4043

扩展阅读 & 参考资料

  • 「oi-wiki」

  • 算法竞赛进阶指南(李煜东著)0x6A 网络流初步

  • 算法导论(机械工业出版社,Thomas H. Cormen, Charles E. Leiserson 等著),第六部分图算法,第 26 章最大流

Cayley's Formula 凯莱公式

对于一棵 nn 个点的完全图,它的生成树共 nn2n^{n-2} 种。

证明 I : Prüfer 序列

为了方便,下文写作 Prufer 序列。

Prufer 序列可以将一棵节点有编号的树与整数序列一一对应的方法。

Prufer 序列多用于组合计数问题,比如要本文证明的 Cayley’s Formula 。

从树构造 Prufer 序列

对于一棵树,每次选取编号最小的且只有一条边相连的节点,删除它,并把它相连的点加入到序列中,共重复 n2n-2 次。

实现

显然可以用堆维护最小的点,时间复杂度 O(nlogn)O(n\log n)

考虑每删除一个点,只有它的父亲(以 nn 为根)可能成为新的叶子节点。

用一个指针 pp 维护最小的叶子节点。

每删除一个点,看看它的父亲是否是叶子节点,并且比 pp 要小,如是,删除它的父亲,并重复这个操作。

然后 pp 自增,直到找到下一个叶子节点。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void tree2prufer()
{
for (int i = 1; i < n; i++) deg[fa[i]]++; // 为了方便,度数 -1

for (int i = 1, j = 1; i <= n - 2; i++, j++) {
while (deg[j]) j++; // 找到下一个叶子节点
pru[i] = fa[j];
for (; i <= n - 2
&& !(--deg[pru[i]]) // 父亲是否是叶子
&& pru[i] < j; // 父亲是否比 p 小
i++) {
pru[i + 1] = fa[pru[i]];
}
}
}

Prufer 序列的性质

对于度数为 dd 的节点,其在 Prufer 序列中出现 d1d-1 次。

证明:

对于最后只剩两个点,显然他们各自度数为 11 ,满足。

否则考虑按照删点的相反顺序加点,每假如一个点 uu ,设相连的点为 vv ,那么 vv 在 Prufer 序列中出现次数多 11 次,同时 vv 的度数加 11uu 的度数为 11

对于度数为 dd 的点,经历 d1d-1 次如上加点,故在 Prufer 序列中出现 d1d-1 次。

从 Prufer 序列构造树

考虑以构造 Prufer 序列相同的方式构造树。

根据 Prufer 序列的性质,我们可以算出每个节点的度数。

然后每次找最小的度数为 11 的点,把他和序列头的点连起来,然后对它和序列头的节点度数减 11 ,然后删去序列头。
最后把剩下的两个点连起来。

实现

和构造 Prufer 序列操作相同。

在最开始时可以往 Prufer 序列后面加 nn 以方便实现。

1
2
3
4
5
6
7
8
9
10
void prufer2tree()
{
for (int i = 1; i <= n - 2; i++) deg[pru[i]]++;
pru[n - 1] = n;
for (int i = 1, j = 1; i <= n - 1; i++, j++) {
while (deg[j]) j++;
fa[j] = pru[i];
for (; i <= n - 1 && !(--deg[pru[i]]) && pru[i] < j; i++) fa[pru[i]] = pru[i + 1];
}
}

最后

由上,感性理解 Prufer 序列是 nn 个点的无根树和长为 n2n-2 ,每个元素为 11nn 之间的序列的双射。

所以 nn 个点的完全图的生成树映射到 Prufer 序列,方案数就是 nn2n^{n-2}

证明 II:对一个计数问题用两种方法

思路大概可以表示为

1
2
3
4
Cayley's Formula ---> 方法 A ---> 问题 Q <--- 方法 B
^ |
| |
+--------------------------+

问题是,有多少个有向边序列可以构成一棵有根树。
(有向边序列是: {(xi,yi)}i=1n\{(x_i, y_i)\}_{i = 1}^nxix_i 为父亲,xiyix_i \not= y_i

方法 A:使用 Cayley’s Formula

1
选定一棵生成树 ---> 选定一个为根 ---> 排列边

设选定一个生成树的方案数为 SnS_n ,答案为 AnA_n
选根共 nn 种方案,排列 n1n-1 条边 (n1)!(n-1)! 种方案。

An=Snn(n1)!=Snn!A_n = S_n \cdot n \cdot (n-1)! = S_n \cdot n!

方法 B:一个一个加

假设已经有了 kk 个森林,当前考虑 (u,v)(u, v)

uu 可以从 nn 个中选取一个, vv 则从除去 uuk1k-1 棵树的根中选取一个。

故方案数为 n(k1)n(k-1)

最开始有 nn 棵树,每加一条边少一棵。

故答案为:

A=n(n1)n(n2)n=nn1(n1)!=nn2n!=Snn!\begin{aligned} A &= n(n-1) \cdot n(n-2) \cdot \ldots \cdot n \\ &= n^{n-1} (n-1)! \\ &= n^{n-2} n! \\ &= S_n \cdot n! \\ \end{aligned}

所以

Sn=nn2S_n = n^{n-2}

参考资料

oi-wiki.org

Kirchhoff 矩阵树定理

Kirchhoff 定理解决的是对于一个图的生成树的计数问题。

无向图

定理内容

GG 为有 nn 个顶点的有向图,允许重边,不允许自环。

D(i)D(i) 为点 ii 的度数,E(u,v)E(u, v)uuvv 之间边的个数。

那么,Kirchhoff 矩阵 LL 为:

L(G)i,j={D(i) ,if i=jE(i,j) ,otherwiseL(G)_{i, j} = \begin{cases} D(i) \ , \text{if}\ i = j \\ -E(i, j) \ , \text{otherwise} \end{cases}

把矩阵 L(G)L(G) 任意删去一行一列,得到 L(G)L'(G) ,即:

L(G)=L(G)(12i1i+1n12i1i+1n)L'(G) = L(G) \begin{pmatrix} 1 & 2 & \cdots & i - 1 & i + 1 & \cdots & n \\ 1 & 2 & \cdots & i - 1 & i + 1 & \cdots & n \end{pmatrix}

GG 生成树的个数为

t(G)=det(L(G))t(G) = \det(L'(G))

有向图

暂不考虑

行列式

本文是作者自己对行列式的理解,不甚严谨,但希望有启发性,若有错漏,欢迎指出。

本文只讨论方阵。

行列式的定义和理解

detA\det A 表示方阵 AA 的行列式。

其的全排列定义为

detA=p(1)τ(p)i=1nAi,pi\det A = \sum_p (-1)^{\tau(p)} \prod_{i=1}^n A_{i,p_i}

其中 pp11nn 的排列, τ(p)\tau(p) 为排列 pp 的逆序对个数。

(由于对于 nn 阶方阵,其考虑的是 nn 为的向量空间,由于对于 44 维及以上空间,没有相应的词汇描述,故使用 33 维空间的相关词汇描述,例如: 44 维 「长方体」的「体积」,各位读者可以感性理解。)

个人认为更好的理解是:一个方阵的行列式,是这个矩阵的列向量夹成的有向「体积」,或这个矩阵对应的线性变换前后一个任意的「体积」的后与前的比值。

行列式的特殊情况

考虑一些特殊情况。

单位矩阵

首先,单位矩阵 II 的行列式 detI=1\det I = 1

根据行列式的定义,只有当 p=1,2,,np = {1, 2, \cdots , n} 时, Ai,pi\prod A_{i, p_i} 为对角线上的元素相乘,为 11 ,此时 pp 的逆序对个数为 00 ,而其他情况 Ai,pi=0\prod A_{i, p_i} = 0 ,所以 detI=1\det I = 1

这不难理解,相当于一个「边长」为 11 的 「正方体」,其「体积」为 11

对角矩阵

其次对于一个对角矩阵:

A=diag{λ1,λ2,λ3,λn}A = \text{diag}\{\lambda_1, \lambda_2, \lambda_3, \cdots \lambda_n \}

其的行列式为对角线上各数的乘积,同理当 p=1,2,,np = {1, 2, \cdots, n} 时,

detA=Ai,i=λ1λ2λn\det A = \prod A_{i, i} = \lambda_1\lambda_2\cdots\lambda_n

也不难理解,考虑一个边长的 λ1,λ2,,λn\lambda_1, \lambda_2, \cdots ,\lambda_n 的「正方体」即可。

三角矩阵

继续推广到三角矩阵。

对于一个主对角线为 λ1,λ2,,λn\lambda_1, \lambda_2, \cdots, \lambda_n 的上三角矩阵 AA 来说,显然只有 p=1,2,,np = {1, 2, \cdots , n} 时,会对答案有贡献,故 detA=i=1nλi\det A = \prod_{i=1}^n \lambda_i

下三角矩阵同理。

为 0 的情况

考虑 detA\det A 什么时候会为 00

显然有一行或一列均为 00detA\det A00 的充分情况。

考虑定义的式子,连乘必然包含每一列,而假如一列都是 00 ,显然每次都是 00 ,所以答案也是 00

行同理。

行列式的部分性质

行列式和矩阵乘法

接下来将行列式和矩阵乘法结合起来:

det(AB)=(detA)(detB)\det (AB) = (\det A)(\det B)

至于证明,我不会,不过可以根据行列式的几何意义理解。

应用线性变换 BB 后,体积乘以 detB\det B ,再应用线性变换 AA 体积再乘以 detA\det A ,所以把线性变换 BBAA 组合起来,应用线性变换 ABAB ,体积乘以 detAB\det AB

或者对 BB 应用线性变换 AA ,原来的体积是 detA\det A ,然后乘上 detB\det B ,和得到的新体积 det(AB)\det (AB) 相等。

行列式和转置

考虑一个重要的性质:一个矩阵转置,行列式不变,即:

detA=detAT\det A = \det A^T

至于证明,我还是不会(太菜了),不过可以根据组合数组合数的定义感性理解一下,每一种排列都可以进行同样的「转置」操作,同时逆序对数不变。

这也可以说明为什么上三角矩阵和下三角矩阵的行列式情况相似。

这两个性质记下来即可,证明反倒不是那么重要。

行列式和初等变换

(这里只考虑初等行变换)

对于一个矩阵进行初等变换,相当于对其左乘初等矩阵,因此一个矩阵进行初等变换后的行列式等于原来的行列式乘上初等矩阵的行列式。

倍乘

倍乘矩阵是单位矩阵上某个数为 kk

Di(k)=diag{1,1,,1,k,1,,1}D_i(k) = \text{diag}\{1, 1, \cdots, 1, k, 1, \cdots, 1\}

其主对角线上第 ii 个数为 kk ,对一个矩阵进行倍乘操作,就是将第 ii 行的数乘上 kk ,由于倍乘矩阵的行列式为 kk ,所以操作后行列式也乘上 kk

det(ADi(k))=(detA)(detDi(k))=kdetA\det (AD_i(k)) = (\det A)(\det D_i(k)) = k \det A

倍加

倍加矩阵如下所示:(来自 oi-wiki)

Ti,j(k)=(11k11)T_{i, j}(k) = \begin{pmatrix} 1 & & & & & & \\ & \ddots & & & & & \\ & & 1 & \cdots & k & & \\ & & & \ddots & \vdots & & \\ & & & & 1 & & \\ & & & & & \ddots & \\ & & & & & & 1\\ \end{pmatrix}

其中 (Ti,j(k))i,j=k(T_{i, j}(k))_{i, j} = k

倍加矩阵就是单位矩阵同时第 iijj 列为 kk ,倍加矩阵是三角矩阵,其行列式为 11

进行倍加操作,就是把第 jj 列乘上 kk 加到第 ii 行,操作后行列式不变。

这里可以引出行列式的一个性质,即假如有一行(列)是另一行(列)的若干倍,那么其行列式为 00

证明也很简单,假如矩阵 AA 满足 Aa,i=kAb,iA_{a, i} = kA_{b, i} ,那么把 ii 行加上 k-k 倍的 jj 行,第 ii 行全是 00 ,行列式为 00 ,可知原来的矩阵的行列式也为 00

\because&A' \larr A (T_{i,j}(-k)) \\ \because& \det A' = (\det A)(\det T_{i, j}(-k)) = 0 \\ \therefore& \det A = 0

对换

对换操作可以由倍加和倍乘操作组合而成,其作用是调换两行(列),操作后行列式乘 1-1

设要调换的两行(列)为 AABB ,初始时为 A0A_0B0B_0 ,下面用表格展示如何操作。

操作 行列式
A0,B0A_0, B_0 11
A1A0+B0A_1 \larr A_0 + B_0 11
B12B0B_1 \larr -2B_0 2-2
B2B1+2A0=2A0B_2 \larr B_1 + 2A_0 = 2A_0 2-2
B312B2=A0B_3 \larr \frac 1 2 B_2 = A_0 1-1
A2A1B3=B0A_2 \larr A_1 - B_3 = B_0 1-1

如何求行列式

题目:luogu P7112 行列式求值

提交记录

由于倍加操作不改变行列式,所以可以多次应用倍加操作。

对于每一个 ii

i+1jn, Ai=AjAj,iAi,iAii + 1\le j \le n,\ A_i = A_j - \frac {A_{j, i}}{A_{i, i}} A_i

这样可以将它化成上三角矩阵,然后把对角线上的数乘起来即可。

1
2
3
4
5
6
7
8
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
auto d = mat[j][i] / mat[i][i];
for (int k = i; k <= n; k++) {
mat[j][k] = (mat[j][k] - d * mat[i][k] % mod + mod) % mod;
}
}
}

行列式的应用

据本人所知,在 OI 中,大概只有 Matrix Tree 定理

NTT 数论变换

在 FFT 中,需要用到复数和 double,这有两个问题:精度较低和无法实现取模。

所幸,这两个问题都可以用一个东西解决:NTT。

NTT 所解决的问题和 FFT 相似:

有两个多项式 f(x)=a0+a1x1+a2x2++akxkf(x) = a_0 + a_1x^1 + a_2x^2 + \cdots + a_kx^kg(x)=b0+b1x1+b2x2++blxlg(x) = b_0 + b_1x^1 + b_2x^2 + \cdots + b_lx^l

h(x)=f(x)g(x)h(x) = f(x) \cdot g(x) ,但 h(x)h(x) 每项系数对 PP 取模。

我们考虑用一个叫做原根的东西代替单位根,设 nn 次单位根为 ωn\omega_n

四条性质

在 FFT 中所用到了四条单位根的性质,它们是:

  1. 对于 0i<n0 \le i \lt nωni\omega_n^i 各不相同。

  2. 折半定理 ωni\omega_n^i = ω2n2i\omega_{2n}^{2i} ,用于分治。

  3. ωni\omega_n^i = -ωni+n2\omega_n^{i + \frac n 2} ,用于分治。

  4. 对于 t=i=0n1ωnkit = \sum_{i=0}^{n-1} \omega_n^{ki} , 当 k=0k = 0 时, t=nt = n ,否则 t=0t = 0 ,用于 IDFT 。

对于质数 P=2tq+1P = 2^t q + 1 ,其的原根 gg 为满足 gi,0i<Pg^i, 0 \le i \lt P 互不相同的数。

考虑这四个性质,令 n=2tn = 2^tωn=gq\omega_n = g^q

  1. 显然 00,q,2q,,(n1)q<P0 \le 0, q, 2q, \cdots , (n-1)q \lt P ,故 ω0,ω1,ω2,,ωn1<P\omega^0, \omega^1, \omega^2, \cdots, \omega^{n-1} \lt P 互不相同,满足。

  2. ω2n2i=(gn2)2i=gni=ωni\omega_{2n}^{2i} = (g^{\frac n 2})^{2i} = g^{ni} = \omega_n^i

  3. 根据费马小定理 ωn=gP1=1 (mod P)\omega^n = g^{P-1} = 1\ (\bmod\ P),又因为 (ωn2)2=ωn=1(\omega^{\frac n 2})^2 = \omega^n = 1 ,所以 ωn2=1 or 1\omega^{\frac n 2} = 1 \text{ or } -1 ,又因为 gig^i 互不相同,所以 ωn2=1\omega^{\frac n 2} = -1 ,所以 ωni=ωni+n2\omega_n^i = -\omega_n^{i + \frac n 2}

  4. k0k \not= 0

S(k)=(ωk)0+(ωk)1++(ωk)(n1)ωkS(k)=(ωk)1+(ωk)2++(ωk)nωkS(k)S(k)=(ωk)n1S(k)=ωnk1ωk1\begin{aligned} S(k) &= (\omega^k)^0 + (\omega^k)^1 + \cdots + (\omega^k)^{(n-1)} \\ \omega^k S(k) &= (\omega^k)^1 + (\omega^k)^2 + \cdots + (\omega^k)^n \\ \omega^k S(k) - S(k) &= (\omega^k)^n - 1 \\ S(k) &= \frac {\omega^{nk} - 1} {\omega^k - 1} \end{aligned}

由于 ωnmodP=1\omega_n \bmod P = 1 ,所以 ωnk1modP=0\omega^{nk} - 1 \bmod P = 0 ,所以 S(k)=0S(k) = 0

求原根

先略过,记住几个常用的模数的原根。

PP 分解 gg
998244353998244353 7×17×223+17 \times 17 \times 2^{23} + 1 33
10045358091004535809 479×221+1479 \times 2^{21} + 1 33

参考资料

oi-wiki.org

「Menci」大大的博客

FFT 快速傅里叶变换

duo xiang shi
本文仅讨论 FFT 在 OI 中的使用。

表述可能不甚严谨,如有错漏,欢迎指出。

能解决什么问题?

有两个多项式 f(x)=a0+a1x1+a2x2++akxkf(x) = a_0 + a_1x^1 + a_2x^2 + \cdots + a_kx^kg(x)=b0+b1x1+b2x2++blxlg(x) = b_0 + b_1x^1 + b_2x^2 + \cdots + b_lx^l

h(x)=f(x)g(x)h(x) = f(x) \cdot g(x)

显然使用朴素算法,时间复杂度是 O(kl)\Omicron(kl)

但 FFT 可以将其优化到 O(nlogn)\Omicron(n \log n) 级别。

引理 I (插值)

nn 个 2元组,为 (x0,y0),(x1,y1),,(xn1,yn1)(x_0, y_0), (x_1, y_1), \cdots , (x_{n - 1}, y_{n - 1})ij,xixj\forall i \not= j, x_i \not= x_j
存在一个唯一的 n1n - 1次多项式,使得 i[0,n) f(xi)=yi\forall i \in [0, n)\ f(x_i) = y_i

唯一性

若有 f(x),g(x)f(x), g(x) 满足以上条件,设 r(x)=f(x)g(x)r(x) = f(x) - g(x)

i[0,n) r(xi)=0\forall i \in [0, n)\ r(x_i) = 0,即 rrnn 个根。

且因为 n1n - 1 次方程最多只有 n1n - 1 个根。

r(x)=0r(x) = 0

存在性

f(x)f(x) 可以通过以下方式构造:

f(x)=i=0n1yi(jixxjxixj)f(x) = \sum_{i = 0}^{n - 1} y_i \cdot (\prod_{j \not= i} \frac {x - x_j} {x_i - x_j})

解释:对于所有的 ii,若 x=xix = x_i 时,对应乘积的部分每一项均为 11,否则会有一项为 00

显然因为 xi,yix_i, y_i 均为已知量,并且乘积中只有 n1n - 1xxjx - x_j 相乘,故 f(x)f(x)n1n - 1 次多项式,符合要求。

具体步骤

由于 引理I,只要有 nn 个不同的 xx 和对应的 f(x)f(x) ,那么就可以确定 f(x)f(x),故我们只需要找出 h(x)h(x)nn 个互不相同的参数和对应的取值即可。

为了方便,我们设 n=k+l1n = k + l - 1

I. 选取 x0,x1,xn1x_0, x_1, \dots x_{n-1}

II. 确定 f(x0),f(x1),f(xn1)f(x_0), f(x_1), \ldots f(x_{n-1})g(x0),g(x1),g(xn1)g(x_0), g(x_1), \ldots g(x_{n-1})

III. 根据以上,得出 h(xi)=f(xi)g(xi)h(x_i) = f(x_i) \cdot g(x_i),求出 hh

乍看时间复杂度还是 O(n2)\Omicron(n^2) 的,但是不要着急,我们慢慢来。

步骤 I

对于 n1n - 1 次多项式,我们选取

xk=ωnk=e2πkni=cos2πn+isin2πnx_k = \omega_n^k = e^{\frac {2 \pi k} n i} = \cos{\frac {2\pi} n} + i \sin{\frac {2\pi} n}

其中 ωni\omega_n^ixn=1x^n = 1 的所有根,即 nn 次单位虚根。

假如放到复平面上理解,就是单位圆上的 nn 等分点。

例如 n=8n = 8

1

为什么选取它呢?因为有一个性质:折半定理。

n,kZ,ωnk=ω2n2k\forall n, k \in \mathbb{Z}, \omega_n^k = \omega_{2n}^{2k}

从直觉上来说比较显然,故不作证明。

那么这个有什么用呢?当然有用,我们可以借此折半问题规模,继续往下看。

步骤 II (DFT)

因为求 f(xi)f(x_i) 与求 g(xi)g(x_i) 本质相同,故仅以 f(xi)f(x_i) 为例。

为了叙述和编写代码的方便,我们令 nn 为总是可以表示成 n=2qn = 2^q 。(多出的项补 00 即可,此处 nn 与上面不同)

同时,用下标表示序列的长度, fm(xi),m=2qf_m(x_i), m = 2^q ,设 am,ia_{m, i}fmf_m 各项的系数。

fn(xi),i[0,n)f_n(x_i), i \in [0, n) 依然是 O(n2)\Omicron(n^2) 的,要优化到 O(nlogn)\Omicron(n \log n)

考虑求 fn(xi)f_n(x_i) 的情况:

n=1n = 1,因为 ω1=1\omega_1 = 1,故 f1=a1,0f_1 = a_{1, 0}

否则,nn 为偶数,所以有:(根据上面的约定 fn(x)f_n(x)n1n-1 次多项式)

fn(x)f_n(x) 根据奇偶性拆开:

fn(x)=an,0+an,1x1++an,n1xn1=(an,0+an,2x2++an,n2xn2)  +(an,1x1+an,3x3++an,n1xn1)=(an,0(x2)0+an,2(x2)1++an,n2(x2)n22)  +x(an,1(x2)0+an,3(x2)1++an,n1(x2)n22)\begin{aligned} f_n(x) &= a_{n, 0} + a_{n, 1}x^1 + \cdots + a_{n, n - 1}x^{n - 1} \\ &= (a_{n, 0} + a_{n, 2}x^2 + \cdots + a_{n, n - 2}x^{n - 2}) \\ &\ \ + (a_{n, 1}x^1 + a_{n, 3}x^3 + \cdots + a_{n, n - 1}x^{n - 1}) \\ &= (a_{n, 0}(x^2)^0 + a_{n, 2}(x^2)^1 + \cdots + a_{n, n - 2}(x ^ 2)^{\frac {n - 2} 2}) \\ &\ \ + x(a_{n, 1}(x^2)^0 + a_{n, 3}(x^2)^1 + \cdots + a_{n, n - 1}(x ^ 2)^{\frac {n - 2} 2}) \\ \end{aligned}

把平方提出来,得到:

fn,0(x)=an,0x0+an,2x1++an,n2xn22fn,1(x)=an,1x0+an,3x1++an,n1xn22f_{n, 0}(x) = a_{n, 0}x^0 + a_{n, 2}x^1 + \cdots + a_{n, n - 2}x^{\frac {n - 2} 2} \\ f_{n, 1}(x) = a_{n, 1}x^0 + a_{n, 3}x^1 + \cdots + a_{n, n - 1}x^{\frac {n - 2} 2} \\

那么:

fn(x)=fn,0(x2)+xfn,1(x2)fn(ωni)=fn,0(ωn2i)+ωnifn,1(ωn2i)f_n(x) = f_{n, 0}(x ^ 2) + xf_{n, 1}(x ^ 2) \\ f_n(\omega_n^i) = f_{n, 0}(\omega_n^{2i}) + \omega_n^i f_{n, 1}(\omega_n^{2i})

发现,可以引用折半定理,令 p=n2p = \frac n 2

fn(ωni)=fn,0(ωpi)+ωnifn,1(ωpi)f_n(\omega_n^i) = f_{n, 0}(\omega_p^i) + \omega_n^i f_{n, 1}(\omega_p^i)

我们可以观察一下这一番操作的结果:

带入的值 00 11 \cdots p1p - 1 p+0p + 0 p+1p + 1 \cdots 2p12p - 1
fn(x)f_n(x) ωn0\omega_n^0 ωn1\omega_n^1 \cdots ωnp1\omega_n^{p-1} ωnp+0\omega_n^{p+0} ωnp+1\omega_n^{p+1} \cdots ωnp+p1\omega_n^{p+p-1}
fn,0(x)f_{n, 0}(x) ωp0\omega_p^0 ωp1\omega_p^1 \cdots ωpp1\omega_p^{p-1} ωpp+0\omega_p^{p+0} ωpp+1\omega_p^{p+1} \cdots ωpp+p1\omega_p^{p+p-1}
fn,1(x)f_{n, 1}(x) ωp0\omega_p^0 ωp1\omega_p^1 \cdots ωpp1\omega_p^{p-1} ωpp+0\omega_p^{p+0} ωpp+1\omega_p^{p+1} \cdots ωpp+p1\omega_p^{p+p-1}

可以发现,由于显然有 ωpi=ωpi+p\omega_p^i = \omega_p^{i+p}
每次只用处理 fn,0f_{n,0} 带入 i[0,p),ωpii\in [0, p), \omega_p^i 的值即可,fn,1f_{n, 1} 同理。

由于 p=n2p = \frac n 2,所以每次处理的规模折半,可以实现 O(nlogn)O(n \log n)

以下是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef double db;
typedef complex<double> comp;

void fft(int n, comp* a /* 输入数组 */ , int op /* 会有其他的用途,这里暂时忽略,可以认为总为 1 */)
{
if (n == 1) return;
comp a0[n >> 1], a1[n >> 1];
for (int i = 0; i < n >> 1; i++) a0[i] = a[i << 1], a1[i] = a[i << 1 | 1]; // 拆成两半
fft(n >> 1, a0, op), fft(n >> 1, a1, op); // 分治

comp wn = comp(cos(2.0 * PI / n), op * sin(2.0 * PI / n)), w = comp(1, 0);
for (int i = 0; i < (n >> 1); i++, w *= wn) { // 合起来
a[i] = a0[i] + w * a1[i]; // 前一半
a[i + (n >> 1)] = a0[i] - w * a1[i]; // 后一半
}
}

步骤 III (IDFT)

ffgg 插值后相乘,现在我们需要根据这个相乘的结果反退出 h(x)h(x) 的系数,即点值。

可以从矩阵的角度考虑,插值过程中发生了什么。

A=[an,0an,1an,n1] V=[ωn00ωn01ωn0(n1)ωn10ωn11ωn1(n1)ωn(n1)0ωn(n1)1ωn(n1)(n1)] Y=[fn(ωn0)fn(ωn1)fn(ωnn1)]=VA\vec A = \begin{bmatrix} a_{n, 0} \\ a_{n, 1} \\ \vdots \\ a_{n, n-1} \\ \end{bmatrix} \\ \ \\ V = \begin {bmatrix} \omega_n^{0\cdot0} & \omega_n^{0\cdot1} & \cdots & \omega_n^{0\cdot(n-1)} \\ \omega_n^{1\cdot0} & \omega_n^{1\cdot1} & \cdots & \omega_n^{1\cdot(n-1)} \\ \vdots & \vdots & \ddots & \vdots \\ \omega_n^{(n-1)\cdot0} & \omega_n^{(n-1)\cdot1} & \cdots & \omega_n^{(n-1)\cdot(n-1)} \\ \end{bmatrix} \\ \ \\ \vec Y = \begin{bmatrix} f_n(\omega_n^0)\\ f_n(\omega_n^1)\\ \vdots \\ f_n(\omega_n^{n-1})\\ \end{bmatrix} = V \vec A \\

现在对于 hh 知道了 Y\vec YVV,求 A\vec A
显然 A=YV1\vec A = \vec Y V^{-1}

U=[ωn00ωn01ωn0(n1)ωn10ωn11ωn1(n1)ωn(n1)0ωn(n1)1ωn(n1)(n1)] VU=[n0000n0000n0000n] V1=1nUU = \begin {bmatrix} \omega_n^{-0\cdot0} & \omega_n^{-0\cdot1} & \cdots & \omega_n^{-0\cdot(n-1)} \\ \omega_n^{-1\cdot0} & \omega_n^{-1\cdot1} & \cdots & \omega_n^{-1\cdot(n-1)} \\ \vdots & \vdots & \ddots & \vdots \\ \omega_n^{-(n-1)\cdot0} & \omega_n^{-(n-1)\cdot1} & \cdots & \omega_n^{-(n-1)\cdot(n-1)} \\ \end{bmatrix} \\ \ \\ VU = \begin {bmatrix} n & 0 & 0 & \cdots & 0 \\ 0 & n & 0 & \cdots & 0 \\ 0 & 0 & n & \cdots & 0 \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & 0 & \cdots & n \\ \end {bmatrix} \\ \ \\ V^{-1} = \frac 1 n U \\

解释:

Xi,jX_{i, j} 为矩阵 XXiijj 列的值。令 B=VUB = VU

要证明

Bi,j={n, if i=j0, otherwiseB_{i,j} = \left\{\begin{aligned} n,\ & \text{if}\ i = j \\ 0,\ & \text{otherwise} \end{aligned} \right. \\

Bi,j=k=0nVi,kUk,j=k=0nωikωkj=k=0nωk(ij)B_{i, j} = \sum_{k=0}^n V_{i,k}U_{k, j} = \sum_{k=0}^n \omega^{ik} \omega^{-kj} = \sum_{k=0}^n \omega^{k(i - j)}

i=ji = j 时,Bi,j=k=0nω0=nB_{i, j} = \sum_{k=0}^n \omega^0 = n

否则设 t=ij,t0t = i - j, t \not= 0,要证明

Bi,j=k=0nωkt=0B_{i, j} = \sum_{k=0}^n \omega^{kt} = 0

即将其看作复平面的单位圆上 nn 个点,第 00 个点为 (1,0)(1, 0),相邻点间隔 2πnt\frac {2\pi} n t,求每个点是否都有唯一的点对应使得二者之和为 00 (过原点对称,且这里只考虑 nn2q2^q 的情况)。

t=2pct = 2^p \cdot cn=2qn = 2^qcc 为奇数,且 p<qp < q

nn 个点分成 2p2^p 组,每组 2qp2^{q-p} 个,显然每组相同,故只考虑第 00 组。

对于该组的第 k<2qp2k \lt \frac {2^{q-p}} 2 个,其与第 k+2qp2k + \frac {2^{q-p}} 2 个相对应,因为有

ωk+t2qp2=ωkω2pc2q2p+1=ωk(ω2q2)c=ωk(ωn2)c=ωk(1)c=ωk\omega^{k + t \frac {2^ {q-p}} 2} = \omega^k \omega^{2^p \cdot c \cdot \frac {2^q} {2^{p + 1}}} = \omega^k (\omega^{\frac{2^q} 2})^c = \omega^k (\omega^{\frac n 2})^c = \omega^k (-1)^c = -\omega^k

所以只要把插值过程中的 ωni\omega_n^i 变为 ωni\omega_n^{-i} ,再对结果乘 1n\frac 1 n 即为答案。

代码 (luogu P3803):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/*
author: byronwan
problem:
url: https://www.luogu.com.cn/problem/P3803
title: FFT
date: 2022-11-12
*/

#include <bits/stdc++.h>

using namespace std;

typedef double db;
typedef complex<double> comp;

const int MAXN = 4e6 + 10;
const double PI = acos(-1);

int k, l, n;
comp a[MAXN], b[MAXN];

void setup()
{
cin >> k >> l;
for (int i = 0; i <= k; i++) cin >> a[i];
for (int i = 0; i <= l; i++) cin >> b[i];
for (n = 1; n <= k + l; n <<= 1)
;
}

void fft(int n, comp* a, int op)
{
if (n == 1) return;
comp a0[n >> 1], a1[n >> 1];
for (int i = 0; i < n >> 1; i++) a0[i] = a[i << 1], a1[i] = a[i << 1 | 1];
fft(n >> 1, a0, op), fft(n >> 1, a1, op);

comp wn = comp(cos(2.0 * PI / n), op * sin(2.0 * PI / n)), w = comp(1, 0);
for (int i = 0; i < (n >> 1); i++, w *= wn) {
a[i] = a0[i] + w * a1[i];
a[i + (n >> 1)] = a0[i] - w * a1[i];
}
}

int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr), clog.tie(nullptr);

setup();
fft(n, a, 1), fft(n, b, 1);

for (int i = 0; i < n; i++) a[i] = a[i] * b[i];
fft(n, a, -1);

for (int i = 0; i <= k + l; i++) cout << (long long)(a[i].real() / n + 0.5) << " ";
cout << endl;
}

迭代实现

刚才的代码是递归实现,由于一些原因(个人猜测是要在栈上开数组),递归实现虽然写起来方便,但可能会被卡,以下介绍不递归的迭代实现。

首先,我们以 n=8n = 8 为例,模拟递归的过程。

1
2
3
4
5
6
               a0 a1 a2 a3 a4 a5 a6 a7
a0 a2 a4 a6 | a1 a3 a5 a7
a0 a4 | a2 a6 | a1 a5 | a3 a7
a0 | a4 | a2 | a6 | a1 | a5 | a3 | a7

binary 000 100 010 110 001 101 011 111

不难发现,每一位上 a 的下标是该位的下标的二进制反过来。

我们首先把 a 按照这个规律放好(跳过了递归实现中分配 a 的步骤),直接开始合并,而合并的方法和原来相同(合并过程中不需要 a,只要位置对了就行)。

那么问题就是如何把 a 放好,设 rev(i)rev(i)ii 二进制反过来的数,显然有 rev(rev(i))=irev(rev(i)) = i,所以 rev(i)rev(i)ii 是一一映射的。

考虑用递推的方式求 rev(i)rev(i)

1
2
3
4
5
6
7
8
9
     i: a b c d e  f
^---+---^ ^
| |
+--+ |
| |
+------|---+
| |
+ +---+---+
rev(i): f e d c b a

所以

1
2
3
rev[i] = 
(rev[i >> 1] >> 1 /* 除以2是因为它的长度为 log n 原来的最高位即现在的最低位为 0 */)
| ((i & 1) << (l - 1 /*长度*/))

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/*
author: byronwan
problem:
url: https://www.luogu.com.cn/problem/P3803
title: FFT
date: 2023-01-10
*/

#include <bits/stdc++.h>

using namespace std;

typedef double db;
typedef complex<double> comp;

const int MAXN = 4e6 + 10;
const double PI = acos(-1);

int k, l, n, m;
int rev[MAXN];
comp a[MAXN], b[MAXN];

void setup()
{
cin >> k >> l;
for (int i = 0; i <= k; i++) cin >> a[i];
for (int i = 0; i <= l; i++) cin >> b[i];
for (n = 1; n <= k + l; n <<= 1, m++)
;
}

void fft(comp* a, int op)
{
for (int i = 0; i < n; i++) {
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (m - 1));
if (rev[i] > i) swap(a[rev[i]], a[i]);
}

for (int p = 1; p < n; p <<= 1) {
int len = p * 2;
auto wn = comp(cos(2.0 * PI / len), op * sin(2.0 * PI / len));
for (int i = 0; i < n; i += len) {
auto w = comp(1, 0);
for (int j = 0; j < p; j++, w *= wn) {
auto x = a[i + j], y = w * a[i + j + p];
a[i + j] = x + y, a[i + j + p] = x - y;
}
}
}
}

int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr), clog.tie(nullptr);

setup();
fft(a, 1), fft(b, 1);

for (int i = 0; i < n; i++) a[i] = a[i] * b[i];
fft(a, -1);

for (int i = 0; i <= k + l; i++) cout << (long long) (a[i].real() / n + 0.5) << " ";
cout << endl;
}

参考资料

「自为风月马前卒」大大的博客

oi-wiki.org