夜雨聆风学习资料网

ARTICLE · 1273696

【一车板子】CSP-S考前模板速记

【一车板子】CSP-S考前模板速记

提高组模板复习,需要同时记住模型的成立条件、维护量和可修改的边界。区间加扩展到乘加,标记就有先后顺序;最短路扩展到次短路,需要重新定义等长路径是否计入;并查集能判断连通,却不能代替割点、桥和强连通分量。

基础工具:把前提和语义一起记

  • 整数二分先证明真假只切换一次。找首个真时,中点向下取整,真则保留左半区间,假则删去中点及其左侧;无解哨兵不参与判定。对答案二分还要分别证明判定算法正确,含负数的分段和不能直接用“超限就切段”的贪心。
  • 前缀和查询闭区间用 sr−sl−1;差分给 [l,r] 加值,改变 l 和 r+1,最后求前缀恢复。二维情形用四个角的容斥。离散化只保序,区间长度保留原坐标。
  • 归并计逆序对时,右侧元素严格更小才增加左侧剩余项数,相等不计;归并前两段应分别有序。快排划分要让相等元素也能推动指针,随机化或三路划分可缓解重复值退化;sort 的比较函数必须满足严格弱序,不能使用小于等于。堆排序为 O(nlog⁡n),桶排序要计入值域;基数排序每轮按位稳定分配,总时间与位数、桶数共同有关。
  • 分治的合并结果必须能从子问题恢复;递归式 T(n)=2T(n/2)+O(n) 对应 O(nlog⁡n)。双端栈共用数组时注意两端相遇,堆只保证堆顶最值;multiset.erase(value) 会删除全部同值项,要删除一次需先找迭代器。迭代器失效、递归调用栈和 bitset 的按机器字并行成本都计入实际实现。
  • 高精度数组通常低位在前,加法带进位、减法先比较绝对值,乘法逐位累加后统一进位。除以正整数时反向扫描,用“旧余数乘进制加当前位”求商;中间值也需放得下。结束后去前导零,零保留一位;符号与绝对值运算分开。

数据结构

单调结构与 ST:能丢弃哪些候选

滑动窗口最小值的队列存下标:先弹出过期位置,再弹出队尾不小于新值的位置,最后入队。被删除的旧值不小于新值,且更早过期,以后不会胜过新值。每个下标入队、出队各一次,总时间为 O(n)。

统计以第 i 项为最小值的子数组,需约定重复值归属。设左边界 Li 为左侧首个严格更小的位置,右边界 Ri 为右侧首个小于等于的位置,则贡献为 (i−Li)(Ri−i),相同最小值归最右位置。两边都严格会重复计数,两边都非严格会漏掉跨越重复值的区间。

静态区间最小值可用 ST。f[i][j] 覆盖从 i 起、长度为 2j 的区间,L 满足 2L>n。

CPP
int lg[N], f[N][L];void ini() {    for (int i = 2; i <= n; i++) {        lg[i] = lg[i / 2] + 1;    }    for (int i = 1; i <= n; i++) {        f[i][0] = a[i];    }    for (int j = 1; j < L; j++) {        for (int i = 1; i + (1 << j) - 1 <= n; i++) {            f[i][j] = min(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);        }    }}int ask(int l, int r) {    int k = lg[r - l + 1];    return min(f[l][k], f[r - (1 << k) + 1][k]);}

查询两块可以重叠,合并运算须满足结合律和幂等性 x∘x=x。最值、最大公约数、按位与或可用;区间和、异或不能这样查询。预处理时间、空间均为 O(nlog⁡n),每次查询只需常数时间。

两棵树状数组:从差分还原前缀和

令 di=ai−ai−1,则

两棵树分别维护 di、(i−1)di。区间加仅改变差分的两个端点;下面初始数组为零。

CPP
ll t1[N], t2[N];void add(ll *t, int p, ll v) {    for (; p <= n; p += p & -p) {        t[p] += v;    }}void chg(int l, int r, ll v) {    add(t1, l, v);    add(t1, r + 1, -v);    add(t2, l, (l - 1) * v);    add(t2, r + 1, -r * v);}ll pre(int p) {    ll s = 0;    for (int i = p; i > 0; i -= i & -i) {        s += p * t1[i] - t2[i];    }    return s;}

闭区间和为 pre(r) - pre(l - 1),单次操作为 O(log⁡n)。有初值可逐点调用 chg(i, i, a[i])。更新下标须为正,查询零合法;右端为 n 时,更新 n+1 被循环跳过。类型须容纳加权差分与区间和。

仿射懒标记:新变换复合在旧变换之后

区间乘、区间加统一写成 x←ax+b(modP)。sm 是已计入标记的区间和,旧标记表示 x↦Mx+A;再施加新变换后为 x↦aMx+aA+b。系数已归一化,2≤P≤2×109,并保证乘积与区间长度乘法在 ll 内。

