模板记录

RMQ求Lca

怕考场被卡,所以临省选重新复习一下。

有个性质:若 \(u,v\) 不成祖先和儿子关系,则 \(u, v\) 的 \(lca\) 的 \(dfs\) 序一定不在 \(dfn_u\) 到 \(dfn_v\) 之间,所以我们只用找到 \(dfn_u\) 到 \(dfn_v\) 之间深度最小点 \(d\) 的就行了,\(d\) 的父亲显然是 \(u,v\) 的 \(lca\)。

manacher

思想是对于一个大的回文串,里面的小回文串在对应的另外一边也一定存在相同的小回文串。

每次要注意对于 \(maxR - i + 1\) 取 \(min\),

推荐博客

PAM

之前学了,但是当时就没学会,现在再学一遍还不太会。

值得注意的是:我们新建点必须在最后。

具体思想:我们每次新加一个点,若上一次加完了是位置是 \(tot\),那么我们 \(GetFail\) 直到找到当前回文串的前一位等于现在这一位,然后更新。对于 \(fail\) 的更新是从当前找到的回文串的 \(fail\) 找然后更新。注意最后才能更新 \(child[to][now]=tot\)。

Tarjan

割点

tarjan果然还不会。

我们每次判断 \(dfn_{now}\) 是不是小于等于 \(low_{to_i}\),是表示 \(to_i\) 到不了 \(now\) 的祖先,所以这个点 \(now\) 就是割点。注意特判根的情况。

我们对于 \(dfn_{to_i} \neq 0\) 的情况,\(low\) 是与 \(dfn_{to_i}\) 取 \(min\),而不是与 \(low_{to_i}\) 取 \(min\)。原因看一下这篇大佬的文章的最后一部分。

缩点

果然还是没理解tarjan。

我们每次对于 \(low_{now}\) 和 \(low_{to_i}\) 取 \(min\) 的时候,只能与在栈里面的取 \(min\)。

不然可能会出现我们先处理的前面的点,在后面点处理的时候又对于前面的点取 \(min\),然后就错了。

还有每次弹栈的时候要把栈顶的 \(visit\) 变成 \(false\)

例如:

5 5
1 3 1 2 2
3 1
4 1
5 1
5 3
5 2
5

点双联通分量

我连定义都忘了。。。。记起来定义就行了。

定义是:一个联通块内没有割点。

边双联通分量

没有割边的连通块。

注意判断割边的条件是 \(low_v > dfn_{now}\)。

CDQ 分治

我怎么写一道板子发现不会一道。。

CDQ还是有很多要注意的。

比如在相等的可以算贡献时要去重,第一次排序要以 \(a,b,c\) 为关键字都排一遍,不能只按 \(a\) 排序。

因为我们要让左边的贡献给右边,所以我们要让能贡献的在前面,被贡献额在后面。

然后还有个要注意清空就没了。

SAM(重点不会)

都忘了,都忘了,哈哈,我直接趋势。

对于每次加点,把从 \(last\) 开始的所有空的点向上跳 \(link\) 并都连上 \(newP\)。

所以当我们之后建新点的时候若从 \(x\) 找到了 \(newP\),并将 \(newP\) 分裂成 \(q\) 和 \(newQ\),我们从 \(x\) 向跳 \(link\) 的过程中只会把 \(x\) 及以上的点连向 \(newQ\)。而 \(last\) 向上跳 \(link\) 一直到 \(x\) 连的边还是 \(q\)。

广义SAM

先建一个trie,然后依靠trie建SAM。

每次建SAM的新节点的时候,都将trie树上这一点的父亲 \(fa\) 在SAM中建出点的 \(newP\) 设为 \(last\) 就行了。

exgcd(重点不会)

破防了哥,我数学就是个jb。

首先我们要证 \(gcd(a, b) = gcd(b,a \mod\ b)\)。

