「批注」大纲

去年写的那个 trick 真的成小丑了感觉。

推荐大佬的总结:1 C202044zxy Alex_Wei

时隔 1 年,我不知道和去年相比,这篇批注是否会更加清楚透彻简洁。

鉴于个人能力与未来发展方向,故只写 8 级以下知识点。

感觉还是有一点乱。并且有些东西大纲是没有的,后面还要补上。

dp 和贪心可能会单独写。dp 参考

last upd on: 10.22 22:17


2.1 入门级

2.1.3 数据结构

  • huffman 树:求最小权的树,权定义为所有点权值乘深度的和。
    • 荷马史诗里面要求儿子不超过 \(k\),需要补 0 过后用堆贪心,每次选小的早一步合成作为更深点。

2.1.4 算法

3. 基础算法

  • 贪心

    • 临项交换法。用 2 个数的最佳排序方式推广到 \(n\) 个数上。Sample
    • 某些 dp 需要先贪心地确定顺序,即:如果 2 个数都要选,那么这两个数选入的顺序必须是最优的。而由于我不知道最终要选哪几个,所以用 dp 进行决策。Sample 1 2
    • 反悔贪心。根据选择和反悔的决策,维护对应的堆,注意代码细节多。Smaple 1 2
  • 二分 & 三分

    • 对于判定性的二分,可以在 \(l,r\) 的边缘取常数个点进行判定,若失败则继续二分,用于卡常
    • 三分可以当 \(r-l\le k\) 的时候就停下来,最后再暴力跑 \([l,r]\) 的答案,避免被平台/精度卡。
  • 倍增

    • st 表。注意 query 的时候分成的 2 个区间是有交的。这道题需要利用这一点。
    • 给几个普通倍增的题:1 2:基环树 \(k\) 级祖先。对于每次操作方式相同的题要特别注意倍增。

4. 数值处理算法 高精度

一般来讲 __int128 就够了。

5. 排序算法

  • 冒泡排序:考点较多。参考 this 以及 this,都是逆序对的转化以及考虑一个元素前面有多少个比他的大,记为 \(f_i\)。(一轮排序后 \(f_i\) 如果非 0 则改变 1)

  • 计数排序:基本上没用。注意下面代码最后一行倒叙遍历是考虑情况:值相同但要保留输入顺序。

void counting_sort() {
  for (int i = 1; i <= n; ++i) ++cnt[a[i]];
  for (int i = 1; i <= w; ++i) cnt[i] += cnt[i - 1];
  for (int i = n; i >= 1; --i) b[cnt[a[i]]--] = a[i];
}

8. 动态规划

  • 背包 dp

    • 多重背包的二进制优化,枚举模 \(v_i\) 的余数然后用单调队列优化。
    • 对于某些背包转移(同一个体积有多个物品 / 每个物品选不同次数有不同代价)在 \(V\) 这一维有决策单调性。Sample 1 2
    • 根号分治
    • 如果只是判定合法,则可使用 bitset 优化。
    • 分组背包然后合并。Sample
  • 区间 dp

    • 有些题目的合法区间具有可合并性,即:\(f(l,r)=f(l,k)|f(k+1,r)\),此时若要计数,则对于转移,必须钦定一边的区间是没有合并形成的合法区间,即 \(dp(l,r)=g(l,k)\times dp(k+1,r)\)Sample 1 2
    • 区间 dp 多数时候都要升维,因为仅凭区间的限制是完全不够的。Sample 1 2
    • 或者使用辅助数组 \(g\) 帮助转移,这是其他 dp 也很常用的方法。(dp 的互相转化)Sample

2.1.5 数学与其他

  • 更相减损术的高级运用:P10463 Interval GCD。维护差分数组,就可以做到单点修改,区间查询 gcd。

2.2 提高级

2.2.2 C++ 程序设计

  • set 方面注意 ODT 的写法。Sample

    • split 是将一个段分开,并返回以 \(x\) 为开头的迭代器。update 中要先 split 右边,erase 删除是左闭右开。如果你先 query 再 upd 复杂度就是正确的。
    • 对于一般的题目随机数据下期望满分。可用于乱搞
  • 注意 map 可以优化某些状态数大,但可用状态不多的 dp。可以用于普通 dp 的乱搞详见

2.2.3 数据结构