CPP
ll sm[N * 4], ml[N * 4], ad[N * 4];void app(int u, int l, int r, ll a, ll b) {    sm[u] = (sm[u] * a + b * (r - l + 1)) % P;    ml[u] = ml[u] * a % P;    ad[u] = (ad[u] * a + b) % P;}void ps(int u, int l, int r) {    int m = l + (r - l) / 2;    app(u * 2, l, m, ml[u], ad[u]);    app(u * 2 + 1, m + 1, r, ml[u], ad[u]);    ml[u] = 1;    ad[u] = 0;}

建树时每个使用节点的乘标记均设为一,加标记为零,叶子存原值,父亲合并孩子。修改完整覆盖就调用 app;部分覆盖先 ps,再递归有交集的孩子,最后重新合并。查询完整覆盖返回 sm,否则先下传再合并查询结果;不对叶子调用 ps。单次操作为 O(log⁡n),空间为 O(n)。

“先加 3 再乘 2”的标记是 (2,6),“先乘 2 再加 3”是 (2,3)。单位标记为 (1,0);区间赋值也可表示为 (0,b)。下传必须沿用同一个复合顺序,不能只更新区间和、忘记孩子已有的标记。

扫描线:线段树维护实际覆盖长度

求轴平行矩形并面积,把左右边分别转成加入、删除纵区间的事件。去重后的纵坐标为 y1<⋯<yk,线段树叶子 i 代表 [yi,yi+1),因此只有 k−1 片。更新 [ya,yb) 对应叶子区间 [a,b−1],长度不能用排名差代替。

cnt[u] 仅记录完整覆盖本节点的活动区间数,len[u] 是该段实际被覆盖的长度,合并规则为:

CPP
void pu(int u, int l, int r) {    if (cnt[u] > 0) {        len[u] = y[r + 1] - y[l];    } else if (l == r) {        len[u] = 0;    } else {        len[u] = len[u * 2] + len[u * 2 + 1];    }}

完整覆盖时只增减当前 cnt,部分覆盖递归孩子,返回时均调用 pu。这里不把 cnt 下传:即使父亲暂时被整体覆盖,孩子仍保留自己的覆盖记录,父亲计数归零后便能恢复它们的长度。

按横坐标分组,先累加“上一组处理后的覆盖长度乘横坐标差”,再处理当前组全部事件。忽略零面积矩形,核算面积乘法范围。总时间为 O(nlog⁡n),空间为 O(n)。

笛卡尔树:中序顺序与堆序同时满足

以数组下标为中序、数值为小根堆序,区间最小值就对应该区间两端在树上的 LCA。以下将相等值中靠左者作为祖先,构造前清零左右孩子,st 始终保存当前树的右侧祖先序列。

CPP
int lc[N], rc[N], st[N], tp = 0;for (int i = 1; i <= n; i++) {    int q = 0;    while (tp > 0 && a[st[tp]] > a[i]) {        q = st[tp--];    }    if (tp > 0) {        rc[st[tp]] = i;    }    lc[i] = q;    st[++tp] = i;}int rt = st[1];

被弹出的最后一棵子树整体移到新节点左侧,新节点接替剩余栈顶原来的右孩子,所以中序仍为原下标顺序。每个位置入栈、出栈各一次,构造为 O(n);树仍可能退化成链,后续遍历需考虑递归深度。若把比较改为大于等于,相等值的归属会变成靠右者,相关区间边界也要统一。

FHQ Treap:分裂后再按中序合并

维护可重复键的有序多重集合,每次出现各占一个节点,中序非降,随机优先级维持堆序。sp 按阈值分裂:eq == false 时左树键严格小于 v,为真时左树键小于等于 v,避免写 v - 1 引起下溢。

CPP
ll vl[N];int lc[N], rc[N], sz[N], tot;unsigned rd[N];mt19937 rng(712367);void pu(int p) {    sz[p] = sz[lc[p]] + sz[rc[p]] + 1;}int crt(ll v) {    int p = ++tot;    vl[p] = v;    rd[p] = rng();    sz[p] = 1;    return p;}void sp(int p, ll v, bool eq, int &x, int &y) {    if (p == 0) {        x = y = 0;        return;    }    if (vl[p] < v || (eq && vl[p] == v)) {        x = p;        sp(rc[p], v, eq, rc[p], y);    } else {        y = p;        sp(lc[p], v, eq, x, lc[p]);    }    pu(p);}int mg(int x, int y) {    if (x == 0 || y == 0) {        return x + y;    }    if (rd[x] < rd[y]) {        rc[x] = mg(rc[x], y);        pu(x);        return x;    }    lc[y] = mg(x, lc[y]);    pu(y);    return y;}

