《算法竞赛进阶指南》笔记

《算法竞赛进阶指南》笔记

\(\texttt{0x00}\) 前言

本文记录了蒟蒻对书中所有例题与习题的见解/分析。题目均收录于Acwing-算法竞赛进阶指南LG 题单(荐)

\(\texttt{2023/6/9}\) 开始动笔,之后将会持续更新。

upd:因为笔者懒得更新,所以暂时取消目录不会有目录了。

upd:暂时断更,CSP 之后考虑继续更新。

upd:大概更新要到 \(\texttt{2024}\) 年去了。敬请期待 qwq。

upd:new

upd:此文永久停止更新。(2024/12/13)

upd:重新开始更新,从数学一章开始。(2025/1/8)

upd:随缘更新。以后将不会发布更新公告。


\(\texttt{0x01}\) 位运算

  • 先放一张个人认为很重要的表
操作 运算
取出整数\(n\) 在二进制下的第 \(k\) \((n>>k) \And 1\)
取出整数\(n\) 在二进制下的后 \(k\) \(n \And ((1<<k)-1)\)
把整数\(n\) 在二进制下的第 \(k\) 位取反 \(n \ \text{xor} \ (1<<k)\)
对整数\(n\) 在二进制下的第 \(k\) 位赋值 \(1\) \(n \mid (1<<k)\)
对整数\(n\) 在二进制下的第 \(k\) 位赋值 \(0\) \(n \And (\sim(1<<k))\)

【例题 \(\texttt{1}\)\(a^b\)

\(b\) 在二进制下有 \(n\) 位,第 \(i\) 位为 \(c_i\),则有:

\( a^b=a^{c_{n-1} \times 2^{n-1}} \times a^{c_{n-2} \times 2^{n-2}} \times ... \times a^{c_0} \)

又因:

\( a^{2^n}=a^{2^{n-1}} \times a^{2^{n-1}} \)

所以我们便可不断地取出 \(b\) 的每一位 \(c_i\),若 \(c_i=1\),则让 \(ans\) 乘上 \(a^{2^i}\);同时让 \(a\) 不断地自乘得出乘积项。

code

【例题 \(\texttt{2}\)】 64位整数乘法

\(b\) 拆成二进制每一位数相加的形式(记共有 \(n\) 位,第 \(i\) 位为 \(c_i\)),可得:

\( a \times b = a \times c_{n-1} \times 2^{n-1} + a \times c_{n-2} \times 2^{n-2} + ... + a \times c_{0} \)

又因:

\( a \times 2^n = a \times 2^{n-1} + a \times 2^{n-1} \)

所以又可以不断地取出 \(b\) 的每一位 \(c_i\),若 \(c_i=1\),则让 \(ans\) 加上 \(a^{2^i}\);同时让 \(a\) 不断乘 \(2\) 得出乘积项。

code

【例题 \(\texttt{3}\)】 最短 Hamilton 路径

\(\text{F}(i,j)\) 表示目前走过点的状态的二进制数为 \(i\),且当前处于点 \(j\) 时的最短路径长度。易知在起点时有 \(\text{F}(1,0)=0\),目标是 \(\text{F}((1<<n)-1,n-1)\)

递推公式:\(\text{F}(i,j)=\min\{\text{F}(i \ \text{xor} \ (1<<j),k) + w(k,j)\}\),其中 \(w(i,j)\) 指点 \(i\) 到点 \(j\) 的距离,需要满足 \(0 \le k < n\)\((i>>j) \And 1\)。因为上一时刻的点 \(k\) 可能是 \(i \ \text{xor} \ (1<<j)\) 任意一个为 \(1\) 的点,所以 \(O(n^3)\) 的枚举此点取最小值即可。

code

【例题 \(\texttt{4}\)】 起床困难综合征

位运算的特点:在二进制下不进位

因此,我们可以枚举每一位,分别计算出这一位填 \(0/1\) 的经过运算后的值,若填 \(1\) 不会超过最大值 \(m\) 且比填 \(0\) 的结果更大,那么填 \(1\);否则填 \(0\)

当每一位都填好后,\(ans\) 也就呼之欲出了。

code


\(\texttt{0x02}\) 递推与递归

  • 放一张有用的表
枚举形式 状态空间规模 一般遍历方式
多项式 \(n^k\)\(k\) 为常数) for 循环、递推
指数 \(k^n\)\(k\) 为常数) 递归、位运算
排列 \(n!\) 递归、C++ next_permutation
组合 \(\dbinom{n}{k}\) 递归\(+\) 剪枝

【例题 \(\texttt{5/6/7}\)】递归实现指数 \(/\) 排列 \(/\) 组合型枚举

这三道题几乎是一样的,所以放到一起讲了。

  • 指数型枚举:用一个 vector 数组存储被选的数,分别对于"不选 \(/\)\(x\)" 分支分别进行处理即可。code
  • 排列型枚举:与指数型枚举类似,只是需要一个 vis 数组来记录每个数是否被选过。笔者使用了 C++ next_permutation 实现,更为简单。code
  • 组合型枚举:一般的,仅需在指数型枚举的 dfs 开头加上 if(a.size()>m||a.size()+(n-x+1)<m) return; 即可,这样便可实现只选 \(m\) 个数;特殊的,若题目要求按字典序输出,则需要用到不降原则的知识,实现详见代码。code

【例题 \(\texttt{8}\)】费解的开关

首先,用位运算枚举第一行的 \(2^5=32\) 种填法。

接着从第一行开始递推,若 \(i\)\(j\) 列的数为 \(1\),则点击 \(i+1\)\(j\) 列的数。

若到达第 \(5\) 行仍不为全 \(0\) 或超过了 \(6\) 步,则视为不合法的操作。

对所有合法操作的步数取最小值即可。

code

【例题 \(\texttt{9}\)\(\text{Strange Towers of Hanoi}\)

对于 \(n\)\(3\) 塔的经典 \(\text{Hanoi}\) 问题,令 \(d(i)\) 表示求解 \(i\)\(3\) 塔所需的最小步数,则有:

\( d(i) = 2 \times d(i-1) + 1 \)

(即先将 \(i-1\) 个盘子移动到 \(\text{B}\) 柱,再将第 \(i\) 个盘子移动到 \(\text{C}\) 柱,最后将 \(i-1\) 个盘子移动到 \(\text{C}\) 柱。)