1. 线性结构

  • st 表在 query 时分成的两个区间是相交的,可以根据这个东西处理一类 \(f(L,R)=\bigcup f(l_i,r_i)\),并要求 \((l_i,r_i)\) 相交的问题:CF1707E。

  • 利用双栈维护双端插入删除,均摊复杂度线性。

2. 集合与森林

  • 并查集可以维护序列里面一段连续的 0/1。

3. 特殊树

  • BIT 上二分的写法:记住 \(Sum(S)=Sum(S/\operatorname{lowbit(S)})\operatorname{ op } f(S)\)。从大到小枚举 bit 即可。Sample

  • Trie 的全局加 1 操作:倒着建树,每次交换左右儿子,并递归到交换之后的 0 儿子。Sample:fusion tree。

  • 笛卡尔树:注意构造方法,维护一条单减链。每次单调栈扫完之后建边。

  • 平衡树:序列问题要维护相对顺序,使用文艺平衡树。注意 split 的引用参数写法。真题:列队。

    • 以及和线段树类似的 pushup。放驼使

5. 哈希表

  • 字符串哈希:一般取单模数 \(10^9+7\)

  • 树哈希:找到重心,调用 xor shift 做和哈希。如果有两个重心就取哈希结果的 min。

  • 和哈希/异或哈希:判断两个可重集是否相等,考虑 xor shift 的写法,注意为了防止被对着卡,mask 变量可以设计成随机数。

mt19937 myrand(chrono::steady_clock::now().time_since_epoch().count()); // 推荐种子
ull shift(ull x) {
  x ^= mask;
  x ^= x << 13; x ^= x >> 7; x ^= x << 17;
  x ^= mask;
  return x;
}
  • 为了避免哈希冲突,对于有 2 个相关联的哈希的题目,应使其哈希方式不同。比如一个使用和随机数,另一个使用 xorshift 后进行累加。Sample

  • 某些题目转化哈希过后满足判定条件的解可能并不是我们最终希望的解,但若此情况出现概率极小,则不考虑。比如星战中我们要求所有 \(k_i=1\),但满足方程的 \(k\) 显然不止这一组,但是那样的 \(k\) 非常难构造(而且一般很大),于是忽略。

2.2.4 算法

3. 分治算法

  • 某些矩阵计数的题可以考虑分治列,然后枚举上下行的边界,计算跨过 \(mid\) 的贡献。注意每次要对于跨度大的一维分治,这样分治总的复杂度才是 \(O(nm\log n)\)

  • 这道题是先考虑了 \(S=2\) 的情况,然后发现每次跨越 \(mid\) 时,每一列只需要指定匹配于另一列就一定可以通过后续调整有解。所以相当于是做 \(\log 2^k\)\(S=2\) 的情况就等价到了 \(S=2^k\)。(注意很多时候题目都会提示 \(S=2^k\) 这种信息,一定要考虑到分治)

  • cdq:考虑左边对右边的贡献,很多时候为了方便计算,通常先计算左边,然后让左右各自内部排序,计算跨越 \(mid\) 的答案,然后再计算右边。

  • 整体二分:算法思路是对询问和修改分治,把答案在 \(mid\) 两边的都分开。多数题目都是【求第 \(k\) 大】,这时要把小的那边的询问减去大的那边的个数。可以看道基础题

5. 字符串相关算法 KMP only

  • 建议频繁复习。筛选了好久选出来的讲解。本质上是一旦失配就一直向前跳 border。

  • Tips:能用 hash 的时候尽量用,最后才想 Kmp,比如这道题直接哈希加暴力就过了。(谁不更喜欢思维量更小的做法?)

  • bitset 在字符串匹配里的妙用。这玩意好写好用到离谱。

  • border 理论

    • 字符串有长为 \(l\) 的 border,则其有周期:\(|s|-l\)
    • 字符串本质不同的 border 数是对数级别。也可以说如果存在长度大于一半的 border,则在交的那部分一定有一个更小的 border。Sample 1 2
    • 弱周期定理:对于周期 \(p>q\),有 \(p-q\) 仍为周期。不断重复使用更相减损术得到 \(\gcd(p,q)\) 也是周期。
  • (后面看到还有一些 8 级的字符串算法,暂时可能不会总结,如果我后面写了会把这句话删掉的。)