零号节点大小为零,新节点孩子为零;不回收编号时,容量按插入总次数开。mg(x,y) 要求左树所有键不大于右树所有键,不能合并任意两棵搜索树。

插入值 v:sp(rt, v, true, a, b),再令 rt = mg(mg(a, crt(v)), b)。删除一次:先按严格小于 v 拆出左树,再把剩余树按小于等于 v 拆出相等部分;相等树非空时,用其根的两个孩子合并后替换该根,最后按原顺序合并三部分。重复值只删除一个节点。

排名定义为“小于 v 的数的个数加一”,按严格小于分裂后取 sz[a] + 1,查询后恢复根。查第 k 小,比较 k 与左孩子大小:等于左大小加一就返回当前键,小则向左,大则减去左大小加一后向右;要求 1≤k≤sz[rt]。随机独立优先级下操作期望为 O(log⁡n),不能把期望复杂度写成最坏保证。

字符串

Trie 与哈希:保存的对象不同

普通 Trie 按字符走边。每经过一个节点增加前缀计数,单词结束处另增终止计数;前者回答“多少次插入以此为前缀”,后者回答“这个单词出现几次”。删除前确认单词存在,沿原路径减计数;只标记终点会把前缀查询写错。字符集大小为 Σ 时,稠密孩子数组占用 O(Σ∑|s|) 空间,单次操作为 O(|s|)。

01 Trie 把整数按固定最高位到最低位存入两叉树。查询最大异或时,优先走与当前位相反且计数非零的孩子,因为高位贡献超过所有更低位之和;删除后不能只看指针存在。位宽应覆盖全部数据,移位采用相应无符号类型,单次操作为 O(B)。

字符串多项式哈希维护 Hi=Hi−1b+ci(modP) 与 Qi=bimodP,子串值为 Hr−Hl−1Qr−l+1,归一化后可在 O(1) 时间比较等长子串。相等哈希不能证明字符串相等;双模数只能降低碰撞风险,处理哈希表冲突仍须比较原键。

KMP 的 πi 表示截至位置 i 的最长相等真前后缀长度。失配时反复令 j←πj−1,保留仍可能成为匹配的后缀;完整匹配后同样回退,才能找到重叠出现并避免访问模式串尾后。模式串不能为空。文本、模式预处理与扫描总时间为 O(|s|+|t|),额外空间为 O(|t|)。

Manacher:半径的定义决定还原公式

插入统一分隔符后,奇偶长度回文可以一起处理。下面把原字符转成非负整数,用负数作分隔符和左右哨兵,避免哨兵与输入字符相同。p[i] 包含中心,回文在变换串中覆盖 [i−pi+1,i+pi−1];r 是已知回文的右端后一位。

CPP
int n = (int)s.size();vector<int> t(n * 2 + 3, -1), p(n * 2 + 3);t[0] = -2;t[n * 2 + 2] = -3;for (int i = 0; i < n; i++) {    t[i * 2 + 2] = (unsigned char)s[i];}int c = 0, r = 0, ans = 0;for (int i = 1; i <= n * 2 + 1; i++) {    p[i] = i < r ? min(p[c * 2 - i], r - i) : 1;    while (t[i - p[i]] == t[i + p[i]]) {        p[i]++;    }    if (i + p[i] > r) {        c = i;        r = i + p[i];    }    ans = max(ans, p[i] - 1);}

对落在已知回文内的中心,镜像半径只有不越过右边界的部分可以直接继承,超出部分仍要比较。右边界只向右移动,成功扩展总次数为 O(n)。原串最长回文长度为 ans;某中心对应原串起点为 (i - p[i]) / 2,长度为 p[i] - 1,起点使用零下标,空串也可处理。

图论

邻接表遍历 O(n+m),矩阵遍历 O(n2)。无权最短路用 BFS,入队即标记;DFS、BFS 须遍历每个连通块。片段独立,编号 1∼n,数组每组清空。距离用 ll;INF 大于有限答案,有限值相加不得溢出。

最小生成树:Prim 与 Kruskal

Prim 维护到已选点集的最小边权,更新 dv←min(dv,wuv),不加 du。起点置零,每次选未入树且 d 最小的点;选不到有限值则不连通。朴素实现 O(n2+m),堆实现 O((n+m)log⁡(n+m))。

Kruskal 按边权递增枚举,并查集不同才合并、计权;选满 n−1 条才有生成树,否则为最小生成森林。时间 O(n+mlog⁡(m+1))。自环不选,重边可留,负权允许。割性质保证轻边可替换生成树跨同一割的较重边。最短路树与最小总边权不能混用。

单源最短路与严格次短路

Dijkstra 要求非负边权,允许零边。弹出距离与当前标签不同则丢弃;第一次入堆时距离尚未定案。