对于 \(n\)\(4\) 塔的 \(\text{Hanoi}\) 问题,则同样令 \(f(i)\) 表示求解 \(i\)\(4\) 塔所需的最小步数,则又有:

\( f(i)=\mathop{\min}\limits_{1 \le j < n} \{ 2 \times f(j) + d(i-j) \} \)

(即先让 \(j\) 个盘子在 \(4\) 塔模式下移动到 \(\text{B}\) 柱,再让 \(i-j\) 个盘子在 \(3\) 塔模式下移动到 \(\text{D}\) 柱,最后让 \(j\) 个盘子在 \(4\) 塔模式下移动到 \(\text{D}\) 柱,并在所有这样的 \(j\) 中取最小值。)

于是对于每一个 \(n\),先 \(O(n)\) 地求出每个 \(d(i)\),再 \(O(n^2)\) 地求出每个 \(f(i)\),最后输出 \(f(n)\) 即可。

\(\texttt{Tips: 多测不清空,爆零两行泪!}\)

code

【例题 \(\texttt{10}\)\(\text{Sumdiv}\)

(注:本题解与书上方法略有不同)

首先对 \(a\) 分解质因数,并记录次幂,注意特判最后 \(a \neq 1\) 的情况。

\( a=p_1^{c_1} \times p_2^{c_2} \times ... \times p_n^{c_n} \)

\( a^b=p_1^{c_1 \times b} \times p_2^{c_2 \times b} \times ... \times p_n^{c_n \times b} \)

因此易知 \(a^b\) 的约数和为

\( (1+p_1+...+p_1^{c_1 \times b}) \times (1+p_2+...+p_2^{c_2 \times b}) \times ... \times (1+p_n+...+p_n^{c_n \times b}) \)

其中每一项均是等比数列,我们知道等比数列的求和公式为

\( sum=\dfrac{p^n-1}{p-1} \)

分子可以使用快速幂求出,由于本题需要进行取模,所以我们考虑将 \(\div \ (p-1)\) 转化为 \(\times \ (p-1)\) 的逆元。

根据费马小定理,当 \(a \nmid p\) 时,有

\( a^{p-1} \equiv 1 \pmod p \)

\( a \times a^{p-2} \equiv 1 \pmod p \)

根据逆元的定义,若

\( ax \equiv 1 \pmod p \)

则称 \(x\)\(a\) 的逆元。

于是在本题中,\(a^{p-2}\) 即为 \(a\) 的逆元,\(a^{p-2}\) 同样也可以用快速幂求出。本题被完美解决~

code

【例题 \(\texttt{11}\)\(\text{Fractal Streets}\)

比较难想的一道题。

首先令 \(calc(n,m)\) 表示编号为 \(m\) 的房屋在第 \(n\) 级城市中的位置。

不难发现 \(n\) 级城市总是由 \(4\)\(n-1\) 级城市组成(因为 \(\dfrac{(2^n)^2}{(2^{n-1})^2}=4\))。

于是可以递归地求解 \(calc(n-1,m \bmod 2^{2n-2})\),记此位置为 \((x,y)\)

再根据 \(\dfrac{m}{2^{2n-2}}\) 的大小即可确定此位置在哪一座 \(n-1\) 级城市之中(\(0,1,2,3\) 分别对应左上、右上、左下、右下)。

  • \((x,y)\) 处于左上的 \(n-1\) 级城市中,则需要将坐标顺时针旋转 \(90^{\circ}\) 再水平翻转,得到坐标 \((y,x)\)
  • \((x,y)\) 处于右上的 \(n-1\) 级城市中,则直接将纵坐标 \(+ \ 2^{n-1}\) 即可。
  • \((x,y)\) 处于右下的 \(n-1\) 级城市中,则直接将横、纵坐标均 \(+ \ 2^{n-1}\) 即可。
  • \((x,y)\) 处于左上的 \(n-1\) 级城市中,则需要将坐标顺时针旋转 \(90^{\circ}\) 再水平翻转,最后将横坐标 \(+ \ 2^{n-1}\),得到坐标 \((2^{n}-y-1,2^{n-1}-x-1)\)

根据以上思路编写 \(calc\) 函数即可将本题解决。

code


\(\texttt{0x03}\) 前缀和与差分

【例题 \(\texttt{12}\)】激光炸弹

前言:\(\text{std}\) 能过 \(\texttt{Luogu}\),但过不了 \(\texttt{Acwing}\),所以本题解以 \(\texttt{Acwing}\) 数据为准。

首先需要用 \(A\) 数组存下每个目标的价值。

接着,很容易想到的思路便是对于 \(A\) 数组求出它的二维前缀和数组 \(S\),接着 \(O(n^2)\) 地枚举 \(R \times R\) 正方形的右下角顶点,算出它的价值并取 \(\max\) 即可。为了优化空间,可以直接省略 \(A\) 数组。这样的思路足以通过 \(\texttt{Luogu}\) 数据。

但是 \(\texttt{Acwing}\) 的数据不保证 \(R\) 一定 \(\le \max\{x_i\},\max\{y_i\}\),因此我们只需要特判一下:

  • \(R>\max\{x_i\}\) 时,取 \(\max\{x_i\}\) 行的最大值。
  • \(R>\max\{y_i\}\) 时,取 \(\max\{y_i\}\) 列的最大值。
  • \(R \le \max\{x_i\},\max\{y_i\}\) 时,取 \(\max\{x_i\}\) 行、\(\max\{y_i\}\) 列的最大值。
  • 否则,按照原来的思路实现。

这样就可以过掉加强版的数据了~

code

【例题 \(\texttt{13}\)\(\text{IncDec Sequence}\)

为使程序效率提升,我们先要求出 \(a\) 的差分数组 \(b\),以将区间操作转化为单点操作。特殊的,需要令 \(b_{n+1}=0\)

那么题中对于 \(a\) 数组的操作,实际上就相当于从 \(b_{1 \sim n+1}\) 中选择任意两数,一个 \(+1\),一个 \(-1\),目标是让 \(b_{2 \sim n}\)\(=0\),即让 \(a_{1 \sim n}\)\(=b_1\)