6. 搜索算法

  • 注意有个算法叫折半搜索 / meet in the middle。可以将时间复杂度开方。例:真题 使方案数减半

  • A*:设计估价函数 \(H(i)\) 表示最优情况下 \(i\) 状态到终点的花费,若此时已经超过限制/劣于当前最优解,那么不必再搜索。

    • 可用优先队列维护 \(F(i)+H(i)\),每次取最优的更新,保证第一次可以找到最优解。可能的应用:K 短路。
  • 如果你写暴力/搜索,时刻注意能否记忆化,说不定会多很多分。

7. 图论算法

  • 二分图一些拓展

    • 判定有两种方式:1 是 dfs 点染色;还有 2:扩展域并查集(线段树分治会用)。
    • 匈牙利匹配的复杂度是 \(O(nm)\),多数情况都不满。给个比较综合的题
    • 某些图论题可以考虑 二分图 和 奇环 的时候分别怎么做,最后拼在一起即可。
  • 欧拉回路:《当看到图论题没有思路时,想想欧拉回路》

    • 欧拉回路构造方法:当前弧优化,dfs 完后入栈。不涉及构造,但涉及思想
    • 一类套路题是把问题转化成图论模型后,要求把所有边重定向,使得每个点入度和出度相同。此时可以考虑欧拉回路,记录每条边是正是反。例题:1 2 3
    • 有时要求入度和出度相差不超过 1,可以加入一个超级源点,连向所有奇点(只有偶数个)。
  • 有向无环图(DAG)

    • 可以反向边 topo dp。
    • 可以状压枚举子集给图分层 / dp。
  • 双连通图:注意 tarjan 的写法。下面主要是记录一些结论,也可能有强连通分量。

    • 边双严格强于点双。即:一个点双一定是边双
      • 考虑构造图:一个点上面接 2 个只有这个公共点的环。
    • 点双中若包含一个奇环,则所有点都在至少一个简单奇环上。出处
    • 若点双中没有偶环,则其中只有 1 个奇环。出处
    • 给图加最少的边使其是一个大边双:边双缩点后的树的叶子数量对 2 上取整。出处
    • 一定存在一种为一个边双连通分量中所有边定向的方案,使其成为一个强连通分量。出处(构造方案)
    • 论文题(Alex 的题解里面涉及了很多点双的结论)。结论题
  • 最小生成树

    • 次小生成树和 mst 之间最多只有 2 条边不同。
    • 完全图 mst 考虑用 Boruvka 的思想。每次对连通块之间作启发式合并:Sample
    • Kruskal 重构树:本质是 kruskal 算法的一个结构形式。这棵树上有许多优秀的性质。
      • 是一棵二叉树,点权表示每条合并的边权(对于每个在 mst 上的边)。
      • 原图中 \((x,y)\) 之间的所有简单路径上最大边权的最小值 = 最小生成树上 \((x,y)\) 之间的简单路径上的最大值 = Kruskal 重构树上 \((x,y)\) 的 LCA 的权值。基于这个限制:简短的背包
      • \(x\) 经过不超过 \(w\) 的边权所能到达的点集:重构树上 \(x\) 的最远的点权小于等于 \(w\) 的祖先 \(y\) 的子树内的所有点。应用
  • 最短路

    • Floyd 算法:

      • 可以用于求传递闭包,bitset 优化至 \(O(\frac{n^3}{w})\)
      • 动态修改一条边每次更新多源最短路复杂度每次是平方。
    • SPFA

      • SLF 优化:

        if (q.size() and dis[q.front()] >= dis[v]) q.push_front(v);
        else q.pb(v);
        
      • 差分约束:求最小值则求最长路,否则最短路。详细见。不保证一年前我理解正确。

    • Dijkstra

      • 根据其松弛的思想,可以解决许多有后效性的环 dp,即每次强制选择最符合条件的那一个环上的入队。Sample 1 2
      • 点权最短路每个点只会被松弛一次,可以优化某些完全图最短路
    • 最短路树/图例题

      • 很多时候只选择一个最短路建树是完全不影响的。
      • 显然:只有更改最短路树上的边才有可能影响最短路。
      • 最短路图如果有向则一定是一个 DAG。
  • LCA几种求法和代码。维护路径简单信息:倍增;复杂信息:树剖;要求快速查询:欧拉序。

  • 树的重心、直径

  • dsu on tree