Bellman–Ford 置源点距离零,至多扫描全部边 n−1 轮,只从有限距离松弛;第 n 轮仍能改进,说明有源点可达负环。仅负环之后可达的点失去有限最短距离。SPFA 只让改变过的点继续松弛,最坏仍为 O(nm)。判负环可随松弛继承路径边数 cnt[v] = cnt[u] + 1,达到 n 即停止;不能换成任意总更新次数。查全图负环时,增设向每点连零边的超级源。

严格次短的长度必须大于最短长度。下面允许重复点、边,即求 walk。例如,一条无向边的权值为一:两端最短长度为一,往返后再到达则长为三,这是严格次短 walk,第二条简单路径却不存在。等长的另一条最短路也不计入。

e[u] 保存 (v,w);d[u][0]、d[u][1] 存两种最小长度,INF 表示不存在。

CPP
void dij(int s) {    for (int i = 1; i <= n; i++) {        d[i][0] = d[i][1] = INF;    }    priority_queue<pair<ll, int>, vector<pair<ll, int>>,        greater<pair<ll, int>>> q;    d[s][0] = 0;    q.push({0, s});    while (!q.empty()) {        ll k = q.top().first;        int u = q.top().second;        q.pop();        if (k != d[u][0] && k != d[u][1]) {            continue;        }        for (auto t : e[u]) {            int v = t.first;            ll z = k + t.second;            if (z < d[v][0]) {                swap(z, d[v][0]);                q.push({d[v][0], v});            }            if (d[v][0] < z && z < d[v][1]) {                d[v][1] = z;                q.push({z, v});            }        }    }}

换下来的旧最短还要竞争次短。同一点的第三种长度可舍弃:接相同后缀,已有两种更小前缀给出两种更小总长度。每点只需保留两种不同的有效长度,旧记录和同值重复只造成常数次额外扩展;时间 O((n+m)log⁡(n+m)),堆空间 O(n+m)。零权环不产生新长度。

Floyd、拓扑序与二分图

Floyd 初始化对角线零、不可达项 INF,重边取小。中转点 k 在最外层,再枚举 i,j,两段可达才更新 dij←min(dij,dik+dkj)。第 k 轮允许的内部顶点为前 k 个点,因此可原地更新;时间 O(n3)、空间 O(n2)。若 dkk<0,经过该负环的可达点对不存在有限最短路。

拓扑排序让所有入度零点入队;弹出 u 后删每条出边,邻点入度降为零再入队。弹出不足 n 点即有环。DAG 最长路先置 fs=0、其余为负无穷,在拓扑序中仅从可达点执行 fv←max(fv,fu+wuv);删除边和减入度不能受可达性判断控制。所有前驱都处理完才处理当前点,负权边也可用。排序、转移共 O(n+m)。

二分图染色遍历每个连通块,沿边令 cv=1−cu;遇同色邻点即失败,自环立即冲突。树深度奇偶给出颜色,冲突边与树路径构成奇环;无奇环则无冲突,时间 O(n+m)。

欧拉道路:先验度数,再取边

无向图只看非零度顶点,它们须连通;欧拉回路要求所有度数为偶数,非闭合道路要求恰有两个奇度点,并从其中一个出发。自环贡献两度。

有向图要求非零度顶点忽略方向后连通。回路各点出入度相等;非闭合道路起点出度多一、终点入度多一,其余相等。从规定起点出发;回路任选有出边的点。终点向起点补边得到平衡图,Hierholzer 不断把未用回路接入已有行走。

邻接项为 (v,id)。无向边两项共用物理边 ID,有向边各有独立 ID;vis[id] 标记整条边,ptr[u] 保证只扫描一次。m 为物理边数。

CPP
vector<int> eul(int s) {    vector<int> st(1, s), ans;    while (!st.empty()) {        int u = st.back();        while (ptr[u] < e[u].size() && vis[e[u][ptr[u]].second]) {            ptr[u]++;        }        if (ptr[u] == e[u].size()) {            ans.push_back(u);            st.pop_back();        } else {            auto t = e[u][ptr[u]++];            vis[t.second] = 1;            st.push_back(t.first);        }    }    if (ans.size() != m + 1) {        return {};    }    reverse(ans.begin(), ans.end());    return ans;}

退栈时加入答案,最后反转。长度不足 m+1 说明漏边,但长度检查不能代替度数条件。无边时任选合法顶点。时间、空间 O(n+m)。

强连通分量:两种低链更新

Tarjan 的 dfn 为发现次序,low 维护沿 DFS 子树与栈内边可回到的早期位置。树边回溯取 low[v];栈内已发现点取 dfn[v],出栈点不参与。st 为栈,ins 标记在栈,bel 为分量编号。