\(b\) 数组中选择任意两数,有以下 \(3\)有效操作:

  • 选择 \(b_i\)\(b_j\),满足 \(2 \le i,j \le n\)。若 \(b_i\)\(b_j\) 一正一负时,需要多进行此操作。
  • 选择 \(b_1\)\(b_i\),满足 \(2 \le i \le n\)
  • 选择 \(b_i\)\(b_{n+1}\),满足 \(2 \le i \le n\)

\(a\) 数组中正数之和为 \(p\),负数之和为 \(q\),则操作 \(1\) 需要进行
\(\min(p,q)\) 次,余下 \(\mid p-q \mid\) 未配对,则继续执行 \(2,3\) 类操作,共需 \(\max(p,q)\) 次操作。

\(\mid p-q \mid\)\(2,3\) 类操作将会产生 \(\mid p-q \mid + \ 1\) 种不同的 \(b_1\) 值,于是就会产生 \(\mid p-q \mid + \ 1\) 种不同的序列。

code

【例题 \(\texttt{14}\)\(\text{Tallest Cow}\)

建立一个数组 \(C\) 表示 \(N\) 头牛与最高牛的差距,初始值为全 \(0\)

则对于每一对关系 \((A_i,B_i)\),我们将 \(C_{A_i+1 \sim B_i-1}\)\(-1\),表示 \(A_i \sim B_i\) 中间的数均至少比它们少 \(1\)

最后对于第 \(i\) 头牛,它的最大身高即为 \(h+C_i\)

这样操作的复杂度是 \(O(nm)\) 的,显然不能支持。

于是,我们考虑求出 \(C\) 数组的差分数组 \(D\),这样每次区间 \(-1\) 的操作均可转化为将 \(D_{A_i+1}-1\) 并且将 \(D_{B_i}+1\)。输出时求出 \(D\) 数组的前缀和便得到了 \(C\) 数组。

时间复杂度便降至 \(O(n)\),可以接受。

注意此题关系可能重复,需要用一个 map 维护某个关系是否被访问过。

code


\(\texttt{0x04}\) 二分

【例题 \(\texttt{15}\)\(\text{Best Cow Fences}\)

考虑二分答案,二分平均值,判定“是否存在一个长度不小于 \(f\) 的子段,使得其平均值不小于 \(mid\)”。

将子段中的每个数均 \(- \ mid\),判定方法又变为“是否存在一个长度不小于 \(f\) 的子段,使得其子段和非负”。

子段 \([i,j]\) 之和则可以表示为前缀和相减的形式,即:

\( \mathop{\max}\limits_{f \le i \le n} \{ sum_i - \mathop{\min}\limits_{0 \le j \le f} \{ sum_j \} \} \)

于是我们可以进行实数二分,对于每个 \(mid\),求出 \(A\) 数组 \(- \ mid\) 后的数组 \(B\),并且求出 \(B\) 数组的前缀和数组 \(C\)

接着从 \(f\) 枚举到 \(n\),维护变量 \(mnx\) 记录 \(\mathop{\min}\limits_{0 \le j \le f} \{ sum_j \}\)\(ans\) 记录 $ \mathop{\max}\limits_{f \le i \le n} { sum_i } - mnx$。每轮循环依次更新 \(mnx\)\(ans\)

最后,若 \(ans>0\),则锁定右区间,否则锁定左区间。

二分结束后,直接输出 \(\lfloor r \times 1000 \rfloor\) 即可,记得强制类型转换!

code

【例题 \(\texttt{16}\)\(\text{Innovative Business}\)

简单交互题。

对于每次询问,二分查找第一个\(k\) 大的位置并插入到其前面即可,最多 \(N \log N\) 次询问,可以接受。

也可使用 stable_sort 原地归并排序实现,更为简洁。

code(实际提交仅需 class 部分)。


\(\texttt{0x05}\) 排序

【例题 \(\texttt{17}\)\(\text{Cinema}\)

首先用一个 \(d\) 数组存下所有珂学家、语音、以及字母的语言,并对其进行离散化操作,再使用 \(cnt\) 数组统计出每个珂学家的语言出现的次数。

接着遍历每一部电影,分别求出它的语音和字幕在珂学家的语言中出现的次数,并与上一电影进行比较、更新答案即可。

code

【例题 \(\texttt{18}\)】 货仓选址

直接对 \(A_{1 \sim N}\) 排序并取中位数即为答案。

  • \(\texttt{if } 2 \mid N \ , \ ans=A_{N/2}\)

  • \(\texttt{otherwise} \ , \ ans=A_{(N+1)/2}\)

code

【例题 \(\texttt{19}\)】 七夕祭

首先进行有解性判断。

显然要使每行 \(\text{cl}\) 喜爱的摊点数相同,必须使 \(t \mid n\);同理,要使每列喜爱的摊点数相同,则必须使 \(t \mid m\),我们据此便可完成有解性判断。

考虑如何求出最小交换次数,我们首先令 \(a_i\) 表示每行 \(/\) 列的 \(\text{cl}\) 最喜爱的摊点数,\(s_i\) 表示 \(\sum^{i}_{j=1} a_i\)

若场地不是环形的,则最小交换次数为:

\( \sum^{m}_{i=1} |\dfrac{t}{m} \times i - s_i| \)

(此公式含义为:前 \(i\)\(/\) 列最初共有 \(s_i\)\(\text{cl}\) 最喜爱的摊点数,期望共拥有 \(\dfrac{t}{m} \times i\)\(\text{cl}\) 最喜爱的摊点数,需要进行两者之差的多退少补操作。)

回到本题,场地是环形的,则可以想到从环上任意一个摊点断开成链,再转化成上面的问题。

若在第 \(k\) 个摊点上断环成链,则这 \(m\)\(/\) 列所拥有的 \(\text{cl}\) 喜爱的摊点数与前缀和分别为:

\( a_{k+1} \ \ \ s_{k+1}-s_k \)
\( a_{k+2} \ \ \ s_{k+2}-s_k \)
\( \cdot \cdot \cdot \)
\( a_{1} \ \ \ s_1+s_m-s_k \)
\( a_{2} \ \ \ s_2+s_m-s_k \)
\( \cdot \cdot \cdot \)
\( a_k \ \ \ s_k+s_m-s_k \)