8. 动态规划

  • 树形 dp / 背包

    • 树上背包的平方写法。
    • 换根 dp
    • 某些 dp 例如:\(dp_{x,0}=\sum dp_{to,1} \prod_{v\neq to} (dp_v+1)\)。要乘上逆元,但是无法处理除数为 0 的情况。此时可以需要维护前后缀积
    • 可能有时候需要考虑是否随便选 1 个根就可以计算所有答案了。
      • 如果不行可能需要换根 / 点分治优化 dp。比如经典连通块背包
      • 或者把每个根都跑一次,最后去重。Sample
    • 树上连通块背包:很多情况都是要子树合并,通常复杂度是 \(O(m^2)\)。此时我们按照 dfn 的倒序,每次对 \(f_i\) 加入单点:\((f_{i+1},I(i))\) 或者 \(f(r_i+1)\)。其中 \(r_i\) 表示 \(i\) 的子树内最大的 dfn。每次单点加入的复杂度就是 \(O(m)\) 了。
    • 树上依赖背包:每次合并子树的时候强制 \(x\) 选物品即可(子树任意),这样就保证了如果一个数选了那么它到祖先的链上都必然选。Sample
  • 状压 dp

    • 枚举子集的复杂度是 \(O(3^n)\)。枚举子集的子集复杂度是 \(O(4^n)\)
    • 很多时候可以钦定 上一维转移过来/强制满足某些条件 的是 \(\operatorname{lowbit}(S)\) 来优化复杂度/避免算重。此题中枚举了 lowbit 所在的连通块进行容斥。此题预处理时将 \(S\) 依次自减 lowbit。
  • dp 优化:

    • 斜率优化:mine。强烈建议写李超,好写好调万能。
    • 其他优化:
      • 数据结构优化 dp。
      • 将判定性状态改为一般状态。例如,若 \(f(l,r)\) 表示区间 \([l,r]\) 是否合法可以改为:\(dp(l)\) 表示使 \([l,r]\) 合法的最大的 \(r\)Sample 1 2
      • 如果值域较小,可以交换值域和状态。

2.2.5 数学与其他

原谅我数学太菜,所以可能会写一些 sb 的东西。

宣传一下:总结 1 总结 2

2. 初等数论

  • 欧拉定理:若 \(\gcd(a, m) = 1\),则 \(a^{\varphi(m)} \equiv 1 \pmod m\)

  • 欧拉函数:注意欧拉反演:\(n=\sum_{d|n} \varphi(d)\)应用

  • 威尔逊定理:任意 \(p\),都有 \((p-1)!\equiv -1 \pmod p\)应用

  • 裴蜀定理:设 \(a_1, a_2, \dots, a_n\) 均是不全为零的整数,若存在整数 \(x_1, x_2, \dots, x_n\), 使得 \(a_1 x_1 + a_2 x_2 + \cdots + a_n x_n=d\),则 \(d = \gcd(a_1, a_2, \dots, a_n)\)应用

  • 扩展欧几里得算法:解方程 \(ax+by=1(a\bot b=1)\)。也可以推广到解 \(ax+by=c\),但必须 \(\gcd(a,b)|c\)

    • 该算法求出的解满足:\(|x|+|y|\) 在所有解中最小。
    • 可用于求解逆元 \(ax\equiv 1\pmod p\),故要求 \(a\bot p\)
  • 中国剩余定理

3. 离散与组合数学

  • 组合、排列

    • 圆排列
    • 盒子与球
    • 卡特兰(Catalan)数
  • 容斥

    • 普通 dp 不好做考虑容斥,把不合法的个数计入状态计算。有些时候可以把转移系数 \((-1)^i\) 直接在转移时加上,不计入状态。Sample
    • \(f(i)\) 为所有方案数,\(g(i)\) 为合法的方案数,则对于要求联通的问题,可以考虑广义容斥:\(g(S)=f(S)-\sum_{x\in S}g(x)f(S / x)\)。一般 \(f\) 是好算的。Sample 1 2
  • 斯特林(stiring)数