我们设 \(r=a \mod b\),因为 \(a \mod b=a-\lfloor \frac{a}{b} \rfloor \cdot b\),设 \(k=\lfloor \frac{a}{b} \rfloor\),所以 \(r=a-k \cdot b\)。因为 \(\frac{a-k \cdot b}{gcd(a,b)}\) 一定是整数,所以 \(\frac{r}{gcd(a,b)}\) 也是整数。反之同理。

然后就可以 \(ax+by=bx+(a \% b) y\) 然后就能解决了。

因为我们求的时候是等同于 \(a\cdot x + b \cdot y = gcd(a, b)\) 求的。所以记得从 \(exgcd\) 出来后将 \(x,y\) 都乘上 \(\frac{c}{gcd(a,b)}\)。

关于通解的求法

我们首先可以知道 \(a(x+k \cdot b)+b(y-k \cdot a)=c\) 是等同与原式子 \(ax+by=c\)。我们设 \(dx = k\cdot b\) 和 \(dy = k \cdot a\)。我们只需要让 \(dx,dx\) 同时是整数就行。当 \(k=\frac{1}{gcd(a,b)}\) 时可以让 \(k \cdot b\) 和 \(k \cdot a\) 最小。

所以我们的通解(其中 \(s\) 为变量):

\[x=x+s \cdot dx \]

\[y=y-s \cdot dy \]

然后求就完事了。

主席树

没啥好说的,算比较简单的一个,只是要注意一下空间,可能不太好把控开多大。

线段树合并

这个有个特别要注意的点,就是我们如果将 \(b\) 合并到 \(a\) 上,那么合并之后会破坏 \(a\) 原本的结构,可能就会导致答案不一样。解决方法是每次合并的时候新建一个点,把 \(a\) 上面的值都赋给新建的点 \(New\),然后接下来合并的时候都合到新建的点 \(New\) 上面就行了。

还有个东西,每次 \(Merge\) 了叶子节点之后,要重新更新一遍叶子结点的各个权值。

点分治

还是很简单的,就是代码细节有点多。

注意要多判 \(visit_{to_i} = true\)。然后每次求重心的时候记得将 \(root=0\) 并且 \(sum=size_{to_i}\)。

平衡树 (重点看)

只写 \(FHQ\) 了,不写其他太麻烦的了。

唯一不太会的就是 \(Merge\) 和 \(Split\) 了

代码贴下面了。

Treap
void Split(int id, int value, int &l, int &r) {
    if (id == 0) {
        l = r = 0;
        return;
    }
    if (val[id] <= value) {
        l = id;
        Split(child[id][1], value, child[id][1], r);
        //这里面第二个child[id][1]取址了,会改变原id点的信息
        //所以对应的id要pushup 下面同理
    }
    else {
        r = id;
        Split(child[id][0], value, l, child[id][0]);
    }
    Pushup(id);
    return;
}
int Merge(int l, int r) {
    if (!l || !r) {
        return l | r;
    }
    if (key[l] <= key[r]) {
        child[l][1] = Merge(child[l][1], r);
        Pushup(l);
        return l;
    }
    else {
        child[r][0] = Merge(l, child[r][0]);
        Pushup(r);
        return r;
    }
}

网络流(重点)

最大流

黑盒算法呜呜呜。

我们每次 \(Bfs\) 时把图分出来层,然后同时把 \(cur\) 数组赋值成 \(head\) 数组。

然后在 \(Dfs\) 的时候每次遍历边都重新更新一下 \(cur\) 以保证复杂度。我们的循环跳出条件既要保证 \(i \neq 0\) 还要保证 \(rest \neq 0\)。

还有个小优化就是每次判断流 \(temp\) 是不是等于 \(0\),若是的话就将 \(deep_{to_i}\) 赋值成 \(0\)。

注意千万不要打顺手了然后把 Dfs(to[i], end, std::min(rest, cap[i])) 打成 Dfs(to[i], now, std::min(rest, flow))!!!!!!

还有一定要在for循环加上 rest!=0

费用流

把 \(bfs\) 写成 \(spfa\)。

但是其中要改变好多细节。