由上表可知,环上最小步数为:

\( \sum^{m}_{i=1}|s_i-s_k| \)

其中 \(s_k\) 选中位数时,上式最小。

于是我们对于行与列分别求出最小步数,累加起来就得到了答案。

code

【例题 \(\texttt{20}\)\(\text{Runnig Median}\)

很容易想到一种朴素做法:维护一个 vector,对于每个数,二分查找它应该处在的位置(STL upper_bound),插入进去,当数字个数为奇数时,输出第 \(\frac{N-1}{2}\)项(\(N\) 为当前序列长度,因为是 \(0\) 下标)即可。该算法的时间复杂度为 \(O(N^2)\) 不可接受。

俗话说:脑子不够,暴力数据结构来凑。于是我们想到了一种对顶堆做法。具体实现如下:

  • 建立两个优先队列,一个大根堆,一个小根堆

  • 维护这两个队列,使其始终保持以下性质 \(^{[1]}\)

    1. 序列中从小到大排名为 \(1 \sim \frac{N}{2}\) 的数放在大根堆;

    2. 剩下的(即从小到大排名 \(\frac{N}{2}+1 \sim N\) 的)以及中位数放在小根堆。

  • 根据上述性质可知,小根堆中的元素个数应当总比大根堆多 \(1\)

  • 由这一结论,我们对于每一个输入的数,便可分两种情况讨论:

    1. 若小根堆为空,则优先放入小根堆。

    2. 否则,将其与小根堆的堆顶比较,若大于则进入小根堆,小于则进入大根堆。接着分别更新大、小根堆,使其符合性质 \([1]\)

至此,本题以 \(O(n \log n)\) 的复杂度被完美解决。

code

【例题 \(\texttt{21}\)\(\text{Ultra-QuickSort}\)

典中典。

输出 \(a\) 数组的逆序对数即可,模板 & code

【例题 \(\texttt{22}\)】 奇数码问题

梅开二度。

将两种状态的所有数存入 \(a,b\) 数组,并分别求出逆序对个数,若奇偶性相同则可以转换,否则不行。code

  • 多测不清空,\(\texttt{\_\_\_\_\_\_\_\_\_\_}\)

\(\texttt{0x06}\) 倍增

【例题 \(\texttt{23}\)\(\text{Genius ACM}\)

要让“校验值”最大,则取的 \(M\) 对数必须为最大、最小,次大、次小......以此类推。

为使 \(A\) 数组分段最少,则可以从头开始分,每段的“校验值”在不超过 \(T\) 的情况下越长越好。

于是问题变成了:“在确定了一个左端点 \(L\) 以后,右端点 \(R\) 保证在 \(L \sim N\) 的区间中且区间 \(L \sim R\) 的校验值不超过 \(T\) 的情况下,最大能取到什么位置。

很容易想到二分的方法,但复杂度无法接受。于是考虑采用倍增。

倍增的方法如下:

  • 初始令 \(p=1,R=L\)

  • 求出 \(L \sim R+p\) 的“校验值”,若 \(<T\)\(R \gets R+p,p \gets p \times 2\),否则 \(p \gets p \div 2\)

  • 重复上一步骤,直到 \(p=0\),此时 \(R\) 即为所求。

以上方法的时间复杂度为 \(O(\log n)\),而计算“校验值”需要进行排序,时间复杂度为 \(O(n \log n)\),总时间复杂度为 \(O(n \log ^2 n)\),仍然不可接受。

于是我们想到仅将先加入的元素排序,在将原数组与新元素合并。利用归并排序,便可实现这一操作,时间复杂度降至 \(O(n \log n)\),完美过掉此题!

code

\(\texttt{0x07}\) 贪心

  • 贪心算法的正确性需要证明,以下题目的证明限于篇幅所以均略过。

【例题 \(\texttt{24}\)\(\text{Sunscreen}\)

此题可以抽象为:“给定 \(C\) 个区间以及 \(L\) 种点的坐标与个数,规定每个区间仅能放一个点,且这个点的坐标必须在区间之内,求将这些点最多能放入多少个区间”

于是,我们对区间按右端点排序,并对这些点按坐标排序。接着对于每一个区间,遍历每一个点,判断其是否能放进此区间,能放则放,不放则已。时间复杂度为 \(O(n^2)\)

注意区间和点必须使用 struct/pair 存储

code

【例题 \(\texttt{25}\)\(\text{Stall Reservations}\)

首先按吃草时间对牛排序。

接着维护一个数组 \(S\),记录下每个畜栏安排进去的最后一头牛。

对于每一头牛,遍历 \(S\) 数组,找到 \(\ge\) 最后一头牛吃草时间的畜栏,插入进去,否则为其新建一个畜栏。

这样的时间复杂度为 \(O(n^2)\),若 \(S\) 数组改用优先队列,则可将时间复杂度降至 \(O(n \log n)\),完美通过。

code

【例题 \(\texttt{26}\)\(\text{Radar Installation}\)

首先遍历每个岛屿,根据第 \(i\) 个岛屿的坐标 \(x_i,y_i\) 计算出它被控制的最大范围的左端点 \(l_i\) 与右端点 \(r_i\)。具体公式:

\( \begin{cases} k_i=\sqrt{d \times d - y_i \times y_i}\\ l_i=x_i+k_i\\ r_i=y_i+k_i \end{cases} \)

(其中 \(d\) 指雷达的覆盖半径,\(k_i\) 是临时变量。)

接着按照左端点排序。令 \(pos\) 初始为 \(-\infty\),对于第 \(i\) 个雷达,若 \(pos < l_i\),则令 \(pos=r_i\),否则令 \(pos=\min(pos,r_i)\)

code

【例题 \(\texttt{27}\)】 国王游戏

按大臣左手上的数 \(l\) 与右手上的数 \(r\) 的乘积排序,接着在所有人中找到奖赏最多的即可。

需要用高精度,所以不贴代码了。。。

【例题 \(\texttt{28}\)\(\text{Color a Tree}\)

题解link(未过审 \(qwq\))。

\(\texttt{0x08}\) 第一章练习