4. 线性代数

  • 矩阵

    • 邻接矩阵自乘 \(k\) 次方就是图上恰好\(k\) 的到达情况。
    • 很多时候可以加入一个一维向量来降低矩乘的复杂度。
    • 很多时候事件是分段的,我们可以对每一段都做矩阵乘法。Sample 1 2
    • 某些题并不支持 \(O(L^3)\) 的运算,此时我们需要式子变形使 \(L\) 变小。一种方法就是把矩阵乘法根据结合律改变乘法的顺序。
  • 高斯消元

    • 当环 dp 变量数过多时,考虑计算出一部分没有后效性的 dp,然后再用高斯消元求解有后效性的部分。这里\(O(n^{4.5})\) 优化到了 \(O(n^3)\)
    • 部分稀疏矩阵的高斯消元可以优化到线性。Sample
  • 线性基

    • 环上最大异或和直接暴力 dfs,进了一个环就直接把当前异或和加进线性基即可。
    • 本来就不怎么考。详细内容见:here

2.3 NOI 级

2.3.2 数据结构

3. 复杂树

  • 树剖

    • 维护每条重链上前后缀和和整体和。
    • 对于树上路径问题,可以提前把链拉出来,转化为序列问题。Sample
  • 长剖

5. 可持久化数据结构

  • 主席树

    • 在树上可以按照深度 / dfn 序建。
    • 标记永久化:可持久化过后不支持 pushdown 操作,所以我们不下传操作;在询问的时候一路累加标记即可。参考
  • 可持久化并查集。本质是可持久化数组。某些时候可以和 kruskal 重构树互换。

2.3.3 算法

1. 算法策略

  • 分块

    • 值域分块(治):以阈值 \(B\) 为分界,设置不同复杂度的算法,然后结合。可以理解为把 2 个暴力拼起来。Sample1 2 3

      • 狭义:对每个块维护整块的信息,查询跳整块,满足条件再跳单点即可。和莫队结合
      • 一个总长度为 \(L\) 的序列集合里,本质不同的长度只有 \(O(\sqrt L)\) 个。字符串同理。
      • 专题:根号分治在背包上的运用。其实不只是背包,只是背包问题有启发性。
    • 数论分块

      • 向上取整:对 \(x/i\) 向上取整等价于 \((x+i-1)/i\) 向下取整。实现
      • 如果每次计算是 \(O(\sqrt{\frac{V}{i}})\) 的,整体复杂度就是 \(O(V^{3/4})\)Sample 1 2
    • 序列分块

      • 根号重构。
  • 离线处理思想:

    • 莫队

      • 带修莫队:
      • 回滚莫队:
      • 决策单调性里面也可以用类似莫队指针的移动来计算代价,均摊线性。
    • 扫描线

      • 一种是维护所有 \(x\le r\) 的点的信息(离线树状数组),另一种是只维护 \(x=r\) 时的 \(y\)(经典线段树扫描线)。
  • 复杂分治思想:线段树分治

    • 不要使用基于均摊复杂度 / 难以撤销的算法。如并查集不可路径压缩。
    • 注意所有数据结构在当前分治完后都要回退到之前的状态。例如并查集需要用栈来存储每次更改,最后依次撤销。
    • Sample 1 2 3 4 5
  • 构造

    • 极力推荐Ad-hoc 题目总结
    • 对于多个位置的还原问题,想想 3 个怎么做,然后延伸到 \(n\) 个。(一般情况下 2 个是不行的)Sample 1 2
    • 多打表。在小数据的多种构造方式中选择最容易推广的那一种。Samlpe

3. 图论算法

  • 基环树总结

    • 找环建议直接 dfs。
    • 核心就是先想树怎么做,然后再想环,最后再想树 + 环怎么做。
    • 某些题还可以考虑删掉环上某一条边,变成树的问题。但是复杂度平方。Joker
  • 2-SAT

    • 线性做法:注意解的构造根据 \(i\)\(inv(i)\) 中谁 scc 编号更小输出。(scc 大小是逆 topo 序,scc 大的先被遍历)
    • 平方的 dfs 做法:可以用于构造满足字典序限制的解。这道题做法就是枚举前面几位相同,再判断后面随便填是否合法,当然还是要尽量选择小的。(注意这里平方都指 \(O(nm)\)

2.3.4 数学与其他

5. 概率论

  • 期望 dp。
    • 转移时有必要记录每个转移方向的概率。对于某些终点状态确定的题可以定义状态为从当前到终点的期望,转移概率就是转移变量个数的倒数。Sample 2
    • 可能转移出现自环,此时直接化简式子,然后用其他方法算出一个 dp 值,以合适的顺序进行递推。如果此法不行,则需高斯消元。
posted @ 2024-10-16 22:18  LCat90  阅读(65)  评论(0编辑  收藏  举报