CPP
void scc(int u) {    dfn[u] = low[u] = ++tim;    st[++top] = u;    ins[u] = 1;    for (int v : e[u]) {        if (!dfn[v]) {            scc(v);            low[u] = min(low[u], low[v]);        } else if (ins[v]) {            low[u] = min(low[u], dfn[v]);        }    }    if (dfn[u] == low[u]) {        tot++;        int v;        do {            v = st[top--];            ins[v] = 0;            bel[v] = tot;        } while (v != u);    }}

枚举未发现点。dfn[u] == low[u] 时,从栈顶弹出至当前点,得到分量。跨分量边连 bel[u] -> bel[v],缩点必为 DAG,否则环上分量应互相可达。平行边按同一种转移计数时去重,按物理边区分方案时保留。时间、空间 O(n+m)。

割点与割边:等号不能混

无向图沿树边 u→v 回溯:lowv>dfnu 时该边为桥,lowv≥dfnu 时非根顶点 u 为割点。前者回不到 u,后者删 u 后回不到祖先。根单独看树孩子数,大于一才是割点。

邻接项 (v,id),物理边共用 ID;递归传入入边 ID,只跳过这条边,不能跳过所有通向父节点的边,否则两条平行边会被误判成桥。

CPP
void dfs(int u, int id) {    dfn[u] = low[u] = ++tim;    int cnt = 0;    for (auto t : e[u]) {        int v = t.first, j = t.second;        if (j == id) {            continue;        }        if (!dfn[v]) {            cnt++;            dfs(v, j);            low[u] = min(low[u], low[v]);            if (low[v] > dfn[u]) {                bri[j] = 1;            }            if (id != -1 && low[v] >= dfn[u]) {                cut[u] = 1;            }        } else {            low[u] = min(low[u], dfn[v]);        }    }    if (id == -1 && cnt > 1) {        cut[u] = 1;    }}

每个未访问块调用 dfs(s, -1)。割点、桥以删除后连通块数增加为准,孤立点不是割点。删去所有桥后的连通块构成边双连通分量;点双连通分量则可能在割点处相交,不能简单删除割点再把剩余块当作完整分量。时间、空间 O(n+m);不能照搬 SCC 的栈内边规则。深链递归可达 n 层,须核算栈或显式保存回溯状态。

树

DFS 序、欧拉序与最近公共祖先

首次进入 u 令 tinu=++tim,处理完孩子令 touu=tim,子树对应 [tinu,touu]。计时器只在首次进入时增加;点权放到 tin 位置,子树和转为区间和,用前缀和或树状数组维护。

倍增设 fu,0 为父亲,fu,j=ffu,j−1,j−1。根深度一、父亲零,零号整行置零;先填父亲表再处理孩子。取 2L>n,层号 0∼L−1。

CPP
int lca(int a, int b) {    if (dep[a] < dep[b]) {        swap(a, b);    }    int k = dep[a] - dep[b];    for (int j = 0; j < L; j++) {        if ((k >> j) & 1) {            a = f[a][j];        }    }    if (a == b) {        return a;    }    for (int j = L - 1; j >= 0; j--) {        if (f[a][j] != f[b][j]) {            a = f[a][j];            b = f[b][j];        }    }    return f[a][0];}

对齐深度后,仅在祖先不同时同时上跳,始终停在 LCA 下方。预处理与空间 O(nlog⁡n),查询 O(log⁡n)。根到点权和为 disu,距离为 disu+disv−2dislca(u,v),负权也适用。

欧拉序 LCA:进入顶点、从孩子返回时均记录当前点,长度为 2n−1。两点首次位置间深度最小的顶点为 LCA,不取最小编号。稀疏表预处理 O(nlog⁡n)、查询 O(1)。此序列重复记录顶点,不能与子树 DFS 序混用。

重心、直径与树上差分

设子树大小为 sizu,删点 u 后的最大块为 max(n−sizu,maxv 为孩子sizv);不超过 n/2 的点为重心。重心与边权无关;有一个或相邻两个,后序统计需 O(n)。

非负权树求直径,从任一点遍历到最远点,再从该点遍历取最远距离,合计 O(n);路径唯一,直接累计边权。负权时此法不成立:三点链边权 −100,5,从负边一端出发把自身当作最远点,漏掉权为 5 的直径。改用树 DP 合并向下路径;要求非空路径时答案初始负无穷,不能用零吞掉全负答案。

路径 u→v 加 w,令 p=lca(u,v)。点差分在 u,v 各加 w,在 p 和 p 的父亲各减 w,父亲为零时跳过。边差分在 u,v 各加 w、在 p 减 2w。修改后后序累加孩子差分到父亲:点差分得到点值;边差分的 du 对应父亲到 u 的边,根无对应边。LCA 处减一次还是两次,由点、边模型决定。共 q 次修改为 O(n+qlog⁡n)。

搜索:剪枝要能证明不会删掉答案