【习题 \(\texttt{1}\)\(\text{The Pilots Brother's Refrigerator}\)

枚举 \(0 \sim 2^{16}\) 中的所有二进制数,依次取出每一位,若此位为 \(1\),则按一下开关。

所有位都取完后,遍历整个 \(4 \times 4\) 的矩形,检验所有开关均为开启状态。若是,则比较步数,若当前步数 \(<\) 上一方案的步数,更新答案。

可以在每次按下开关时用一个 pair 类型的数组记录下开关的 \((x,y)\),最后输出这个数组即可。

坑点:

  • 多测要清空。

  • 按下开关等价于改变当前开关 \((x,y)\) 的状态,并且改变 \(x\) 行和 \(y\) 列的所有开关(包括当前开关)。

code

【习题 \(\texttt{2}\)】 占卜 \(\text{DIY}\)

没啥好说的,直接开一个 deque 模拟所有操作即可。

建议写个 get 函数将扑克牌转换为数字。

code

【习题 \(\texttt{3}\)\(\text{Practal}\)

一图胜千言。

转自 https://www.acwing.com/solution/content/823/

注意递归边界。

code

【习题 \(\texttt{4}\)\(\text{Raid}\)

被卡了一周的题

如果你乐意,可以先切掉P1257P1429P7883

不难看出,此题其实是求平面上两组点的最近点对。

于是我们先考虑一种简单情况:只有一组点。

这种情况可以使用分治算法,分别计算出 \([l,mid]\) 以及 \([mid+1,r]\) 的答案,合并即可。计算过程如下:

  • \([l,r]\) 中所有距离小于当前答案的点存入 \(a\) 数组中;

  • 按照 \(x\) 坐标对 \(a\) 数组排序;

  • 对于 \(a\) 数组中的任意两点,若它们的 \(y\) 坐标 \(<\) 当前答案,计算出它们之间的距离,对于所有这样的距离取 \(\min\) 就是答案。

时间复杂度 \(O(n \log ^2 n)\),使用归并排序可以降至 \(O(n \log n)\)

回到本题,考虑增加一组点后,在什么情况下会影响答案。

显然,选择一个原来的点,若存在一个新点,它们之间的距离 \(<\) 当前答案,则会更新答案。

于是我们可以先对于所有点按 \(x\) 轴排序,接着对于所有原来的点,以这些点为圆心,当前答案为半径画一个圆,将其中的新点个数存入 \(a\) 数组中。

处理 \(a\) 数组中的所有点对,若它们的 \(y\) 坐标 \(<\) 当前答案,计算出它们之间的距离,对于所有这样的距离取 \(\min\) 就是答案。

若使用归并排序,则时间复杂度还是 \(O(n \log n)\)

结果这个被 \(\text{hack}\) 了。。。

于是,我们使用了 \(zky\) 大佬的人类智慧

首先,对所有点按照 \(x \times y\) 排序。接着进行 \(5\) 次随机旋转,接着在 \(2n\) 个点中选出前 \(100\) 个点对,对于所有的类型不同的点对,计算出它们之间的距离,对于所有这样的距离取 \(\min\) 就是答案。

时间复杂度约为 \(O(n)\)

code

【习题 \(\texttt{5}\)】 防线

题意简述:给定 \(n\) 个起点为 \(s\)、终点为 \(e\)、公差为 \(d\) 的等差数列,求奇数位的位置。

二分答案。首先需要求出最小的起点和最大的终点,分别记为 \(minx\)\(maxx\)

考虑如何设计 check 函数。我们可以通过求出某个区间的数字和,判断它的奇偶性来确定应该前往哪个区间。

如何求出某个区间的数字和?可以枚举所有的等差数列,计算包含在此区间的所有数列的和即可。

code

【习题 \(\texttt{6}\)\(\text{Corral the Cows}\)

依然是二分答案。依然是考虑如何设计 check 函数。

使用双指针法。建立两个指针 \(i\)\(j\),对于所有 \(1 \sim n\)\(i\),找到最大的且与 \(a_i\)\(x\) 坐标不超过 \(mid\)\(j\)

遍历 \(i \sim j\),记当前的数为 \(k\),则 \(y\) 坐标范围在 \(a_{k_y} \sim a_{k_{y+mid}}\) 的点的个数即为三叶草个数。

code

【习题 \(\texttt{7}\)】 糖果传递

首先令 \(x_i\) 表示每个小朋友向左边传递的糖果数,\(avg\) 表示每个小朋友应有的糖果数。则有:

\( \begin{cases} avg=a_i-x_i+x_{i+1} \ (1 \le i < n) \\ avg=a_n-x_n+x_1 \ (i = n) \end{cases} \)

变形得:

\( x_i=avg \times i-\sum_{i=1}^{n}a_i+x_1 \)

这是我们再令 \(c_i\) 表示:

\( \sum_{i=1}^{n}a_i-avg\times i \)

则有:

\( x_i=x_1-c_i \)

要使代价最小,则需要使 \(x_i\) 最小。而 \(c_i\) 是可以预处理出来的,因此 \(x_1\) 必须最小。

根据“货仓选址”的知识,我们知道,\(x_1\) 选取中位数时最小。

于是我们可以预处理出 \(c_i\),接着对 \(x_i\) 排序,取中位数计算答案即可。时间复杂度 \(O(n)\)

注意开 long long

code

【习题 \(\texttt{8}\)\(\text{Soldiers}\)

既然要令所有士兵的位置离目标 \((x,y)\) 距离最近,则 \(y\) 均取中位数即为最优 \(y\) 坐标。

对于任意的 \(x_i\),有 \(x_i=x_0+i-1\),则取 \(x_i-i+1\) 的中位数 \(x_0\) 即为最优 \(x\) 坐标。

code

【习题 \(\texttt{9}\)\(\text{Number Base Conversations}\)

很容易想到的思路便是将 \(n\) 进制先转换为 \(10\) 进制,在转换为 \(m\) 进制。

具体实现:

  • 先写一个 getN 函数将 \(n\) 进制转为十进制,存入 \(a\) 数组;

  • 观察进制转换的过程可知,一个 \(n\) 进制数的每一位 \(x\bmod m\) 的值即为它在 \(m\) 进制下的值。根据这一结论,便可遍历 \(n\) 进制数的每一位数,求出余数,从而构建出对应的 \(m\) 进制数,并存入栈 stk 中。

  • 去除前导 \(0\),并输出倒序输出栈 stk 即可。

code

【习题 \(\texttt{10}\)\(\text{Cow Acrobats}\)

依照国王游戏的思路,对所有牛按 \(x+y\) 排序,接着算出所有牛的风险值取 \(\min\) 即为答案。

code

【习题 \(\texttt{11}\)\(\text{To the Max}\)

首先,对于矩阵的每一行计算出前缀和。

接着枚举 \(1 \sim n\) 中的两列,继续枚举这两列中间的所有行,令 \(sum\) 不断累加前缀和,同时更新全局最优答案。

code

【习题 \(\texttt{12}\)\(\text{Task}\)

贪心策略:按照时间从大到小的顺序排序任务与机器,并在时间充足的机器中选择等级最小的。

于是我们在排序后,倒序处理每个任务,并且将时间充足的机器加入一个可重集合,并用 lower_bound 求出等级 \(\ge\) 当前任务等级且最小的机器。

code

\(\texttt{0x09}\)

【例题 \(\texttt{29}\)\(\text{Push,Pop,GetMin}\)

rt,此题就是要我们维护一个栈,来实现入栈、出栈以及取栈中最小值这几个操作。

普通的栈是无法满足要求的,这时我们便想到用两个栈来解决问题。其中一个栈 \(a\) 保存原始数据,另一个栈 \(b\) 则保存当前栈中的最小值。

具体实现如下:

  • 对于 Push 操作,将读入的 \(x\) 压入栈 \(a\),设当前 \(b\) 栈顶为 \(y\),则将 \(\min(x,y)\) 压入栈 \(b\)

  • 对于 Pop 操作,同时弹出栈 \(a\) 与栈 \(b\) 的栈顶。

  • 对于 GetMin 操作,直接输出 \(b\) 栈栈顶。

这样的做法使得每个操作都做到了 \(O(1)\) 的时间复杂度,完美通过本题~

code

【例题 \(\texttt{30}\)\(\text{Editor}\)

很容易想到用两个栈维护这个编辑器,一个栈 \(a\) 维护开头到光标处的数字,另一个栈 \(b\) 维护光标处到结尾的数字。

同时维护两个数组 \(s,f\),分别表示当前的前缀和与最大前缀和。

具体的(记光标位置为 \(p\)):

  • I x:将 \(x\) 压入 \(a\) 中,并且令 \(s_p \gets s_{p-1}+x,f_p \gets \max(f_{p-1},s_p)\)

  • D:若 \(a\) 非空,则令 \(a\) 弹出栈顶。

  • L:若 \(a\) 非空,则令 \(a\) 弹出栈顶,并将其压入 \(b\) 中。

  • R:若 \(b\) 非空,则令 \(b\) 弹出栈顶,并将其压入 \(a\) 中;同时令 \(s_p \gets s_{p-1}+x,f_p \gets \max(f_{p-1},s_p)\)

  • Q k:直接输出 \(f_k\)

code

【例题 \(\texttt{31}/\texttt{32}\)】 进出栈序列问题

解法一:dfs\(O(2^n)\)

  • 用于输出所有方案。code

解法二:递推,\(O(n^2)\)

  • 所有数的进出站过程如下:

    1. \(1\) 进栈;

    2. \(2 \sim k\)\(k-1\) 个数出栈;

    3. \(1\) 出栈,排在第 \(k\) 个;

    4. \(k+1 \sim n\)\(n-k\) 个数进出栈。

  • 于是可以得到递推公式(记 \(n\) 个数的方案数为 \(s_n\)):

\( S_n=\sum^{n}_{k=1} S_{k-1} \times S_{n-k} \)

解法三:Catalan\(O(n)\)

  • 计算公式:

\( \dfrac{\binom{2n}{n}}{n+1} \)

py code

\(\texttt{0x10}\) 单调栈

好久没更笔记了 \(qwq\)

【例题 \(\texttt{33}\)\(\text{Largest Rectagle in a Hisgoram}\)

一个显而易见的思路便是,枚举每个矩形,尝试将其高度作为最大矩形的高度,并尽量向右边界扩展,对于所有这样得到的矩形面积取 \(\max\) 即为答案。

稍作思考便可发现,对于一个高度 \(<\) 上一矩形高度的矩形,若以它的高度为最大矩形高度,则其他矩形比它高的部分均毫无用处。

于是我们就可以想到用一个高度为当前矩形高度,宽度为超出部分累加的一个矩形替代,这样就保证了所有矩形均是单调递增的。

进而我们就得到了一个可行的实现:

  • 维护一个栈,存放所有矩形,高度单调递增。

  • 枚举所有矩形,若当前矩形高度 \(>\) 栈顶矩形高度,则直接入栈。

  • 否则,不断弹出栈顶直到栈顶矩形高度 \(<\) 当前矩形高度或栈为空,其间不断累加宽度与答案,最后将一个高为当前矩形高度,宽为宽度累加之和的矩形入栈。

code

\(\texttt{0x11}\) 队列

【例题 \(\texttt{34}\)\(\text{Team Queue}\)

建立 \(n+1\) 个队列,其中第 \(0\) 个保存每个小组的编号,第 \(1 \sim n\) 个保存每个小组的成员。

当接收到入队指令时,直接将 \(x\) 插入进第 \(y\) 个队列的末尾,若插入前队列为空,则需要将 \(y\) 插入进第 \(0\) 个队列的末尾。

当接收到出队指令时,直接将 \(x\) 弹出第 \(y\) 个队列,若弹出后队列为空,则将 \(y\) 从第 \(0\) 个队列中弹出。

code

【例题 \(\texttt{35}\)】 蚯蚓

闲话:\(q=0\)\(set\) 做法都有人写挂了,是谁我不说。

考虑一种 \(\text{brute-force}\)

  • 维护一个集合保存每条蚯蚓的长度,并且维护一个变量 \(delta\),记录集合中每个数的偏移量。

  • 对于第一问,在第 \(s\) 秒 中,若 \(t \mid s\),取出集合中的最大值 \(x\),并输出 \(x+delta\)

  • \(\lfloor px \rfloor - delta - q\)\(x-\lfloor px \rfloor -delta -q\) 插入集合中。

  • \(delta \gets delta+q\)

  • 对于第二问,将集合改为大根堆,在第 \(s\) 秒中,若 \(t \mid s\),则输出堆顶 \(+ \ delta\)

这样的复杂度是 \(O(m \log n)\) 的,由于 \(m\) 的数据范围过大,因此是不可接受的。

于是我们推了一下式子,发现从堆中取出的数和新产生的数都是单调递减的。\(dbxxx\) 大佬的证明

这样我们就可以建立三个大根堆 \(A,B,C\),分别维护原始数据以及新产生的两个数,因为 \(A,B,C\) 均是单调递减的,所以最大值一定在 \(A,B,C\) 的堆顶之一。配合上面的思路,可以做到 \(O(m+n \log n)\),完美过掉~

code

【例题 \(\texttt{36}\)】 双端队列

反向考虑将数组排序后,对应着几个双端队列。

建立一个 \(B\) 数组,存储 \(A\) 数组的下标,并对 \(A\) 数组排序。

稍加分析便可发现,\(B\) 数组中的若干个单谷段就对应着 \(A\) 数组的入队顺序(谷点第一个入队,递减的从队头入队,递增的从队尾入队)。

于是我们可以按照 \(A\) 数组中各个数据的异同来给 \(B\) 分段,然后统计 \(B\) 中单谷段的数量即为答案。

code

\(\texttt{0x12}\) 单调队列

  • 如果有人比你小,还比你强,那你就永远超不过 \(ta\) 了——\(cz\)

【例题 \(\texttt{37}\)】 最大子序和

为什么只有一道例题啊 \(qwq\)

建立一个队列,保存“下标位置递增,且前缀和值也递增的”最优决策集合下标

循环 \(1 \sim n\),对于每一个满足 \(1 \le i \le n\)\(i\),若队首元素与 \(i\) 的决策范围超过 \(m\),则不断弹出队头。

此时队头就是最优答案,更新全局答案。

接着将队尾所有前缀和 \(<i\) 的元素全部出队。

这便是著名的单调队列算法,时间复杂度 \(O(n)\)

code


链表那一章有心情再补。


\(\texttt{0x13}\) \(\text{Hash}\)

\(\text{Hash}\) 表可以看成是一个 vector \(+\) 链表的组合。

它主要支持如下操作:

  • 对于某个元素,可以根据其 \(\text{Hash}\) 函数值确定它所对应的链表,并执行遍历、比较、插入操作。

\(\text{Hash}\) 表的期望时间复杂度近似为 \(O(n)\),其中访问是 \(O(1)\) 的。


【例题 \(\texttt{38}\)\(\text{Snowflake Snow Snowflake}\)

首先,需要依次将这 \(n\) 片雪花插入 \(\text{Hash}\) 表中。同时定义 \(\text{Hash}\) 函数为

\( H(a_{i,1},a_{i,2},...,a_{i,6})=\sum^{6}_{j=1} a_{i,j}+\prod^{6}_{j=1} a_{i,j} \)

对于每一对雪花,我们依次顺时针和逆时针将它们遍历一次,计算 \(\text{Hash}\) 函数值,若顺时针或逆时针遍历的结果完全一致,则说明这两片雪花相同。

code

\(\texttt{0x14}\) 字符串 \(\text{Hash}\)

这种算法主要是将字符串映射为一个整数。

我们考虑,取一个固定值 \(P\),将字符串看作是 \(P\) 进制数,并且给每个字符分配一个 \(\ge 0\) 的数值(例如 \(a=1,b=2,...,z=26\))。此时再取一个固定值 \(M\),这个 \(P\) 进制数 \(\mod M\) 的值即为这个字符串的 \(\text{Hash}\) 值。

一般来说,我们习惯取 \(P=131,13331\)\(M=2^{64}\)

于是,对于字符串的各种操作,便可以转换成整数操作:

  • 若我们已知字符串 \(S\)\(\text{Hash}\) 值,现需要在 \(S\) 后接上一个字符 \(c\),则接上后的 \(S\)\(\text{Hash}\) 值为 \(H(S) \times P +value_c) \bmod m\),其中 \(value_c\) 表示分配给字符 \(c\) 的值。

  • 若我们已知字符串 \(S\)\(\text{Hash}\) 值,且已知字符串 \(S\) 后接字符串 \(T\)\(\text{Hash}\) 值,则字符串 \(T\)\(\text{Hash}\) 值为 \(H(S+T)-H(S) \times P^{len(T)}\),其中 \(len(T)\) 表示字符串 \(T\) 的长度。


【例题 \(\texttt{39}\)】 兔子与兔子

有了前面的铺垫,这题的思路就十分明了了。

首先,根据公式 \(H(S+c)=H(S) \times P +value_c) \bmod m\),可以 \(O(n)\) 地预处理出 \(\text{DNA}\) 序列所有前缀的 \(\text{Hash}\) 值,保存在 \(f\) 数组中。

