ToB企服应用市场:ToB评测及商务社交产业平台

标题: OI-Wiki 学习笔记 [打印本页]

作者: 立聪堂德州十三局店    时间: 2024-7-22 19:28
标题: OI-Wiki 学习笔记
算法底子

\(\text{Update: 2024 - 07 - 22}\)
复杂度

定义
衡量一个算法的快慢,一定要考虑数据规模的巨细。
一样平常来说,数据规模越大,算法的用时就越长。
而在算法竞赛中,我们衡量一个算法的效率时,最重要的不是看它在某个数据规模下的用时,而是看它的用时随数据规模而增长的趋势,即时间复杂度
符号定义
相等是 \(\Theta\),小于是 \(O\),大于是 \(\Omega\)。
大 \(\Theta\) 符号
对于函数 \(f(n)\) 和 \(g(n)\),\(f(n)=\Theta(g(n))\),当且仅当 \(\exists c_1,c_2,n_0>0\),使得 \(\forall n \ge n_0, 0\le c_1\cdot g(n)\le f(n) \le c_2\cdot g(n)\)。
大 \(O\) 符号
\(\Theta\) 符号同时给了我们一个函数的上下界,如果只知道一个函数的渐进上界而不知道其渐进下界,可以使用 \(O\) 符号。\(f(n) = O(g(n))\),当且仅当 \(\exists c,n_0\),使得 \(\forall n \ge n_0,0\le f(n)\le c\cdot g(n)\)。
研究时间复杂度时通常会使用 \(O\) 符号,因为我们关注的通常是步调用时的上界,而不关心其用时的下界。
大 \(\Omega\) 符号
同样的,我们使用 \(\Omega\) 符号来描述一个函数的渐进下界。\(f(n) = \Omega(g(n))\),当且仅当 \(\exists c, n_0\),使得 \(\forall n \ge n_0, 0 \le c \cdot g(n) \le f(n)\)。
主定理
建议背下来,不是很好理解。

\[T(n) = aT(\frac{n}{b}) + f(n) \qquad \forall n > b\]
那么

\[T(n) = \begin{cases} \Theta(n^{\log_b a}) & f(n) = O(n^{\log_b (a) - \epsilon}), \epsilon > 0 \\ \Theta(f(n)) & f(n) = \Omega(n^{\log_b (a) + \epsilon}), \epsilon\ge 0\\ \Theta(n^{\log_b a} \log^{k+1} n) & f(n) = \Theta(n^{\log_b a} \log^k n), k \ge 0 \end{cases}\]
需要注意的是,这里的第二种情况还需要满意 \(\text{regularity condition}\), 即 \(a f(n/b) \leq c f(n)\),\(\text{for some constant}\) \(c < 1\) \(\text{and sufficiently large}\) \(n\)。

几个例子:
空间复杂度
类似地,算法所使用的空间随输入规模变化的趋势可以用空间复杂度来衡量。
枚举

实际上一些题的正解就是枚举,但需要很多优化。
要点:
模拟

模拟即为将题目描述中的操作用代码实现,码量一样平常比较大,写错比较难调,相当浪费时间。
写模拟题时有以下注意的点:
递归

定义
我们可以用以下代码理解递归:
  1. long long f(传入数值) {
  2.     if(终止条件) return 最小子问题解;
  3.     return f(缩小问题规模);
  4. }
复制代码
递归的优点
递归的缺点
在步调执行中,递归是使用堆栈来实现的。每当进入一个函数调用,栈就会增加一层栈帧,每次函数返回,栈就会减少一层栈帧。而栈不是无限大的,当递归层数过多时,就会造成栈溢出 的后果。
显然偶然间递归处理是高效的,比如归并排序;偶然间是低效的,比如数孙悟空身上的毛,因为堆栈会消耗额外空间,而简朴的递推不会消耗空间。比如这个例子,给一个链表头,计算它的长度:
  1. // 典型的递推遍历框架
  2. int size(Node *head) {
  3.   int size = 0;
  4.   for (Node *p = head; p != nullptr; p = p->next) size++;
  5.   return size;
  6. }
  7. // 我就是要写递归,递归天下第一
  8. int size_recursion(Node *head) {
  9.   if (head == nullptr) return 0;
  10.   return size_recursion(head->next) + 1;
  11. }