深搜中,可行性剪枝检查当前状态是否已经违反约束;最优性剪枝比较“当前代价+剩余代价下界”与已知答案。下界必须不大于实际剩余代价;例如覆盖剩余需求至少还要两个操作,就不能因经验判断而写成至少三个。相同状态合并时,要保留影响后续选择的全部信息:已用集合、剩余资源、所在位置等。只按当前位置记忆化,可能把拥有不同剩余资源的状态混为一谈。

迭代加深按深度上限递增,每轮重新清理与该上限有关的访问记录;IDA* 用 g+h 控制上限,h 仍须是可采纳下界。A* 按 g+h 出队,不能首次入队就封死状态;若启发式不一致,需要允许更优代价重新进入。双向 BFS 适用于可逆或能构造反向转移的无权状态图,每次完整扩展一层并正确判断相遇距离,有向图的反向搜索应沿反边走。按较小边界扩展可以减小搜索量,不改变距离的层次定义。

动态规划:状态、转移与优化依据

背包、区间与多维状态

0/1 背包对容量倒序,完全背包对正体积物品正序;分组背包的同组候选统一读取上一组状态。计数时,物品在外通常统计无序组合,容量在外、枚举末项通常统计有序序列。恰好使用资源的模型只有零资源状态可达,其余置为负无穷或单独标记不可达;不能用零代替不可达后直接做加法。

二维资源背包的状态可设为 f[j][k],0/1 物品对两个资源都倒序,时间为 O(nAB)。多维状态不是机械增加下标:每个维度应区分会改变后续转移的历史信息,能由其他量算出的维度可以删去。

相邻石子合并,设 f[l][r] 为把区间合成一堆的最小代价:

f[l][r]=minl≤k<r{f[l][k]+f[k+1][r]}+sr−sl−1。

单点初值为零,按区间长度递增枚举,时间为 O(n3)、空间为 O(n2)。环形问题复制一遍序列,只计算长度不超过 n 的区间,最后在所有长度为 n 的区间中取答案。未经证明,不能直接限制决策点的搜索区间。

树形 DP 与换根

树上最大权独立集有两种状态:选当前点与不选当前点。对每个孩子,前者只能接“不选孩子”,后者取孩子两态的较大值。若所有权值为负,答案能否为零取决于是否允许空集。

树形背包设 fu[j] 为当前已处理子树恰选 j 个点的最优值。合并孩子 v 时,转移为 g[j+k]=max(g[j+k],fu[j]+fv[k]);读取旧父数组或正确倒序,并检查两态均可达。若要求选点连通且包含 u,孩子选非空时才接入孩子根。朴素上界为 O(nm2),剪去不超过子树大小的无效容量后,还要按实际合并范围重新核算。

换根的两遍处理分别解决“子树内部”和“子树外部”。例如无权树各点距离和,第一遍求子树大小和根的答案;从 u 换到孩子 v 时,v 子树内的点都近一,其余点都远一,所以

ansv=ansu+n−2szv。

带权边把增量乘以边权;带点权统计则把点数换成点权和。总时间为 O(n)。

状压与子集枚举

每点至多访问一次、访问集合和当前末点共同决定后续路线时,设 f[S][u] 为恰好访问集合 S、停在 u 的最小代价,向不在 S 中的点转移。一般完全图需要 O(n22n) 时间和 O(n2n) 空间,位数与内存都要先核算。固定起点时,初始只有对应单点集合可达。

枚举非空子集可写成下面的循环;处理空集需要另列,不能让无符号或负数回绕决定终止。

CPP
for (int t = s; t > 0; t = (t - 1) & s) {    upd(s, t);}

所有集合及其子集一起枚举通常为 O(3n),因为每个元素有“不在集合、在集合但不在子集、在子集”三种归属。将它误记成 O(2n) 会低估一整层枚举。

数位 DP 与记忆化边界

例如统计 [0,X] 中数位和能被 k 整除的整数。令 a 为十进制数位的逆序数组,dfs(p, s, lim) 表示剩余最高位为 p、已填数位和模 k 为 s、是否贴住上界。f 初始化为负一,只缓存不受上界限制的状态。

CPP
ll f[20][K];int a[20], k;ll dfs(int p, int s, bool lim) {    if (p < 0) {        return s == 0;    }    if (!lim && f[p][s] != -1) {        return f[p][s];    }    int r = lim ? a[p] : 9;    ll z = 0;    for (int d = 0; d <= r; d++) {        z += dfs(p - 1, (s + d) % k, lim && d == r);    }    if (!lim) {        f[p][s] = z;    }    return z;}

要求 1≤k<K,计数不超出 ll。前导零不改变数位和,所以这例不需要另记“是否开始”;若统计数字出现次数、相邻位限制或有效位数,前导零就可能改变含义,必须加状态或单独处理。零本身在这例中计入。区间答案用 F(R)−F(L−1),约定负上界返回零。更换 k 或状态语义后清空记忆表;单次上界的时间为 O(log⁡X⋅k⋅10)。

