算法学习笔记(∞):杂项

算法,学习,笔记,杂项 · 浏览次数 : 16

小编点评

**生成内容时带简单的排版** * **使用 **printf()** 函数**打印**内容时,需要将 ****数据**按照 ****顺序** 排版。** * **使用 ****字符串** **拼接** 内容时,需要将 ****数据**按照 ****顺序** 排版。** * **使用 ****格式** **输出**内容时,需要将 ****数据**按照 ****顺序** 排版。** * **使用 ****变量** **存储**数据时,需要将 ****数据**按照 ****顺序** 排版。** * **使用 ****循环** **遍历**数据时,需要将 ****数据**按照 ****顺序** 排版。** * **使用 ****控制**语句** **控制**数据时,需要将 ****数据**按照 ****顺序** 排版。** **以下是一些示例:** ```c // 按顺序打印字符串 printf("hello world\n"); // 按顺序拼接字符串 printf("%s\n", "hello world"); // 按顺序输出格式 printf("%d\n", 123); // 按顺序存储变量 int age = 30; printf("%d\n", age); // 按顺序遍历变量 for (int i = 0; i < 10; i++) { printf("%d\n", i); } // 按顺序控制变量 if (i == 5) { printf("hello world\n"); } ``` **注意:** * 排版需要在 ****代码**中进行 ****排序**。** * ****数据****的排序顺序可以根据需要进行 ****调整**。**

正文

杂项

\(\mathcal {I~Hate~NTT~}\)

代码规范

  • 边界开闭找清楚

  • 指针空悬判清楚

  • 时空复杂算清楚

  • 特殊情况模清楚

  • math.h 加清楚

  • 思路正确证清楚

卡常小技巧

  • using uint = unsigned int,在代码中非负的地方都用 uint,需要注意溢出的可能,尤其是两者相减的时候。

  • 利用 fread 优化读入,主要是代码有点长。

  • 取模的时候有 x >= mod && (x -= mod); 优化掉 if

算法优化的本质

优化算法其实就是对于信息的充分利用,将信息的价值最大化。

无论是性质还是什么的,都属于信息的部分。

记忆化搜索

这是最核心的方法,是开始,也是终止。

记忆化搜索是最简单的,但是也是最直观的符合优化算法的本质的方法。

核心在于利用变量中的不变量,以此求出不同状态下的答案。

重点就转化为状态的定义,状态不应该和方案相关,它可以是一个性质,一个区间,一个点,甚至一个边。而状态也不应该被后面的状态限制。

基于边的记忆化

这是个小技巧,已经用这个方法切了两道 \(T1\) 了 QwQ。

其用的前提在于对于一条边,经过前无论状态如何都不会影响后面的答案。

其复杂度为 \(O(m)\),但是需要带上 \(6\) 倍的常数,不过可以接受。

动态规划

属于是特类的记忆化了。通过增量维护某个信息,然后基于前面某个状态的信息求解。

DDP 就属于动态的动态规划,本质在于维护增量的叠加,然后统一转移。

补充:关于 dfs 的一点性质

  • 在无向图上 dfs 不存在横边,要么是树边,要么是返祖边。

树上每一个点求答案

目前有三种套路:

  • 换根 DP

  • 基于边的记忆化

  • 找到点对于其他点的贡献

如何理解依据边的记忆化搜索:可以理解为对于一个点,不同的根会造成不同的父亲,于是记忆化每一个点给不同父亲的贡献即可。

计数题

一般来说,还是有套路的:

  • 组合计数,利用组合数直接求解。

  • 转换计数,也就是转换模型,再利用组合计数。而转换也有一定的套路:

    • 转化为操作之间的关系然后计数。有 \(2023.08.23\) flip\(..08.26\) explorer\(..08.28\) ring
  • 加法原理,分情况讨论。

  • 容斥原理,总方案数好算,但是目标方案数不好算,就可以考虑这么算。有 \(2023.08.28\) ringmex(题解做法),\(..08.30\) au

  • 乘法原理,将答案分成两个部分,并且每个答案两个部分间有共用的部分。有 \(2023.08.28\) mex

关于仙人掌 DAG 的拓扑序计数

关于微扰贪心的证明