复制代码

递归优化见后文搜索优化影象化搜索
要点
明白一个函数的作用并信赖它能完成这个任务,千万不要跳进这个函数里面企图探究更多细节,否则就会陷入无穷的细节无法自拔,人脑能压几个栈啊。
递归与枚举的区别
枚举是横向地把题目划分,然后依次求解子题目;而递归是把题目逐级分解,是纵向的拆分。
递归与分治的区别
递归是一种编程本领,一种解决题目的思维方式;分治算法很大程度上是基于递归的,解决更具体题目的算法头脑。
分治

定义
就是把一个复杂的题目分成两个或更多的相同或相似的子题目,直到最后子题目可以简朴的直接求解,原题目的解即子题目的解的合并。
过程
分治算法的焦点头脑就是「分而治之」。
大概的流程可以分为三步:分解 \(\to\) 解决 \(\to\) 合并。
分治法能解决的题目一样平常有如下特征:
注意:如果各子题目是不独立的,则分治法要重复地解公共的子题目,也就做了很多不必要的工作。此时虽然也可用分治法,但一样平常用动态规划较好。
贪心

适用范围
贪默算法在有最优子结构的题目中尤为有效。
最优子结构的意思是题目能够分解成子题目来解决,子题目的最优解能递推到终极题目的最优解。
证实
贪默算法有两种证实方法:反证法和归纳法。
一样平常情况下,一道题只会用到其中的一种方法来证实。
常见题型
在提高组难度以下的题目中,最常见的贪心有两种。
二者的区别在于一种是离线的,先处理后选择;一种是在线的,边处理边选择。
排序解法
用排序法常见的情况是输入一个包罗几个(一样平常一到两个)权值的数组,通过排序然后遍历模拟计算的方法求出最优值。
后悔解法
思路是无论当前的选项是否最优都接受,然后举行比较,如果选择之后不是最优了,则反悔,舍弃掉这个选项;否则,正式接受。如此往复。
排序

几种排序算法的比较

一张表格速通
排序方法均匀情况最好情况最坏情况空间稳定性选择排序\(O(n^2)\)\(O(n^2)\)\(O(n^2)\)\(O(1)\)不稳定冒泡排序\(O(n^2)\)\(O(n)\)\(O(n^2)\)\(O(1)\)稳定插入排序\(O(n^2)\)\(O(n)\)\(O(n^2)\)\(O(n)\)稳定计数排序\(O(n + w)^1\)\(O(n + w)\)\(O(n + w)\)\(O(n + w)\)稳定基数排序\(O(kn + \sum\limits_{i = 1}^{k} w_i)^2\)\(O(kn + \sum\limits_{i = 1}^{k} w_i)\)\(O(kn + \sum\limits_{i = 1}^{k} w_i)\)\(O(n + k)\)稳定快速排序\(O(n \log n)\)\(O(n \log n)\)\(O(n^2)\)\(O(\log n) \sim O(n)\)不稳定归并排序\(O(n \log n)\)\(O(n \log n)\)\(O(n \log n)\)\(O(n)\)稳定堆排序\(O(n \log n)\)\(O(n \log n)\)\(O(n \log n)\)\(O(1)\)不稳定桶排序\(O(n + \frac{n^2}{k} + k)^3\)\(O(n)\)\(O(n^2)\)\(O(n + m)^4\)稳定希尔排序\(O(n \log n) \sim O(n^2)\)\(O(n^{1.3})\)\(O(n^2)\)\(O(1)\)不稳定锦标赛排序\(O(n \log n)\)\(O(n \log n)\)\(O(n \log n)\)\(O(n)\)稳定\(\text{tim}\) 排序\(O(n \log n)\)\(O(n)\)\(O(n \log n)\)\(O(n)\)稳定\(^1\):其中 \(w\) 为排序元素的值域。
\(^2\):其中 \(k\) 为排序元素的最大位数,\(w_i\) 为第 \(i\) 个关键字的值域。
\(^3\):其中 \(k\) 表现将 \(n\) 个元素放进 \(m\) 个桶中,每个桶的数据为 \(k = \frac{n}{m}\)。
\(^4\):其中 \(m\) 表现将 \(n\) 个元素放进的 \(m\) 个桶。
前缀和