常用优化先找转移结构

滚动数组要求旧层不再被其他转移读取。前缀和优化适用于连续区间上的加法汇总;单调队列适用于候选随窗口进入、过期且能按价值淘汰的转移。例如 fi=vi+maxi−R≤j≤i−Lfj,先加入 i−L,再删除小于 i−R 的下标,且不可达状态不进入队列。

LIS 维护每种长度的最小末尾,较小末尾能接上的后续元素包含较大末尾的选择;严格递增用 lower_bound,不下降用 upper_bound,尾值数组本身不保证是原序列的一条子序列。斜率优化要把转移展开为“关于查询量的一次函数”;若用单调队列维护凸包,插入斜率和查询横坐标的单调性都需成立。分治优化需证明最优决策点单调,不能仅因题目有“最小值”就套用。

数论:整除条件先于模意义下的除法

线性筛与欧拉函数

p 存质数,vis 标记合数,phi[1] = 1。每个合数由其最小质因子对应的一次乘法筛掉;遇到整除即停止,是线性复杂度的关键。

CPP
int p[N], phi[N], cnt;bool vis[N];void sie(int n) {    phi[1] = 1;    for (int i = 2; i <= n; i++) {        if (!vis[i]) {            p[++cnt] = i;            phi[i] = i - 1;        }        for (int j = 1; j <= cnt && p[j] <= n / i; j++) {            int v = i * p[j];            vis[v] = true;            if (i % p[j] == 0) {                phi[v] = phi[i] * p[j];                break;            }            phi[v] = phi[i] * (p[j] - 1);        }    }}

一次筛表的时间与空间均为 O(n)。大于一的整数可唯一分解为质数幂之积,不计因子顺序;试除一个质因子时要除尽,循环后剩余的大于一部分也要计入。分解 m 后,按每个不同质因子 q,将欧拉函数答案依次改为 ans / q * (q - 1)。欧拉定理 aφ(m)≡1(modm) 要求互质;非互质时不能随意把指数对 φ(m) 取模。例如模 8 时,21 与 25 不同。

质数模数 p 下,费马小定理给出非零元素的逆元 ap−2;威尔逊定理为 (p−1)!≡−1(modp)。后者不意味着用 O(p) 求阶乘就是高效的素性测试。

快速幂、裴蜀与扩欧

下列模幂要求 b≥0、1≤p≤2×109,使余数乘积落在 ll 内。扩欧要求参数非负且不全为零,其系数运算也要在类型范围内。

CPP
ll qpw(ll a, ll b, ll p) {    ll s = 1 % p;    a = (a % p + p) % p;    while (b > 0) {        if (b & 1) {            s = s * a % p;        }        a = a * a % p;        b >>= 1;    }    return s;}ll exg(ll a, ll b, ll &x, ll &y) {    if (b == 0) {        x = 1;        y = 0;        return a;    }    ll d = exg(b, a % b, y, x);    y -= a / b * x;    return d;}

裴蜀定理说明 ax+my=c 有整数解当且仅当 gcd(a,m)∣c,这也就是线性同余 ax≡c(modm) 的有解条件。扩欧先求出右侧为最大公约数时的一组系数,再乘 c/d,解的模数为 m/d。逆元存在当且仅当 d=1;合数模数也可能有逆元,关键是互质。

中国剩余定理与非互质合并

已有 x≡a(modm),加入 x≡b(modn)。写成 x=a+mt,就得到 mt≡b−a(modn)。下面原地更新 a,m,初始各余数已归一化、模数为正,所有中间乘法与合并后的模数均须能用 ll 表示。

CPP
bool crt(ll &a, ll &m, ll b, ll n) {    ll x, y;    ll d = exg(m, n, x, y);    ll c = b - a;    if (c % d != 0) {        return false;    }    ll k = n / d;    ll t = c / d * x % k;    if (t < 0) {        t += k;    }    a += m * t;    m *= k;    a %= m;    return true;}

新模数是最小公倍数,不是直接相乘。合并 x≡1(mod4) 与 x≡3(mod6) 得到 x≡9(mod12);若第二式余数改为 2,差值不能被最大公约数 2 整除,无解。单次合并做一次扩欧;大模数还需安全模乘或更宽的中间类型。

组合计数:先区分对象是否可辨

对互不相同元素,排列数为 n!/(n−r)!,组合数为 (nr);有重复元素的全排列为 n!/∏ci!。从 n 类物品中允许重复地选 r 件、只看各类数量,答案为 (n+r−1r),不是普通组合数。n 个互异元素按旋转视为相同的圆排列为 (n−1)!;是否再把翻转视为相同,需要另行判断对称关系。