参考例题:[yLOI2019] 梅深不见冬 - 洛谷皇后游戏 - 洛谷[NOIP2012 提高组] 国王游戏 - 洛谷。其中皇后游戏极其经典。

考虑证明该如何证明,据此看来有两种证明方法:

  • 假定上一个状态固定,考虑这一次先放 \(i\) 再放 \(j\) 或者反过来的贡献。可能会存在类似于 \(\max(last, a) \le \max(last, b)\) 的存在,可以通过分讨,与题意产生神秘的冲突然后消掉。于是可以得到 \(a \le b\) 的某种偏序关系。

  • 也可以考虑从 \(0\) 开始,也就是没有初始的情况,得到一个结论,然后归纳证明。

这种题该是有比较明显的特征,也就是最小化最后一个值,并且是需要枚举一个排列(代表着朴素的做法为 \(O(n!)\)。所以我们把它变成一种偏序,并以此作为排列依据求出最合理的排列即可。

组合数前缀和

\[S(n, m) = \sum_{i = 0}^{m} {n \choose i} \]

考虑莫队转移:

\[S(n, m) = S(n, m - 1) + {n \choose m} \\ S(n, m) = 2 \times S(n - 1, m) - {n - 1 \choose m} \]

而还有一种 \(O(n \sqrt n)\) 预处理,\(O(\sqrt n)\) 询问的方法(其实还可以减少其常数以提升其上界),如图:

每隔 \(\sqrt n\) 设置一个断点,在杨辉三角上向上跳即可。

预处理空间是 \(O(n \sqrt n)\) 的,但是可以通过离线变为 \(O(\sqrt n)\),如果在每一层微调,那么可以做到 \(\sum_{i = 1}^n \sqrt i\),虽然还是 \(O(n \sqrt n)\),但是小常数。

单位根反演

\[[n|a] = \frac 1n \sum_{k = 0}^{n - 1} \omega_n^{ak} \]

考虑求 \([x \equiv y \pmod p]\),有 \(p | x - y\)

于是

\[= \frac 1p \sum_{k = 0}^{p - 1} \omega_p^{(x - y)k} \\ = \frac 1p \sum_{k = 0}^{p - 1} \omega_p^{xk} \omega_p^{-yk} \\ \]

P5591

利用有封闭形式的式子和单位根反演可以批量生产好题

\(O(n^2)\) 状态求和

对于求形如:

\[\sum_{l = 1}^n \sum_{r = l}^n f(l, r) \]

有一些些套路。

  • 类似扫描线的思路,\(O(n)\) 扫描右端点,考虑利用数据结构或者性质维护,更新后缀和。

  • 分治,考虑分治经过 \(mid\) 的线段的贡献,只要可以对于每一个左端点 \(O(1)\) 或者 \(O(\log n)\) 求,就可以做到 \(O(n \log n)\) 或者 \(O(n \log^2 n)\)

    不过需要注意边界的问题,对于 \(mid\),两个端点的区间为 \([l, mid], (mid, r]\),或者采用左闭右开区间 \([l, mid), [mid, r)\)

矩形式子求和

对于求形如:

\[\sum_{i \le x, j \le y} f(i, j, x, y) \]

类似的也有一些些套路:

  • \(O(n^2)\) 枚举上下端点,考虑 \(O(n)\) 求解,总复杂度为 \(O(n^2 m)\)

  • \(O(\log(nm))\) 分治矩形,考虑 \(O(nm)\) 合并,总复杂度为 \(O(nm \log (nm))\)。类似于一维分治,需要注意区间边界问题:\([l, mid], (mid, r]\)

  • 考虑 \(O(n \log n)\) 基于上一行求此行,总复杂度为 \(O(nm\log n)\)

  • 如果满足区间加的信息,可以考虑 \(nm\log n \log m\) 预处理分解矩阵,每次矩阵求和(合并)做到 \(q \log n \log m\)。有点类似于 Spare Table 的小常数区间和的求法。

\(O(n^2)\) 状态 \(O(n)\) 单点问题

这是对于上面两个分点的一个小小的汇总。

都可以利用类似分治的思路。将求答案的部分小小合并一下。

如何分?考虑一般的套路:

  • \([l, mid]\)\((mid, r]\) 的分治。

  • 枚举长度 \(len\),考虑 \((i - len, i], [i, i + len)\) 的贡献。也就是撒点,然后求,也就是 NOI 优秀的拆分的正解做法。

  • 扫描线的思路

  • 考虑优化 \(O(n)\) 求解的复杂度

CDQ 分治

本质上是考虑数据分治后产生单面贡献的情况。

例如多维偏序,半在线卷积等都可以利用 CDQ 分治。

FFT 循环卷积

\[f^m \bmod (f^n - 1) = f^{m \bmod n} \]

也就说如果空间没有开够,那么溢出的贡献不会消失,而是转移到最前面。

减法卷积 优化时空常数。

对于:

\[h_i = \sum_{j = 0}^n f_j g_{i + j} \]

翻转 \(f\) 有:

\[h_i = \sum_{j = 0}^n f_{n - j} g_{i + j} \]

发现,\(h_i\) 实际上是得出卷积的第 \(i + n\) 项。

如果取长度 \(2n\),溢出的部分贡献到 \([0, n)\),不会对 \([n, 2n)\) 造成影响,所以可以放心大胆的溢出。

常数优化 \(\frac 23\)

根号多项式算法

主打一个短代码,小常数。

但是最难的是整理出根号式子。

斯特林变换

遇到 \(n^k\) 考虑转化为斯特林数:

\[n^k = \sum_{i = 0}^k {k \brace i} n^{\underline i} \]

然后进行种种神秘的操作变化式子。

多项式的多项式前缀和

对于 \(n\) 阶多项式 \(f\) 已知 \(f(0), f(1), \cdots, f(n)\)。需要求:

\[S(x) = \sum_{i = 0}^{k} f(i) x^i \]

存在结论,存在一个 \(n\) 阶多项式 \(g\) 满足:

\[S(k - 1) = x^k g(k) - g(0) \]

可以参见 whx 的 blog

所以可以考虑 \(O(n)\) 差值求出 \(g(k + 1)\) 即可求出 \(S(k)\)

\(O(n)\) 求连续点值多项式单点取值

对于 \(n + 1\) 个点 \((x_i, y_i)\),考虑拉格朗日差值公式:

\[f_i(x) = \prod_{j \ne i} \cfrac {x - x_j}{x_i - x_j} \]

最终的式子是:

\[f(x) = \sum y_i f_i(x) \]

考虑如何快速求出 \(f_i(x)\)

首先是分子部分,可以考虑利用前后缀和。

其次是分母,假定 \(x_0\) 最小,\(x_n\) 最大,所以对于 \(x_i\) 的分母部分为:

\[(x_i - x_0)! * (-1)^{n - i} (x_n - x_i)! \]

\(O(n)\) 预处理阶乘即可 \(O(1)\) 算出。

于是剩下的问题就是如何对于所有的分母求逆。

类比阶乘求逆元的方法,先求出前缀积,求逆后逆推出前缀积的逆。利用这两者即可求出每一项的逆。复杂度为 \(O(n + n + \log n) = O(n)\)

于是 \(O(n)\) 算出最终答案即可,总复杂度为 \(O(n)\)。常数还行,大概为 \(8\),因人而异。

// 来自 https://www.luogu.com.cn/problem/AT_arc033_4
// x_0 = 0, x_n = n
// 循环较多是考虑到缓存友好问题

lint qpow(lint a, int x) {
    lint r = 1;
    for (; x; x >>= 1, a = a * a % mod)
        if (x & 1) r = r * a % mod;
    return r;
}

int n, x;
lint y[N];
lint pre[N], suf[N];
lint fac[N], rfac[N];
lint up[N], down[N];
lint tmp[N], dinv[N];

int main() {
    scanf("%d", &n); ++n;
    for (int i = 1; i <= n; ++i)
        scanf("%lld", y + i);
    scanf("%d", &x);

    pre[0] = suf[n + 1] = 1;
    for (int i = 1; i <= n; ++i)
        pre[i] = pre[i - 1] * (x - i + 1) % mod;
    for (int i = n; i; --i) 
        suf[i] = suf[i + 1] * (x - i + 1) % mod;

    fac[0] = rfac[0] = 1;
    for (int i = 1; i <= n; ++i)
        fac[i] = fac[i - 1] * i % mod;
    for (int i = 1; i <= n; ++i)
        rfac[i] = rfac[i - 1] * (mod - i) % mod;

    for (int i = 1; i <= n; ++i)
        up[i] = pre[i - 1] * suf[i + 1] % mod;

    for (int i = tmp[0] = 1; i <= n; ++i) {
        down[i] = fac[i - 1] * rfac[n - i] % mod;
        tmp[i] = tmp[i - 1] * down[i] % mod;
    } tmp[n] = qpow(tmp[n], mod - 2);

    for (int i = n; i; --i) {
        dinv[i] = tmp[i] * tmp[i - 1] % mod;
        tmp[i - 1] = tmp[i] * down[i] % mod;
    }

    lint ans = 0;
    for (int i = 1; i <= n; ++i) {
        ans = (ans + y[i] * up[i] % mod * dinv[i] % mod) % mod;
    } printf("%lld\n", ans);
    return 0;
}

牛顿级数求点值

牛顿级数形如:

\[f(x) = \sum_{i \ge 0} c_i {x \choose i} \]

考虑差分在 \(0\) 处的取值:

\[(\Delta^n f)(0) = \sum_{i \ge 0} c_i {0 \choose i - n} \]

由于仅 \({0 \choose 0} = 1\) 所以 \(= c_n\)

问题转化为求 \(\Delta^k f(0)\),根据差分序列性质:

\[\Delta^k f(0) = \sum_{i}(-1)^{n - i} {n \choose i} f(i) \]

这是一个卷积形式,所以可以 \(O(n \log n)\) 求出所有的 \(c_i\)

反之,类似二项式反演的式子,也可以 \(O(n \log n)\) 求出 \(0 \sim n\) 的点值。

混合图的欧拉回路

混合图的欧拉回路 Euler Circuit - 洛谷

存在欧拉回路的条件:对于有向图,每个点出度等于入度。

由于存在双向边,所以考虑 \(d_x = in_x - out_x\),如果 \(d_x \& 1\) 那么一定无解。

欧拉回路实质是出度和入度的转移,故利用网络流求解。

SPFA 判断负环存在性

注意对于一个不存在负环的图,从起点到任意一个点最短距离经过的点最多只有 \(n\) 个。

也就是说,一个点的松弛路径最多为 \(n\)

所以在 SPFA 中可以如此写:

if (dis[y] > dis[x] + w) {
    dis[y] = dis[x] + w;
    if ((cnt[y] = cnt[x] + 1) > )
}

复杂度还是 \(O(nm)\),但是平均表现好很多。

但是还是不够快,考虑基于 DFS 的 SPFA 判负环,这是相对最快的。

注意初始化时 \(dis_x = 0\),以及每一个点都需要 DFS 一次!

bool SPFA(int x) {
    vis[x] = true;
    for (auto e : G[x]) {
        if (dis[e.to] > dis[x] + e.w) {
            dis[e.to] = dis[x] + e.w;
            if (vis[e.to]) {
                vis[e.to] = false; return true;
            } else if (SPFA(e.to, mid)) {
                vis[e.to] = false; return true;
            }
        }
    }

    return vis[x] = false;
}

bool check() {
    fill(dis + 1, dis + 1 + n, 0);
    for (int i = 1; i <= n; ++i)
        if (SPFA(i)) return true;
    return false;
}

归并树

好东西,可以做到 \(O(\log^2 n)\) 单点修改(基于 vector erase 和 insert 的复杂度,这里视作 \(O(\log n)\),还是蛮快的),\(O(\log^2 n)\)\(rank(x)\)\(O(\log^2 n)\) 求前驱或者后继,只是需要 \(O(\log^3n)\) 找到 \(kth(k)\)

这不比树套树香多了?可是不如分块,\(\log^2 n\)\(\sqrt n\)\(1e5\) 差不了太多,分块对于缓存还更友好一些。

关于并查集维护关系

关系,例如食物链的关系,同类,猎物。

认为有三种角色,自己,猎物,天敌,分别维护 的关系即可。

扩展出来,发现对于关系,找出链循环长度,维护长度个并查集即可。

关于树上倍增优化建图的问题

有没有一种可能,倍增建图并不需要划分为 \(O(\log n)\) 个区间,而是类似于 Spare Table 的做法,两个区间即可。(当然,前提是可以如此。

【XR-1】逛森林 - 洛谷

关于贪心中的反悔自动机

Buy Low Sell High - 洛谷 也就是维护反悔答案的插值,从而得到全局最优解。

好吧,本质上就是反悔贪心,只是利用 \(\Delta\) 优化了而已 QwQ

与算法学习笔记(∞):杂项相似的内容:

算法学习笔记(∞):杂项

杂项 目录杂项代码规范算法优化的本质记忆化搜索基于边的记忆化动态规划树上每一个点求答案计数题关于仙人掌 DAG 的拓扑序计数关于微扰贪心的证明组合数前缀和单位根反演\(O(n^2)\) 状态求和矩形式子求和\(O(n^2)\) 状态 \(O(n)\) 单点问题CDQ 分治FFT 循环卷积根号多项式算

算法学习笔记(6): 树链剖分

树链剖分 树链剖分是一个很神奇,但是在树上可以完成一些区间操作问题 简单来说,就是把一棵树分成一条条的链,通过维护链上的信息来维护整棵树的信息 基础知识可以参考我的另外一篇博客:算法学习笔记(5): 最近公共祖先(LCA) 这里假设你已经掌握了上述博客中的所有相关知识,并清晰了其背后的原理 性质?发

算法学习笔记(11): 原根

原根 此文相对困难,请读者酌情食用 在定义原根之前,我们先定义其他的一点东西 阶 通俗一点来说,对于 $a$ 在模 $p$ 意义下的阶就是 $a^x \equiv 1 \pmod p$ 的最小正整数解 $x$ 或者说,$a$ 在模 $p$ 意义下生成子群的阶(群的大小) 再或者说,是 $a$ 在模

算法学习笔记(30):Kruskal 重构树

Kruskal 重构树 这是一种用于处理与最大/最小边权相关的一个数据结构。 其与 kruskal 做最小生成树的过程是类似的,我们考虑其过程: 按边权排序,利用并查集维护连通性,进行合并。 如果我们在合并时,新建一个节点,其权值为当前处理的边的权值,并将合并的两个节点都连向新建的节点,那么就可以得

算法学习笔记(3.1): ST算法

ST表 在RMQ(区间最值)问题中,著名的ST算法就是倍增的产物。ST算法可以在 \(O(n \log n)\) 的时间复杂度能预处理后,以 \(O(1)\) 的复杂度在线回答区间 [l, r] 内的最值。 当然,ST表不支持动态修改,如果需要动态修改,线段树是一种良好的解决方案,是 \(O(n)\

C++算法之旅、09 力扣篇 | 常见面试笔试题(上)算法小白专用

算法学习笔记,记录容易忘记的知识点和难题。详解时空复杂度、50道常见面试笔试题,包括数组、单链表、栈、队列、字符串、哈希表、二叉树、递归、迭代、分治类型题目,均带思路与C++题解

C++算法之旅、08 基础篇 | 质数、约数

算法学习笔记,记录容易忘记的知识点和难题。试除法、分解质因数、筛质数、约数个数、约数之和、最大公约数

算法学习笔记(1): 欧几里得算法及其扩展

扩展欧几里得算法详解 在了解扩欧之前我们应该先了解欧几里得算法 欧几里得算法 这是一个递归求最大公约数(greatest common divisor)的方法 $$ gcd(a, b) = gcd(b, a % b) $$ 可以通过一个类似的简单公式推导而来 好像叫做辗转相减法来着? $$ gcd(

算法学习笔记(3): 倍增与ST算法

倍增 目录倍增查找 洛谷P2249重点变式练习快速幂ST表扩展 - 运算扩展 - 区间变式答案倍增更多的用法优化矩形查询优化建图优化 DP作者有话说 倍增,字面意思即”成倍增长“ 他与二分十分类似,都是基于”2“的划分思想 那么具体是怎么样,我们以一个例子来看 查找 洛谷P2249 依据题面,我们知

算法学习笔记(4): 并查集及其优化

# 并查集 并查集,Disjoint-Set,或者通俗一点,叫做MergeFind-Set,是一种可以动态维护若干个不重叠的集合,并支持集合之间的合并与查询的数据结构。 集体来说,并查集支持下列两个操作: - Find,查询元素所属集合 - Merge,将两个元素所属集合合并 一般来说,为了具体实现