警示自己&后人
从 2024.6 开始维护。
文章目录
基础
想到一种方案之后,看看到底假了没有
第一次发癫叉正解:
场上算错了复杂度,自己把自己的可持久化线段树的正解给叉掉了。
对于复杂度不是很好算的题目,要认真算。像这样也就一百来行的代码,思路又已经证明是正确的,可以把它实现出来,再拿极限数据去测。
第二次叉正解:
如果构造反例的话,一定记得看看分析是否全面。
离散化
离散化的时候,如果会多次重复使用,那要把旧的对应新的编号在原数组给存起来,这样后面就不需要每次都要 O ( log n ) O(\log n) O(logn) 查询,极大地节约时间。不过后面只会查一次就算了。
离散化 cnt = unique(hdld + 1, hdld + n + 1) - hdld - 1;
,-1
不能丢!
指针
指针 delete
之后,指针变量不会自动归零,要记得把指针变量归零!
一个指针变量占 8 个字节,很多时候比数组模拟指针更占空间。指针慎用。
应该因为指针:数组开小了未必会 Segmentation Fault,可能 只是 WA or TLE!
位运算
注意运算优先级,移位的优先级低于加减。
int
类型,通过 x^1
实现 0 变 1,1 变 0。~
运算是把所有位全部取反,注意区分。
一些低级&降智问题
2024年8月24日,SMS OI队的两个…,在多重循环内,需要退出掉包括最外层的所有循环,只写了一个break,挂分并几乎爆零,这两位因此疯狂掉Rate,蓝变青。
No goto
, No break
, you can code a function, with a return 0
or return 1
.
i j 检查有没写反,发现之后更正的时候,注意 有没有全部都更正掉。(upd0824,今天调题有发现了这种问题。)
多测清空,估算要不要 long long
,这些都是基础操作。
看明白题,注意边界应该怎么处理。
跑大样例的时候,vscode 或者机房的 Devc++,里面不要打开输入文件或者输出文件,否则会死机。
不管什么时候,只要是递归(深搜),都要检查边界特判写了没有,没写就会死循环然后爆栈。
有的时候,注意会不会前面写了 2k 的函数,然而后面 main() 里面没有调用。
仿函数有两个括号!
bool operator() (int x, int y) {
return x < y;
}
模拟
大模拟一般考虑要面向对象。
2023 CSP-S T3,本质就是DAG,注意转化。
数据结构
很多时候,动态开点是个好东西。(e.g. 主席树、线段树合并、FHQ-Treap、01-Trie)
线段树
240629:线段树,update() 和 query() 里面的 mid,应该是 mid = (s[cur].l + s[cur].r) >> 1
而非 mid = (l+r)>>1
。
这个 l
和 r
是函数参数传入的修改/查询左右端点。
void update(int cur, int l, int r) { // 区间修改
if (l <= s[cur].l && r >= s[cur].r) {
change(s[cur]);
return;
}
spread(cur); //别丢了
int mid = (s[cur].l + s[cur].r) >> 1; // !!!
if (l <= mid) update(cur * 2, l, r);
if (r > mid) update(cur * 2 + 1, l, r);
s[cur].sum = s[cur*2].sum + s[cur*2+1].sum;
}
int query(int cur, int l, int r) { // 区间查询
if (l <= s[cur].l && r >= s[cur].r) {
return s[cur].sum;
}
spread(cur);
int mid = (s[cur].l + s[cur].r) >> 1, res = 0;
if (l <= mid) res += query(cur*2, l, r);
if (r > mid) res += query(cur*2+1, l, r);
return res;
}
int query(int cur, int p) { // 单点查询
if (s[cur].l == s[cur].r) return s[cur].sum;
spread(cur);
int mid = (s[cur].l + s[cur].r) >> 1;
if (p <= mid) return query(cur*2, p);
else return query(cur*2+1, p);
}
240630:前面的 if-return
写完之后,spread(cur)
记得要写。
修改操作,递归完左右子树之后需要把两个子树的信息重新合并,即 pushup
操作。
线段树非常重要,应用十分广泛,代码要熟悉!(包熟练的)
240701:
时刻关注 long long
问题!!!算法本身没问题很有可能是数据类型的问题。
利用和角公式更新的时候:
(线段树sin和 问题)
sin
(
α
+
β
)
=
sin
α
cos
β
+
cos
α
sin
β
cos
(
α
+
β
)
=
cos
α
cos
β
−
sin
α
sin
β
\sin(\alpha + \beta) = \sin\alpha\cos\beta + \cos\alpha\sin\beta\\ \cos(\alpha + \beta) = \cos\alpha\cos\beta - \sin\alpha\sin\beta
sin(α+β)=sinαcosβ+cosαsinβcos(α+β)=cosαcosβ−sinαsinβ
void change(Segment &x, long long v) { // 就是这里这个 long long
double sinv = sin(v), cosv = cos(v), tmp = x.stot;
x.stot = x.stot * cosv + x.ctot * sinv;
x.ctot = x.ctot * cosv - tmp * sinv;
x.add += v;
}
类似于 exgcd
的问题。
int exgcd(int a, int b, int &x, int &y) {
if (b == 0) {
x = 1, y = 0;
return a;
}
int d = exgcd(b, a%b, x, y);
int z = x;
x = y;
y = z + a / b * y;
return d;
}
tmp = x.stot
和 z = x
作用相同,因为会覆盖原来的值,所以要把原来的存起来。
240703
必要时 query
可以返回整一段 Segment
,方便处理。(对于复杂信息/多种询问方式,建议这样处理)
240719
一般的线段树一定要 四倍空间,动态开点一定要 二倍空间。
240724
主席树老老实实开到 MAXN<<5
!
主席树维护可持久化并查集,要开到 (MAXN+MAXM)<<5
。
并查集启发式合并,在 x
和 y
的祖先不同时才能合并(否则会产生错误大小),相同时应令 root[i] = root[i-1]
。
李超线段树,左端点是自变量的最小取值,右端点是自变量的最大取值,不是平常的 [ 1 , n ] [1, n] [1,n]。
240919
QOJ5425 / CF_GYM104128L(ICPC2022 南京) https://qoj.ac/submission/576389
typedef vpi vector<pair<int, int>>;
void queryL(int cur, int l, int r, int L, int R, vpi& res) {
if (l == r) return res.push_back(make_pair(c[l], l)), void();
spread(cur);
int mid = (l + r) >> 1;
if (L <= mid && s[cur<<1].mn < L) queryL(cur<<1, l, mid, L, R, res);
if (R > mid && s[cur<<1|1].mn < L) queryL(cur<<1|1, mid+1, r, L, R, res);
}
线段树上每查一个区间,一次查找多个满足条件的值,可以使用 std::vector
(前提是 复杂度正确)。
我们用线段树维护区间最远前驱和最远后继,就能 O ( c n t log n ) O(cnt \log n) O(cntlogn) 地找到所有被切开的位置,其中 c n t cnt cnt 是被切的位置数量。由于每次被切会增加双向链表的数量,且最多只有 n − 1 n − 1 n−1 条链(每个链边单独一条)(即 ∑ c n t ≤ n − 1 \sum cnt \le n-1 ∑cnt≤n−1),因此这里复杂度为 O ( n log n ) O(n \log n) O(nlogn) 。
这个 trick 很经典, 另一道经典题 “观光公交” 中我的做法也用到了.
动态开点的时候,一般以 0 号节点作为空子树。需要注意对空子树的处理,如算左右儿子的较小值,一个儿子为空,但是 s[0].mn
没有赋值 INF
,会寄掉。
分块
240716
注意边界处理,右端点不能超过右边界。
莫队
void update(int lastL, int lastR, int curL, int curR) {
if (lastR < curR)
for (int i = lastR + 1; i <= curR; ++i) inc(a[i]);
if (lastL > curL)
for (int i = curL; i < lastL; ++i) inc(a[i]);
if (lastR > curR)
for (int i = curR + 1; i <= lastR; ++i) dec(a[i]);
if (lastL < curL)
for (int i = lastL; i < curL; ++i) dec(a[i]);
}
void solve() {
int len = sqrt(m);
int x = 1, y = 0; // !!!
for (int lt = 1, rt; lt <= m; lt = rt + 1) {
rt = min(m, lt + len - 1);
if ((lt / m) & 1) sort(q + lt, q + rt + 1, cmp22);
else sort(q + lt, q + rt + 1, cmp21);
for (int i = lt; i <= rt; ++i) {
update(x, y, q[i].l, q[i].r);
q[i].ans = ans;
x = q[i].l, y = q[i].r;
}
}
}
模板 Luogu 2709。注意让 x = 1, y = 0
,最初区间长度为 0。
莫队 奇偶不同,排序方法不同。
平衡树
FHQ-Treap
如果有 rotate
(区间翻转,文艺平衡树)操作,注意 spread
的位置:
pii split(int cur, int rank) {
if (!cur) return make_pair(0, 0);
spread(cur);
if (s[s[cur].lc].siz >= rank) {
// spread(s[cur].lc); WA
pii res = split(s[cur].lc, rank);
s[cur].lc = res.second;
pushup(cur);
return make_pair(res.first, cur);
} else {
// spread(s[cur].rc); WA
pii res = split(s[cur].rc, rank-s[s[cur].lc].siz-1);
s[cur].rc = res.first;
pushup(cur);
return make_pair(cur, res.second);
}
}
int merge(int l, int r) {
if (!l) return r;
if (!r) return l;
if (s[l].dat > s[r].dat) {
spread(l);
s[l].rc = merge(s[l].rc, r);
pushup(l);
return l;
} else {
spread(r);
s[r].lc = merge(l, s[r].lc);
pushup(r);
return r;
}
}
应该先 spread
整一棵子树再进行后续处理。
线段树也好,平衡树也罢,只要有延迟标记,都要在开始处理当前节点之前完成 标记下传 操作。
如果有必要,一些高级数据结构可以进行封装。
平衡树建议把值相同的存一个点上面。
树
树上差分
树上差分分为边差分和点差分,边差分:
int a = lca(x, y);
++val[x], ++val[y];
val[a] -= 2;
点差分:
int a = lca(x, y);
++val[x], ++val[y];
--val[a], --val[pa[a][0]];
这个问题之前听人在哪里说过。当然要先判断 pa[a][0]
是否存在。
树上差分,查询需要遍历整个子树,如果一边修改一边查询的话,就不太适用。
树链剖分
240707
void dfs2(int x) {
dfn[x] = ++cnt;
oldID[cnt] = x;
if (!top[x]) top[x] = x;
if (!hvy[x]) return; // !!!
top[hvy[x]] = top[x];
dfs2(hvy[x]);
for (int i = head[x]; i; i = nxt[i]) {
int y = ver[i];
if (y == pa[x] || y == hvy[x]) continue;
dfs2(y);
}
}
这个 if(!hvy[x]) return;
千万不能掉。叶子节点就需要回溯了,不能再 dfs 下去。
丢了这个就会 RE
!
如果树剖查询一条路径上的信息,可以重复利用 pushup
函数(改为 merge
了),减少代码量,减少出错的可能。
240719 续上一条:
避免惯性思维:
void merge(Segment &x, Segment lc, Segment rc);
merge(xseg, query(1, dfn[top[x]], dfn[x]), xseg); // AC
merge(xseg, xseg, query(1, dfn[top[x]], dfn[x])); // WA
原因:
线段树维护的时候,查询得到的线段一定是左端点 < 有端点,从点 u 到 lca(u,v) 之间的路径,一定有 dfn[lca(u,v)] < dfn[u],而小的在前,所以新加入的这一部分应该作为左儿子,旧的作为右儿子来合并。
完整的 queryPath
(Luogu 3976):
Segment queryPath(int x, int y) {
Segment xseg, yseg;
xseg.add = yseg.add = -1;
while (top[x] != top[y]) {
if (dep[top[x]] >= dep[top[y]]) {
merge(xseg, query(1, dfn[top[x]], dfn[x]), xseg);
x = pa[top[x]];
} else {
merge(yseg, query(1, dfn[top[y]], dfn[y]), yseg);
y = pa[top[y]];
}
}
if (dep[x] >= dep[y]) merge(xseg, query(1, dfn[y], dfn[x]), xseg);
else merge(yseg, query(1, dfn[x], dfn[y]), yseg);
swap(xseg.inc, xseg.dec);
merge(xseg, xseg, yseg);
return xseg;
}
如果用重载加法写 pushup()
的话:xseg = query(1, dfn[top[x]], dfn[x]) + xseg;
树形 DP
换根的时候,不要把包含这个子节点的当成由父亲传来的,然后错误地传下去了。
动态树
目前只学了 LCT。
留心 0 号节点应该为空,所有值都应该是 0。
保险起见,link
操作和 cut
操作之前,都要判断一下是否合法。
理论上 cut
操作之后应该是需要 pushup
的。
splay
之前记得 spreadanc
递归下传祖先的标记。
个人习惯,翻转标记:已交换左右子,未交换左右子的左右子。
idt
特判父亲节点是 0,此时一定返回 -1。
namespace LCT {
int son[MAXN][2], pa[MAXN], tag[MAXN];
#define lc(x) son[x][0]
#define rc(x) son[x][1]
struct Data {
int val, sum, siz;
int add, mul;
Data(){}
Data(int v) {
val = sum = v, siz = 1;
add = 0, mul = 1;
}
void modify(int x, int y) { // add, mul
val = (1ll * val * y + x) % mod, sum = (1ll * sum * y + 1ll * x * siz) % mod;
mul = 1ll * mul * y % mod, add = (1ll * add * y + x) % mod;
}
} s[MAXN];
void pushup(int x) {
s[x].sum = (s[lc(x)].sum + s[rc(x)].sum + s[x].val) % mod;
s[x].siz = s[lc(x)].siz + s[rc(x)].siz + 1;
}
void addtag(int x) {
tag[x] ^= 1, swap(lc(x), rc(x));
}
void pushdown(int x) {
if (tag[x]) {
if (lc(x)) addtag(lc(x));
if (rc(x)) addtag(rc(x));
tag[x] = 0;
}
if (lc(x)) s[lc(x)].modify(s[x].add, s[x].mul);
if (rc(x)) s[rc(x)].modify(s[x].add, s[x].mul);
s[x].add = 0, s[x].mul = 1;
}
int idt(int x) {
if (!pa[x]) return -1;
if (x == lc(pa[x])) return 0;
if (x == rc(pa[x])) return 1;
return -1;
}
void spreadanc(int x) {
if (~idt(x)) spreadanc(pa[x]);
pushdown(x);
}
void rotate(int x) {
int f = pa[x], ff = pa[f], tp = idt(x);
if (~idt(f)) son[ff][idt(f)] = x; pa[x] = ff;
son[f][tp] = son[x][tp^1], pa[son[x][tp^1]] = f;
son[x][tp^1] = f, pa[f] = x;
pushup(f), pushup(x);
}
void splay(int x) {
spreadanc(x);
for (int f = pa[x]; ~idt(x); rotate(x), f = pa[x]) {
if (~idt(f)) rotate(idt(f) == idt(x) ? f : x);
}
}
void access(int x) {
for (int s = 0; x; s = x, x = pa[x]) {
splay(x), rc(x) = s, pushup(x);
}
}
void makeroot(int x) {
access(x), splay(x), addtag(x);
}
int findroot(int x) {
access(x), splay(x);
while (pushdown(x), lc(x)) x = lc(x);
return splay(x), x;
}
void split(int x, int y) {
makeroot(y), access(x), splay(x);
}
void link(int x, int y) {
makeroot(x);
if (findroot(y) != x) pa[x] = y;
}
void cut(int x, int y) {
split(x, y);
if (lc(x) == y && !rc(y)) lc(x) = pa[y] = 0, pushup(x);
}
}
图论
只要是从 floyd 改过来的算法,都具有 dp 的特性,既然是 dp,就必须 k-i-j
的循环次序。f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
。
注意重边和自环是否需要特殊处理。题目有没有明确交代无重边无自环。
差分约束,求最短路, ∀ y − x ≤ z \forall y-x\le z ∀y−x≤z, x x x 向 y y y 连权值为 z z z 的边。
相当于 y ≤ min { x + z } y \le \min \{x + z\} y≤min{x+z},取等号即可。其他情况都要转化成上面的情况。
spfa 不要乱用,如果有必要,加双端队列成为 slf,slf 注意 if (!dq.empty() && dis[y] < dis[dq.front()])
,避免越界。
Tarjan
注意是否有已知根节点,还是每个点都要检查一遍。最好就对每个点都进行遍历。
无向图,low[y] > dfn[x]
割边,low[y] >= dfn[x]
割点。
割边传入参数 from
,从哪条边来的,要 if (i ^ from ^ 1)
检查一下,同时注意链式前向星 tot
初始值为 1
。
割点,如果是根节点要特殊处理,如果根节点只有一个子节点,那么删去它也没法分成不连通的两部分。
有向图强连通分量模板,注意区分 low[x] = min(low[x], low[y]);
和 low[x] = min(low[x], dfn[y]);
:
void tarjan(int x) {
dfn[x] = low[x] = ++cnt;
sta[++top] = x, ins[x] = 1;
for (int i = head[x]; i; i = nxt[i]) {
int y = ver[i];
if (!dfn[y]) {
tarjan(y);
low[x] = min(low[x], low[y]);
} else if (ins[y]) {
low[x] = min(low[x], dfn[y]);
}
}
if (dfn[x] == low[x]) {
++num;
while (top) {
int y = sta[top--];
ins[y] = 0;
c[y] = num;
if (y == x) break;
}
}
}
2-SAT 问题
for (int i = 1; i <= n * 2; ++i) if (!dfn[i]) tarjan(i);
这个 *2 不能丢,因为它 实际上有 2 n 2n 2n 个节点。
for (int i = 1; i <= n; ++i) {
printf("%d ", c[i] > c[i + n]);
}
最后输出方案的时候,因为 tarjan()
得到的是逆拓扑序,而给方案是 拓扑序小的优先,所以:
c[i] < c[i+n] 时,取 0;c[i] > c[i+n] 时,取1。
(当然 c[i] == c[i+n]
要记得判 No Solution
。)
无向图所属环完全相同的边
Luogu 6914 关键性质(怎样找非割边且所属环编号完全相同的边)
观察图发现,桥边一定不在环上,不做考虑,对于非桥边,暴力枚举,把当前边删除,然后再跑一遍 tarjan,所有新增的桥边都是和当前所删的的边所属环编号完全相同的边,这些边共同组成一条符合上述条件的链。( x x x 和 y y y 所属环的集合完全相同,当且仅当: x x x 不是桥, y y y 不是桥,删去 x x x 后 y y y 是桥。)
- 原先不是桥:必然属于某些环上;
- 断开这条边之后成为桥:
- 显然没有 x x x 的环,一定没有 y y y(桥的性质,即:有 y y y 的环一定有 x x x)。
- 下面证明有 x x x 的环一定有 y y y。
假设存在一个环 A A A,使得 x ∈ A x \in A x∈A 且 y ∉ A y \notin A y∈/A。
根据上述结论,存在一个环 B B B,使得 x ∈ B x \in B x∈B 且 y ∈ B y \in B y∈B。
则 x ∈ A ∩ B x \in A \cap B x∈A∩B, y ∉ A ∩ B y \notin A \cap B y∈/A∩B。
记 A ⊕ B = ∁ A ∪ B A ∩ B A \oplus B = \complement_{A \cup B} A \cap B A⊕B=∁A∪BA∩B(对称差,类似 xor 运算)。
显然 A ≠ B A \neq B A=B, A ⊕ B ≠ ∅ A \oplus B \neq \varnothing A⊕B=∅, x ∉ A ⊕ B x \notin A \oplus B x∈/A⊕B, y ∈ A ⊕ B y \in A \oplus B y∈A⊕B。
与 ∀ x ∉ X , y ∉ X \forall x \notin X, y \notin X ∀x∈/X,y∈/X 矛盾。
所以 ∀ x ∈ X , y ∈ X \forall x \in X, y \in X ∀x∈X,y∈X。
二分图
写书上了。
通用模板:
int n, head[MAXN], ver[MAXM], nxt[MAXM], tot;
int vis[MAXN], mch[MAXN];
void add(int x, int y) {
ver[++tot] = y, nxt[tot] = head[x], head[x] = tot;
}
bool dfs(int x, int root) {
for (int i = head[x]; i; i = nxt[i]) {
int y = ver[i];
if (vis[y] == root) continue;
vis[y] = root;
if (!mch[y] || dfs(mch[y], root)) {
mch[y] = x;
return 1;
}
}
return 0;
}
int solve() {
int res = 0;
for (int i = 1; i <= n; ++i) {
if (dfs(i, i)) ++res;
}
return res;
}
solve
里面千万不要 memset
,会因此 TLE 到爆!干脆让 vis
数组变成 int
类型,每一次访问 记录/比较 根节点。(这是一个防止 memset 爆炸的好技巧)
注意空间有没有开够。
动态规划
转移的时候如果出现了类似关于 i i i 和 j j j 的乘积式,既要尝试斜率优化,也应该尝试决策单调性优化。
P6932 Money for Nothing 这道题就差这一步!!!
斜率优化
struct Point {
LL x, y;
} q[MAXN];
double rate(Point A, Point B) {
return 1.0 * (B.y - A.y) / (B.x == A.x ? 1e-9 : B.x - A.x);
}
int n, m, t;
LL f[MAXN], g[MAXN], h[MAXN];
int main() {
// ... 略
int l = 1, r = 0;
for (int i = 0; i < t + m; ++i) {
if (i >= m) {
Point cur = {h[i-m], f[i-m] + g[i-m]};
while (l < r && rate(q[r-1], q[r]) >= rate(q[r], cur)) --r;
q[++r] = cur;
}
while (l < r && rate(q[l], q[l+1]) <= i) ++l;
if (l <= r) f[i] = q[l].y - i * q[l].x + h[i] * i - g[i];
else f[i] = h[i] * i - g[i];
}
}
这个写法能够合适的处理决策点 x x x 相同但是 y y y 不同的情况。
斜率优化的关键思想:
前提,乘积项能表示成 a ( x ) ⋅ b ( y ) a(x) \cdot b(y) a(x)⋅b(y),方程的一般形式:
f ( x ) = min { c ( x ) + d ( y ) − a ( x ) b ( y ) } f(x) = \min\{c(x) + d(y) - a(x)b(y)\} f(x)=min{c(x)+d(y)−a(x)b(y)}
那么假定某一个 y y y 时:
f ( x ) = c ( x ) + d ( y ) − a ( x ) b ( y ) f(x) = c(x) + d(y) - a(x)b(y) f(x)=c(x)+d(y)−a(x)b(y)
移项:
d ( y ) = a ( x ) b ( y ) + f ( x ) − c ( x ) d(y) = a(x)b(y) + f(x) - c(x) d(y)=a(x)b(y)+f(x)−c(x)
把 ( b ( y ) , d ( y ) ) (b(y), d(y)) (b(y),d(y)) 当成平面上的点, a ( x ) a(x) a(x) 是斜率, f ( x ) − c ( x ) f(x)-c(x) f(x)−c(x) 是截距。欲求最小化 f ( x ) f(x) f(x),即求最小截距。所以每次计算当前状态,是在处理一条已知斜率的直线,决策就是平面上的点。显然这些点应该是下凸的。
总结:当前状态是直线,决策是点,要求最小截距,维护决策凸性。
线性代数
矩阵乘法
单位矩阵, ∀ 0 ≤ i < n \forall 0 \le i < n ∀0≤i<n, a i , i = 1 a_{i,i} = 1 ai,i=1,其他位置为 0 0 0。显然单位矩阵十分有用。
如果封装的话:
struct mat {
int f[32][32], n;
mat(int x) {
n = x;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
f[i][j] = 0;
}
}
}
int& operator()(int x, int y) {
return f[x][y];
}
mat operator* (mat& a) {
mat res(n);
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
for (int k = 0; k < n; ++k) {
res(i, j) = (res(i, j) + 1ll * (*this)(i, k) * a(k, j)) % mod;
}
}
}
return res;
}
mat pow(LL k) {
mat res(n), x = *this;
for (int i = 0; i < n; ++i) res(i, i) = 1;
for (; k; k >>= 1) {
if (k & 1) res = res * x;
x = x * x;
}
return res;
}
};
多项式乘法与离散卷积
下标:0 起!!!0 起!!!0 起!!!
注意构造 rev
数组的策略。
for (n = 1; n <= (m<<1); n <<= 1); // m << 1 可以换成两个多项式的次数之和
for (int i = 0; i < n; ++i) {
rev[i] = rev[i>>1] >> 1;
if (i&1) rev[i] |= n >> 1;
}
傅里叶逆变换,最后结果一定要除以 n n n!!!
void fft(Complex* a, int tp) {
for (int i = 0; i < n; ++i) {
if (i < rev[i]) swap(a[i], a[rev[i]]);
}
for (int len = 1; len < n; len <<= 1) {
Complex w(cos(pi / len), tp * sin(pi / len));
for (int l = 0; l < n; l += len << 1) {
Complex cur(1.0, 0.0);
for (int i = l; i < l + len; ++i, cur *= w) {
Complex x = a[i], y = a[i+len] * cur;
a[i] = x + y, a[i+len] = x - y;
}
}
}
}
int w = qpow(tp == 1 ? 3 : inv, (mod-1) / (len<<1)); // NTT
如果是处理整数,最好用 NTT,否则可以预见的精度爆炸。
998244353 998244353 998244353 的原根是 3 3 3,
单位根直接反方向转就行了,原根的话就要求一下逆。
根据 FFT 的原理,y = a[i+len] * cur
,对于奇数次的位置,需要乘上一个 cur
。
字符串
KMP
单模 KMP,多模 ACAM。
原串 - 最长公共前后缀(nxt[i]
) = 最小循环元(可能最后一个循环有多出来的)
体会公共前后缀与循环元两两之间的关系。
ACAM
多模匹配 ACAM(AC 自动机),在 Trie 的基础上,fail
指针一方面用于应对失配的情况(自动机),另一方面可以借此找在 Trie 上出现过的后缀。
void calcFail() {
queue<int> q;
for (int i = 0; i < sigma; ++i) {
if (son[0][i]) q.push(son[0][i]);
}
while (!q.empty()) {
int x = q.front(); q.pop();
for (int i = 0; i < sigma; ++i) {
int y = son[x][i];
if (y) fail[y] = son[fail[x]][i], q.push(y);
else son[x][i] = son[fail[x]][i];
}
}
}
在 dp 计数的时候,为了防止串与串之间包含,应该在上述代码第 10 行加上 ed[y] |= ed[fail[y]]
。
深入体会 ACAM 中 fail
指针的妙处,尤其是 fail tree(构成树形结构)。
如果要显式建立 fail tree,记得在初始加入根的子节点的时候也要连边。
PAM
namespace PAM {
int son[MAXN][sigma], a[MAXN], fail[MAXN], len[MAXN];
int tot, last, cnt;
void init() {
len[0] = 0, len[1] = -1;
tot = 1, last = 0, cnt = 0;
fail[0] = 1, a[0] = -1;
}
int getFail(int x) {
while (a[cnt-len[x]-1] != a[cnt]) x = fail[x];
return x;
}
void add(int x) {
a[++cnt] = x;
int p = getFail(last);
if (!son[p][x]) {
++tot;
fail[tot] = son[getFail(fail[p])][x];
son[p][x] = tot; // fail 和 son 这两行代码顺序不可调换
// 如果调换了的话,getFail 会陷入死循环
len[tot] = len[p] + 2;
}
last = son[p][x];
}
}