定义
前缀和可以简朴理解为「数列的前 \(\text{i}\) 项的和」,是一种重要的预处理方式,能大大降低查询的时间复杂度。
一维前缀和
预处理递推式为:

\[p_i = p_{i - 1} + a_i\]
查询 \([l, r]\) 的数值:

\[ans = p_r - p_{l - 1}\]
二维前缀和
预处理递推式为:

\[p_{i, j} = p_{i - 1, j} + p_{i, j - 1} - p_{i - 1, j - 1} + a_{i, j}\]
查询 \((i, j) \sim (k, w)\) 的数值:

\[ans = p_{k, w} - p_{k, j - 1} - p_{i - 1, w} + p_{i - 1, j - 1}\]
树上前缀和
设 \(s_i\) 表现结点 \(i\) 到根节点的权值总和。
若是点权,\(x, y\) 路径上的和为 \(s_x + s_y - s_{lca} - s_{fa_{lca}}\)
若是边权,\(x, y\) 路径上的和为 \(s_x + s_y - 2 \times s_{lca}\)
差分

定义
差分是一种和前缀和相对的计谋,可以看成是求和的逆运算。
这种计谋的定义是令 \(b_i = \begin{cases} a_i - a_{i - 1} \, &i \in[2, n] \\ a_1 \, &i = 1 \end{cases}\)
性质
它可以维护多次对序列的一个区间加上一个数,并在最后询问某一位的数或是多次询问某一位的数。注意修改操作一定要在查询操作之前。
树上差分
树上差分可以理解为对树上的某一段路径举行差分操作,这里的路径可以类比一维数组的区间举行理解。例如在对树上的一些路径举行频繁操作,并且询问某条边或者某个点在经过操作后的值的时间,就可以运用树上差分头脑了。
树上差分通常会结合树底子和迩来公共祖先来举行考察。树上差分又分为点差分边差分,在实现上会稍有不同。
点差分
举例:对树上的一些路径 \(\delta(s_1, t_1), \delta(s_2, t_2), \delta(s_3, t_3)\dots\) 举行访问,问一条路径 \(\delta(s, t)\) 上的点被访问的次数。
对于一次 \(\delta(s, t)\) 的访问,需要找到 \(s\) 与 \(t\) 的公共祖先,然后对这条路径上的点举行访问(点的权值加一),若采用 \(\text{DFS}\) 算法对每个点举行访问,由于有太多的路径需要访问,时间上蒙受不了。这里举行差分操作:

\[\begin{aligned}&d_s\leftarrow d_s + 1\\&d_{lca}\leftarrow d_{\textit{lca}} - 1\\&d_t\leftarrow d_t + 1\\&d_{f(\textit{lca})}\leftarrow d_{f(\textit{lca})} - 1\\\end{aligned}\]
其中 \(f(x)\) 表现 \(x\) 的父亲节点,\(d_i\) 为点权 \(a_i\) 的差分数组。

可以以为公式中的前两条是对蓝色方框内的路径举行操作,后两条是对红色方框内的路径举行操作。不妨令 \(\textit{lca}\) 左侧的直系子节点为 \(\textit{left}\)。那么有 \(d_{\textit{lca}} - 1 = a_{\textit{lca}} - (a_{\textit{left}} + 1)\),\(d_{f(\textit{lca})} - 1 = a_{f(\textit{lca})} - (a_{\textit{lca}} + 1)\)。可以发现实际上点差分的操作和上文一维数组的差分操作是类似的。
边差分
若是对路径中的边举行访问,就需要采用边差分计谋了,使用以下公式:

\[\begin{aligned}&d_s\leftarrow d_s + 1\\&d_t\leftarrow d_t + 1\\&d_{\textit{lca}}\leftarrow d_{\textit{lca}} - 2\\\end{aligned}\]

由于在边上直接举行差分比较困难,所以将本来应当累加到红色边上的值向下移动到附近的点里,那么操作起来也就方便了。对于公式,有了点差分的理解底子后也不难推导,同样是对两段区间举行差分。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4