我们要新加一个 \(visit\) 数组,表示我们有没有经过。在 \(spfa\) 中是判断在不在队列里, \(Dfs\) 中判断有没有在路径里,防止出现负环无限跑的情况。

然后每次要更新 \(cur\),还有判断 \(rest \neq 0\)。这些我老容易忘。

扩展域并查集

原本要写线段树分治的,但是发现自己不会“板子题”的前置知识,就只能来学了。

其实就是我们让两个点必须在不同的集合,然后就没了。例如:关押罪犯。

算了,我发现自己还是不会,重新在学一遍。(膜拜5k)。

我们判断一个图是不是二分图:

我们每次将两个点 \(u,v\) 连边的时候,将 \(u\) 与 \(v+n\) 连边,将 \(u+n\) 与 \(v\) 连边。表示 \(u\) 与 \(v\) 是敌人。

若我们 \(u,v\) 在同一个并查集里面,那么就说明 \(u\) 是 \(v\) 敌人的敌人,也就是朋友。

若我们此时再让 \(u\) 与 \(v+n\) 连边,那么 \(u,v\) 又是敌人又是朋友,就肯定不能成为二分图了。

数学上来解释,就是当从左半部分连向右半部分再连会左半部分时,连的边数一定是偶数,若我们再加一条边就成奇数了,肯定不是二分图了。

线段树分治

没时间写了,具体说一说思想吧。就是每次我们将输入的内容中,可以影响的区间放到线段树上。

然后每到一个节点,就将该节点的信息维护一下,直到叶子节点时输出答案。记得撤销维护。

李超线段树

好像思想还是比较好理解的,也比较好写。

我们加线段分成三种情况考虑:

  1. 新加的线段整个的比原来的高,那么直接覆盖就行。

  2. 新加的线段整个的比原来的低,那么直接退出不用管了。

  3. 新加的线段与原来的线段有交点,那么让 \(mid\) 处 \(y\) 大的做线段树上的线段,接着递归到剩下线段高的内一半去更新。

查询点就直接从根节点一直查询到叶子节点,每次把当前线段树节点代表的线段和之前查询的最优点,比较,更新一下就行了。

高斯消元(易错)

呃呃呃,果然我们高斯消元是有实力的。我一直被狂 \(hack\) 不止。

下面说一说易错点:

  1. 我们在找主元的时候,找的是绝对值最大的一行,所以要 std::fabs(a.matrix[i][j]) > std::fabs(a.matrix[result][j])。为了不找到 \(0\)。

  2. 我们在判断主元的值是不是 \(0\) 的时候,不能直接判断等不等于 \(0\),而是要判断绝对值是不是小于等于\(1e-10\)。注意是 绝对值。

  3. 我们在判断是不是无解的时候,判断的是 绝对值,注意是 绝对值 是不是大于等于 \(1e-9\)。

emmmmm,我最喜欢绝对值了。QAQ。

2-SAT

捏麻麻滴,tarjan狂错不止。

其实思想和扩展域并查集有点像。我们对于 \(a=0\) 或 \(b=0\) 的情况,我们如果 \(a \neq 0\), 那么 \(b\) 肯定等于 \(0\)。所以将 \(a \neq 0\) 的情况和 \(b=0\) 的情况建边就行了, \(b\) 的建边同理。

我们最后统计答案的时候,对于不可能的情况一定存在 \(a\) 与 \(a+n\) 在同一个联通块里,判断一下。还有输出是 \(true\) 还是 \(false\) 的时候,看谁的联通块的编号小就输出谁。因为编号小的一定是后走到的。

但是最后注意 \(tarjan\) 里弹栈时要清空弹出来元素的 \(visit\)。

哈哈,逆天 \(tarjan\)。

欧拉路径

重学一遍感觉受益匪浅。

转载自大佬的文章

欧拉路径定义:

图中经过所有边恰好一次的路径叫欧拉路径(也就是一笔画)。如果此路径的起点和终点相同,则称其为一条欧拉回路。

