错题本杂记

警示自己&后人

从 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

这个 lr 是函数参数传入的修改/查询左右端点。

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.stotz = x 作用相同,因为会覆盖原来的值,所以要把原来的存起来。

240703

必要时 query 可以返回整一段 Segment,方便处理。(对于复杂信息/多种询问方式,建议这样处理)

240719

一般的线段树一定要 四倍空间,动态开点一定要 二倍空间

240724

主席树老老实实开到 MAXN<<5

主席树维护可持久化并查集,要开到 (MAXN+MAXM)<<5

并查集启发式合并,在 xy 的祖先不同时才能合并(否则会产生错误大小),相同时应令 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 n1 条链(每个链边单独一条)(即 ∑ c n t ≤ n − 1 \sum cnt \le n-1 cntn1),因此这里复杂度为 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 yxz x x x y y y 连权值为 z z z 的边。

相当于 y ≤ min ⁡ { x + z } y \le \min \{x + z\} ymin{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 xA y ∉ A y \notin A y/A
根据上述结论,存在一个环 B B B,使得 x ∈ B x \in B xB y ∈ B y \in B yB
x ∈ A ∩ B x \in A \cap B xAB y ∉ A ∩ B y \notin A \cap B y/AB
A ⊕ B = ∁ A ∪ B A ∩ B A \oplus B = \complement_{A \cup B} A \cap B AB=ABAB(对称差,类似 xor 运算)。
显然 A ≠ B A \neq B A=B A ⊕ B ≠ ∅ A \oplus B \neq \varnothing AB= x ∉ A ⊕ B x \notin A \oplus B x/AB y ∈ A ⊕ B y \in A \oplus B yAB
∀ 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 xX,yX

二分图

写书上了。

通用模板:

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 ∀0i<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];
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值