质数模数下用阶乘和逆阶乘求组合数,要求预处理上限小于模数;超过模数时阶乘可能为零,不能继续取逆。二项式定理提供 ∑k(nk)xkyn−k=(x+y)n,鸽巢原理则常用于证明重复、余数碰撞或下界,不是直接代入一个计数公式。

错排列满足 D0=1,D1=0、Dn=(n−1)(Dn−1+Dn−2):先确定一个元素的去处,再按它与目标元素是否互换分两类。卡特兰数可写成

Cn=(2nn)−(2nn−1)。

它统计前缀左括号数不小于右括号数的合法括号序列;用反射将首次越界的路径对应到第二项。模意义下使用差式可避免直接除以 n+1,但求组合数本身仍需满足算法前提。

容斥把所有限制同时满足的数量写成 ∑S(−1)|S|F(S),其中 F(S) 统计至少违反集合 S 中全部限制的对象。求若干集合的并时,改为只枚举非空集合、符号取 (−1)|S|+1。统计同时被若干数整除,用最小公倍数而非乘积;最小公倍数已超过上界时,贡献为零,及早终止也能避免溢出。

矩阵、高斯消元与几何运算

矩阵快速幂

线性递推写成列向量 vt+1=Avt,则 vk=Akv0。状态中要保留递推需要的历史项;含常数的仿射递推,可添一维恒等于一的状态。幂为零返回单位阵。矩阵乘法中 Cij=∑kAikBkj,顺序不能交换;朴素乘法的矩阵快速幂为 O(d3log⁡k)。模运算应在累加中取模,并保证每次乘法仍不溢出。

转置满足 (AB)T=BTAT。初等行变换是交换两行、非零倍乘一行、将某行的倍数加到另一行,消元依靠它们保持方程组的解集。

实数高斯消元

a[0..n-1][0..n] 为增广矩阵,最后一列是常数。每列选绝对值较大的主元并消去其余行;返回零、一、二分别表示无解、唯一解、无穷多解。

CPP
double a[N][N], eps = 1e-9;int n;int gau() {    int r = 0;    for (int c = 0; c < n && r < n; c++) {        int t = r;        for (int i = r + 1; i < n; i++) {            if (abs(a[i][c]) > abs(a[t][c])) {                t = i;            }        }        if (abs(a[t][c]) < eps) {            continue;        }        swap(a[t], a[r]);        double v = a[r][c];        for (int j = c; j <= n; j++) {            a[r][j] /= v;        }        for (int i = 0; i < n; i++) {            if (i == r) {                continue;            }            v = a[i][c];            for (int j = c; j <= n; j++) {                a[i][j] -= v * a[r][j];            }        }        r++;    }    for (int i = r; i < n; i++) {        if (abs(a[i][n]) > eps) {            return 0;        }    }    return r == n ? 1 : 2;}

唯一解时,第 i 个未知数为 a[i][n]。跳过没有主元的列,所以秩可能小于未知数个数;不能把“主元接近零”直接等同于无解,矛盾发生在系数全零而常数非零的行。时间为 O(n3)、空间为 O(n2)。浮点阈值要结合量级;病态矩阵仍可能放大误差。质数模数下消元改为乘逆元并判断精确的零,合数模数不能随意除以非零元。

向量与几何边界

二维向量点积 u⋅v=uxvx+uyvy 判断夹角、投影与垂直,叉积 u×v=uxvy−uyvx 判断方向及有向面积。线段相交不能只比较叉积异号:共线时还需检查包围盒是否相交,端点接触也要按题意纳入。整点运算可保持精确,但叉积中间乘法必须容纳坐标差的乘积;浮点几何则需统一误差与边界约定。

微信 · 果壳一只羊

 

长按识别二维码,添加微信;请注明来意。

公众号 · 三金家的Y同学

 

长按识别二维码,关注公众号。

YZY-WIKI 目录

0x0
0x1
0x2
大纲框架
算法思想
语言基础
基础算法
实战技巧
一车板子
信竞课件
笔记系列
数据结构
树上专项
图论合集
动态规划
字符串
搜索合集
线性规划
Y题多解
计算几何
多项式
博弈论
算法论文
数学合集
数学思维
数论专项
组合数学
线性代数
抽象代数
概率论
GESP训练
题目推荐
好题选讲
模拟赛
初赛合集
一车八百
一车蓝题
一车紫题
一车黑题
DP题目
图论题目
数论题目
思维精选
会员服务
交流群
养生计划
果壳杯
Y言蔽之
岱陌存理
趣味对话
漫谈系列
杂项合集
抽象大赏
漫画算法
Y言Y语
组合精选
交流社群
OI模拟赛系列
NOIP模拟赛
CSP-J第二轮模拟赛
GESP模拟赛
DTOJ平台
CSP-S第二轮模拟赛

相关学习资料