幻影彭的彩虹

寻遍这星空

CF1698F

思路

变过来变过去,还 TM 离谱的操作当然要找不变量,算是半个套路。

reverse 两端相等的区间,可以发现每两个相邻元素构成的无序对不变,然后第一个元素和最后一个元素不改变。

换句话说,\(\{(a_i,a_{i+1},i \in [1,n)\}\) 这个集合不随 \(a\) 中操作改变。或者说,如果从 \(a_i\)\(a_{i+1}\) 连边,这张无向图是不变的,同时 \(a_1,a_n\) 不改变。这个条件和上个条件之间的转换是解题的关键点之一。

套路又来了,看看样例,集合相同并且首尾相同这个条件貌似挺充分的,所以考虑证明。

对于构造问题,常用的证明方式是数学归纳法。

事实上,如果我们能证明如果第二个数可以成功调整为相同,那么整个数组也可以,因为满足条件的 \(a,b\) 如果同时删掉第一个数,仍满足条件。

考虑构造方案,由结论可以知道,如果 \(a\) 中必定存在 \((b_1,b_2)\) 无序对,因为 \(a_2\neq b_2\),不妨让这对无序对是 \((a_x,a_{x+1}),x\in[2,n)\),如果 \(a_x=b_2\),考虑直接将这一对中的 \(a_{x+1}\)\(a_1\) 子数组 reverse ,完事。如果 \(a_{x+1}=b_2\),事情有点麻烦。

if only i could find a pair which...

补上那句话,如果能找到一对可以被翻转的,左端点在 \([1,x]\) ,右端点在 \((x,n]\) 的,那么我们就翻转,改变了 \(a_x,a_{x+1}\) 的顺序。

尝试证明一定能找到,即 \(\{a_i ,i\in [1,x]\} \bigcap \{a_i.i\in(x,n]\} \neq \emptyset\)

反证法,如果找不到,那么可知 $a_i,i$ 与 \(a_j,j\in (x,n]\) 除开 \((a_i,a_i+1)\) 这一次相邻外均不相邻,考虑 \(b_2\)\(a_{x+1}\) 的情况,它与 \(b_3\) 相邻,可知 \(b_3 \in \{a_i.i\in(x,n]\}\),同理有 \(\forall j\ge 2,b_j\in\{a_i.i\in(x,n]\}\)

考虑 \(a_2\),它显然不存在于 \(b\),故矛盾。

回顾

这题比较难的有两个点,一是注意到这个不变量极大可能是充分条件,二是发现 \(a_{x+1}=b_2\) 的 case 中一定存在可交换项,搞定了这两个,问题就迎刃而解。关键点在于第二个,找到 \(a_{[1,x]},a_{[x+1,n]}\) 交集的关系,并尝试用反证法证明交集不为空是比较困难的。

ABC259G

很有意思的网络流题。

最开始想到二分图相关,因为 \(A_{i,j} <0\) 的限制指向性比较明确,然后发现如果二分图的决策正数话会出现不同块之间相互影响,所以考虑决策负数,决策负数不同联通块互不影响,但是无法计算答案,因为最终还是需要确定到底哪些正数被选了。

解法一

题解给出了一个新思路,考虑先只把所有正数选了,然后再来看满足条件的代价。

代价被分为了三类,第一类是顺带选择的负数的代价,如果选了一行或者一列,就会有这一行或列所有负数绝对值之和的代价。

第二类是无法选择正数的代价,如果正数所在的行和列都没被选择,那么就会有这个正数的代价。

第三类是重复选择负数的代价,如果一个负数被行和列同时选择(为了付出更少的第二类和其它第一类代价),那么这个代价是无穷大。

我们需要最小化代价。

0/1 决策问题,考虑套最小割上去,每一行每一列视为一个点。

令与 \(s\) 同集合的为选择,与 \(t\) 同集合的为不选,选一行或一列的代价为该行或列负数绝对值之和,从个点到 \(t\) 连边就行,行列同时选择负数,代价为 inf,woc,怎么连呢?从行连向列,意义为选了行不选列的代价,从列向行连,意义为选了列不选行的代价。所以我们前面的安排有些问题,需要做出调整。

对于行和列,我们让属于 s,t 所在集合对它们有不同意义,下面让属于 s 的行为不选择,属于 t 的列为不选择,我们让 \(s\) 向行连边,这条边表示选该行的代价,让列向 \(t\) 连边,表示选该列的代价。于是,对于一个点,行列都选的代价当且仅当 \(A_{i,j}<0\) 时为 inf,此时从列向行连边,表示都选的代价,行列都不选的代价当且仅当 \(A_{i,j}\ge0\) 时为 \(A_{i,j}\),此时从行向列连边。

注意,我们的割中如果出现了行列都不选,那么对应的行和列与 \(s,t\) 的边一定没有断开,所以必须断开行到列的边。如果出现了行和列都选的不合法情况,我们发现,断掉的行能到 \(t\),从 \(s\) 一定能到断掉的列。如果不满足,那么这个割就不是最小割,不会被我们考虑。所以我们需要从列到行连边,保证不会给负数打上两个标记。

这种思路和某类 dp 的思路很类似,相当值得学习,其实在原问题的求解中,并没有什么条件来保证不会给一个负数打上两个标记,但是我们在通过最小割求解时,限定了决策的范围和最优性,获得了额外的信息,也就能帮助我们排除掉难处理但是不可能的情况,本质上,这种排除还和我们先假定所有正数都选上的前提有关系,这种解法相当精妙。

解法二

从直觉上来看,应该不会选择和为负数的行或者列。考虑一个最终方案,它的答案会是 \(\sum 选择的行+\sum 选择的列 -\sum 行列交叉处的正数\)。考虑从这里面剔除和为负数的列或行,发现最终值一定变大。

所以删掉和为负数的行和列。

考虑先把剩下的行和列全选了,然后解决冲突。

解决冲突的方式有三种,一种时不选行,一种是不选列,另一种是硬吃同时选的代价。

这样的建图就很简洁了,从 \(s\) 向行,列向 \(t\) 连边,对于交点,正数连其值的边,负数连 inf 边。很容易发现一个合法解和一个割一一对应,over。

Sum up

最小割解决实际问题的核心,在于用一个割,或者可能成为最小割的割,来代表一个实际的决策方案,最小割的容量,代表代价,每个点在哪个割集分别代表什么含义并不重要,重要的是割掉每条边的意义,和决策方案与最小割的对应关系。

其实这道题还给我们一个启发,就是在考虑0/1决策问题时,可以先考虑钦定一个决策,再来调整使得它合理或者变优,这可能会使得问题变得简单,也许算是一个套路。

本质上,最小割表达了一种最优的解决决策冲突的方案,我们在 0/1 决策问题钦定决策的过程中,制造了一些冲突,用一个图的割来表达解决着些冲突的方案。

20220305

ABC242F(二项式反演的进一步理解)

题意:

给定一个 \(n*m\) 的矩阵,需要放 \(a\) 个白块 \(b\) 个黑块,让它们互不侵犯,\(n,m\leq 50\)

最开始看到这道题以为又要挑战 \(\text{npc}\) 了,仔细一看互不侵犯的定义,是不能放在同一行或同一列。

思考:

很有意思的容斥题,考虑枚举白色占了多大的地盘,设 \(f[i][j]\) 表示允许白色占用了 \(i\)\(j\) 列的方案数,然后组合数容斥。我们可以设 \(g[i][j]\) 为白色恰好占用 \(i\)\(j\) 列的方案数,考虑列出二项式反演的式子。思考 \(f,g\) 的关系,这是一类高维容斥问题。

结论如下: \[ g[i][j] = f[i][j] - \sum\limits_{x\leq i,y\leq j,x+y \neq i+j } \tbinom{i}{x} \times \tbinom{j}{y} \times g[x][y] \] 理解这个式子不难,就是减去恰好被占用的部分,注意容斥系数。

我们考虑能不能不用 \(g\) 参与这个式子。

回想线性的二项式反演问题,我们有式子: \[ f(n) = \sum\limits_{i=n}^{m} \tbinom{i}{n} \times g(i) \Leftrightarrow g(n) = \sum\limits_{i=n}^{m} (-1)^{n-i}\times\tbinom{i}{n} \times f(i) \] 其中 \(f(n)\) 表示钦定满足 \(n\) 个条件的方案数。\(g(n)\) 表示恰好满足 \(n\) 的条件的方案数。

左边很好理解,就是钦定 \(n\) 个条件满足的方案数就是从满足 \(i\) 个条件的方案中选出 \(n\) 个钦定满足。

右边可以稍微变换一下以便理解 \[ g(n) = f(n) - \sum\limits_{i=n+1}^{m} \tbinom{i}{n} g(i) \] 就是钦定 \(i\) 个的情况减去恰好有大于 \(i\) 个的情况乘上一个组合数,这和上面那个二维的很类似。

给出高位二项式反演公式: \[ g(n_1,n_2,⋯,n_m)=∑\limits_{k_i = 0}^{n_i} ∏\limits_{i=1}^{m}\tbinom{n_i}{k_i}f(k_1,k_2,⋯,k_m) \]

\[ \ \ \ \ \ \ \ \ \ \ \ \Updownarrow \]

\[ f(n_1,n_2,⋯,n_m)=∑\limits_{k_i=0}^{n_i}∏\limits_{i=1}^{m}(−1)^{n_i−k_i}\tbinom{n_i}{k_i} g(k_1,k_2,⋯,k_m) \]

第一个二维式子和第二个一维式子很类似,所以考虑推出 \(f,g\) 的关系。 \[ f(n,m) = \sum\limits_{i=0}^{n} \sum\limits_{j=0}^{m} g(i,j) \times \tbinom{n}{i} \times \tbinom{m}{j} \] 组合解释是钦定 \(n\)\(m\) 列可以被占用的方案数就是先从其中选出 \(i\)\(j\) 列。然后计算恰好这么多被占用的方案数。 因为行和列是有标号的。

直接代公式有 \[ g(n,m) = \sum\limits_{i=0}^{n} \sum\limits_{j=0}^{m} (-1)^{n-i+m-j} \tbinom{n}{i}\times \tbinom{m}{j}\times f(i,j) \] 结束。

ABC242H(初步理解 min-max 容斥)

题意:

给定 \(m\) 条线段,一个长度为 \(n\) 的数轴,每次随机选一条把数轴上对应位置涂黑,问全部涂黑的期望选择次数。

思考:

\(E(i)\) 表示第 \(i\) 个格子被涂黑的期望时间。记 \(E(S)\) 表示集合 \(S\) 全部被涂黑的期望时间,也就是所有格子中被涂黑时间的最大值,\(E'(S)\) 表示集合 \(S\) 中任意一个被涂黑的期望时间,即最小值。

显然 \(E(S) \neq \max\limits_{i\in S} (E(i))\),因为期望只有线性性,\(max,min\) 是非线性操作。

但是我们发现一件事 \(\max(S) = \sum\limits_{T \subseteq S} (-1)^{|T| - 1} \min(T)\),即 \(\text{min-max}\) 容斥的公式在期望意义下也成立。

而对这道题来说,计算一个集合的最小值是容易的,只需要计算出有多少个包含任意一个元素的线段就行了。

我们设 \(dp[i][j][k][0/1]\) 表示考虑到第 \(i\) 个位置,上一个位置为 \(j\),已经包含 \(k\) 条线段的方案数,集合大小为奇数或偶数的方案数,转移可以预处理 \([l,r]\) 会新增多少线段,做到 \(O(1)\),事实上最后一维可以省掉,直接带系数转移就行。

20220309

haltoj128

思考:

欧拉图计数相关问题。关于无向欧拉图有一个结论,欧拉子图的个数为 \(2^{m-n+c}\) 个,也就是其生成森林中非树边组成的集合个数,公式中 \(c\) 代表连通块个数。

理解比较容易,考虑构造方案,任意一个非树边集合会唯一对应一种合法方案,选一条非树边则将它覆盖的树边状态反转(选变为不选,不选变为选),可以得到唯一合法方案。

这个选非树边集合的方式给这道题目带来了启发。然而这种类似异或的方式并不便于统计 \(|S|^2\) 这种东西。我们考虑它的组合意义。发现其组合意义为每对边在同一子图便贡献两次,一条边在某一子图贡献一次。

一条边的情况是简单的,考虑两条边。

两条非树边是可以任选的,这一部分答案为 \(k * (k-1) * 2^{k-2}\),因为已经钦定这两边要选,其它的任意选。

一条非树边和一条树边的贡献可以分两种情况,分别是树边是否受到非树边影响。不受影响答案为 \((k-cover[v]) * 2^{k-2}\)\(cover[v]\) 影响这条树边的非树边条数,\(k-2\) 因为钦定了选择的非树边要选,并且影响这条树边的边有一个的选择情况是不能任意,因为要让树边被选择。

如果受到非树边影响,那么答案也为 \(cover[v] * 2^{k-2}\),但如果 \(cover[v] = 1\),那么答案为 \(2^{k-1}\)。理解方式类似。

两条树边的情况,考虑影响这两条树边的边集,我们断言如果边集完全相同,那么答案为 \(2^{k-1}\),否则答案为 \(2^{k-2}\),边集完全相同的情况不难理解,如果不完全相同,我们在每条边特有的部分钦定一个来控制该边,所以答案为 \(2^{k-2}\)。如果是包含关系,先钦定里面的,再钦定外面的即可。

如果要选择树边,记得不要考虑 \(cover[v] = 0\) 的边。计算 \(cover\) 可以树上前缀和,对于两条非树边,需要统计边集相同的个数,这个可以异或哈希,取 \(2^{63}\) 为上界,做双哈希,错误概率在本题数据规模下小于 \(10^{-9}\)

haltoj132(分治NTT的思路)

思考:

假设不考虑 \('>'\),即令 \('>'\) 为无限制,那么序列会被 \('>'\) 划分为若干段,记每一段的长度为 \(a\),那么答案为 \[ \dfrac{n!}{\prod a_i !} \] 我们考虑容斥,枚举一个子集表示那些位置上的 \('>'\) 强制为 \('<'\),也就是不合法的情况,然后就可以用总数减去这些不合法情况得到答案。

上面的那个 \(n!\) 在做转移的时候很麻烦,先不管。

\(dp[i]\) 表示对前 \(i\) 个符号做容斥,考虑到第 \(i\) 个符号后的数字的结果。

注意到这里 \(dp[i] \times (i+1)!\) 也就是前缀 \(s_i\) 的答案。

顺便设 \(f_i\) 表示前 \(i\) 个符号中 \('>'\) 的个数。

显然有 \(dp[0] = 1\)

我们得到以下式子: \[ dp[i] = (-1)^{f_i}\times \dfrac{1}{(i+1)!} +\sum\limits_{s_j = '>',j\in[1,i]} dp[j-1] \times (-1)^{f_{i}-f_{j}} \times\dfrac{1}{(i-j+1)}! \] 考虑如何理解这个式子。

我们枚举上一个不受限制的位置 \(j\),然后乘上对应的容斥系数和计算转移系数,最后加上全部受限制的情况。

因为要优化,所以把式子小小的变一下: \[ dp[i] = (-1)^{f_i}\times \dfrac{1}{(i+1)!} +\sum\limits_{s_{j+1} = '>',j\in[0,i-1]} dp[j] \times (-1)^{f_{i}-f_{j+1}} \times\dfrac{1}{(i-j)}! \] 传说中的分治 \(NTT\) 可以解决这一类 \(dp\) 的优化问题,它的核心思路大概是这样的:

分治 \(NTT\) 解决形如这样的问题:

假设要求的函数为 \(f\),有另一个函数 \(g\)

满足 \(f(i) = \sum\limits_{j\ < i} f(j) \times g(i-j)\)

类似于 \(\text{CDQ}\) 一样,考虑左边对右边的贡献即可,容易发现这是一个好做的卷积形式。

对于这道题来说,\(-1\) 的次幂可以被拆到两边,剩下的事情有手就行。

收获

容斥原理可以解决这样一类问题,有一个全集 \(S\)\(|S|\) 好求,现在有若干属性 \(p_i\),构成集合 \(T\),需要求满足属性集合 \(T\) 的元素个数。并且可以很容易求出这样一种情况的答案:限定某些属性不满足,其它属性不做要求。

20220320

HaltOJ 7

思考:

想一下小学的时候做过的奥数题,一个圆里画 \(n\) 条线最多分成几部分,答案是 \(n*(n+1)/2 + 1\)。再考虑下平行线和多点共线的情况,发现答案只和每个交点的情况和交点个数有关。手玩一下可以发现,在逐个加入直线的情况下,部分的个数增量为此条直线和其它所有直线交点个数 \(x\),再加上 \(1\),注意,相同交点只算一个。

证明可以参考平面欧拉定理,此处不做赘述。

所以我们模拟这个过程,每次加入一条直线判断新增了多少交点,具体可以先暴力枚举直线,求出所有交点之后带上 \(eps\) 去重。然后发现 \(y\) 只和 \(x\) 有关,所以只用计算一个。然后我们发现如果按照斜率为第一关键字,截距为第二关键字排序加入线段,那么 \(x\) 是一个相当优美的形式 \(\dfrac{j-b}{a-i}\)\(a,b\) 表示当前线段的斜率和截距,\(i,j\) 表示枚举的线段的斜率和截距,\(i,j\) 的取值都连续,所以这个式子中,分母会取遍 \([1,i]\),分子会取遍 \([-b,B-j-1]\),正负数分开考虑,现在问题变成了问 \(\dfrac{[1,x]}{[1,y]}\) 中有多少个不同的数,\(A^2\) 预处理,\(O(1)\) 回答即可,约定每个数在最简分数被统计。

可以莫比乌斯反演优化,这个可以很方便的转化成 \([gcd(x,y)=1]\) 的形式并整除分块计算,可以 \(n\sqrt n\),但没必要。

HaltOJ8

思考

这道题出出来就展现出对直接 \(dp\) 的恶意,无论那种合并方法都无法解决这个问题。我们不妨另辟蹊径,考虑字符串的另一种生成方式——插入。

具体的,我们将不同的花视为不同字符,那么我们需要生成一个字符串,相邻字符不同,每个字符个数指定。

我们发现当前的插入方式仅仅受到当前相同字符位置个数的限制,所以设 \(f_i\) 表示考虑到现在,有 \(i\) 个字符相同位置的方案数。

转移可以枚举当前字符划分为多少段,其中有多少段插入相同字符位置,具体每一段放几个可以通过插板法计算方案。这样的转移看上去是 \(O{(10^{5}})^3\),但是如果我们将字符按个数排序后并按照 \(3,1,2,4\) 的顺序插入,因为保证了有两个只有 \(200\),所以复杂度为 \(O(1+200^2 + 200^3 + 10^5)\) 的复杂度,最后一次转移强制要求了段数和插入相同字符位置的数量,第三次则是因为第二次转移后有效位置仅有 \(200\) 个。

听说可以做到 \(O(200^2 + 10^5)\) ,但显然我不会。

20220322

ARC137D

题意

给定一个序列 \(a\),反复做前缀异或操作,问若干次操作后 \(a_n\) 的值,询问所有 \([1,k]\) 的答案。

思考

考场上一直在考虑分析单纯的 \(01\) 系数,而忽略了系数之间的内在其它联系,事实上,对于反复执行的可加前缀操作,设距离为 \(d\),操作次数为 \(k\),那么贡献应该为从 \((0,0)\) 走到 \((d,k-1)\) 的方案数。证明比较简单,将原点的贡献转移拆分到横坐标即可。

更严谨的证明可以使用归纳法,记贡献函数为 \(f(x,y)\)\(f(n,k) = f(n-1,k)+f(n,k-1)\),那么考虑其组合意义,第一项代表了前 \(n-1\) 项的和,第二项则是自己在上面的步骤中的累计。

那个 \(-1\) 很讨厌,先不管。

用组合数的形式表示答案,即为 \(\tbinom{n+k}{n}\),对其应用卢卡斯定理求出 \(\bmod2\) 的结果,发现当且仅当 \(n\&k = 0\) 时有值,那么对于一个固定的 \(n\) ,有值的 \(k\) 一定可以描述为一个 \(s\) 在二进制意义下的子集。

然后对其做一次 \(\text{FMT}\) 变换即可得到结果。

注意处理被忽略的 \(-1\)

20220323

CF1657D

思路

先做乘法转换,这个没啥说的。然后我考虑的是根号分治,先把不在凸包上的扔掉,对于代价大于 \(B\) 的,枚举选的个数,然后尺取法搞定,对于代价小于 \(B\) 的,直接计算。场上过了,赛后被叉。实际上,对于这种整除的题目,我们都可以考虑枚举倍数约数,然后可以直接计算代价为 \([1,C]\) 的最大权值,然后直接在上面二分就行。

CF1647E

思路

不难发现充要条件是每个点向 \(1\) 的边为最小值。然后考虑 \(dp\),直接对点 \(dp\) 不太好做,我们发现它是有关大小的,所以考虑按照向 \(1\) 的边权值从小到大 \(dp\),每次枚举一段连续的区间,以及填的值转移,转移是一个前缀和乘上一个组合数再乘上一个幂次。

CF1647F

思路:

看上去就非常暴力,对每个限制建点,两种状态,限制和树上的点的状态推出关系连边,然后暴力确定每个限制的状态看是否冲突即可。实际上这就是模拟了 \(\text{2-SAT}\)

考虑这个东西为什么和 \(\text{2-SAT}\) 的正常做法一样的,正常做法是找强连通分量, 拓扑排序后对每个点选拓扑序大那个值。和我们的暴力模拟过程没啥区别。

HaltOJ126(2-SAT 的简单理解)

思考:

有一些比较奇怪的限制,然后每个人在每个点就两种状态,要想起一个东西叫 \(\text{2-SAT}\),我们令 \(statu[i][j]\) 表示 \(i\) 子树内是否有 \(j\),那么很容易构造出标准的 \(\text{2-SAT}\) 限制,然后我们考虑约束,分类讨论一下。以 \(\text{2-SAT}\) 的形式来说,对于 \(x\) 的每个儿子 \(v\),两个点都不能同时在 \(v\) 中。同时,如果 \(lca(x,r) \neq x\) ,那么两个人都必须在 \(x\) 子树内。如果 \(lca(x,r) = x\),那么 \(a,b\) 只能有一个在子树外,就是一个为假那么另一个为真,除此以外,如果 \(x \neq r\),那么 \(a,b\) 不能在 \(x\)\(r\) 的儿子里。

然后跑一个标准的 \(\text{2-SAT}\)

\(\text{Tarjan}\) 跑出来的 \(\text{SCC}\) 编号是反拓扑序。

\(\text{2-SAT}\) 的一些限制:强制 \(u\) 为真,那么把假连向真,反之亦然。其它情况下注意逆否命题也要连边

\(\text{2-SAT}\) 图的一些性质:\(u \rightarrow v \Rightarrow \overline{v} \rightarrow \overline{u}\)

\(\text{2-SAT}\) 合法性:同一变量的两个状态不在同一 \(\text{SCC}\) 内是存在方案的充要条件,必要性显然,充分性用构造法证明。

\(\text{2-SAT}\) 方案构造:依次考虑每个变量,选择 \(\text{SCC}\) 编号较小那个值,那么同一变量不会直接冲突,假设先前的点 \(v\) 推出了 \(\overline{u}\),并且 \(\overline{u} \rightarrow u\),因为刚刚提到的性质,一定有 \(u\rightarrow \overline{v}\),我们一定会选拓扑序较大的 \(\overline{v}\),因此选择不会冲突。

20220325

CF1656D

题意:

  • 您有一个 \(n\),您需要找一个 \(k \in[2,\infty)\),使得 \(n\) 可以被表示为 \(k\) 个模 \(k\) 意义下不同的数,多解可以输出任意一个。

  • 多测 \(T\leq 10^5,n\leq10^{18}\)

思考:

因为要找的 \(k\) 个数模 \(k\) 意义下不同,我们又知道任意一个数 \(a\) 都可以表示为 \(a=b \times k +r\),所以不妨先把模 \(k\) 的余数和,也就是上式中的 \(r\) 提取出来,我们得到了一个新的式子。下面设 \(n\)\(k\) 个数的和。 \[ n = c\times k + \dfrac{k\times(k+1)}{2} \] 其中 \(c\) 表示这 \(k\) 个数对应 \(b\) 的和。分母让人很不爽,所以乘过去。至于为什么是 \((k+1)\times k\),是为了让 \(c\) 可以取到 \([0,\infty)\) \[ 2n = (2c+k+1)\times k \] \(2n\) 的两个因子奇偶性不同,所以如果我们的 \(n\) 为奇数就可以直接令 \(k=2\),否则我们每次令 \(k\)\(2,4,8,\cdots\),直到 \(\dfrac{2n}{k}\) 为奇数为止,这个时候记 \(res = \dfrac{2n}{k}\),如果 \(res\) 较大,则取对应的 \(k\),否则取 \(k=res\)

注意特判掉一些边界情况,比如 \(2\) 的次幂,\(2\) 的次幂,还有 \(2\) 的次幂。

参考代码

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
#include<bits/stdc++.h>
#define INF 1000000000
#define int long long
#define pii pair<int,int>
using namespace std;
template<typename _type>
inline void read(_type &x){
x=0;int f=1;char ch=getchar();
while(ch!=45&&(ch>'9'||ch<'0'))ch=getchar();
if(ch==45){f=-1,ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-48;ch=getchar();}x*=f;
}
template<typename _type>void cmin(_type &a,_type b){a=min(a,b);}
template<typename _type>void cmax(_type &a,_type b){a=max(a,b);}
int i,j,k,n,s,t,m;
signed main()
{
read(t);
while(t--){
read(n);int ans=-1;
for(i=2;i<1ll<<62;i*=2){
int res=2*n/i;
if(res%2){
if(res>i){
ans=i;
break;
}
else{
ans=res;
break;
}
}
}
if(ans==1)puts("-1");
else printf("%lld\n",ans);
}
return 0;
}

CF1656E

题意

  • 您有一棵 \(n\) 个点的无向无根树,您需要为每个点安排一个权值 \(a_i\in[-10^5,10^5]\),使得删掉任意一个点之后剩下连通块的权值和相同,注意安排的权值不能为 \(0\)
  • 多测 \(T\leq 10^4,\sum n \leq 10^5\)

思考

诈骗题。我们考虑指定一个根,就让它是 \(1\),然后随便删一个点 \(u\),那么 \(u\) 的所有儿子的子树都必须有同一个值,我们可以尝试安排每一棵子树的权值和,这是可以做到的,因为可以让根控制这个值。不妨安排每颗子树的权值和为 \(1\), 那么我们再安排整个树的权值和为 \(2\),就可以让删掉每个点 \(u\) 后剩下的连通块权值和相同。\(u\) 的所有儿子子树的权值和都为 \(1\),而除开 \(u\) 的子树后的那个连通块的权值和就是整棵树的权值和 \(2\),减去 \(u\) 子树的权值和,就是 \(1\),也和 \(u\) 所有儿子的子树的权值和相同。

但是有一个问题,如果一个点 \(u\) 只有一个儿子,那么 \(u\) 的权值会被安排为 \(0\),是不合法的,所以我们需要更改一下安排的方式,对于一个点,设它子树的权值和为 \(x\), 并且它所有儿子的子树的权值和均为 \(y\),而且整棵树的权值和为 \(x+y\)。这是合法的必要条件,因为我们需要让 \(a_i \neq0\),所以安排根的权值为 \(0\),其它点按深度模 \(2\),的值安排 \(-1\)\(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
#include<bits/stdc++.h>
#define INF 1000000000
#define LL long long
#define pii pair<int,int>
using namespace std;
template<typename _type>
inline void read(_type &x){
x=0;int f=1;char ch=getchar();
while(ch!=45&&(ch>'9'||ch<'0'))ch=getchar();
if(ch==45){f=-1,ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-48;ch=getchar();}x*=f;
}
template<typename _type>void cmin(_type &a,_type b){a=min(a,b);}
template<typename _type>void cmax(_type &a,_type b){a=max(a,b);}
const int N=1e6+10;
int i,j,k,n,s,t,m;
vector<int> e[N];
int val[N],fa[N],dep[N],deg[N],tar[N];
void dfs(int u){
if(u==s)tar[u]=val[u]=0;
else tar[u]=val[u]=dep[u]%2?1:-1;
for(int v:e[u]){
if(fa[u]==v)continue;
dep[v]=dep[u]+1,fa[v]=u;dfs(v);val[u]-=tar[v];
}
}
signed main()
{
read(t);
while(t--){
read(n);dep[1]=1;s=1;
for(i=1;i<=n;i++)e[i].clear(),val[i]=0,tar[i]=deg[i]=0,fa[i]=0;
for(i=1;i<n;i++){
int x,y;read(x),read(y);
e[x].push_back(y),e[y].push_back(x);
deg[x]++,deg[y]++;
}
for(i=1;i<=n;i++)if(deg[i]>deg[s])s=i;
dfs(s);
for(i=1;i<=n;i++)printf("%d ",val[i]);
puts("");

}
return 0;
}


CF1656F

题意

  • 您有 \(n\) 个点,每个点有一个权值 \(a_i\),定义一张有权无向完全图 \(K(t)\) 为每个点 \(i\)\(j\) 连一条权值为 \(a_i\times a_j+(a_i+a_j)\times t\) 的无向边后所构成的图,定义 \(f(t)\)\(K(t)\) 最小生成树的权值和。您需要对所有的实数 \(t\) 求出 \(f(t)\) 的最大值并输出它,如果最大值不收敛,那么输出 INF

  • 多测,\(T\leq 10^4,\sum n\leq 2\times 10^5,-10^6\leq a_i\leq 10^6\)

思考

场上差那么一点点就 15Ton 了

一个瓶颈在排序的线性做法。

\(d_i\) 表示点 \(i\) 的度数。

判断 \(\text{INF}\) 是简单的,只需要看能不能凑出 \(\sum a_i\times d_i\) 分别为正和负或者 \(0\) 就行,如果凑不出来,令 \(t\) 为正无穷或者负无穷,然后它就不收敛了。

现在已知有解。

考虑固定一个 \(t\) 后怎么快速求 \(\text{MST}\)。做个恒等变形,权值为 \((a_i+t)\times (a_j+t)-t^2\),把 \(t^2\) 给扔掉。

结论是排序后按正负性断开,\(a_1\) 向所有 \(a_i\) 大于 \(0\) 的连边, \(a_n\) 向所有 \(a_i\) 小于 \(0\) 的连边,\(0\) 无所谓。

证明可以考虑 Prime 算法的过程,最开始一定是 \(a_1\)\(a_n\)。然后后面不会取到正负性相同的,并且一个点一定是连向 \(a_1\) 或者 \(a_n\)

如果处理一个前缀和,知道正负交界的位置之后可以快速算,从小到大枚举 \(t\),然后双指针维护交界处。

可以证明 \(t\) 一定取到每个 \(-a_i\)。如果夹在两坨中间,那么由于具体选哪些边是固定的,根据 \(\sum a_i\times d_i\) 正负性调整即可。

不会取到 \([a_1,a_n]\) 外面去,因为我们已经判了无解,所以取到边界外面时 \(\sum a_i\times d_i\) 的正负性会导致向里面调整更优。

20220329

HaltOJ129(Powerful Number 筛)

思考:

打个表发现 \(f\) 是积性函数。

然后 \(f(p^c)\) 是好求的,考虑亚线性筛法。

我也不知道为啥会想到 PN 筛,总之这种东西各种筛法都可以尝试一下。

PN 筛和其它亚线性筛法一样,是用来求一些积性函数的前缀和的,它的关键在于构造一个好求的前缀和的 \(g\),满足 \(g(p)=f(p)\),然后构造一个 \(h\) ,满足 \(f=h * g\),乘法为迪利克雷卷积。

这里我们构造 \(g(x)=1\)

积性函数有个性质,\(f(1)=1\),所以展开下 \(f\),发现 \(f(p)=1=h(1) * g(p)+h(p) * g(1)\),然后 \(h(p)=0\),由于 \(h\) 也是个积性函数(迪利克雷卷积的性质),所以 \(h\) 只会在 PN 处有取值,PN 的定义为每个质因子次数都大于等于 \(2\) 的数。

可以证明所有比 \(n\) 小的 PN 个数是 \(O(\sqrt n)\) 的 。

可以预处理 \(\sqrt n\) 以内的所有质数,然后枚举指数得到每个 PN,这个 \(dfs\) 的过程中可以记录一下对应的 \(h\)\(h\) 的转移是好做的,因为只需要求 \(h(p^c)\)

这里不难发现 \(h(p^c)=f(p^c)-f(p^{c-1})\)

原因是 \(g\) 实际上是 \(1\),而 \(f / 1 = f * \mu\)

20220331

20220330考试T2

思考

限制不好弄,考虑转化限制,不难发现,如果建图,并依次加入有向边,那么任意一个时刻,都需要满足当前图及其补图都是一个传递闭包,即,如果能间接 \(a\rightarrow b\) ,那么 \(a,b\) 有边。这样的图不是很多,是 \(n!\) 个的,只需要考虑图之间的转移就行。

这样的图和一个 \(n\) 的排列一一对应,构造方案为如果 \(a,b\) 为逆序对,那么加入一条边 \((a,b)\)。加入一条边时,只需要枚举相邻的逆序对并交换,转移可以康托展开得到交换后的编号。限制也比较好做,转移的时候看看强制在前面的边在不在里面就行。

复杂度为 \(O(n!\times n \times (n+m))\),常数较小,可以过。

20220402

20220401考试T1

思考

对于这种翻转的博弈问题,其实都可以做一个转化:不要翻转颜色,而是在每个翻转的地方再放一个棋子,这样不会改变游戏的胜负性,因为如果原处没有棋子,那么等效,如果有棋子,那么有两个,在 \(SG\) 的意义下,这是可以抵消的。如果以组合方式理解,那么因为放了之后如果有人操作了,那么另一个人把它的操作复制一遍即可,由于两个人都是最优操作,所以可以抵消。

有了这个转化,每个棋子都变成了一个独立的游戏,该局面的 \(SG\) 值就不难计算了,按照定义计算出每个位置的 \(SG\) 值然后异或查表即可。

20220401考试T3

思考

场上写了个 \(O(n^3)\) 卡常卡过去了,实际上 \(O(n^2)\) 的有点难想,它是把构造表达式的过程视作了一个添加左右括号的过程,总之非常神仙。

有个对于区间 \(dp\) 的常数优化思路,可以改变一下枚举顺序,让数组访问尽量连续,以便最大程度利用好 \(L1\),可以节省大量内存操作时间。

咕一篇常数优化文章。

2022.2.7

CF1634E

题意:

给定一些数组,长度都为偶数,需要将每个数组的元素划分到两个可重集合里,要求两个集合相同,问方案或输出无解,\(n,m=10^5\)

思考:

考虑每个数组单独划分,不太行,所以不能一个数组一个数组的考虑,显然有解的必要条件是每个数字个数为偶数。所以一个数字一个数字的考虑,其实也不太行,因为这和刚刚那种方法是本质上一样的。

注意到也保证每个数组长度为偶数(这不是废话吗),都是偶数,不由得让我们想到欧拉回路,所以尝试建图,每个不同的数和数组视为一个点,由数组向数连边,数组内存在一个数,就由数组向这个数连一条边,现在需要给图定向为一张欧拉图,我们将数组向数连的边是为将这个数分到第一个集合,反之则为第二个,构造欧拉图即可。

实现上的问题:

存边用 set 存,不然 TLE 没商量。

图不一定联通,所以一定要多起点。

20220209

22020208考试 T1

题意:

定义矩阵 \(Mat_n\)

  • \(n = 0\) 时,为一个 \(1\)
  • \(n > 0\) 时,左上角为 \(0\) 矩阵,其余部分为三个 \(Mat_{n-1}\)

在一个平面上画出两个 \(Mat_n\),左下角分别为 \((0,0),(x,y)\),求都为 \(1\) 的位置有多少个。

思考:

考虑旋转矩阵转化为位运算计数问题,转化为求整数对 \((i,j)\) 满足 (i&j) == 0 and ((i+x)&(j-y)) == 0

考场上看到要高精就润了,没有认真思考。

实际上可以考虑类似 数位dp 的算法,也不难。

22020208考试 T2(后缀自动机复习)

题意:

给定一个字符串 \(s\)\(q\) 次询问 \(l,r\),表示所有 \([l,r]\) 开头的子串中本质不同的子串个数。

\(n,q = 2 \times 10^5\)

思考1:

考虑后缀自动机求本质不同子串个数的方式,发现它可以在线完成。

所以反着建后缀自动机。

对于 \(r=n\) 的部分分,显然先离线,可以建后缀自动机时统计下每一个结尾的答案,输出即可。

如果 \(r \neq n\),那么不妨仍然考虑在后缀自动机上如何统计答案。

后缀自动机本质上维护的是 \(endpos\) 集合,对于一个询问,如果一个 \(endpos\) 集合中包含一个位置 \(x\in[l,r]\),那么这个 \(endpos\) 集合所代表的子串应该被计入贡献。

后缀自动机上每个子串仅在一个 \(endpos\) 集合中出现,所以可以求出至少有一个元素被包含的集合,并直接加和。

考虑对于每个询问该如何统计答案,限制一共有两个 \(l,r\),并不好做,然而我们发现后缀自动机是在线构建的,所以如果将询问离线,那么限制就只剩下一个 \(r\) 了。我们在构建后缀自动机的同时只需要记录每个 \(endpos\) 集合最小的元素 \(val\),查询时查询所有满足 \(val \leq r\) 的集合的子串数量和即可。考虑维护一个数组 \(c\)\(c[i]\) 表示 \(val = i\)\(endpos\) 集合子串个数和,用 \(BIT\) 查前缀和,现在只剩下修改了。

考虑 \(endpos\) 树的性质,发现修改操作是把一段到根的路径赋值,同时还有修改父亲等操作,于是考虑动态树,我们不难发现一段实链上的集合,\(val\) 都是相同的,所以在每个节点维护一下 \(val,sum\) 值即可,注意 \(push\_down\) 操作时需要把赋值也 \(push\_down\) 下去。

这样做未免显得有些麻烦,我们考虑最终构建的后缀自动机,倒序激活 \(endpos\) 中每一个点,这样就不需要 \(Link\)\(Cut\) 操作,只需要写 \(access\) 就行。

实际上未必需要用后缀树来实现,通过激活点的方式,树已经时静态的了,这本质上还是一个区间染色问题,我们完全可以直接重链剖分,在每条重链上开个栈维护断点,记录下前缀和。

复杂度都是 \(O(nlog_n^2)\),个人认为类似动态树的方法会好写一些。

思考2:

考虑使用后缀数组,回想后缀数组统计不同子串个数的方式,实际上就是排序后减掉相邻两个的 \(lcp\),于是对原串后缀排序,然后离线询问莫队,拿个 set 维护当前所有串的排名集合,再来个 \(ST\) 表计算 \(lcp\),插入和删除都很好写,复杂度 \(O(n\sqrt n \ log_n)\),难以通过本题。

考虑优化,有一个技巧,这种需要查前驱后继的东西,实际上可以用链表搞,加入相对困难一些,因为我们不知道到底应该放在哪里,但删除就很容易了,双向链表上直接删就完事了,所以可以回滚莫队,非常无脑,时间复杂度 \(O(n\sqrt n)\)

稍加卡常即可通过,\(ST\) 表查询常数相对较大,卡常应考虑尽量减少查询次数。

20220210

20220208考试T3

恶心的题意

思考

发现由于编号连续,所以最后经过的一定是一段连续区间。想到可以枚举右端点,二分左端点。

一个区域到另一个区域的路径可以分为区域到中转站和中转站到中转站两部分,区域到中转站的情况可以只计算到左右两边最近的中转站,所以容易计算。

我们按照右端点顺序激活中转站,问题就是要最小化左端点。

两个中转站只能通过同一条线连接,把最低点也看成中转站,所以可以认为从一个中转站到另一个的代价为两者所在区域编号的较小值。

中转站到中转站的距离可以 Floyd 暴力,我们就已经计算出了每对中转站点相互抵达的代价,所以二分都不需要了,我们可以直接用这个代价计算出最终答案。

实现细节

Floyd 的时候,因为加入的中转站并不是按下标顺序,所以需要整个跑一遍。

20220211

20220211 测试 T1

题意

思考

这道题算是考场上想出来的,考场上摸了一会儿鱼,发现有个地方假了的时候只剩 \(5min\) 了,所以紧急修复成了\(50pts\),实际上不管直接交有 \(80pts\)

先把最上面那些没有的行删掉。

考虑每一行的可能性,只有穿过和不穿过两种,穿过又可以分为去的时候穿过和回的时候穿过。

来的时候穿过,回的时候一定不会穿过,因为一定不优,于是三进制枚举穿过状态,大的路径框架已经被构建出来了,现在要考虑经过那些没有穿过的点。

被穿过的行不用管,现在就剩没穿过的行,每一行可以从左到右,也可以从右到左,如果两边都有经过,还可以两面包夹,算出每一行的结果,然后加上去就行。

这样可以拿到 \(80pts\),因为它处理不了这种情况。

1
2
3
4
3 7
#.....#
.#####.
.#####.

它会穿过第一行和第二行,到第三行后穿过或者是走到底再回来,实际上在穿过第一行走到第二行的时候就应该上去拿掉右上角。

所以把大的路径画出来后,记录每一个边缘位置是否到达过,还要自下而上 \(dp\) 一遍求出经过所有未经过的点的最小代价。

提供一种思路,设 \(dp[i][j][k]\) 表示到第 \(i\) 行,左右最前面的可以已经经过的点为 \(j,k\) 的最小代价,转移的时候看 \(j,k\) 是否大于 \(i\) 或者 \(i\) 的两端是否已经经过,如果可行,就选经过一行三种方案的最小值,如果不行,因为一定有一边已经经过,所以只考虑另一边是否补全和从经过的点走一遍再回来的情况,补全的方案有两种,从上面和从下面,直接转移即可。

时间复杂度 \(O(n^3 * 3^n)\),实际上剪枝后跑的飞快,\(10ms\) 就跑完了。

20220212

20220212测试T2

题意

思考:

考虑直接 \(dp\) 转移,设 \(dp[i][j][k][0/1]\) 表示考虑前 \(i\) 个数, 正色子已经用了 \(j\) 个,反色子已经用了 \(k\) 个,是否已经赢了的方案数,直接转移是 \(O(n^4 * t)\) 的,可以拿到 \(20pts\) 的好成绩。

考虑优化,转移式子是:

1
2
3
4
5
6
7
8
9
10
11
12
for(int i1=0;i1<=n;i1++)
for(int i2=0;i2<=m;i2++)
for(int j1=0;j1<=i1;j1++)
for(int j2=0;j2<=i2;j2++)
{
if(i<=t&&i1-j1>i2-j2)
Inc(dp[i][i1][i2][1],1ll*dp[i-1][j1][j2][0]*C[n-j1][i1-j1]%mod*C[m-j2][i2-j2]%mod);
else
Inc(dp[i][i1][i2][0],1ll*dp[i-1][j1][j2][0]*C[n-j1][i1-j1]%mod*C[m-j2][i2-j2]%mod);

Inc(dp[i][i1][i2][1],1ll*dp[i-1][j1][j2][1]*C[n-j1][i1-j1]%mod*C[m-j2][i2-j2]%mod);
}

转移目标

dp[i][i1][i2][1]

前面一部分

1ll*dp[i-1][j1][j2][0]*C[n-j1][i1-j1]

和后面一部分

C[m-j2][i2-j2]

只有前一部分依赖 \(j1\),考虑能不能预处理前一部分的转移,然后优化一个 \(n\)

显然是可以的,手推一下式子可以得到一个新的转移。

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
for(int j1=0;j1<=n;j1++)
for(int i2=0;i2<=m;i2++)
{
sum[j1][i2]=1ll*dp[i-1][j1][0][1]*C[m][i2]%mod;
sum2[j1][i2][0]=1ll*dp[i-1][j1][0][0]*C[m][i2]%mod;
for(int j2=1;j2<=i2;j2++)
{
if(dp[i-1][j1][j2][1])
Inc(sum[j1][i2],1ll*dp[i-1][j1][j2][1]*C[m-j2][i2-j2]%mod);
sum2[j1][i2][j2]=sum2[j1][i2][j2-1];
if(dp[i-1][j1][j2][0])
Inc(sum2[j1][i2][j2],1ll*dp[i-1][j1][j2][0]*C[m-j2][i2-j2]%mod);
}
}
for(int i1=0;i1<=n;i1++)
for(int i2=0;i2<=m;i2++)
for(int j1=0;j1<=i1;j1++)
{
int val=C[n-j1][i1-j1];
Inc(dp[i][i1][i2][1],1ll*val*sum[j1][i2]%mod);
if(i<=t)
{
int pos=i2+j1-i1,val1=pos<0?0:sum2[j1][i2][pos],val2;
val2=sum2[j1][i2][i2];Dec(val2,val1);
if(i<t)Inc(dp[i][i1][i2][0],1ll*val*val1%mod);
Inc(dp[i][i1][i2][1],1ll*val*val2%mod);
}
}

优化到了 \(O(n^3*t)\),记此时的常数 \(k = 1\),它需要约 \(15s\) 通过数据规模最大的测试点。

这份代码可以拿到 \(50pts\) 的好成绩,感觉上已经不太能优化复杂度了,我们考虑卡常。

第一步,发现 \(n=6\) 的时候只需要转移 \(dp[6][n][m][1]\)

第二步,发现 \(n=1\) 的时候被转移的只有 \(dp[0][0][0][0]\)

常数变为 \(k = \dfrac{2}{3}\)

此时可以拿到 \(70pts\) 的好成绩。

继续考虑卡常。

发现其实在 \(i>=t\) 时转移 \(dp[i][i1][i2][0]\) 没有意义,直接剪掉。常数没有变化。

发现 \(dp[i][i1][i2][1]\) 实际上可以由 \(dp[i][i1][i2][0]\) 直接得到,所以只转移 \(dp[i][i1][i2][0]\) ,记作 \(dp[i][i1][i2]\),常数变为 \(k = \dfrac{1}{3}\)

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
if(i<=t)
{
for(int j1=0;j1<=n;j1++)
for(int i2=i==6?m:0;i2<=m;i2++)
{
sum[j1][i2][0]=1ll*dp[i-1][j1][0]*C[m][i2]%mod;
int *p=&sum[j1][i2][1];
for(int j2=1;j2<=i2;j2++)
{
*p=sum[j1][i2][j2-1];
Inc(*p++,1ll*dp[i-1][j1][j2]*C[m-j2][i2-j2]%mod);
}
}
for(int i1=i==6?n:0;i1<=n;i1++)
for(int j1=0;j1<=i1;j1++)
for(int i2=i==6?m:max(0,i1-j1);i2<=m;i2++)
Inc(dp[i][i1][i2],1ll*C[n-j1][i1-j1]*sum[j1][i2][i2+j1-i1]%mod);
}
else
{
for(int i2=i==6?m:0;i2<=m;i2++)
{
for(int j1=0;j1<=n;j1++)
sum[j1][i2][0]=1ll*dp[i-1][j1][0]*C[m][i2]%mod;
for(int j2=1;j2<=i2;j2++)
for(int j1=0;j1<=n;j1++)
Inc(sum[j1][i2][0],1ll*dp[i-1][j1][j2]*C[m-j2][i2-j2]%mod);
}
for(int i1=i==6?n:0;i1<=n;i1++)
for(int i2=i==6?m:0;i2<=m;i2++)
for(int j1=0;j1<=i1;j1++)
Inc(dp[i][i1][i2],1ll*sum[j1][i2][0]*C[n-j1][i1-j1]%mod);
}

此时的成绩还是 \(70pts\),但我们可以观察一下代码,在 \(i>t\) 后的转移实际上没有任何意义,可以直接计算,优化这一部分,可以拿到 \(90pts\) 的好成绩。

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
for(i=2;i<=min(t,5);i++)
{
for(int j1=0;j1<=n;j1++)
for(int i2=0;i2<=m;i2++)
{
sum[j1][i2][0]=1ll*dp[i-1][j1][0]%mod*C[m][i2]%mod;
int *p=&sum[j1][i2][1];
for(int j2=1;j2<=i2;j2++)
{
if(dp[i-1][j1][j2]>=mod)dp[i-1][j1][j2]%=mod;
*p=sum[j1][i2][j2-1];
Inc(*p++,1ll*dp[i-1][j1][j2]*C[m-j2][i2-j2]%mod);
}
}
for(int i1=0;i1<=n;i1++)
for(int j1=0;j1<=i1;j1++)
for(int i2=max(0,i1-j1);i2<=m;i2++)
{
dp[i][i1][i2]+=1ll*C[n-j1][i1-j1]*sum[j1][i2][i2+j1-i1];
if(dp[i][i1][i2]>=8e18)dp[i][i1][i2]%=mod;
}
}
if(t==6)
{
for(i=0;i<=n;i++)
for(j=0;j<=m;j++)
Inc(dp[6][n][m],(n-i<=m-j)*dp[5][i][j]%mod);

printf("%lld\n",((quick(6,n+m)-dp[6][n][m]%mod)+mod)%mod);
}
else
{
int all=0;
for(i=0;i<=n;i++)
for(j=0;j<=m;j++)
Inc(all,1ll*dp[t][i][j]%mod*quick(6-t,n+m-i-j)%mod);

printf("%d\n",((quick(6,n+m)-all)+mod)%mod);
}

最后的 \(10pts\) 可能需要一些卡常技巧。

我们发现实际上复杂度的瓶颈在数组寻址和取模,考虑优化其中之一,被寻址的数组经过循环变量的调整已经相当连续了,我们考虑优化取模。

long long\(dp\),每 \(8\) 次加法取一次模,可以通过本题。

一个很常见的,在取模运算较多时的优化技巧。

20220211 测试T3

题意

思考:

这道题暴力思路无非就二进制枚举和容斥。正解的做法很神仙。

在这种 \(\text{NP}\) 问题中,我们完全可以考虑将枚举的部分减少,这道题的思路是对稀疏图删点变为森林,然后二进制枚举一些删掉的点的状况再进行树形 \(dp\),着实是一个很有启发性的思路。

20220212

CF1637F

思考:

对于这种条件为最小值的覆盖问题,我们不妨考虑最大的那个点是如何被覆盖的,首先有一个结论是只会在叶子有基站,证明简单,略去。然后考虑最大的那个点是如何被覆盖的,把它看作根,一定来源于它不同儿子的两个叶子,所以我们考虑其它点的时候一定可以认为这个根上的值为 \(\text{INF}\) ,理由不多说了。所以每个点 \(u\) 的包含的叶子至少有一个最大值为 \(h_u\),贪心即可,最后决策最大值的点应该放在哪里,同样的贪心。

实现较为简单,没什么说的。

20220225

haltoj116

思考:

考虑团和独立集的性质,我在考场上就想到了答案一定不多,极大概率无需取模这一特性,于是用类似分治的方式得到了 \(60pts\),如果我们找出了一个合法解,那么其它合法解的构造也是简单的,考虑合法解的性质,不妨设团的大小为 \(s\) ,那么因为独立集只能向团连边,所以独立集中的点最大度数为 \(s\),而这样又导致了团中的点最小度数增加,所以我们不难发现团中的点一定是度数最大的那一段前缀。

对于度数相同的点,看似无法下手,但是我们知道边的构成是团内加上团和独立集之间,团内的边数我们是知道的,而如果记录一下团中点的总度数,我们可以很轻松的推出独立集间是否有边,我们设团内点的总度数为 \(p\),团的大小为 \(i\),团内边数为 ,那么其他点之间的边数就为 \((2*m-2*p+i*(i-1))/2\),所以我们考虑满足 \(2m + i * (i-1) = 2p\) 这个条件的选择有什么性质,因为这是成立的必要条件,考虑证明这也是充分条件。如果团内边数不足 \(i*(i-1)\),那么我们发现 \(p\) 这边会减小。考虑如果独立集内边数有边,那么 \(p\) 这边一样相对减少,所以这也是充分条件。

这道题结束了。

haltoj117:

思考:

考虑在原序列中是前缀 \(max\) 的点,它们在新划分的序列中一定也是前缀 \(max\),如果新增了前缀 \(max\),那么我们可以很轻松的改变划分情况去除这个前缀 \(max\),所以如果原序列中前缀 \(max\) 的数量为偶数,就一定可以。如果为奇数,我们不妨考虑只有一个序列有新增的前缀 \(max\) 的情况。我们考虑从原序列中抽出一个新序列出来,设两个序列的权值之差为 \(k\),如果抽了一个原先的前缀 \(max\),那么 \(k\) 减少 \(2\),如果新增了一个前缀 \(max\)\(k\) 减少量为 \(1\),所以我们将原先是前缀 \(max\) 的数的权值视为 \(2\),其余的视为 \(1\),问题就是是要选一个上升子序列,使得权值和为前缀 \(max\) 的个数,这个问题就相当简单了。

20220228

ABC215H(子集容斥的另一种思路)

考虑霍尔定律,枚举不满足条件的子集 \(mask\),可以用 \(\text{FMT-DP}\) 计算出必须由 \(mask\) 供给的白菜数量,考虑减少一个子集中的白菜使得不满足条件,就可以计算出最少需要吃多少个。问题变成了统计答案,我们发现对于一个 \(mask\) 直接组合数计算然后相加会算重,又因为答案不是全集所以并不能简单容斥。而暴力计算强制每种白菜都选一个的复杂度是 \(O(3^n)\) 的,我们仍然考虑应用 \(\text{FMT-DP}\) 计算答案。事实上,这种组合数问题都可以考虑这种方式。

\(f_{mask}\) 表示所选白菜集合被 \(mask\) 包含的方案数,设 \(f'_{mask}\) 表示所选白菜种类集合恰好为 \(mask\) 的方案数,列有等式。 \[ f'_{mask} = f_{mask} - \sum\limits_{x \sub mask} f'_{mask} \]\(f[i][mask]\) 表示当前子集为 \(mask\) ,低 \(i\) 位不一定存在,但高位都严格符合要求的方案总数,列有

1
2
3
4
5
for(i=n-1;i>=0;i--)
for(int mask=0;mask<1<<n;mask++){
f[i][mask]=f[i+1][mask];
if(1<<i&mask)f[i][mask]-=f[i+1][mask^(1<<i)],f[i][mask]%=mod;
}

\(i+1\) 推到 \(i\) 的过程中,因为高位已经强制存在了,所以我们减去的就是 \(f[i][mask]\) 中所有不存在第 \(i\) 位,但高位符合要求的情况。

最后计算出来的 \(f[0]\) 就是上述的 \(f\)

然后这道题就好做了。

20220301

考试 T1(SAM的进一步理解)

题意:

给定一颗字符在点上的 \(\text{Trie}\),求 \(\text{Trie}\) 代表的所有字符串本质不同子串个数,顺便询问钦定字符大小关系后第 \(k\) 小的子串,保证 \(\text{Trie}\) 上字符随机且答案长度和不超过 \(800KB\)

思考:

考场上想到可以类似 \(\text{SAM}\) 一样乱搞,然后就写了 \(\text{dfs}\) 构建 \(\text{SAM}\)\(lst\) 指针被置为它父亲插入后的 \(p\),这个做法在数据随机的情况下是没有问题的,但是如果出现构造数据,它会没掉。

首先是如果拉一条 \(a\) 链,链上每个点再挂一个 \(b\),然后 \(dfs\) 回跳的时候重置 \(q\) 的来边到 \(np\) 时时间复杂度是 \(O(n^2)\) 的。

我所理解的 \(\text{SAM}\) 时间复杂度正确性基于两个东西,一个是 \(p\) 的深度变化情况,这证明了第一次构建边的时候时间正确。在 \(dfs\) 构建 \(SAM\) 的时候,大量回溯操作会导致 \(p\) 深度的异常变化,这样就不对了。第二个是重置 \(q\) 的来边的循环,这个可以分析 \(p\)\(fa\) 所代表最短字符串长度来理解,每次重置 \(q\) 的来边,我们都认为是找到了一个更短的 \(np\) 所代表的字符串,不妨看成一个指针在插入的字符串上移动,它只会向右。重置完 \(p\) 的来边再插入下个点的过程中,我们至少会跳到 \(np\) 处(只针对单个串),然后重设的 \(p'\)\(fa\) 的最短长度一定可以从 \(np\) 那里继承过来,所以指针只会右移。\(dfs\) 加入时同样导致了这个指针在 \(\text{Trie}\) 上乱跳,复杂度就没有了保证。

然后是正确性相关的问题,这种 \(\text{SAM}\) 写法会导致无效的空节点建立,比如说插入的时候就碰到了满足 \(a[lst].ch[c]\) 存在的情况,这样新建出来的点实际上是无效的,在绝大部分题目中这个无效点是不影响答案的,但是少部分写法会导致爆炸。

接下来讨论下 \(\text{BFS}\) 建树的正确性。由于是按深度加点,所以 \(a[lst].ch[c]\) 一定是不存在的,因此绝不会导致无效节点的建立,时间复杂度证明不会,省略。

时间复杂度我并不会证明 emmmm.

ABC241H

思考:

生成函数套路题。

20220302

ARC136E

思考:

显然偶数链是很好走的,所以考虑从偶数边怎么到另一个节点,显然偶数只能选一个,我们先考虑不选偶数的情况。

定义 \(f[x]\)\(x\) 的最小质因数,\(x\) 能走到 \(y\) 的充要条件是 \(y>x,x+f[x]\leq y-f[y]\),如果 \((x,y) = 1\),那么显然,因为 \(x\) 第一步至少走 \(f[x]\),到 \(y\) 的最后一步至少走 \(f[y]\)。如果 \((x,y) \neq 1\),设 \(x = p \times f[x]\),则 \(y\) 至少为 \((p+2)\times f[x]\),故结论仍然成立。

由此可以 \(x\) 走不到 \(y(y>x)\) 的充要条件为 \(x+f[x]>y-f[y]\),将每个点视为 \((x-f[x],x+f[x]])\) ,题目就是要求出一些区间的集合,使得所有区间有公共点,要求权值最大,不妨枚举公共点,然后差分计算即可。

考虑下偶数,实际上做法类似。

20220303

AGC027E&&haltoj119

思考:

观察不变量是这种变换题的思考方向之一,我们设 \(a\) 的权值为 \(1\)\(b\) 的权值为 \(2\),总能发现权值和对 \(3\) 取模的结果不变。

先考虑一个字符串在什么情况下可以变成另一个字符串,权值相同显然是必要条件,其次,源串 \(s\) 不能是交替串 \(abababa\) 这类,这有点困难,不妨先考虑变成一个字母的情况。

充要条件为权值相同且不交替,证明考虑归纳法。

现在考虑目标为一个字符串的情况。我们先贪心的匹配,用最少的字母构造出目标串,然后考虑调整剩下的部分使得它满足条件。如果特判掉交替串,我们发现只要权值相同且源串的一个前缀能和目标串贪心匹配上,那么一定可以变成目标串,于是考虑 \(dp[i]\) 表示考虑到前 \(i\) 的源串字母,能贪心的对应上多少不同串,显然这样的贪心对应是不重不漏的,转移分为匹配 \(i+1\) 和不匹配 \(i+1\) 两种,预处理一个类似 \(nxt\) 的东西可以做到线性。

20220304

CF917D&&haltoj122(初探二项式反演)

思考:

考场上已经观察出原题需要求一张完全图有多少棵最小生成树与给定树至少有 \(n-k-1\) 条边相同。\(prufer\) 序列有一个结论,\(n\) 个点 \(k\) 个连通块的图构成有标号无根树的方案总数为 \[ n^{k-2}\times\prod_{i=1}^{k} sz[i] \] 显然我不会证明。场上看到 \(n\) 仅为 \(50\) 就想乱搞,误打误撞弄了一个容斥 \(dp\) 出来,实际上正解差不多就是这个。我们考虑钦定选了 \(k\) 条边一定存在的方案总数 \(f(k)\),由二项式反演的套路可以得知恰好 \(k\) 条边存在的方案数 \(g(k)\) 满足 \[ g(k) = \sum_{i=k}^{n-1}(-1)^{i-k}\times C(i,k) \times f(i) \] 其中 \(C(i,j)\) 表示组合数。

显然我仍然不会证明,但是可以感性理解下,首先 \(f(k)\) 满足以下式子 \[ f(k) = \sum_{i=k}^{n-1}C(i,k) \times g(i) \] 这个很好理解,枚举实际上选了多少条边,钦定 \(k\) 条边的方案数就是从实际选的边中钦定一些出来。注意这个钦定和至少有区别,不是简单的后缀和,因为实际选一条边的方案可以被多种钦定的情况包含。

我不会证明二项式反演的式子,所以我们从容斥的角度来考虑 \(g(k)\),第一项为钦定 \(k\) 条边,然后减去被多考虑了的存在 \(k+1\) 条边的情况,依此类推。组合数的存在则是因为 \(f(i)\) 会被额外考虑 \(C(i,k)\) 次。

\(f\) 肯定比 \(g\) 好算,我们现在需要计算的是选 \(k\) 条边,然后得到的联通块组成不同有标号无根生成树的方案数。注意到生成树计数公式中 \(n^{k-2}\) 与怎么选边完全无关,所以只需要记录一下 \(\prod sz_i\),这个就可以 \(dp[i][j][k]\) 表示考虑以 \(i\) 为根的子树,选了 \(j\) 条边,\(i\) 所在连通块大小为 \(k\) 的方案数。优化的话,可以考虑 \(\prod sz_i\) 的组合意义。于是就是需要在每个连通块内选一个代表元,于是状态第三维简化为是否选了代表元,复杂度 \(O(n^2)\)

写法优美的常用函数

收录一些优美的常用函数写法。

分解质因数

有人根本不会用 do whilevector

1
2
3
4
5
vector<int> v(0);
for(int i=2;i*i<=n;i++)if(n%i==0){
v.push_back(i);
do n/=i; while(n%i==0);
}

SCC

缩点。

1
2
3
4
5
6
7
8
9
10
11
12
int dfs(int u){
int low=dfn[u]=++df;st[++top]=u;
for(auto v:e[u]){
if(!dfn[v])cmin(low,dfs(v));
else if(!scc[v])cmin(low,dfn[v]);
}
if(low==dfn[u]){
++cnt;int v;
do val[cnt]+=a[v=st[top--]],scc[v]=cnt; while(v!=u);
}
return low;
}

最短路 Dijskstra

如果 \(dis\times n\le 8\times 10^{18}\),完全可以它们压成一个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
priority_queue<LL,vector<LL>,greater<LL>> q;
void dij(int s,int t){
memset(vis,0,sizeof(vis));
memset(dis,1,sizeof(dis));
while(!q.empty())q.pop();
dis[s]=0;q.push(s);
while(!q.empty()){
int u=q.top()%M;q.pop();
if(vis[u])continue;vis[u]=1;
if(vis[t])return ;
for(int i=head[u];~i;i=a[i].next){
int v=a[i].v,w=a[i].w;
if(dis[u]+w>=dis[v])continue;
dis[v]=dis[u]+w;q.push(dis[v]*M+v);
}
}
}

网络流

内容主要为自己对网络流的一些理解和一些典例。

一些约定:

  • s 为源点,t 为汇点
  • 对于集合 \(S,T,T\subseteq S\),约定 \(S-T\) 为从 \(S\) 中删掉 \(T\) 中每个元素之后的集合
  • 网络流图为 \(G=(V,E)\),边 \((u,v)\) 容量记为 \(c_{u,v}\)

最大流最小割定理和增广路定理

这两个定理是网络流问题的核心定理。

内容

最大流 = 最小割。

残量网络中不存在增广路是当前流为最大流的充要条件。

证明

  1. 若当前流为最大流,显然不存在增广路。

  2. 若当前流等于某个割,显然当前流为最大流,且该割为最小割。

  3. 若不存在增广路,我们证明当前流等于一个割。

\(S=\{v,\exist\ p_{s,v}\}\)\(S\)\(s\) 在残量网络中能到达的点的集合。令 \(T=V-S\)。显然 \((S,T)\) 是一个割,对于当前残量网络 \(G'=(V,E')\),一定有 \(\forall x \in S,\forall y\in T\),边 \((x,y)\) 满流,否则 \(y\in S\)。容易证明当前流恰好为 \(S\)\(T\) 的所有边的容量和,也就是割的大小。因为此时原图 \(S\)\(T\) 所有的边流量为容量,\(T\)\(S\) 的边流量为 \(0\),所以流量为 \(S\)\(T\) 的边的流量之和,也就是割 \(S,T\)

算法

Dinic

考虑对残量网络 bfs 分层,强制流量只能流向下一层,再进行一次 dfs,求限制下所有的增广路,搞定。

复杂度 \(O(n^2m)\)

每次增广复杂度为 \(O(nm)\),共计增广 \(n\) 次,因为每次增广都会让 \(dep[t]\) 增加 1。

每次增广的复杂度不是很好证,但加了当前弧优化其实就很松。

代码记在脑子里了,不放了。

ISAP

咕咕咕

HLPP

咕咕咕

费用流相关

一般指最小费用最大流,暂时没啥感觉,不写。

最小割

非常常见的一个网络流模型应用。

核心思想

通过构造一个图的割到决策方案的映射,其中决策必须可拆分计算,求出最小的代价,一般来说决策的限制如果比较奇怪就应该考虑最小割。

常用于规划 \(0/1\) 独立贡献决策问题。

适用条件

  • 不能存在负容量边权,我不知道有没有人会,反正我是不会。

1.最大闭合权子图问题

给定一张有向图,点带权(权可以为负),选一个子图出来,要求如果 \(u\) 选了那如果存在 \((u,v)\),那 \(v\) 也要选。求最大权值和。

决策贡献独立,决策类型为 0/1。应该可以最小割。令割中与 \(s\) 同集合的点为选择的点,与 \(t\) 同一个集合的点为未选择的点。那么每个点应该和 \(t\) 连一条流量为权值的相反数的边,代表选它的代价,再对于每条 \((u,v)\)\(u\)\(v\) 连一条 inf 边,代表选了 \(u\) 不选 \(v\) 的代价。考虑这张图的一个最小割,它必然不会包含 inf 边,也就是说,我们选了 \(u\)\(s\) 中一定会让 \(v\)\(s\) 中,所以一定合法。然后发现原问题的每一个答案都可以和图上的一个不含 inf 边的割一一对应,故最小割就是原问题的答案的相反数。

这张图有负数,不行,所以考虑先默认选所有正数。那么选负数的代价为相反数,不选正数的代价为本身。

那么应该从 \(s\) 向正权点连边,从负权点向 \(t\) 连边,最后对于 \((u,v)\)\(u\)\(v\) 连 inf 边即可。

总结一些科技

主要收录比较神仙的,实用的算法技巧。

快速取模

原理

找到一个近似 \(m^{-1}\) 的形如 \(m'>>k\) 的数。

不妨就取 \(k=64,m'=\lceil\frac{2^{64}}{m}\rceil\)

然后 \(a\%b = a-a\times\lfloor\frac{a}{b}\rfloor = a-(a\times m'>>64)\)

纯纯的整数运算,经过误差分析,可以知道后式结果最多多减去一个 \(m\),判断掉就行。

因为 \(a\) 常常是 \(\text{long long}\) 级别的数,所以开 __int128

优化据说有 \(5-6\) 倍,如果模数是 const,编译器会自动帮忙用这个优化。

代码

1
2
3
4
5
6
7
8
9
10
11
12
struct Barrett{
unsigned long long im; int m;
Barrett(unsigned m) :m(m), im(~0ull/m+1) {}
int operator ()(int a, int b){
unsigned long long z=(unsigned long long)a*b;
int v=z-((__int128)z*im>>64)*m;
return v<0?v+m:v;
}
}bt(1);
bt=Barrett(p);
c=bt(a, b);
//c=a*b%p

注意事项

  • 为啥 im,mull,uint,因为 m=2 时,会爆 long long
  • 可以重载括号。

光速乘

用于计算两个 long long 级别的数乘积对一个 long long 级别的数取模的结果。

1
2
3
4
long long multi(long long a,long long b,long long mod){
long long x=(unsigned long long)a*b-(unsigned long long)((long double)a*b/mod-0.5)*mod;
return x>=mod?x-mod:x;
}
1
2
3
4
long long mul(long long A, long long B, long long P){
long long C = A * B - (long long)((long double)A * B / P + 0.1) * P;
return C < 0 ? C + P : C;
}

原理很简单,long double 的精度误差虽然有,但是我们 -0.5 之和核查范围变成了 [0,1],肯定不会超出这个。

C++ 标准中要求了 unsigned long long 类型在溢出后保证为原值对 \(2^{64}\) 取模的结果,所以直接用就行。

第二个原理类似,在 G++ 编译器中,O2 中,保证 long long 的溢出行为有定义。

推荐用第一个,各个平台都不会出锅。

如果 \(P\leq 10^{14}\) 可以改成 double

判断一下是否减少了就行。

简单事实

  1. \(\gcd(a,x_1)=1,\gcd(a,x_2)=1 \iff \gcd(a,x_1x_2)\)
  2. \(n\ |\ m\)\(\phi(n)\ |\ \phi(m)\)
  3. \(\gcd(a,p)=1\),则 \(ax,x \in [1,p]\)\(p\) 意义下互不相同,反之亦然。

模运算和满足的运算率

注:符号均为模 \(p\) 意义下。

  • 加法交换律
  • 加法结合律
  • 乘法交换律
  • 乘法结合率
  • 如果 \(p\) 为质数 ,\(g\)\(p\) 的一个原根,则 \(\log_x(y) = \frac{\log_g(y)}{\log_g(x)} \pmod {p-1}\)

复数运算满足的运算性质

  • 加法交换律
  • 加法结合律

一些抽象代数内容

事实1

\(g\)\(p\) 的一个原根,记 \(\log_g(x) = y\),模 \(p\) 意义下 \(x\) 的阶数为 \[ \Large ord = \frac{\phi(p)}{\gcd(\phi(p),\log_g(x))} \]

证明1

显然 \(g\) 的阶数为 \(\phi(p)\),又因为 \(g^y = x\)。所以 \(x^{ord} = 1\)

然后证明 \(x^i,i \in [1,ord]\) 互不相同。

反证,如果相同,设为 \(i,j(i\ge j)\),那么 \(g^{yi} = g^{yj}\) 得到 \(\phi(x) |\ y(i-j)\) 两边同时除以 \(\gcd(\phi(p),y)\),得到 \(ord |\ y'(i-j)\),其中 \(\gcd(y',ord) = 1\) ,得出 \(i-j \ge ord\) 矛盾。

所以 \(x\) 的阶数为 \(ord\)

裴蜀定理

内容

\(\exist\ x,y \in \Z\) 使得 \(ax+by=\gcd(x,y)\)

证明

归纳构造。

先证明 \(\exist\ x',y'\in \Z\) 使得 \(bx'+(a\%b)y' = \gcd(a,b)\)

\(ay' + b(x'-\big\lfloor\dfrac{a}{b}\big\rfloor y') = \gcd(a,b)\)

构造 \(x = y',y=x'- \big\lfloor\dfrac{a}{b}\big\rfloor y'\) 即可满足条件。

递归证明构造式子,得到边界证明 \(\exist \ x,y\) 使得 \(\gcd(a,b)x + 0 \times y =\gcd(a,b)\)

\(x=1,y=0\)

欧拉函数是积性函数

即证明若 \(\gcd(n,m) =1,\)\(\phi(n*m) = \phi(n) *\phi(m)\)

证明一

考虑若干个同余方程组 \[ x ≡ r_1 \pmod n\\ x ≡ r_2 \pmod m\\ \] 列出 \(n,m,nm\) 意义下的最小缩系 \(S_n,S_m,T\)

容易证明 \(\forall\ r_1\in S_n, r_2 \in S_m\),存在唯一 \(x \in [1,nm]\) 是上同余方程组的解,且 \(x \in T\)

存在唯一就是 EXCRT, \(x\in T\) 由事实 1 显然。

\(\phi(n*m) \ge \phi(n) *\phi(m)\)

再证明 \(\forall\ x\in T\),可以对应一个以上同余方程组的解,假设不对应,那么它一定与 \(n,m\) 中的一个不互质,由事实 1 推出矛盾。

\(\phi(n*m) \le \phi(n) *\phi(m)\)

证毕。

证明二

欧拉定理

内容

\[ \Large 若 \ \gcd(a,m)= 1 ,\ 则\ a^{\phi(m)}≡1\mod m \]

证明

考虑模 \(m\) 意义下的最小缩系,即最小完全剩余系删去与 \(m\) 不互质的元素后的剩余系,记为 \(S\)

构造 \(T = \{ax,x \in S\}\)

可以证明 \(T=S\), 若 \(\exist\ x_1,x_2\) 使得 \(x_1\neq x_2\)\(ax_1 ≡ ax_2 \pmod m\) ,因为 \(\gcd(a,m)=1\)

所以 \(m\ |\ x_1 - x_2\),并推出 \(x_1 \neq x_2\),故 \(T\) 中元素两两不同且均与 \(m\) 互质,即为 \(S\)

考虑 \(T,S\) 中所有元素的乘积,得到 \(\prod\limits_{i=1}^{\phi(n)} ax_i ≡ \prod\limits_{i=1}^{\phi(n)} x_i \pmod n\),又因为 \(\prod\limits_{i=1}^{\phi(n)} x_i\)\(m\) 互质,所以 \(a^{\phi(n)} ≡ 1\pmod n\)

扩展欧拉定理

内容

\[ \Large 若 \ b≥φ(m) ,\ 则\ a^b≡a^{b \mod φ(m) +φ(m)}\mod m \]

证明

\(m\) 考虑唯一分解定理。

对于任意因子 \(p_i^{k_i}\),若与 \(a\) 互质,那就有 \(a^b≡a^{b \mod φ(m) +φ(m)}\mod p^{k_i}_i\)

如果和 \(a\) 不互质,因为 \(b\ge \phi(m)\),那么因为有 \(b\ge \phi(m)\ge k_i\),所以 \(a^b,a^{(b \mod φ(m)) +φ(m)}\) 都是 \(p^{k_i}_i\) 的倍数。

得到 \(a^b - a^{(b \mod φ(m)) +φ(m)}\)\(p_i^{k_i}\) 的倍数,故同余。

原根:

定义:

如果 \(x^1,x^2,x^3 \cdots x^{\phi(n)}\)\(n\) 意义下互不相同,且 \(\gcd(x,n)=1\),则称 \(x\)\(n\) 的原根。

性质:

质数 \(p\) 的原根的方幂能取遍 \([1,p-1]\)

求质数的原根

数学大佬证明了一个数 \(n\) 的最小正原根不超过 \(n^{\frac{1}{4}+\epsilon}\),所以枚举每个数,检查所有 \(n\) 的约数是否是 \(a\) 的阶,如果不是,那么 \(a\) 为一个原根,复杂度 \(\sqrt n\),瓶颈在分解质因数。

应用

开离散对数的时候上原根和换底公式有奇效。

原根存在定理

一个数 \(x\) 有原根当且仅当 \(x= 2,4,p^n,2\times p^n\),其中 \(p\) 为奇素数。

证明不会。

中国剩余定理

咕咕咕

平面图欧拉定理

顶点数-边数-连通块数+区域数=1

几何体欧拉定理

顶点数-边数+面数=2

除法下取整相关:

  1. \(\lfloor\frac{a}{b}\rfloor\ge x\iff b\le \lfloor\frac{a}{x}\rfloor\)
    • 含义:\(b=\lfloor\frac{a}{x}\rfloor\) 是满足 \(\lfloor\frac{a}{b}\rfloor\ge x\) 的最大的 \(b\)
  2. \(\lfloor\frac{a}{b}\rfloor< x \iff b>\lfloor\frac{a}{x}\rfloor\)
    • 上面那个反过来。
  3. \(\big\lfloor\dfrac{\lfloor\frac{n}{a}\rfloor}{b}\big\rfloor=\lfloor\dfrac{n}{ab}\rfloor\)
    • \(n=kab+r,r< ab\),则 \(\lfloor\frac{n}{a}\rfloor<(k+1)b\),自然有 \(\big\lfloor\dfrac{\lfloor\frac{n}{a}\rfloor}{b}\big\rfloor\le \lfloor\dfrac{n}{ab}\rfloor\)。又因为 \(\lfloor\frac{n}{a}\rfloor\ge kb\),因此 \(\big\lfloor\dfrac{\lfloor\frac{n}{a}\rfloor}{b}\big\rfloor\ge \lfloor\dfrac{n}{ab}\rfloor\),得证。

余数相关

  1. \(\gcd(x,y)=1,k\in[0,y)\),则 \(kx\pmod y\) 互不相同。
    • 证明反证法,移到同一边然后是倍数。

解析几何相关

  1. \((x_0,y_0)\) 关于 \(y=x+m\) 的对称点:\((y_0 - m,x_0 + m)\)
  2. \((x_0,y_0)\) 关于 \(y=-x+m\) 的对称点:\((- y_0 + m,- x_0 + m)\)

数学算法介绍

主要介绍一些数学相关的算法。

多项式

  • DFT:离散傅里叶变换,方式有两种, FFT 和 NTT。
  • IDFT:逆离散傅里叶变换。

FFT 多项式乘法

最开始学这玩意的时候感觉非常迷,后面数学水平上去了其实也不难。

多项式有两种表示方法,一种是系数法,另一种是点值法,总所周知 \(n\) 个不同点唯一确定一个 \(n-1\) 次多项式。

原理:多项式的两个系数表达式相乘是 \(O(n^2)\) 的,但是其点值表达式相乘却是 \(O(n)\) 的,所以考虑将系数表达式转成点值表达式然后相乘。

事实上,点值表达式和系数表达式的互相转化,如果点值取特殊点,可以做到 \(O(n\log n)\),即使是任意点,即多项式多点求值和多项式多点插值也可以做到 \(O(n \log^2n)\)

设最终多项式次数为 \(n-1\),我们进行多项式乘法时选择的点值叫单位根,即 \(x^n=1\) 在复数域上的所有根。

这玩意有一些性质,不过我们得先把次数变为 \(n=2^k\) 形式。

无法想象发明这个东西的人是怎么想到的,可能这就是被记在历史书上的人的水平。

以下内容如果将坐标系视为极坐标系会更好理解 $$ \[\begin{align} W_n^i &= -W_n^{i+\frac{n}{2}}\\ W_{\frac{n}{2}}^{i} &= W_{n}^{2i}\\ \end{align}\] $$ 考虑这样一个问题,对于一个多项式 \(a_0+a_1x+a_2x^2 \cdots a_{n-1}x^{n-1}\) ,我们需要同时求出它在 \(W_n^{0},W_n^{1}\cdots W_n^{n-1}\) 处的取值。发现由于第一个性质,貌似可以偷个懒,因为后 \(\frac{n}{2}\)\(W_i\) 就是前 \(\frac{n}{2}\) 个数的相反数。

相反数的性质,奇数次幂的符号要改变。考虑对系数按奇偶性分类,式子变成了这个样子。

\((a_0+a_2x^2+\cdots +a_{n-2}x^{n-2})+x(a_1+a_3x^2+\cdots + a_{n-1}x^{n-2})\)

然后考虑前后两个部分,都是一个多项式,需要对他们各自求 \(x=W_\frac{2}{n}^0,W_\frac{2}{n}^1\cdots W_\frac{2}{n}^{\frac{n}{2}-1}\) 处的取值,本质上是求 \(x^2=W_{\frac{2}{n}}^0,W_{\frac{2}{n}}^2\cdots W_{\frac{2}{n}}^{n-2}\) 处的取值,结合第二个性质,woc,就是两个子问题,解决之后就可以 \(O(n)\) 得到原问题的解,边界显然是 \(n=1\)

复杂度 \(T(n)=2T(\frac{n}{2}) + O(n) = O(n\log n)\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void FFT(int now,com *a,int op)
{
if(now==0)
return;
com a1[1<<now],a2[1<<now];
for(i=0;i<1<<now;i+=2)
{
a1[i/2]=a[i];
a2[i/2]=a[i+1];
}
FFT(now-1,a1,op);
FFT(now-1,a2,op);
com w0=(com{cos(2.0*Pi/(1<<now)),op*sin(2.0*Pi/(1<<now))}),w=(com){1,0};
for(i=0;i<1<<now-1;i++,w=w*w0)
{
a[i]=a1[i]+w*a2[i];
a[i+(1<<(now-1))]=a1[i]-w*a2[i];
}
}

但是如果递归的话,常数会比较拉跨。因为递归必然需要复制数组重新弄成一个下标 \(1-n\) 的问题,无论用什么办法解决,你的高速缓存都会表示意见很大,所以考虑迭代写法。

本质上递归是一层一层合并了两个数组,那么能不能直接模拟这个合并的过程呢,答案是可以的。

观察发现本质上是将下标二进制 reverse 之后逐层合并的,我们也这么做就行。

reverse 可以 \(O(n)\),如下(如果你不了解运算顺序,请老老实实打括号)

这个原理很简单,不看最后一位,其它位先 reverse,然后处理一下最后一位就行。

1
for(int i=1;i<1<<lim;i++)res[i]=res[i>>1]>>1|(i&1)<<1-lim;

下面是迭代写法代码,本质是模拟了递归合并的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void FFT(com a[],com b[],int op)
{
for(i=0;i<1<<maxn;i++)
if(r[i]>i)swap(a[r[i]],a[i]),swap(b[r[i]],b[i]);
for(i=1;i<=maxn;i++){
for(j=0;j<1<<maxn;j+=1<<i){
com w0=(com){cos(2.0*Pi/(1<<i)),op*sin(2.0*Pi/(1<<i))},w=(com){1,0};
for(k=0;k<1<<i-1;k++,w=w*w0){
com x=a[j+k],y=a[j+k+(1<<i-1)]*w;
a[j+k]=x+y;
a[j+k+(1<<i-1)]=x-y;

x=b[j+k],y=b[j+k+(1<<i-1)]*w;
b[j+k]=x+y;
b[j+k+(1<<i-1)]=x-y;
}
}
}
}

搞完了系数转点值,接下来是点值转系数。

前人告诉我们只需要将单位根改为 \(W_n^{0},W_n^{-1}\cdots W_n^{-n+1}\),再做一遍系数转点值的过程就可以得到系数,但是系数会变成原来的 \(n\) 倍,除掉就行。

给出简要证明,\(i\) 次项的系数为 \(a_i\),转一次点值

之后变为 \(b_i\),再做一次变成 \(c_i\) \[ \begin{eqnarray} c_x &=& \sum\limits_{i=0}^{n-1}b_iW_n^{-ix} \\ &=& \sum\limits_{i=0}^{n-1} W_n^{-ix}\sum\limits_{j=0}^{n-1}a_jW_n^{ij}\\ &=& \sum\limits_{i=0}^{n-1} \sum\limits_{j=0}^{n-1}a_jW_n^{i(j-x)}\\ \end{eqnarray} \] 对于 \(j=x\),贡献显然为 \(\sum\limits_{i=0}^{n-1}a_xW_n^{i\times0} = na_x\)

对于 \(j\neq x\) 贡献为 \(a_j \sum\limits_{i=0}^{n-1}(W_n^{j-x})^{i}\)

对这个式子的求和用等比数列求和公式有贡献为 \(\dfrac{W_n^{n(j-x)}-1}{W_n^{j-x}-1}\)

显然分子为 \(0\),分母不为 \(0\),所以贡献是 \(0\),所以结果就是 \(na_x\)

搞定。

NTT 多项式乘法

FFT 多项式乘法是由缺陷的,由于浮点数精度和运算速度问题,FFT 可能并不能很好的解决一些问题,所以引入了 NTT,NTT 从有限整数域中找到了这样一组具有同样优秀性质的 \(W_n\),即 \(g\),也就是原根。

原根的内容可以参考数学证明总结中的介绍。

注意,和 FFT 一样,NTT 也需要严格的按照 \(2^k\) 取次数,因为我们利用了 \(W_n^{2i} = W_{\frac{n}{2}}^{i}\) 这一重要性质

所以能取出较大的 \(2^k\) 作为阶的质数才可以作为 NTT 的模数,常见的 NTT 模数是 \(998244353=2^{23}\times 7\times 17 +1\) ,我们可以取它的原根 \(g=3\) 作为基本单位根带入,实际上如果要找到一个应用于 \(n\) 的单位根 \(W_n\),需要取 \(W_n=g^{\frac{p-1}{n}}\)。这样它就满足了我们在 FFT 证明中用到的一切性质。

然后照着 FFT 打一遍就行,只是基本运算这些换为模 \(p\) 意义下的运算就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void NTT(int *a,int type)
{
for(i=0;i<1<<s;i++)
if(rk[i]>i)swap(a[rk[i]],a[i]);
for(int len=1;len<=s;len++)
{
int w=1,wn=quick(g,mod-1>>len);
if(type==-1)wn=quick(wn,mod-2);
for(j=0;j+(1<<len)<=1<<s;j+=1<<len,w=1)
{
for(k=j;k<j+(1<<len-1);k++,w=1ll*w*wn%mod)
{
int x=a[k],y=a[k+(1<<len-1)];
a[k]=(x+1ll*w*y%mod)%mod,a[k+(1<<len-1)]=(x-1ll*w*y%mod)%mod;
}
}
}
}

其它多项式乘法和一些优化

FFT 三次变两次

FFT 三次变两次,把 \(b\) 扔到 \(a\) 的虚部去,变成了 \(A(x) = (a_0+b_0i) + (a_1+b_1i)x+ \cdots + (a_{n-1}+b_{n-1}i)x^{n-1}\)

然后求 \(A^2(x)\),得到的系数表达式的虚部就是 \(2ab\)

证明显然,会比 NTT 略快。

任意模数 NTT

NTT 解决的是模意义下的乘法问题,当然,如果确保值域不超出模数,那么模意义下的结果就是正确结果。

如果模数不是 NTT 模数,就需要写任意模数 NTT。

实现方式有两种,其一是拆系数 FFT,其二是多模数 NTT 后用 CRT 合并。

多模数 NTT

一般选取 998244353 1004535809 469726049 作为 NTT 模数,它们的原根都是 3。分别进行 NTT 后,可以得到在模它们意义下的结果,用中国剩余定理合并,得到一个模约 \(5\times 10^{26}\) 的数的结果,一般来说任意的模数不会超过 \(10^9\),所以乘起来的结果不超过 \(10^{24}\),直接用合并的结果就行。

提一下实现细节。

一种方式是写个 Num 类,里面放三个 int,重写一下加减乘除,正常做。

另一种方式是写个 Poly 类,封装个乘法,用 for 做三次,都比较阳间。

用 CRT 时记得 __int128。或者用一个科技

一共三个方程,考虑这样合并。

其中 \(mod_1^{-1}\) 表示 \(mod_1\)\(mod_2\) 意义下的逆元。 \[ x = x_1 \pmod {mod1} \\ x = x_2 \pmod {mod2} \\ k_1 mod_1 + x_1 = x_2 \pmod{mod2}\\ x = (x_2-x_1)mod_1^{-1}mod_1 + x_1 \pmod {mod_2*mod_1} \]

发现这样第二次合并的时候可以直接在模需要的模数 \(p\) 下进行计算。

注意,务必考虑好取模的顺序,计算第二个时不能先对 p 取模,因为可能结果还需要先模上 \(mod_1*mod_2*mod_3\)

怎样避免 __int128 ?

\((x_2-x_1)mod_1^{-1}\)\(mod_2\) 取模即可,注意处理正负号,保证任意时刻结果在 [0,对应模数) 内。

拆系数 FFT

FFT 精度不高,一般来说 double 跑个 \(10^{14}\) 问题不大(最后除掉 \(n\) 以后的结果,也就是乘出来的多项式的系数,举个例子就是如果跑 \(n=10^4\),那需要保证 \(a_i,b_i\leq 10^5\)),但是跑 \(10^{24}\) 次方, FFT 表示我做不到,用 long double 也不行。

所以考虑拆系数,把每个数写成 \(a_1\times2^{15} + a_2\) 的形式。

然后乘法就变成了 \(a_1b_1*2^{30}+(a_1b_2+a_2b_1)*2^{15}+a_2b_2\)

设新的四个多项式为 \(A_1,A_2,B_1,B_2\)

一共 8 次 FFT,但可以优化,考虑复多项式 \[ P=B_1+B_2i\\ T_1=P\times A_1 \\ T_2=P\times A_2 \]

发现需要的系数在 \(T_1,T_2\) 的实部和虚部,对这俩做 IDFT,收工。

一共 5 次 DFT。

也可以这样:

先考虑 \[ P=A_1+A_2i\\ P'=A_1-A_2i\\ Q=B_1+B_2i \]

再考虑: \[ T_1=PQ=(A_1B_1-A_2B_2)+(A_1B_2+A_2B_1)i\\ T_2=P'Q=(A_1B_1+A_2B_2)+(A_1B_2-A_2B_1)i \] 最后: \[ T_1+T_2=2(A_1B_1+A_1B_2i)\\ T_1-T_2=-2(A_2B_2-A_2B_1i) \]

弄出 \(T_1,T_2\) 的系数表示法就行。其实复系数多项式乘法和普通系数多项式乘法没有什么区别。

这种 5 次的思路还有其它方式,不一一介绍了。

听说有种方法可以把两次实数域多项式的 DFT 和 IDFT 整成一次复数域的,但我觉得没啥必要会。

DFT 思路是构造一个多项式 \(P=A+Bi\),对其做 DFT,然后线性求 \(Q=A-Bi\) 的 DFT 结果。

IDFT 思路不会。

更推荐第一种写法,不会出现精度问题,如果效率不够再考虑改成第二种

C 语言编译过程

  • 源文本-------预处理,处理 define,include 等
  • 预处理文本--------编译
  • 汇编文本----------汇编得到二进制文件 .o
  • 目标格式----------链接
  • 可执行文件

二进制补码原理和 C 语言处理类型转换

  • 二进制补码最高位本质是一个 \(-2^x\)
  • 同时处理有符号和无符号整数比较时或者进行其它二元运算时,C 语言会默认无符号且均为正数。
  • 编译器处理一个 -x 的表达式时,会先读 x 然后取反,所以 -2147483648 是不合法的,应该写为 -2147483647-1

简单的汇编语言

  • 操作系统将物理内存抽象为了一个一维数组,所有汇编层面的操作均在一维数组内进行

  • 每一条汇编指令都可以被描述为两个部分,指令和操作对象,这两部分的整体可以被一个或多个字节描述,此规则本质上是一颗哈夫曼树。

  • 一个程序的汇编指令会被保存在主存上,有一个程序计数器 epi 指出当前执行到的地方。

  • 常用寄存器名有 eax,ecx,edx,ebx,esi,edi,esp,ebp。前三者的数据由当前程序保存在栈中,后三者的值由下级程序保存,最后二者为帧指针和栈指针,一般不使用。前四个寄存器可以通过 ah,al,ax 等形式访问低二字节和低一字,但都可以用 di 形式访问低一字。

  • 传送指令分为三种 movb movw movl 分别表示字节,字和双字。传送对应类型时,应该使用对应的寄存器位置。 push,pop 指令为压栈和弹栈,本质上是操作了主存和栈指针。

  • lea 指令可以快速计算 a+b*(1,2,4,8)+ca 必须为常量,b,c 必须为寄存器中的数。

  • 操作数分为三类,一类是立即数,一类是寄存器,一类是主存数据,访问格式如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    $imm //立即数 imm
    eg. $0x3f

    E //寄存器 E
    eg. eax

    Imm(e1,e2,s) //主存上 Imm+e1+e2*s 位置上的数据,Imm 可以缺省,后二者可以缺省,e1 缺省时不能省略 ','
    eg. 0x3f(eax,ebx,4)
    eg. (eax)

  • testcmp 指令来控指条件,本质上,它们改变了条件寄存器内的值,test x x 等价于 cmp 0 x。执行 cmp 后,jl 等条件跳转语句会在 cmp 成立时执行,jl 表示小于时跳转,cmp x y 可以看作是 y<x 时执行 jl

  • call 指令会将返回地址 (call) 语句后那条语句的地址入栈后跳转到函数所在位置。ret 指令利用返回地址跳转回去,一般来说,用 eax 保存返回内容。

  • 由于程序寄存器中的某些值会被缓存到主存中,所以使用 gets 等不安全函数读取时,如果读取的内容过长超出了为此缓存区分别的字节后,会污染一些先前被存储在主存中的寄存器值,因为超出分配的内存后,会写在外面,通过这样的操作,我们可以改变一些系统寄存器的值,让程序跳转执行一些我们希望让它执行的代码(这样的代码通常被写在我们的输入里),这就是缓冲区攻击,通常操作是改变 ebp 的值使得 ret 操作跳到我们希望的地方。

语法,语法糖,标准库函数

介绍一些有用常用但鲜为人知的 C++ 语法,语法糖。

运算顺序

记好表,记不住打括号,最好打括号。

运算符说明 运算符 替代方法
第 1 组优先级,无关联性
范围解析 ::
第 2 组优先级,从左到右关联
成员选择(对象或指针) ->
数组下标 []
函数调用 ()
后缀递增 ++
后缀递减 --
类型名称 typeid
常量类型转换 const_cast
动态类型转换 dynamic_cast
重新解释的类型转换 reinterpret_cast
静态类型转换 static_cast
第 3 组优先级,从右到左关联
对象或类型的大小 sizeof
前缀递增 ++
前缀递减 --
二进制反码 ~ compl
逻辑“非” ! not
一元求反 -
一元加 +
Address-of &
间接寻址 *
创建对象 new
销毁对象 delete
强制转换 ()
第 4 组优先级,从左到右关联
指向成员的指针(对象或指针) ->*
第 5 组优先级,从左到右关联
乘法 *
除法 /
取模 %
第 6 组优先级,从左到右关联
加法 +
减法 -
第 7 组优先级,从左到右关联
左移 <<
右移 >>
第 8 组优先级,从左到右关联
小于 <
大于 >
小于或等于 <=
大于或等于 >=
第 9 组优先级,从左到右关联
等式 ==
不相等 != not_eq
第 10 组优先级,从左到右关联
位与 & bitand
第 11 组优先级,从左到右关联
位异或 ^ xor
第 12 组优先级,从左到右关联
位或 | bitor
第 13 组优先级,从左到右关联
逻辑与 && and
第 14 组优先级,从左到右关联
逻辑或 || or
第 15 组优先级,从右到左关联
条件逻辑 ? :
转让 =
乘法赋值 *=
除法赋值 /=
取模赋值 %=
加法赋值 +=
减法赋值 -=
左移赋值 <<=
右移赋值 >>=
按位“与”赋值 &= and_eq
按位“与或”赋值 |= or_eq
按位“异或”赋值 ^= xor_eq
引发表达式 throw
第 16 组优先级,从左到右关联
逗号

标准库

函数

std::swap

  • 对于除了 array 容器之外的所有标准库容器,都可以 \(O(1)\) 交换

  • 交换数组的复杂度为 \(O(size)\),对二维数组的某一维使用,会将当前行视为一维数组进行交换。

memcpy

memcpy(dst, src, size)

dst:目标数组起始位置

src:源数组起始位置

size:需要复制的字节数

lower_bound,upper_bound

看下实现吧:

  • upper_bound 如果对类使用,那么类的比较函数需要定义为友元函数
  • lower_bound 返回第一个大于等于它的位置,upper_bound 返回第一个大于它的位置,comp 为默认小于号。
  • 如果要对不增序列操作,那么 comp 为大于号,lower_bound 返回第一个小于等于它的位置,upper_bound 返回第一个小于它的位置。
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
template<class ForwardIt, class T, class Compare>
ForwardIt lower_bound(ForwardIt first, ForwardIt last, const T& value, Compare comp)
{
ForwardIt it;
typename std::iterator_traits<ForwardIt>::difference_type count, step;
count = std::distance(first, last);

while (count > 0) {
it = first;
step = count / 2;
std::advance(it, step);
if (comp(*it, value)) {
first = ++it;
count -= step + 1;
}
else
count = step;
}
return first;
}
template<class ForwardIt, class T, class Compare>
ForwardIt upper_bound(ForwardIt first, ForwardIt last, const T& value, Compare comp)
{
ForwardIt it;
typename std::iterator_traits<ForwardIt>::difference_type count, step;
count = std::distance(first,last);

while (count > 0) {
it = first;
step = count / 2;
std::advance(it, step);
if (!comp(value, *it)) {
first = ++it;
count -= step + 1;
}
else
count = step;
}
return first;
}

atan2

1
2
3
4
double atan2(double x,double y){
//返回 [actan(y/x)],[-Pi,Pi)
//计算几何的神,怎么神就不用说了吧。
}

for_each(begin,end,fun)

对 begin 到 end 区间的每一个元素 x,执行 fun(x)。

如果需要传参,可以利用类的构造函数。

即给类重载一个 () 运算,然后构造输出。

accumulate(begin,end,start)

begin end 区间运用 + 操作,起始为 start

效率不开 \(O2\) 时略高于循环,开了没差别。

count(begin,end,val)

返回 begin end==val 的数的个数。

开了 \(O2\) 效率和循环没差别。

结构体

一般来说 classstruct 竞赛上差别不大,struct 是默认 publicclass

重载括号

重载小括号运算符可以让你把结构体当函数用,其实本质上少写了一个 .{function name}

它和构造函数不冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct barrett1{
long long m,im;
int operator ()(int a,int b){
ULL z=(ULL)a*b;
int v=z-((__int128)z*im>>64)*m;
return v<0?v+m:v;
}
}bt1;
int a=1,b=1;
int c=bt1(a,b);

struct barrett2{
long long m,im;
int foo(int a,int b){
ULL z=(ULL)a*b;
int v=z-((__int128)z*im>>64)*m;
return v<0?v+m:v;
}
}bt2;
c=bt2.foo(a,b);

两者没有本质区别。

中括号可以重载,一般来说,重载中括号返回的是一个引用,中扩号只接受一个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct array_2d{
int a[10005],b[10005];
int n,m;
void init(int nn,int mm){
n=nn,m=mm;
}
int& operator[](int x){
return a[x];
}
}a;
a.init(4,5);
a[3]=1;a.b[4]=2;
a[4]=4;
cout<<a[4]<<' '<<a[3]<<' '<<a[1]<<endl;

构造

结构体的构造函数可以返回一个结构体实例,也可以允许在声明结构体的时候同时构造。

举个例子

1
2
3
4
5
6
7
8
9
10
struct st{
int a,b;string str;
st(int aa,int bb){
a=aa;
b=bb;
}
};
st t1=st(2,3);
st t2(2,3);
// st t3; 这句会 CE

这两种写法都行,注意不能变量重名,不会 CE,但是函数参数里会那个名字会覆盖掉全局的。

注意写了构造函数,所有的构造都必须带参数。

定义结构体的时候还可以给变量赋初值。

1
2
3
4
5
6
7
8
9
10
11
12
struct st{
int a,b=1;string str="str";
st(int aa,int bb){
a=aa;
b=bb;
}
};
st t1=st(2,3);
st t2(2,3);
// t1.str="str",t1.b=3
// t2.str="str",t2.b=3

但是如果构造函数里写了,就会被覆盖。声明的局部变量写了的初值会固定,没写的初值就随机。

如果没写构造函数,那么会有一个默认的列表构造函数,按照结构体内声明变量的顺序将列表中的每一个值依次赋给对应变量。

1
2
3
4
5
6
struct st{
int a,b=1;string str="str";
};
st t1=st{2,3,'huan_yp'};
st t2{2,3,"huan_yp"};

其实构造函数还有另一种写法

1
2
3
4
5
6
struct st{
int a,b;string str;
st(int aa,int bb): a(aa), b(bb) {}
};
st t1=st(2,3);
st t2(2,3);

和最开始的写法是等效的。

注意,如果写了构造函数,默认的列表构造函数会调用它,所以如果你想不同参数个数构造,需要填默认参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct st{
int a,b;string str;
st() :a(), b(), str(){}
//如果没有这一行,下面的第一个构造会 CE


st(int aa, int bb,string cc) :a(aa), b(bb), str(cc){}
// st() :a(), b(), str() {}
// st(int aa,int bb): a(aa), b(bb) {}
};
st t1;
t1={2,3,"str"};
t1={2,3};
//最后一行会 CE

列表构造式还可以自推导。

1
2
3
4
5
6
7
8
9
10
11
struct st{
int a,b=1;string str="str";
st(int aa,int bb){
a=aa;
b=bb;
}
};
st t1={2,3};
t1={3,4};
//t1(2,3)
//括号式式不能自推导的,这个的含义参考第一条。

语法概念

运算优先级

容易出问题的是几个位运算。

永远记住,位运算除了左右移位外,优先级低于比较操作。

比较操作之间,等于和不等的优先级低于大于小于。

引用

引用可以认为是一个隐式指针,不需要 * 的修饰便可以直接访问内容。它既然是一个指针,自然会占用一个 long 的空间。

定义,返回一个引用

可以在函数中返回一个引用,只需要在函数名前面后加一个 &,定义引用也在变量名加一个 &,和指针类似。

1
2
3
4
double &setValues(int i) {  
double &ref = vals[i];
return ref; // 返回第 i 个元素的引用,ref 是一个引用变量,ref 引用 vals[i]
}

定义引用的示例:

1
2
3
4
5
6
7
8
int n=0,m=0;
int& nn=n,mm=m;
n=10,m=20;
printf("n,m:%d,%d\n",n,m);
printf("nn,mm:%d,%d\n",nn,mm);
//输出:
//10,20
//10,0

在重载结构体 +=,= 运算符号的时候比较常用。

引用传参应该已经很熟悉了吧。

左值引用和右值引用

C++11 新概念。参考阅读

一般的,使用引用传参时,如果接受的参数为右值,那么不能修改右值,参数必须声明为 const 类型。

算了,先咕咕咕了吧。

模板

如果用的不好,容易出现找不到实现的问题。

一个原则:所有东西的类型必须在使用前就得知。

类模板

继承的时候需要写明父类的类型参数,如果子类也是模板类,可以用子类的类型模板参数作为父类的类型模板参数。

函数模板

函数模板是可以自动推导类型的,如果出现类型冲突,那么会报 CE

常见例子是 std::max(1,1ll) 报错。

注意字面量的类型。

指定模板参数可以仅指定一段前缀,剩下的采用自动推导。

多文件的一些问题

按惯例编写的问题

通常情况下,你会在 .h 文件中声明函数和类,而将它们的定义放置在一个单独的 .cpp 文件中。但是在使用模板时,这种习惯性做法将变得不再有用,因为当实例化一个模板时,编译器必须看到模板确切的定义,而不仅仅是它的声明。

定义和声明一起写

可以将模板的声明和定义都放置在同一个 .h 文件中。这就是为什么所有的 STL 头文件都包含模板定义的原因。

习惯来说,一般把这种包含定义的头文件后缀名写为 .hpp

使用 export

咕咕咕

Static 关键字

声明一个静态的东西,可以用来避免命名冲突,也可以用来更好的实现一个类。

Static 关键字声明的东西生命周期是整个程序的生命周期,但作用域仅限定为声明处的作用域。

举个例子,某个函数的执行过程和它被调用的次数有关,这个可以弄个全局变量记一个次数,但是我们也可以在它的内部写个 static 变量记录,这样就不会与外部空间变量名冲突。

所有静态变量的初始值为 0

1
2
3
4
5
6
7
8
int cnt=0;
void fun(){
static int cnt;
cout<<++cnt<<endl;
}
fun();cnt=10;
cout<<cnt<<endl;
fun();fun();cnt++;fun();

也可以用静态成员来维护一个类,比如写链表的时候,我们通常要先写个 node 类,然后才能实现 list 类,但我们可以直接将 head 指针作为一个静态变量放入结构体中,这样,这个静态变量就可以被所有结构体的实例访问修改。

注意,如果需要两个链表,那种这种方式是不可取的,因为静态变量 head 对所有该结构体的实例来说都只有一个。

我们当然也可以声明静态成员函数。

对于结构体的静态资源,我们可以用 {结构体名}.{成员名} 来访问。

函数,匿名函数

C++ 的函数名,本质上是一个指向某个函数的指针,函数在二进制层面和数据没有差异。

functor (一般译为算子),也可以调用,但它是一个对象。

函数类

C++ 的函数可以当作一个对象处理的,函数名是指向该对象的指针,比如说:

1
2
3
4
5
6
7
void add(int &x){x++;}
void del(int &x){x--;}
void doit(int &x,function<void(int&)> f){(x);}
n=10;doit(n,del);
printf("%d\n",n);
n=20;doit(n,add);
printf("%d\n",n);

匿名函数

格式:["捕获列表"]("参数列表")->"返回类型"{"函数体"}

捕获列表指的捕获局部变量,在函数中可以使用和修改。

参数列表无参数时可以省略,返回值如果不指定则会自动推导。

捕获列表不可省略,[] 为强制不捕获,[&] 为引用捕获全部,[=] 为取值捕获全部,也可以用变量名 [x,&y] 取值捕获 x,引用捕获 y。

1
sort(v.begin(),v.end(),[](const int &x,const int &y){return x>y;}); //降序排序

匿名函数也可以作为一个对象处理。

虚函数

这个东西自己写的时候用的比较少,毕竟不太注重面向对象,

这玩意和 Pythonabstract_method 类似,我也不知道怎么解释,反正会用就够了。

竞赛中应用的话,可以用来重写 pd_ds 里面的平衡树,但是真的用的很少。

语法糖

函数相关

返回值为 void 的函数

你是否碰到过这样的情况,函数返回值为 void,但是返回又是有条件,并且还需要执行一些简单的操作,用 {} 括起来显得代码冗长,不妨试试:

1
2
3
4
5
6
7
void do_cool_thing(int arg){
if(some_condition) return void(a_simple_expression);
do_cool_thing(arg);
}
void do_cool_thing(int arg){
if(some_condition) return void(ans++);
}

有返回值的函数

, 可以用在 return 语句,会返回最后一个值,当然也可以用在其它 Case

1
2
3
4
5
6
7
int get_v(){
return 3,5;// return value is 5
}
int new_node(int x){
return a[++t]=node{mt_rand(), 1, x, 0, 0}, t;
//can you guess what it do and when we use it?
}

列表初始化器

这东西说白了就是 {1,2,3,4,5} 这种,注意区分类成员初始化器,后者可以多类型,前者只能单类型。

循环

1
2
3
for(int v:{0,1}){
do_cool_thing();
}

分支更少,效率更高,代码更优美!

0%