同时,我们也需要预处理出 \(P\) 的若干次幂的值,保存在 \(p\) 数组中。

对于每个询问,根据公式 \(H(T)=H(S+T)-H(S) \times P^{len(T)}\),则区间 \([l,r]\)\(\text{Hash}\) 值即为 \(f_r-f_{l-1} \times p_{r-l+1}\)

因此仅需比较 \(f_{r_1}-f_{l_1-1} \times p_{r_1-l_1+1}\) 是否等于 \(f_{r_2}-f_{l_2-1} \times p_{r_2-l_2+1}\) 即可 \(O(1)\) 地回答每个询问。

时间复杂度 \(O(len(S)+Q)\)

code

【例题 \(\texttt{40}\)\(\text{Palindrome}\)

我们知道,回文串分为两类,分别是奇回文串偶回文串

于是,我们分别考虑两种回文串:

  • 对于奇回文串,二分 \(p\) 的值,若该回文串以 \(i\) 为中心,则需要使得 \(S_{i-p \sim i} = rev(S_{i \sim i+p})\),其中 \(rev(S)\) 表示将字符串 \(S\) 倒置。此时该回文串的长度为 \(2 \times p + 1\)

  • 对于偶回文串,二分 \(q\) 的值,若该回文串以 \(i\)\(i-1\) 之间的夹缝为中心,则需要使得 \(S_{i-q \sim i-1} = rev(S_{i \sim i+q})\)。此时该回文串的长度为 \(2 \times q\)