欧拉路径判定(是否存在):

  1. 有向图欧拉路径:图中恰好存在 \(1\) 个点出度比入度多 \(1\)(这个点即为 起点 \(S\)),\(1\) 个点入度比出度多 \(1\)(这个点即为 终点 \(T\)),其余节点出度=入度。

  2. 有向图欧拉回路:所有点的入度=出度(起点 \(S\) 和终点 \(T\) 可以为任意点)。

  3. 无向图欧拉路径:图中恰好存在 \(2\) 个点的度数是奇数,其余节点的度数为偶数,这两个度数为奇数的点即为欧拉路径的 起点 \(S\) 和 终点 \(T\)。

  4. 无向图欧拉回路:所有点的度数都是偶数(起点 \(S\) 和终点 \(T\) 可以为任意点)。

注:存在欧拉回路(即满足存在欧拉回路的条件),也一定存在欧拉路径。

当然,一副图有欧拉路径,还必须满足将它的有向边视为无向边后它是连通的(不考虑度为 \(0\) 的孤立点),连通性的判断我们可以使用并查集或 dfs 等。

我们用栈的原因是可以防止走错路径,而如果走错了路径,我们还能新加没走的路径。

具体情况如图:

我们可能走成下图:

这个时候我们若 \(DFS\) 到哪里就输出哪里显然会错。

但是如果我们这个时候用栈存上了,我们就会把右半部分存到红色路径的上面,输出的时候会先输出,然后就对辣。

扫描线

忘了,又重新学了一遍。

里面其实是有很多细节的呀。

例如我们的线段树里面的每一个节点(即使是叶子节点也是)表示的是一段区间。

若我们线段树的区间是 \(l,r\) 那么我们表示的 \(x\) 的范围是 \(x_l,x_{r+1}\)。

这样是为了我们更新的时候可以断开区间,不然 \(mid\) 到 \(mid+1\) 的这一段区间取不到辣。

我们对于 pushup 也要改一下下。

我们每次给完全覆盖的区间的 \(sum\) 加 \(1\),然后在 \(pushup\) 的时候判断一下当前节点的 \(sum\) 是不是大于 \(0\)。

若是则当前区间的被覆盖的范围就是整个区间 \(len_{id}=x_{r+1}-x_{l}\),否则就是 \(lid\) 和 \(rid\) 覆盖的区间和 \(len_{id}=len_{lid}+len_{rid}\)。

要离散化一下, \(x\) 数组去重后若有 \(n\) 个,线段树的区间范围是 \(1,n-1\),我们遍历线段的时候也是到 \(2*N-1\)。

虚树

我们先按 \(dfn\) 排一次我们要找的点,然后将两个点间的 \(lca\) 加入数组。继续排序,然后再将 \(temp_{i+1}\) 加入到 \(temp_i\) 和 \(temp_{i+1}\) 的 \(lca\) 的儿子里。

光速幂

根号分治。

点分树

其本质思想是与淀粉质一样的。

可以处理的问题大多数是与树的形态无关的问题,如:路径问题,联通块问题,寻找关键点问题。

下面说说具体思想。

建树

我们每次找出来当前子树的重心,然后再找当前重心儿子们的子树的重心,建边。

这样我们就把树变成了大约为 \(log_N\) 深度的树。

更新

我们每次更新都要从当前点 \(x\) 作为重心时的点一直往上跳到根。(注意这里的往上跳是指在重心树上跳)。

然后对于跳到的每一个重心,我们更新时要维护两个东西, \(x\) 到当前重心某一范围可以做出贡献,和 \(x\) 对于当前重心的父亲在某一范围可以做出的贡献,

答案

最后统计答案的时候也是从当前点 \(x\) 作为重心时,一直向上跳去统计答案。

每次对于当前的重心,先求出当前重心父亲子树可以贡献的答案,然后减去当前重心子树此时在重心父亲子树里的贡献就行。

posted @ 2024-02-29 20:01  觉清风  阅读(63)  评论(0)    收藏  举报