知识点梳理
Day1 6.7
可能是图论专题吧
Tarjan求强连通分量
- dfn[]表示dfs序的标号
- low[]表示能连到的最小的dfn标号的点的标号
- sta[],top是栈
- instack[]标号是表示这个点在栈中,还是已经遍历完被弹栈了
- col[]记录每个点所在的颜色
1.Tarjan缩点后是一张拓扑图
2.标号的倒序是这张拓扑图一个合法的拓扑序
Tarjan求双连通分量,割点与桥
- dfn[]表示dfs序的标号
- low[]表示能连到的最小的dfn标号的点的标号
- sta[],top建立一个栈
- 并没有instack
1.是点双而不是边双的只有两点一线
2.边双缩点后是一棵树
3.一个点可能再多个点双中,于是我们求点双的时候需要在儿子处进行统计low[v] >= dfn[u]
Day2 6.8
昨天打比赛然后今天颓废去了
所以还是图论
争取完结图论?(不可能的。。
最短路
Floyd
这个没啥好说的(f[i][j])表示从(i)到(j)的最短路,要先枚举中介点(k),顺序不能错
因为这实际上是个三维的滚动数组
复杂度(O(n^3))
Dijkstra
- dis[]距离数组,初始设成正无穷
- vis[]标记是否访问过
- 一个优先队列,里面扔一个pair是距离和标号,为了方便一般把距离取负,因为优先队列返回最大值
1.不能处理带负权的边
优化后是(O((n + m)log n))
Bellman-Fold算法
就是SPFA复杂度的上界吧,还是SPFA好写,过了
复杂度(O(nm))
K短路
会玄学A*可以水过很多题啊
有一个论文算法,关于可持久化平衡树维护最短路树的
6.19 upd
差分约束
如果我们有一些限制形如(x_{a} - x_{b} <= d)
那么我们可以把它联系到最短路上,如果(b ightarrow a)有一条长度为(d)的边,那么必然有(x_{a} <= x_{b} + d)
这样我们求最短路,可以得到(x_{a})的最大值
为什么是小于等于号却是最大值呢,因为我们是从正无穷向下规约的
6.19 upd end
最小生成树
prim
- dis[]表示每个点距离当前联通块距离最小是多少
- 每次选择一个最小的点加入然后删掉它的距离
复杂度(O(n^{2})),可以类似dij的优化但是优化之后不如kruskal
kruskal
- 直接把边排序,然后加个并查集就好了
复杂度(O(E log E))
kruskal的正确性可以在很多思维题求最小生成树中作为优化边数的一种方法
二分图
6.16upd:
二分图的一些性质
|最大匹配数|=|最小顶点覆盖|
最小顶点覆盖的意思是我们选择个数最少的点然后覆盖所有的边
我们考虑每一条边一定连着一个被匹配的点,如果一条边连着一个未匹配的点,那么这条边也一定连着一条被匹配的点,否则匹配可以增加
|最大独立集|+|最小顶点覆盖|=顶点个数
最大独立集就是选出一些点,这些点之间没有边相连
我们把最小顶点覆盖去除了,那么剩下的顶点就没有边了啊
|最大匹配|+|最小边覆盖|=顶点个数
我们往最大匹配中加边,就可以覆盖所有顶点了,所以这个式子告诉我们,最小边覆盖就是选择最大匹配的那些边,然后加入新的边将未匹配点链接起来
6.16 upd end
匈牙利算法
- matc[]数组表示(Y)部的点匹配的(X)部点是什么
- vis[]当前这个(Y)部点有没有被遍历过
- 每次清一遍vis,可以只连从(X)到(Y)的边
复杂度(O(n^3))
KM算法
- ex_l[]表示左部期望的最大值,初始是每个点能连的最大值
- ex_r[]表示右部期望的最大值,初始都是0
- slack[]表示断层,每次在不能取的时候取最小
- 注意每次只把遍历过的点的期望值,左边减小,右边加大,其余不动,因为我是失败了以后给这个点能到的一些点加入至少一条可以供新选择的边
复杂度(O(n^3))但常数非常小,适合梦想选手
2-SAT
- 建图,缩点,Tarjan的标号的正序选择即可
四种连边
(A,B)不能同时取
那么选了A必须选B',选了B必须选A'
A->B' B->A'
(A,B)不能同时不取
那么选了A'必须选B,选了B'必须选A
A'->B B'->A
(A,B)要么都取,要么都不取
那么选了A'必须选B',选了B'必须选A',选了A必须选B,选了B必须选A
A'->B' B'->A' A->B B->A
(A,A')必须选A
A’->A
选了A'必须选A,保证了A拓扑序在前
解的多解依赖于拓扑序的多情况,这样的话去除了A'在A前面的情况
Day3 6.9
今天写树
圆方树
Tarjan缩点即可
具体题目具体分析了
基环树
- 遍历整个树的时候遇到一条非树边,记录下来,这条边加树边一定是一个环
会出的题一般都是讨论环上的如何操作
点分治
- 一个遍历的函数
- vis[]表示这个点有没当重心,遍历的时候和求重心的时候只经过vis=0的点
- 一个计算重心的函数
- son[]重儿子
- fa[]父亲
- size[]子树大小
- 还有一个队列
- 进队列时清零
- 可能还会有对树进行遍历计算的函数
经常按照过重心和不过重心两种方式考虑
多叉树转二叉树,即左儿子右兄弟
动态点分治
实质非常暴力,就是在加入叶子的时候,走到重心的路径,若遇到一棵树大小超过一定比例的父亲子树的大小(设置一个分数,如0.7),则重构之
斯坦纳树
我怎么会这么个玩意
(dp[i][S])表示特殊点的点集为(S),根为(i)的最小代价
然后转移有两种,一种是以同一个点为根
(dp[i][S] = min(dp[i][T] + dp[i][S oplus T],dp[i][S]))
一种是从另一个根换过来
(dp[i][S] = min(dp[j][S] + w[i][j],dp[i][S]))
前面是子集枚举,后面是暴力SPFA
Day4 6.10
学弟和小詹去广州集训了,zxg去六盘水支教了,全机房就剩我一个人了,感觉有点玄妙
今天写网络流
最大流
ISAP和Dinic的本质都是分层图跑暴力
ISAP
- gap[]某个深度的层有多少个
- dis[]表示这个点到根的深度
- 每次回到根的时候都将根的dis++,如果失败的话条件就是(dis[S] >= MAXV)
- 一层只能流向下一层,即(dis[u] == dis[v] + 1)才能往下流
- 若出现断层(gap[x] == 0)则认为不能再增广了
- 还有防止循环流的方法是记一个last表示这个点的边集上次遍历到哪里
Dinic
- 先BFS,再DFS
- BFS为了给整张图标号,DFS就是在(dis[u] == dis[v] + 1)的时候跑流量
- BFS如果标号失败就退出了
都要注意sumE = 1,使得边的标号从2开始
最大权闭合子图
即选择一个点集,保证这些点集连向的所有点都在这个点集里
方法是源点向所有权值为正的点连一条边,容量为权值,权值为负的点向汇点连一条边,容量为权值的负数
如果问选择什么点集就直接在残余网络中搜走到的点就行了
6.19 upd
有些情况下比如每个点有两种选择,每种选择有不同的价值,则可以转化为最小割问题
源点往i流一条容量为代价1的边,i往汇点流一条容量为代价2的边,中间可能还会有一些限制之类的,每个点代价1和代价2的总和减去网络流就是答案,因为较小的那种方案会被割掉
6.19 upd end
最小费用最大流
一种是暴力跑SPFA增广(已经可以糊弄过很多张图了,何况貌似没有复杂度下限一样优秀的了)
一种是zkw费用流
- modlabel()函数修改标号,每次在遍历过的边选一条延伸向未遍历的位置中价值最小的一条边,然后累加,然后将所有遍历过的点中延伸向未遍历的位置中都减掉这个价值(具体实现就是将未遍历的点的所有边正边减去反边加上,如果边两端的点都遍历过那么就相当于没有操作)
- aug()暴力跑流,并且为每个没有遍历过的点标记,流量乘上累加的费用
这个算法常数比较小,每次至少选了一条边进入可增广的范围
有限制的网络流
无源汇上下界可行流
每条边有一个上界up和下界low
- 把每条边的流量改成(up - low)
- 由于每个点要流量平衡,我们计算每个点入边的low就加上,出边的low就减去
- 如果流量平衡了,就不管它,否则新建源点,往入边流量多的点连一条流量为多余的入边流量的边,新建汇点,往出边流量多的点连一条流量为多余出边的点
有源汇上下界最大流
和无源汇的上下界可行流一样,不过可以加上从汇点到源点容量为正无穷的边,从超级源和超级汇跑一遍可行流
然后再从给定的源汇跑一遍最大流
忽然发现ISAP67ms,Dinic3000ms,不知道发生了什么
有源汇上下界最小流
和无源汇的上下界一样,要先跑一边无源汇的上下界可行流,把能满足的都先跑满,然后再加上从汇点到源点的一条边,继续跑无源汇的上下边可行流,若合法,最后答案就是这条边反向边的权值
这道题ISap因为循环流会被卡。。。
Day5 6.11
不知道写点啥。。。
分块
一般是给出一个序列,把序列分成(sqrt{n})块,每块大小为$sqrt{n} $
然后考虑块间合并和块内处理,一般要求合并和块内都是(O(1)),然后最多遍历(sqrt{n})个块,代价是(O(sqrt{n}))
(O(nsqrt{n}log n))适合梦想选手(误
一道著名的分块题是求区间众数,做法是对于每一个块求前i个块j个数的前缀和
然后再处理出以每个块为开头的块众数是什么,这一步是(O(nsqrt {n}))的,即从每个块开始把往后所有的数往以个桶里扔
这样我扔两边的那些零碎的时候,就可以即时更新区间众数了
莫队
普通莫队
即将左端点分块,按块排序,右端点从小到大排序
对于一个块内右端点移动次数是(O(nsqrt{n})),而左端点移动次数每次至多(sqrt{n}),总移动次数也是(O(nsqrt{n}))
按时间分的莫队
块的大小为(n^{2 / 3})共有(n ^{1 / 3})块
左右端点都按块排序,时间维从小到大排序
那么时间轴的移动,对于两个不同的块,移动次数是(O(n)),所以时间维移动了(O(n^{5 / 3}))
对于左端点的移动,每次在块内最多是(O(n^{2/3}))一共(O(n))次询问所以是(O(n^{5/3}))
右端点的移动,右端点只在块里是(O(n^{5/3})),左端点改变后右端点移动是(O(n))的,但是不超过(O(n^{4/3}))
树上莫队
首先分块变成了给树分块,方法就是用一个栈,如果遍历一个儿子后栈中元素增加的量超过了我要分的块,注意要先把儿子能分的块分完了,再把父亲扔进去
然后我移动两个指针就是
先把(u,v)的lca取反
将(u)移动到目标节点(tu),然后把(lca(u,tu))不动
(v)同理
然后将(tu,tv)的(lca)取反
Day6 6.12
忘了说,这是一件要坚持二十四天的事情,数字比较吉利
每天写一点,看看能写到哪吧。。
有点不想在学校待着,只有我一个人……
普通分治
就是考虑能不能分成左右两个子问题,把合并控制在(O(n))或者(O(n log n))级别
平面分治
例如,求平面最近点对
我们把平面分为两部分使得每一部分点数相等
然后我们可以用两边计算的最小值作为中间合并时最小距离的限制
实际上这样限制下来对于左边的一个点,右边可能对答案有贡献的点不超过6个
CDQ分治
CDQ分治主要解决的是三维偏序问题
首先我们按照某一维排序
我们在每一层计算左边对右边的贡献,即用在每一层按照另一维排序,并且将最后一维用数据结构维护,对于合法左边的点把贡献加给右边
三维是(O(n log^{2} n))的,拓展到更高维度也是可以的,但是没有暴力好,例如著名的五维偏序就是bitset。。。
三分
都带个分字,我也就一起说了
这个东西其实没什么好说的,就是对于一个单峰(谷)函数,分成三段,左右两端点各走(1/3),看哪个值比较小(大),那么就里峰顶(谷底)更近一些,而不至于走过头
有些函数可以说一下,例如绝对值函数取最大值(斜率相同)是比较常见的三分函数
(x + frac{1}{x})有名的对勾函数也可以考虑一下
二分
。。。。单调函数二分
当然也有些奇怪的应用,不一定是函数,而是一个集合至少扩增到哪个位置合法,是个隐蔽的单调函数
01分数规划
如果我选了(k)个数,求平均值最大,设平均值为(g)的话,那么可以得到
如果我们二分一个平均值看合不合法呢???
如果
我们只要把(k)移过去可以得到
而这个(k)其实是我们选择的数的个数
那么呢我们再考虑一个转化
这样的话,我们只要选一个数就减一下(g),如果能得到(geq 0)的值证明这个平均值合法
6.16upd:有些时候需要大于0
Day7 6.13
计算几何(我疯了我为啥要开这个。。。)
叉乘
(x_{1}y_2 - x_{2}y_{1})
得到的是两个向量的有向面积,逆时针正顺时针负
点积
(x_{1}y_{1} + x_{2}y_{2})
得到的是(|a||b|cos<a,b>)
向量模板
struct Point {
db x,y;
Point(db _x = 0.0,db _y = 0.0) {
x = _x;y = _y;
}
friend Point operator + (const Point &a,const Point &b) {
return Point(a.x + b.x,a.y + b.y);
}
friend Point operator - (const Point &a,const Point &b) {
return Point(a.x - b.x,a.y - b.y);
}
friend db operator * (const Point &a,const Point &b) {
return a.x * b.y - a.y * b.x;
}
friend db dot(const Point &a,const Point &b) {
return a.x * b.x + a.y * b.y;
}
friend Point operator * (const Point &a,db d) {
return Point(a.x * d,a.y * d);
}
db norm() {
return sqrt(x * x + y * y);
}
}P[MAXN];
点在线段上
friend bool InSegment(const Line &s,const Point &b) {
db d = (b - s.a) * (s.b - s.a);
if(!dcmp(d,0.0)) return false;
return dot(s.a - b,s.b - b) <= 0;
}
直线相交
一条直线可以用两个点表示
friend Point GetInsect(const Line &s,const Line &t) {
db S1 = (t.a - t.b) * (s.a - t.b);
db S2 = (t.b - t.a) * (s.b - t.a);
return s.a + (s.b - s.a) * (S1 / (S1 + S2));
}
对于线段相交只需要判这个点在不在这个线段上就好了
点在多边形内
做一条平行于x轴或y轴的射线,若交点为奇数则在内,偶数则不在内
凸包
选择左下角的点,最下的同时最左或最左的同时最下,这个点一定在凸包上,然后把其余点按照极角排序,极角一样的长度大的在后面
然后只需要用一个栈,看看新加入的这个点加进去后凸包是不是凸的
旋转卡壳
一般就是如果枚举点逆时针移动,最优答案也逆时针移动,这就是旋转卡壳了
半平面交
每条直线按照平面内的幅角排序
Gter是如果(a > b + eps)返回真
for(int i = 1 ; i <= tot ; ++i) {
while(ql < qr) {
if(Gter(0.0,(S[i].b - S[i].a) * (p[qr - 1] - S[i].a))) --qr;
else break;
}
while(ql < qr) {
if(Gter(0.0,(S[i].b - S[i].a) * (p[ql] - S[i].a))) ++ql;
else break;
}
q[++qr] = S[i];
if(ql < qr) {
if(dcmp(q[qr].d,q[qr - 1].d)) {
if(Gter((q[qr].b - q[qr].a) * (q[qr - 1].a - q[qr].a),0.0)) --qr;
else q[qr - 1] = q[qr],--qr;
}
}
if(ql < qr) p[qr - 1] = Cross_Point(q[qr],q[qr - 1]);
}
while(ql < qr) {
if(Gter(0.0,(q[ql].b - q[ql].a) * (p[qr - 1] - q[ql].a))) --qr;
else break;
}
p[qr] = Cross_Point(q[qr],q[ql]);
求凸包公切线
把凸包每个点以左下角为起点按极角排序,看看在哪两个点之间,然后往两边二分即可
具体实现很麻烦,题PKUWCD2T3出过,但是题目到现在都没公布
给cy写过一个凸包切点的流程,现在粘到这里吧
我字怎么那么丑,为什么,枯了。
Day8 6.14
要不要开数据结构呢。。。
先不开,等下周复习吧(咕咕咕
今天是各种类型的动态规划
动态规划
众所周知,动态规划只需要脑子就够了
插头DP
实际上是状压加hash压缩轮廓线
树型DP
看看能不能分割成子树中的问题
通常有一类dp需要在状态中加入假定的未来状态,如JLOI / SHOI2016侦查守卫
还有线段树合并等优秀方法
背包dp
这个会的话就很简单
看不出来的例如十二省联考的皮配(像我)只能垫底了。。
区间dp
通常是(n^{3})的,可能会使用bitset
一般是(f[i][k])和(f[k + 1][j])与(f[i][j])的合并
数位dp
通常都会加入一维([0/1])表示判断是否和给定的上界相同,如果有下界通常会转化乘(F(R) - F(L - 1))的前缀和容斥形式,也有把上下界是否相等一起压进状态里的
一些优化方法
矩阵乘法
我喜欢类似Floyd那样的更新,也就是(a ightarrow b) 乘上(b ightarrow c),可以更新(a ightarrow c)
不仅仅是线性齐次递推式可以更新,+和取max也是可以更新的
(a ightarrow c = max(a ightarrow c,a ightarrow b + b ightarrow c))
并且也满足结合律
1D/1D动态规划优化
单调性优化
(f(x) = min_{i = 1}^{x - 1} {f(i) + w[i,x]})
满足对于(x < y),最优的决策点(k(x) < k(y))
这里有一点要注意的!
就是如果对于(x),(i)是最优的,那么不代表(i + 1)转移到(y)比(i)转移到(y)更优
因为有可能是,优 非常差 最优
那么算法的流程其实是这样的,我们一开始只有(f(0))于是这些就设成
000000000000000
然后我们计算出(f(1))
会更改为
001111111111111
得到2可以改为
001111112222222
就是记录每个连续段,得到一个数二分这个数可以从哪个位置开始更新,如果可以覆盖掉整个连续段就跳过
单调队列优化
(f(x) = min_{k = b[x]}^{x - 1}{g(k)} + w[x])
容易发现我们要做的就是区间取最小值
单调队列很容易可以实现这一点
斜率优化
$f(x) = min_{i = 1}^{x - 1} {a[x]f(i) + b[x]g(i)} $
由于是二元组,我们把它画在平面直角坐标系上
设(f(i) = x),(g(i) = y),(f(x) = P)
P = ax + by
(y = -frac{a}{b}x + frac{1}{b} P)
很容易发现就是一条斜率固定的直线,从负无穷上移,第一个碰到的点就是答案
我们只要维护一个下凸壳就可以做到了
若斜率有单调变化,我们可以用第二种形式做到单调队列优化
如果没有则二分凸包
Day 9 6.15
众所周知sjq是个颓废的女孩子
所以她昨天颓废去了,今天(6.16)打算补两天
FFT
这个大家耳熟能详就是推不出来卷积式子
具体细节有点赘余(毕竟是复习),所以直接上个板子好了
for(int i = 1,j = L >> 1; i < L - 1 ; ++i) {
if(i < j) swap(p[i],p[j]);
int k = L >> 1;
while(j >= k) {
j -= k;
k >>= 1;
}
j += k;
}
for(int h = 2 ; h <= L ; h <<= 1) {
int wn = W[(MAXL + on * MAXL / h) % MAXL];
for(int k = 0 ; k < L ; k += h) {
int w = 1;
for(int j = k ; j < k + h / 2 ; ++j) {
int u = p[j],t = mul(w,p[j + h / 2]);
p[j] = inc(u,t);
p[j + h / 2] = inc(u,MOD - t);
w = mul(w,wn);
}
}
}
一开始的二进制翻转要设置成(i < L - 1)
然后(h)是步长,最底层是(2)
(k)是每一块的起点
(j)遍历这一块的前一半,和后一半的依次进行蝴蝶操作
如果是逆DFT,那么要除上模长
单位复根的本质是把一个单位圆(2^{k})等分,而复数相乘的结果是模长相乘,幅角相加
NTT
换成原根就好了,快乐
MTT不会,再见
FWT
tf表示正变换,utf是逆变换
异或卷积
(tf(A) = (A_{0} + A_{1},A_{0} - A_{1}))
(utf(A) = (frac{A_{0} + A_{1}}{2},frac{A_{0} - A{1}}{2}))
或卷积
(tf(A) = (A_{0},A_{0} + A_{1}))
(uft(A) = (A_{0},A_{1} - A_{0}))
注意对于或卷积通常有一类题需要在每一次卷积用$2^{n} $的复杂度还原出(2^{n} - 1)的值(就一个位置)
方法就是有奇数个0加上这个位置的值,偶数个0减去这个位置的值
与卷积
(tf(A) = (A_{0} + A_{1},A_{1}))
(utf(A) = (A_{0} - A_{1},A_{1}))
来一个FWT的板子(异或)
void FWT(int *a) {
for(int i = 1 ; i < (1 << N) ; i <<= 1) {
for(int j = 0 ; j < (1 << N) ; j += (i << 1)) {
for(int k = 0 ; k < i ; ++k) {
int t0 = a[j + k],t1 = a[j + k + i];
a[j + k] = inc(t0,t1);
a[j + k + i] = inc(t0,MOD - t1);
}
}
}
}
void IFWT(int *a) {
for(int i = 1 ; i < (1 << N) ; i <<= 1) {
for(int j = 0 ; j < (1 << N) ; j += (i << 1)) {
for(int k = 0 ; k < i ; ++k) {
int t0 = a[j + k],t1 = a[j + k + i];
a[j + k] = mul(inc(t0,t1),Inv2);
a[j + k + i] = mul(inc(t0,MOD - t1),Inv2);
}
}
}
}
FMT
来自VFK的论文。。
本质是或卷积,然鹅。。。代码短啊!!
本质上是求(f[S] = sum_{T in S} g[T])
用一个dp实现,我们按位考虑,从最低位开始数i - 1位已经考虑完了,我把第(i)位与它不同加进来
逆变换改成减掉就好了
void FMT(T *a,T ty) {
for(int i = 1 ; i < L ; i <<= 1) {
for(int j = 0 ; j < L ; ++j) {
if(j & i) {
a[j] = a[j] + ty * a[j ^ i];
}
}
}
}
然后是集合幂级数,我第一次做到这题还是WC2018(然鹅不会就是了
我们要求的是这个东西
(h[S] = sum_{T in S}g(T)f(S oplus T))
我会子集枚举!!!
但是我们要会一个(n^{2}2^{n})的算法才可以获得满分
设一个多项式(g(S)x^{|S|}),最后答案就是(h(S)x^{|S|})
具体是设一个二维的数组,(g[i][S]),然后进行(FMT),点值相乘的时候(n^2)枚举(i,j)用(g(S)f(S)x^{i}x^{j})更新(h(S)x^{i + j})最后再每一层都卷回去就好了
Day 10 6.16
众所周知sjq是个肥宅的女孩子
所以她补两天的感觉很累,打算写一些边角余料的知识点糊弄过去
整体二分
和cdq分治很像?但不是一个东西啦
Solve(int l,int r,int ql,int qr)
表示我二分的区间是(l,r),而询问的区间是(ql,qr)
复杂度?当然是(q log V)的,(V)是值域,因为每个询问往下走都走了(log V)层
虚树
(我应该放到图论那里的)
建树点的个数应该是2倍的询问点(上界)
建树方法是按dfn排序后,维护右链,求插入点和栈顶点的lca,如果栈顶深度大于插入点则不断弹出,在最后一个深度大于等于插入点的地方可能会被修改lca
之后如果lca不存在则插入lca
注意如果不需要树而需要虚树的边长和,直接把虚树按照dfn排序后,相邻两个点距离相加就是二倍的虚树边长和
void build_auxtree(int aN) {
num=aN;
sort(aux+1,aux+num+1,cmp);
stk[top=0]=0;
siji(i,1,aN) {
int u=aux[i];
if(!top) {
stk[++top]=u;
faAux[u]=0;
}
else {
int Lca=lca(stk[top],u);
while(dep[stk[top]]>dep[Lca]) {
if(dep[stk[top-1]]<=dep[Lca]) {
faAux[stk[top]]=Lca;
}
--top;
}
if(Lca!=stk[top]) {
faAux[Lca]=stk[top];
aux[++num]=Lca;
stk[++top]=Lca;
}
stk[++top]=u;
faAux[u]=Lca;
}
}
sort(aux+1,aux+num+1,cmp);
}
倍增求lca
(fa[i][j])表示第(i)个点往上走(2^{j})步是哪个点,若不存在则为0
把两个点调整到同一深度,然后同时往上跳
(可以同时维护链上最小值,很好用)
ST表求lca
维护一个序列,求dfn序的同时在遍历完每个儿子之后把父亲加进去,两点之间的lca就是两点之间的最小深度的点,可以做到(O(1))查询
Day11 6.17
今天开始数据结构吧
第一天就瞎写一些简单的
按照这个进度我觉得我要复习不完了(因为颓)
单调栈,单调队列
这个真的是数据结构么。。。
单调队列可以实现一个区间内的最大值从远到近排序(右端点递增,左端点递增)
而单调栈实现的差不多
单调队列像一个开口的单调栈
并查集
int getfa(int u) {return fa[u] == u ? u : fa[u] = getfa(fa[u]);}
可以带权维护很多东西,例如食物链那题
堆
priority_queue默认返回最大值
手写堆的方法是把新加入的数放在最后一个,然后如果根小于它,则交换它和根
树状数组
struct BIT {
int tr[MAXN],s;
int lowbit(int x) {return x & (-x);}
void insert(int x,int v) {
while(x <= s) {
tr[x] += v;
x += lowbit(x);
}
}
int query(int x) {
int res = 0;
while(x > 0) {
res += tr[x];
x -= lowbit(x);
}
return res;
}
};
可以维护一个前缀和,有了前缀和的同时可以查询区间和
单点修改
减掉原来的,直接插入
区间修改,单点查询
给一段区间([l,r])加(d),相当于在(l)处加上(d),在(r + 1)处减去(d)
jiukeyile然后查询一段前缀和即可获得单点的值
区间修改,区间查询
我们还是考虑区间加之后如何得到前缀和,前缀和相减就是区间和
还是把加改成差分,一个(p)加上(d)的形式,给前(r)个数带来的贡献是
((r - p + 1) * d)
我们可以用两个树状数组,一个维护一个前缀里加上过的差分的和,一个维护((p - 1) * d)的和,就可以了
二维树状数组
while写两层即可实现,可获得一个([1,x])和([1,y])的矩形内的值的前缀和
前缀最大值
更新log个位置的最大值即可,用它只是因为太好写了,写个线段树不愉快
hash表
就是类似边表一样,算完hash之后再给这个值取模一个较小的数扔进边表里,扔的是这个数的取模前的值
Day12 6.18
线段树
这个太普遍了,而且有很多变种,有你从未体验过的全新玩法
通常可以优化一类dp,例如把一段数分成两个集合(dp[i][j])表示第一个集合的最后一个位置,和第二个集合的最后一个位置
不说了,这个会写就很简单,不会写的题死也想不到这么维护的
李超线段树
平面上有很多条直线,然后询问一个位置的最小值
我们每个区间维护一个优势线段,就是这个区间内暴露最多的线段
如果新加入的线段两端都大于优势线段,就不要
否则会有一个交点,我们取较长的那个作为优势线段,然后另一个额外处理
这样一次是(O(log n))的
平衡树
可以维护一个序列,并且可以取出一段序列,实现区间翻转移动之类的操作
我一般写无旋Treap
树链剖分
- fa[] 表示父亲
- son[]表示子树大小最大的那个儿子
- siz[]表示子树大小
- top[]表示链顶
- dfn[]表示dfs序,idx用来增加dfs序
- dep[]表示深度
每次往深度大的链顶跳
长链剖分
选择深度大的一个儿子进行剖分
可以在有关深度的复杂度做到线性
Day13 6.19
今天打包字符串类数据结构(和一些算法)
kmp
求一个(nxt)数组,(nxt[i])表示([1,nxt[i]])的字符串和([i - nxt[i] + 1,i])的字符串相等
求法是找到从(nxt[i - 1])开始跳(nxt)链,如果链上某一个位置(p)使得(s[p + 1] = s[i])则(nxt[i] = p + 1),否则(nxt[i])就是0
manacher
这个用来求最长回文子串
复杂度$ O(n)$
我们为了方便,可以给每个字符中间插入一个特殊字符,来同时考虑奇数回文串,和偶数回文串
设(r[i])为(i)向两边扩展的最大长度使得([i - r[i] + 1,i + r[i] - 1])是一个回文串
我们记录一个扩展到最远的位置(mx),和这个位置的中心(p)
当我们遇到下一个位置(i),可以用(min(mx - i + 1,r[2 * p - i]))来作为初始的(r[i])更新
这样每次(mx)不降,是(O(n))的
扩展kmp
这个用来求每个位置和1的最长公共前缀
相当于砍了一半的manacher,我们也记录扩展到最远的位置(mx)和一个开头(p)
新的位置(i)用(min(mx - i + 1,r[i - p]))作为扩展的位置
最小表示法
求循环串的最小表示法
用两个指针(i,j)和这两个开头能匹配的长度(len)
若(i)失配了,则([i,i + len])都是不合法的位置
(j)失配同理
如果(i,j)相等则把其中一个+1
如果有一个大于字符串长度则退出
Lyndon分解
一个字符串是Lyndon word当它是自己所有后缀中的最小值
如果字符串(s)和字符(c)组成(sc)是一个Lyndon Word的前缀,那么(d > c),则(sd)是个Lyndon Word
维护上一个未分解的位置是(last),看last开头的下一个Lyndon 串是否被循环构造
int s = 1,t = 2,last = 1;
while(last <= LEN) {
s = last;t = s + 1;
while(1) {
if(str[s] == str[t]) ++s,++t;
else if(str[s] < str[t]) s = last,++t;
else {
do {
last += t - s;
out(last - 1);putchar(' ');
}while(last <= s);
break;
}
}
}
7.9 upd
如何快速求出每个后缀的Lyndon分解
首先需要把后缀排序(当然更方便的是直接二分哈希求lcp,我就是这么写的,会多一个log,冬令营的课件上似乎有(O(n))做法,不太懂)
我们用一个栈,求出后缀([i + 1,N])的lyndon分解后,我们新加一个字符(s[i]),如果这个串比前一个lyndon串小,就把这个串和后一个串合并起来
因为若(u,v)都是lyndon串,(u < v),则(uv)也是lyndon串,因为v严格小于自己所有后缀,(u < v),u也会小于新加进来的所有后缀
7.9 updend
trie树
每个点连出的边两两不同,边权是字符
AC自动机
为每个trie树上的节点找一条前缀节点表示若在此处失配能同时匹配的最大的深度到哪,显然这个节点是唯一的
新建一个0号点往根连字符集的边
这个节点就是父亲节点的前缀节点的该边,若不存在则继续跳前缀节点,类似kmp
或者一开始就补齐所有边
AC自动机的每个节点也可以唯一的表示多模式串匹配中的状态
后缀自动机
每个节点维护
- len代表维护的字符串是根节点到这个节点路径中长度为len的一条
- nxt表示出边
- cnt 表示这个点被统计次数
- par表示以这个点为结束点的后缀中最长的在之前出现过的字符串的代表节点
last表示当前最后一个节点
每次新加一个节点,则在last链中没有该字符的节点上连一条边
若last遍历到空,则当前节点的par是根
若没有,则看当前指针p该字符的出边指向节点q是否是len[q] = len[p] + 1
否则复制一个q,长度设置为len[p + 1],cnt设乘0,其余不变,q与当前节点的par都设置成复制的节点
把last设置为当前节点
后缀树
反串的后缀自动机的par树等于正串的后缀树
7.12 upd
回文树
建上两个根,一个表示偶数点回文串的根,一个表示奇数点回文串的根,偶数点回文串的根的父亲节点是奇数点回文串的根
然后我们初始在偶数点回文串的根,如果失配则不断跳父亲
如果存在一个回文串,即当前节点维护的回文串长度为len,当新加一个节点后,存在(s[i - len - 1] == s[i]),那么就可以从当前节点延伸出去一条值为(s[i])的边
那么如何更新父亲节点
同样的类似AC自动机,父亲节点的后缀节点的该边,我们也不断的跳父亲链直到找到一个使得又一个(s[i - len - 1] == s[i])的地方,如果父亲是奇数根则默认这个点的父亲是偶数根
可以在前面插入,也可以在后面插入,是类似的,具体可以参考
而回文树的fail树,从底到根表示以这个位置结尾的回文串各是什么
板子:APIO2014回文串
CF上有一个技巧
感觉这个技巧就是应用了以一个位置i为结尾的回文串,构成的回文串长度形成了(log n)个等差数列
然后一个等差数列,若首项为(a),公差是(d),则可以转移的位置是(i - a - d),(i - a - 2d),(i - a - 3d)...这一连串的位置,然而我们可以从(i - d)的位置维护一个(series-ans),这个位置的(i - a - d)就是(i)的(i - a - 2d)
关于为什么不是(i - a)?因为不断向上找最后找到的一定是偶数根或者奇数根,没有长度,对统计没有意义,所以最后所有位置还是会被考虑到
7.12 upd end
Day14 6.20
我jio得要复习不完了(还有十天……我数论还没开)
主席树(可持久化线段树)
一个是新树的根,一个是旧树的根,把新树的根分配一个节点,然后复制的和旧树一模一样,再进行修改
将修改递归到左儿子或右儿子继续操作
这是单点修改
对于主席树的区间修改来说,要用树状数组套主席树,每次修改log个树根,查询的时候也是
可持久化平衡树
Spilt的时候每次新建一个节点,Merge的时候也是
炸内存愉快
可持久化Trie
每次也是新建一个节点,把之前节点的信息都复制过来
左偏树
可并堆,具有堆的性质(根的权值大于两个儿子)
每个节点额外维护一个(dis)表示到最远的左儿子的距离
每次优先和右儿子合并
如果右儿子的dis大于左儿子的dis就交换
K-D树
维护一个平面上的点集
每次两维(或更多维)交替着来,每次按照需要划分的那一维排序,然后中位数的点作为这个节点维护的点,之后分到两边处理
可以维护很多东西,通常是维护这个区间点集横坐标和纵坐标的范围,便于剪枝
正交范围复杂度是(O(n^{frac{k - 1}{k}})),通常(k = 2),复杂度$O(sqrt{n}) $
LCT
LCT里分实边和虚边,每个节点往下只连一条实边,其余都是虚边
实边把树分成了好几条链,我们把这些链当做一个平衡树维护,而这个平衡树的顶的父亲就是虚边连接的父亲(实际上,在树中这个父亲是这个平衡树最左节点的父亲,而非根的父亲)
用Splay维护每条树链,用Splay实现一个把每个点转到树根的操作
Access(x)
实现的功能是构造一条x到根的路径
1.如果x有实边,断掉,方法是把x转到根,然后断掉右儿子
2.找到路径的父亲(实际上是Splay中最左节点的父亲),把父亲转到根,断掉右儿子,换成x所在splay的根(也就是切换偏爱边)
3.平衡树的根不是空节点,说明没有连到根,继续
Findroot(x)
找到x所在树的根
先Access(x),然后找到最左节点
MakeRoot(x)
先Access(x),然后该平衡树翻转
Link(x,y)
把x变成根,然后加一条虚父亲到y
Cut(x,y)
把x变成根,然后Access(y),把y旋到根,此时x是y的左儿子,断掉即可(同时修改y的左儿子和x的父亲
Select(x,y)
把x变成根之后Aceess(y)即可得到一棵平衡树
一般有些题不是特别需要MakeRoot,可以简化一下代码
如果需要选两个点之间的链就拆成两段做就好了
杨表
从上到下每一行长度递减,如果是递增的样表,则每个数要大于它上面的值和左边的值
维护方法比较暴力,就是每一行二分一个插入位置,如果是空的就插入,如果有值,先插入,再把这个值扔到下一行去继续插入
有个结论是把排序方法反过来可以得到样表的转置
还有一个公式是钩长定理
有n个数的杨氏矩阵的个数为(固定了形状)
设每个格子钩子长度为(h(x,y)),钩子长度是每个点右边的格子数+下边个格子数+1
答案是(frac{n!}{prod h(x,y)})
还有半标准杨表(行单调不增,列单调减)
(prod_{h(i,j)} frac{r + j - i}{h(i,j)})
其中(r)是值域的大小
Day15 6.21
埃氏筛
复杂度(n loglog n)
每遇到一个质数,就用这个质数的倍数去更新区间里所有的数
对于需要筛区间里的质数且区间长度较小而左右端点较大时就只能用埃氏筛而不是欧拉筛
例如
(10^9 leq l leq r,r - l leq 10^{7})
欧拉筛
对于每个数(i),枚举质数(p),把(ip)标记为合数,如果(p | i)则退出
这样每个合数只在最小的质数那里被筛了一次,是(O(n))的
可以用来预处理积性函数,互质的情况显然乘起来就好,不互质考虑这个质数带来的贡献即可
扩展欧拉定理
若(a,p)互质,且(p)是质数可以得到(a^{p - 1} equiv 1 pmod p)
若(a,m)互质,(m)不一定是质数,可以得到(a^{varphi(m)} equiv 1 pmod m)
若(a,m)不互质,若(c > varphi(m)),可以得到(a^{c} = a^{c \% varphi(m) + varphi(m)} pmod m)
扩展gcd
求(ax +by = g)的一组解
首先我们可以转化成求(frac{a}{g}x + frac{b}{g}y = g)于是我们只剩下(ax +by = 1)的方程要解了
考虑gcd的求法
若存在一组(ax + by = 1)
上一次递归中求出了(bx' + a \% by' = 1)
可以得到
于是(x = y'),(y = x' - lfloor frac{a}{b} floor y')
可以用来求逆元,求线性同余方程
中国剩余定理
如果有一组方程
(x equiv a_{1} pmod {m_{1}})
(x equiv a_{2} pmod{m_{2}})
(cdots)
(x equiv a_{n}pmod {m_{3}})
其中(m_{1}m_{2}...m_{n})两两互质
我们设(M = m_1m_{2}cdots m_{n})
设(M_{i} = frac{M}{m_{i}})
求出(M_{i}t_{i} equiv 1 pmod {m_{i}})
(x = a_{1}t_{1}M_{1} + a_{2}t_{2}M_{2} + cdots + a_{n}t_{n}M_{n})
在([1,M - 1])内有且只有一个解,剩下的每个解都要加上M
线性同余方程
(ax equiv b pmod m)
可以这么认为
(ax = b + mk),k是任意整数
这个方程有解的条件是(gcd(a,m) | b)
于是可以把两边同时除以(gcd(a,m))是等价的
(frac{a}{g}x = frac{b}{g} + frac{m}{g}k)
然后把这个当成新的(a,b,m),这个时候认为(a,m)互质,求出(a)在(m)意义下的逆元,
于是(x equiv frac{b}{a} pmod m)
于是我们可以认为所有的形式都是
(x equiv a pmod {m})
然后怎么求这个呢
假如
(x equiv a_{1} pmod {m_{1} })
(x equiv a_{2} pmod{m_{2} })
(x = a_{1} + km_{1})
(x = a_{2} - hm_{2})
(a_{1} + km_{1} = a_{2} - hm_{2})
(km_{1} + hm_{2} = a_{2} - a_{1})
为了解这个方程方便,我们还是可以两边都除上(gcd(m_{1},m_{2}))
然后我们解得了一个(k),于是新的(x equiv a_{1} + km_{1} pmod {lcm(m_{1},m_{2})})
于是这么两两合并下去即可
Day16 6.22
由于sjq过于颓,她把该一天写完的分成两天写完,并且理所当然的认为复习不完就复习不完,国赛打铁就打铁吧
高次不定方程
(A^{x} equiv B pmod C)
BSGS求解
基础版(A,C)互质
我们把(A^{0})到(A^{varphi(C) - 1})分成(m = sqrt{C})个一组
对于第一组,暴力处理出组内(A^{y}B)的值,存在一个哈希表里
剩下组中的值相当于(A^{im - y} equiv B pmod C)
我们把(A^y)移过去,就是(A^{im} equiv BA^{y} pmod C),于是每组可以只询问一个(A^{im})看是否存在
非基础版,(A,C)不互质
(A^{x} equiv Bpmod C)
(A^{x} + Ck = B)
我们每次除掉一个gcd(A,C),若(B)不能整除这个gcd,则无解
(frac{A}{d_{1}} A^{x - 1} + frac{C}{d_{1}} k = frac{B}{d_{1}})
这个时候(frac{C}{d_{1}})可能还和(A)不互质,我们就一直除下去
直到互质为止
设(C_{n} = frac{C}{d_{1}d_{2}cdots d_{n}})
(B_{n} = frac{C}{d_{1}d_{2}cdots d_{n}})
(D = frac{A}{d_{1}d_{2}cdots d_{n}})
得到了
(A^{x - n} equiv frac{B}{D} pmod C)
于是我们又回到了刚刚那个问题
原根
每个质数内可以找到一个数(g)
使得(g^{1})到(g^{p - 1})构成了([1,P - 1])里的每个数
这个原根通常很小,可以暴力求,求得方法就是判每个(a|P - 1),a不为1
(g^{frac{P - 1}{a}})是否等于1,若等于1则g不是原根
N次剩余
(x^{K} equiv apmod P)
为了简单一些……只讨论质数……非质数的实在过于难写
可以参考这里
找到(g^{t} = a),这个用BSGS解
设(x = g^{i})
求一个指标可以得到
(g^{iK} equiv g^{t} pmod P)
(iK equiv t pmod {P - 1})
这个只需要解一个同余方程就好了
Day17 6.23
可能是一些线代相关?(得了吧你会线代吗)
矩阵乘法之前好像说了……
高斯消元
有一组方程有(n)个变量,和(n)个方程
我们在第(i)次循环中,排名在(i)以后且(x_{i})系数最大的方程,使其变成第(i)个方程
然后用这个方程消掉排名在(i + 1)及以后的方程第(i)项的系数
如果某一次发现(x_{i})系数最大就是0,那么这个位置可以任意取值,题目里可能会有一些无解之类的可能是这种情况
行列式
行列式是一个排列(p_{1},p_{2}...p_{n}),求逆序对数为(r(P))
求(sum (-1)^{r(p_{1},p_{2},p_{3}...p_{n})}a_{1,p_{1}}a_{2,p_{2}}...a_{n,p_n})
求行列式的一般方法是把矩阵通过基础行变换或者列变换把矩阵消成一个上三角矩阵,然后对角线上的值相乘即可
具体的变换是
1.行列交换,行列式不变 就是(a_{i,j})变成(a_{j,i})(但是一般用不上这个)
2.行列式一行的因子可以提出 就是一行都除(k),求完这个行列式后再乘上(k)
3.两行互换,行列式反号
4.将一行的倍数加到另一行上,行列式不变
好像写的话只需要34就够了
矩阵树定理
入度矩阵对应的外向树,出度矩阵对应着内向树(都是指向父亲的边的事是出度或者入度)无根树就是两条有向边都加上
有向树必须删掉根所在的那一行和一列,无根树可以任意
然后对于这(n - 1)阶的矩阵求一个行列式就行了,也叫主子式
矩阵求逆
建单位矩阵,和要求逆的矩阵同时进行求行列式用到的变换
注意要么只换两行要么只换两列,不能都换(不过一般你都是在换行……不会换列……)
目标是把要求逆的矩阵消成单位矩阵,此时的单位矩阵就是逆矩阵
线性基
就是每加入一个数,从最高位到最低位遍历,如果有这一位就和这个数异或一下消掉
一个(k)个数组成的线性基可以构成(2^{k})个不同的数([0,2^{k} - 1])
带修改的线性基可以通过额外维护一个二进制数表示这个数是由哪些数异或而成的
如果要修改第(k)个,且有包含第(k)个的数值为0,则可以用这个0和所有包含第k个数的值异或一下,再将修改后的值尝试插入线性基
若包含第k个的数值为线性基中的数,则选择最小的那个数,和前面的更新,把这个数删除,然后尝试插入线性基
Day18 6.24
天哪居然只剩7天了,真的复习不完了
今天是数论函数变换,由于我很喜欢这个东西,于是我决定去51nod刷点数论函数题再回来写(然鹅窝根本做不出来那些题= =
莫比乌斯反演
(g(n) = sum_{d|n}f(d))
(f(n) = sum_{d | n}mu(frac{n}{d})g(d))
(mu)函数大家很熟悉不再赘述
这个虽然可以暴力推式子证明,但是有比较好的思路就是容斥
(g)是什么,(g)是在质因数分解的意义下的一个高维前缀和
那么,我们把这个高维前缀和尝试推掉其中的几维,也就是这一维下标-1,如果推掉了奇数维就是+1,偶数维就是-1
这也就是(mu(d))的意义了,(d)是一定是若干个质数相乘
还有另一个形式!
(g(n) = sum_{n | d} f(d))
(f(n) = sum_{n | d} mu(frac{d}{n})g(d))
就是后缀和和前缀和的区别了,差不多的。。。
对于(g(n) = sum_{d | n}f(d))写过一个很蠢的(O(n log log n))求法,实际上很简单。。但是装模作样的写了一堆
积性函数
- 积性函数 对于两个互质的数(a,b),满足(f(ab) = f(a)f(b))
- 完全积性函数 对于所有正整数(a,b),满足(f(ab) = f(a)f(b))
积性函数均有(f(1) = 1)
可以通过讨论质数的指数幂应该的函数值从而得到整个函数值
狄利克雷卷积
(h(n) = sum_{d | n}f(frac{n}{d}) g(d))
则(h = f * g)
有交换律,结合律,分配律,还有单位1
还有(f,g)如果都是积性函数,则卷积后的(n)也是矩形函数,这个比较常见……
(varepsilon(u) = [u == 1])就是单位1
然而数论题呢~其实没怎么用到莫比乌斯反演,主要还是……用到了一些常用的卷积,当然如果你细心的话可以发现这些卷积可以通过莫比乌斯反演推导而来,然而这里直接写了
(d(n)=sum_{d|n}1)约数个数和
(sigma(n) = sum_{d|n} d)也就是(d * 1)
(n = sum_{d|n} varphi(d)) 这个的意义是(n/d)乘上和(d)互质的数,这样就计算了和每个数和(n)gcd为(n / d)的方案数
这个时候反演一下可以得到
(varphi(n) = sum_{d | n}frac{n}{d}mu(d))
(varepsilon(n)= sum_{d|n} mu(d))这是最常用的一个!因为它可以把一个判别式转化成代数式!很神奇!
容易忘记的技巧
感觉技巧挺多的(然而我做题少QAQ)
(i)以内和(i)互质的数的的和是(frac{i imes varphi(i)}{2}),1除外,1的话就是1
n以内无平方因子数可以通过(sum_{i = 1}^{sqrt{N}} mu(i) lfloor frac{N}{i^{2}} floor)
杜教筛
假如我们有一个积性函数要求和
不会
假如我们有个积性函数,在和一个已知函数卷积的时候,卷出来的函数可以(O(1))求和
嗯??????感觉事情变奇妙了
那么我们以最普通可爱的(mu)来举例吧
(varepsilon(n) = sum_{d | n} mu(d))
那么我们尝试列一个式子
然后把这个式子改为枚举一个(t),再枚举(i),求的是(it)这个倍数在(n)以内的这样的(mu(i))
然后就变成了
咦……如果设(M(n) = sum_{i = 1}^{n} mu(i))
后面的就是
那么,只有(t = 1)的时候,才是我们要求(M(n))
这样的话,我们再转换成
这样的话,我们只要处理对后面的部分递归下去就好了,通常为了快一点我们会使用哈希,并且预处理出1e7以内的前缀和
好像还有高阶小量啥的,不太懂,反正复杂度是(O(n^{frac{2}{3}}))的
通常还可以用到的卷积有
(n = sum_{d | n}varphi(d))可以用来求(varphi(n))函数的前缀和
还有……
(n^{2} = sum_{d|n}dvarphi(d) frac{n}{d})
可以用来求(dvarphi(d))的前缀和……
但是这个限制很大,只有有一个好求的卷积后的函数才可以快速出解……
min25筛
留个坑!一定补!我去翻zsy博客了!qwq
天哪我居然马上要去国赛了还要学新知识点,感觉要废
Day19 6.25
Day20 6.26
二项式反演
精妙无比但是根本用不上的原式
不精妙但是非常实用的另一种式子
不精妙但是非常实用的另一种式子2
这两种式子分别代表什么呢
第一种是至多和恰好的转化
设(g(n))表示恰好有(n)个元素合法,(f(n))表示至多有(n)种元素合法,则至多的每一种方案是从恰好(i)个中转移过来的
举个栗子
有k个方格,染上恰好n种颜色,每种颜色必须出现
这个时候,至多有(a)种颜色很好算,就是(f(a) = a^{k})
这样直接套进这个式子就可以算出来恰好了!
为了方便书写,我把它叫至多,事实上你可以认为它叫“钦定”,或者“硬点”
因为真正的“至多”,就是(f(n) - f(n - 1)),这里显然不是这样
什么?为什么不是这样
例如“钦定”填3种颜色,和“钦定”填2种颜色,填3个格子各有什么区别
第一种是
1 1 1 & 1 1 2 & 1 2 1 & 1 2 2
2 1 1 & 2 1 2 & 2 2 1 & 2 2 2
1 1 3 & 1 2 3 & 2 1 3 & 2 2 3
1 3 1 & 1 3 2 & 2 3 1 & 2 3 2
3 1 1 & 3 1 2 & 3 2 1 & 3 2 2
3 3 1 & 3 3 2
3 1 3 & 3 2 3
1 3 3 & 2 3 3
3 3 3
第二种是
1 1 1 & 1 1 2 & 1 2 1 & 1 2 2
2 1 1 & 2 1 2 & 2 2 1 & 2 2 2
显然,三种颜色填三个格子方案数是(3! = 6)
然而(27 - 8 imes inom{3}{2} = 3)
少到哪里了?
就少在,3个格子只出现了一种数,被这两种情况减了两遍
第二种是至少和恰好的转化
你肯定会熟悉(至少0个 - 至少1个 + 至少2个....)的公式,请带入(k = 0),你就会明白,它没有组合数是因为(inom{i}{0} = 1),于是,大家在做至少(k)个(k)不为0的时候,记得追根溯源,把无法省略的组合数补上
设(g(n))是恰好(n)个的方案数,(f(n))是至少(n)个的方案数
在这里顺便介绍一种常见的容斥,含有上界的计数,即对于(n)个数,只能选([0,a])之间,上界给定是(S),求方案数
方法就是枚举至少有几个数不合法,然后剩下的数随便分配
当然,这里也不要当成真正的“至少”,你也可以叫“钦定”,或者硬点
第一类斯特林数
第一类斯特林数的组合意义是把(n)个元素放在(m)个环里
递推方法是
组合意义就是,要么这个数新建一个环,要么插在任意一个数的前面
一点小性质
因为斯特林数本质是环排列,可以和置换对应
(翻了翻yyb的博客抄来了一点别的……事实上第一类我跟本不熟……)
其中(x^{underline{j}})认为是(frac{x!}{(x - j)!}),不过我们要从(x)减下去那么乘,有些时候没有逆元
证明(也是抄的)
利用数学归纳法(n = 0)时
然后
还有一个式子
可以类似的证明
第一类斯特林数的生成函数
从递推式考虑,要么是(m - 1)变成(m),会有一个(x),要么是从(n - 1) 变成(n)会乘上一个(n - 1),从第一行往下推即可得到这个式子
于是我们就可以分治FFT求第一类斯特林数了!
(抄来的一点倍增方法)
(F_{n}(x) = prod_{i = 0}^{n - 1}(x + i)),有(F_{2n}(x) = F_{n}(x)F_{n}(x + n))
考虑用(F_{n}(x))去求(F_{n}(x + n))
第二类斯特林数
第二类斯特林数的组合意义是把(n)个不同的数放到(m)个相同的盒子里,且每个盒子必须有数
第二类斯特林数可以转化成乘方
表示选了至多(i)个盒子放东西
这样的话我们反演一下可以得到
第一个式子也是FFT预处理第二类斯特林数的方法
由于斯特林数在(n < m)的时候答案为0,所以可以第二个式子的上界也可以是n,在指数远小于底数的时候作用很大
自然数幂和也可以用有关第二类斯特林数的一个公式
(斯特林反演不会呀qwq)
Day21 6.27
今天是一堆博弈论吧。。。(可能是开天辟地级别的……因为博弈论除了sg我啥也不会……)
(yyb的博客抄的真开心)
必败态和必胜态
必败态转移一定会到必胜态,无法转移到另一个必败态
必胜态转移会到至少一个必败态
AtCoder上有一类题无需过多的博弈论知识,只需要找到必胜态必败态,非常锻炼脑洞能力
找的方法就是按照上面两种方法分析,通常需要分析游戏结束时的状态的性质
模型
巴什博弈(Bash Game)
取一堆石子有n个,每次至少取1个,最多取(m)个
分析
当((m + 1) | n)先手必败,否则先手必胜
就是如果((m + 1) | n)的时候先手取什么,后手都可以跟一步使得总和是(m + 1)的倍数,后手必然胜利
否则先手取(n \% (m + 1)),转化到必败态
尼姆游戏(Nim Game)
有(n)堆石子,每堆石子有(a_{i})个,每次可以选择一堆取任意多个,不能取的人失败
分析
所有石子异或的总和如果为0就是先手必败,否则先手必胜
证明就是存在一堆石子含有异或和(x)的最高位,这个数(k)和(x)异或后一定小于(k),我们取那么多石子出来,石子的异或和就重新变为0
变形
- Bash + Nim 每堆取模((m + 1))后再做(nim)
- 每次可以取k堆石子,转化乘(k)进制下不进位加法,如果不等于0则先手胜
- 阶梯博弈 每次可以将一堆石子移动到下一阶,石子在0阶不可移动,无法操作者输
- 由于0阶必败,如果对手移动了一个偶数阶梯上的石子,则顺势把这堆石子移到下一个偶数阶梯上,游戏不变,如果对手移动了一个奇数阶梯上的石子,就相当于石子没了,所以我们只要统计奇数阶梯上的石子来做一个普通的nim游戏
SG函数
对于一个游戏的必败态,(SG(X) = 0),否则(SG(X))是当前游戏可以到达的局面中未出现过的(SG)值的最小值
当所有游戏异或起来为0的时候先手必败,否则先手必胜,证明类似nim游戏
Anti-SG游戏
有n堆石子,每次每人必须取走大于等于1个石子,拿走最后一个石子的人输
分析
SJ定理
当所有单一游戏的SG值为0时游戏结束,先手必胜的条件是
游戏的SG值不为0,存在一个单一游戏的SG值>1
游戏的SG值为0,不存在一个单一游戏的SG值>1
Multi-SG游戏
也是普通的nim游戏,但是加上了可以把一堆石子分成两堆,不能操作者输
分析
对于分成两堆的游戏打表后可以发现是
0 1 2 4 3 5 6 8 7 9 10 12 11
就是除了0以外每四个划分一下,然后第三个和第四个交换位置
Every-SG游戏
对于每个没有结束的任何一个单一游戏,操作者必须进行一步操作,无法操作者输
分析
看似和每个游戏的胜负没有关系了,然而如果先手胜的话,那么走的次数一定是奇数次,证明这个游戏会让后手无法操作,所以能赢的尽量赢,效果不会更差
然而对方也是这么想的,于是我们想让对方能赢的局面快点结束
游戏实际上是两两独立的,分开考虑
如果对于一个游戏,我在自己必胜态,我肯定希望这个游戏尽可能的慢点结束,如果我在必败态,我想让它尽快结束
于是我在必胜态选择距离终点最远的一个必败态走,在必败态选择一个距离终点最近的必胜态走
这样我一个状态走到终点的步数通过类似这样的min-max搜索实际上是确定的
而对于一个必败态,走的步数是偶数,对于必胜态,走的步数是奇数,对于这样的游戏,必胜的条件就是所有点走到终点步数的最大值是一个奇数
翻硬币游戏
不是裸题……但是差不多的样子?
每次可以翻转一些连续的硬币或者其他的什么约束,但是要求最右边的硬币必须正面朝上,无法进行者输
分析
结论是所有正面朝上的硬币单一存在时的sg函数的异或和
树的删边游戏
轮流删边,每次保留和根节点连通的块,不能删者输
分析
结论是一个树的sg值是所有儿子的sg值+1后的异或和
具体证明可以类似数学归纳法那么证
证明,把每个儿子当成一棵树加一条边,设新加的边为(u,v),(u)是(v)的祖先,若断掉新加的边,则sg值是0
否则断(v)子树中的边,sg函数值为([0,sg[v] - 1]),从小到大取,使得(v)为根游戏状态为0的那条边,这条边断了之后,由于子游戏中存在一个状态是断掉新边游戏状态是0,则这个状态给(u)为根的贡献是(1)
使得(v)为根贡献为1的断边状态,子游戏中断边为0的状态可以转化为1,再加上断掉新边的状态是0,则这个对(u)的贡献变成了2
以此类推,这种情况游戏状态就是(sg[v] + 1)
总而言之
博弈论这方面,自己推结论是不可能呢,这辈子 都不可能自己推结论的
数学数学又不好,又找不到必胜必败态……
一打表就可开心了,打表不用动脑子,找规律比用脑子硬想感觉好多了,本来就没有智商,还怎么做题呢,也只有打打表找找规律,才能勉强维持的了生活的样子。
Day22 6.29
Lucas定理
(inom{n}{m} = inom{n \% p}{m \% p}inom{n / p}{m / p}),其中(p)是质数
看到这个可以考虑数位dp,因为都是n和m在p进制下分解后按位求组合数
凸优化
如果随着选的个数(m)递增,答案构成了一个凸函数
而m是固定的,这个时候我们二分最后一次的斜率,选一个数就减少q,如果这个时候最大值恰好是选了m个则这个斜率就是答案
如果选了大于M个,则斜率需要缩小,否则需要增大
拉格朗日插值
给出平面上(n)个点值((x_{i},y_{i})),找到一个(n - 1)次多项式使得满足这个多项式过这n个点
本质是一种构造
我们需要拉格朗日基本多项式
多项式(xi_{i}(x))在(x = x_{i})的时候值为(1),在(x = x_{j}),(j eq i)的时候是0
然后构造多项式
(f(x) = sum_{i = 1} ^{n} y_{i}xi_{i}(x))
在横坐标连续的情况下,通过(O(n))计算出一个点的值
如果要求出这个多项式,可以通过(O(1))求出下半部,(O(n^{2}log n))的复杂度求出上半部分
具体就是(Solve(l,r))计算一个多项式表示没有乘上(i in [l,r])的((x - x_{i}))的多项式是什么
然后分治下去
7.12 upd
分拆数
题意很简单,就是求(n)拆分成若干个正整数的和有几种方案
一个简单的背包dp是(O(n^2))的
那么还有一个(n log n)的多项式求逆做法(只写做法,不写证明)
利用五边形数可以得到
设(n)的分拆数是(P_{n})
五边形数对于一个相同的(k)有两个,分别是(frac{k(3k - 1)}{2})和(frac{k(3k + 1)}{2}),前面的系数是((-1)^{k})
(P_{n} - P_{n - 1} - P_{n - 2} + P_{n - 5} + P_{n - 7}.... = 0)
然后写成生成函数
(P(x) - xP(x) - x^{2}P(x) + x^{5}P(x) +x^{7}P(x)... = x^{0})
然后两边都除上一个(P(x))
得到
(1 - x - x ^{2} + x^{5} + x^{7}... = frac{1}{P(x)})
于是就变成了一个多项式求逆了
7.12 upd end
Day23 6.29
群论
置换类似矩阵,也是可以快速幂的
burnside引理
对于一个置换f,若一个染色方案(s)经过置换后不变,则(s)是(f)的不动点,设(f)的不动点是(C(f)),置换总数为(s),则答案是(frac{C(f)}{s})
Polya定理
不动点的颜色就是一个置换中染的颜色相同的方案数
如果有(m)种颜色,有(k)个圈,则一个置换的不动点是(m^{k})
特征方程
如果有一个线性齐次递推式(通常不会太长)
拿斐波那契数列举例吧
(F_{i} = F_{i - 1} + F_{i - 2})
列一个方程
(x^{2} = x + 1)
然后解出来两个根是
(frac{1 + sqrt{5}}{2})和(frac{1 - sqrt{5}}{2})
然后代入(F_{1} = 1),(F_2 = 1)可以得到
(A = frac{sqrt{5}}{5},B = -frac{sqrt{5}}{5})
于是通项就变成了
(f_{n} = frac{sqrt{5}}{5}[(frac{1 + sqrt{5}}{2})^{n} - (frac{1 - sqrt{5}}{2})^n])
Berlekamp-Massey算法
求一个数列的最短线性递推式
当然可能一不小心就求出递推式了,然后就愉快矩阵乘法orn^2多项式取模orO(nlogn)多项式取模了。。。
还是数列1 2 4 9 20 40 90吧
我们一开始递推序列是一个空序列
记为$R_{0} = {} $,序列的标号到(0)
delta是利用真实值和递推数列算出来的值的差值
(i = 1)时,delta = 1,(R_{0}) 在 (1) 的时候出错,所以 (fail[0] = 1) ,由于 (a_{1})是一个非0元素,我们就填上一个0到序列里,所以(R_{1} = {0})
(i = 2)时,delta = 2,于是我们找到(cnt - 1)也就是(R_0)第一个失败的地方,1,我们想利用这个1构造一个递推数列使得前(i - 1)个数都是0,而第(i)个数是1,然后(R_{1})加上delta倍的这个递推数列就可以了
具体来说这次操作可以归纳成这样,若(R_{cnt})到了(i),则记录一个(fail[cnt] = i)不合法,然后找到(fail[cnt - 1])的位置,构造一个新的递推序列,把前(i - fail[cnt - 1] - 1)个位置都填成0,然后后面加上(1),再加上取反的(-R_{cnt- 1})这个数列,这样可以使得第(i)个位置有值是(delta_{fail[cnt - 1]}),剩下都是0,这个时候就是再乘上一个常数可以得到我们需要的那个序列
(也相当于我把(R_{cnt - 1})的答案通过加0平移了一下……)
接着上面的例子说,这个时候(fail[1] = 2),然后构造递推数列加了0个0,然后加上一个1,再加上取反的(-R_{0}),再乘上常数,可以得到(R_{2} = {2})
(i = 3),递推数列成立,继续
(i = 4),(fail[2] = 4),(delta = 1),然后构造的数列是({0,1,0}),乘上的常数是(frac{1}{2}),于是我们可以得到新的数列是(R_{3} = {2,frac{1}{2},0})
(i = 5),仍然成立
(i = 6),(delta = -frac{9}{2}),(fail[3] = 6),构造的数列是({0,1,-2 }),乘上的常数是(-frac{9}{2})
然后可以得到(R_{4} = {2,-4,9})
(i = 7),(delta = 9),(fail[4] = 7),构造的数列是({1,-2,-frac{1}{2},0}),乘上的常数是(-2)
然后得到数列(R_{5} = {0,0,10,0})
Day 24 6.30
又是今天补昨天的,不过是最后一天了
Miller_Rabin
根据质数的两个必要条件
$a^{p - 1} equiv 1 pmod p $
(a^{2} equiv 1 pmod p),(a = 1)或(a = -1)
这两个条件是必要条件
我们随机几个比p小的质数做a,大概100以内选7个,判错的概率很小
判的方法是把(p - 1)分解成(2^{k} * s),的形式,这个时候我们计算(a^{s}),然后把(a)不断翻倍,看最后会不会得到1,若得到1看上一次的是不是(-1),否则p一定不是质数
Pollard_rho
我们在分解一个合数,他的最小质因子必然小于等于(sqrt{n}),根据生日悖论,随机一个数然后和这个数取gcd能找到这个合数的质因子的随机次数是(n^{1/4})(事实上,应该设最小质因子是(p),复杂度为(sqrt{p}))
我们用一个伪随机数,(f(x) = x^{2} + c),不过这个函数无法处理含有2的质因子,因为它在模4意义下只会生成(a + 1)和(a),所以我们只需要暴力去掉2就好了
这个(c)可以在每筛一次的时候递增,初始值随机,(a = b = rand()),然后(a)走一步,(a = f(a)),(b)走两步(b = f(f(b))),由于这个函数的特殊性质,它的循环节期望是$O(sqrt{p}) $
在筛的时候注意判n是质数的情况,这个可以用miller_rabin
模拟退火
大概剩下的都归于奇怪的乱搞
我只记录一下模拟退火如何以一定概率接受不优的解
设温度为(T),错解是(tmp),最优的是(ans)
那么可以用指数函数来计算这个概率(exp((tmp - ans) / T)),其中我们需要(tmp - ans)是一个负值,显然这个负值越大,概率越大,且可以认为温度越大,取错解的概率越大
这样我们就可以自欺欺人了!
(24天的一轮复习完结了)
(我到底复习了啥啊qwq)