对于字符串的倒置比较,可以分别预处理出一个字符串 \(S\) 的前缀和后缀的 \(\text{Hash}\) 值,从而实现 \(O(1)\) 的进行比较操作。

时间复杂度为 \(O(n \log n)\),有种名为 \(\text{Manacher}\) 的算法能够 \(O(n)\) 解决此问题。由于笔者太菜所以虽然学过但并未学懂

code

【例题 \(\texttt{41}\)】 后缀数组

首先定义 \(SA\) 数组,初始 \(SA_i=i\)

\(SA\) 数组进行排序。对于两个相邻下标 \(a,b\),二分它们最长公共前缀的长度,利用预处理出的 \(\text{Hash}\)\(O(1)\) 地进行判断当前前缀是否合法,时间复杂度 \(O(n \log^{2} n)\)

输出 \(SA\) 数组后,循环 \(2 \sim n\),输出所有下标为 \(i\)\(i-1\) 的最长公共前缀的长度即可。

code

\(\texttt{0x15} \ \text{KMP}\)

\(\text{KMP}\) 算法能够实现在线性时间内判定字符串 \(A\) 是否为 \(B\) 的子串,并且求出字符串 \(A\)\(B\) 中出现的位置。

\(\text{KMP}\) 算法的核心就是 \(next\) 数组,它可以帮助字符串 \(A\) 完成“自我匹配”。具体的,\(next_i\) 的定义为:\(A\) 中以 \(i\) 结尾的非前缀子串和 \(A\) 的前缀的最大匹配长度。即:

\( next_i=\max{\{j\}} \ (j < i \ \text{且} \ A_{i-j+1 \sim i}=A_{1 \sim j}) \)

特别的,若不存在这样的 \(j\),则令 \(next_i=0\)

同时,若某个整数 \(j\) 满足 \(j < i \ \text{且} \ A_{i-j+1 \sim i}=A_{1 \sim j}\),则我们称其为 \(next_i\)候选项

如何在线性时间内求出 \(next\) 数组?这里有一个引理:

  • \(j_0\)\(next_i\) 的一个候选项,则 \(< j_0\) 的最大的 \(next_i\) 的候选项为 \(next_{j_0}\)

根据引理,若计算出了 \(next_{i-1}\),则它的候选项从大到小依次为 \(next_{i-1},next_{next_{i-1}},next_{next_{next_{i-1}}}......\);又由于若 \(j\)\(next_i\) 的候选项,则 \(j-1\) 必定是 \(next_{i-1}\) 的候选项,则 \(next_i\) 的候选项从大到小依次为 \(next_{i-1}+1,next_{next_{i-1}}+1,next_{next_{next_{i-1}}}+1......\)

综上,我们便可写出求 \(next\) 数组的流程了:

  • 初始化 \(next_i=j=0\)

  • \(i=2\) 开始循环,其间不断尝试扩展长度 \(j\),若失败(下一字符不相等)则令 \(j=next_j\),直至 \(j=0\)(应从头开始)。

  • 若扩展成功,则令 \(j=j+1\),此时 \(next_i=j\)

code

【例题 \(\texttt{42}\)\(\text{Period}\)

又一个引理:

  • \(S_{1 \sim i}\) 具有长度为 \(len\) 的循环元的充要条件是 \(len < i\)\(len \mid i\)\(S_{len+1 \sim i} = S_{1 \sim i-len}\)

根据引理,当 \(i-next_i\) 能整数 \(i\) 时,\(S_{1 \sim i-next_i}\)\(S_{1 \sim i}\) 的循环元,它的循环次数为 \(\dfrac{i}{i-next_i}\)

进一步的,根据 \(next\) 数组的引理,我们同样可以求出 \(S\) 的所有循环元。

code

\(\texttt{0x31} \ 质数\)

其他的什么埃氏筛、试除法之类应该都会,此处不再赘述,重点是要记住这个结论:对于任意合数 \(n\),其必定有一个不超过 \(\sqrt{n}\) 的(质)因子。反证法易证。

然后讲一下线性筛,其基本原理就是根据唯一分解定理,对于每个合数从大到小地累积质因数,从而使得每个合数只有唯一的一种方式被凑出,将时间复杂度降至 \(O(n)\)

在每个合数被筛到之前,它的质因子一定全部被筛到了,所以正确性是显然的。

an example code

【例题 \(\texttt{43}\)\(\text{Prime Distance}\)

记住一个区间筛技巧:对于闭区间 \([l,r]\),可以筛出 \(2 \sim \sqrt{r}\) 的所有质数,然后把 \([l,r]\) 中能被它们任意一个整除的数标记为合数,剩余的即为质数了。这里运用的就是开头提到的结论。

然后这题就被解决了。时间复杂度 \(O(\sqrt{r}+(r-l) \times \log \sqrt{r})\)

code

总结:

  • 记住上述套路。

  • 不要用 map,学会数组平移,注意多测尽量少清空。

  • 考虑边界情况。

【例题 \(\texttt{44}\)】 阶乘分解

1s 1e9,LG测评机有点过于强大了。

显然暴力的 \(O(n \sqrt{n})\) 无法承受。考虑优化。

从质因子下手,显然 \(n!\) 的所有质因子都是 \(1 \sim n\) 的所有质数,先将 \(1 \sim n\) 的质数筛出来。

对于一个质数 \(p(1 \le p \le n)\)\(n!\) 中包含它的个数就是 \(1 \sim n\) 中包含它的个数的总和(包括包含一个它的、两个它的......)。于是运用容斥原理可知这个值是:

\( \lfloor \frac{n}{p} \rfloor + \lfloor \frac{n}{p^2} \rfloor + ... + \lfloor \frac{n}{p^{\lfloor \log_p n \rfloor}} \rfloor = \sum_{p^k \le n} \lfloor \frac{n}{p^k} \rfloor \)

因为一共 \(\frac{n}{\log n}\) 个质数,每个质数需要 \(\log n\) 的时间计算上式,于是时间复杂度为 \(O(n)\)

code

总结:

  • 暴力优化考虑缩小范围、转换角度。

  • 学会推式子。

posted @   _XOFqwq  阅读(68)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示