好用的东西2

2022/8/24 23:26:28

本文主要是介绍好用的东西2,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

合并果子(加强版)

有若干堆果子,每次合并两堆果子 \(S_1,S_2\) 需要付出 \(|S_1|+|S_2|\) 的代价,问合并为一堆的最小代价。\(n\le 10^7\)

我们开两个队列,一个存初始每个果子并升序排序,另一个存合并后的若干堆果子。每次比较两个队首,取出最小和次小,并把合并后的一堆插入到队尾。(蚯蚓 的 trick )

易证第二个队列是升序的。

分卷子

有一摞卷子,要按等级分类(A,B,C,D) 。每次可以挑选一摞卷子分成两堆,一堆包含某些等级的卷子,另一堆包含某些等级的卷子(换句话说,同一时刻不存在等级相同的卷子在不同的堆中),直到分成给定等级为止。每次分卷时需要比较该堆的每一张卷子,问比较次数总和最小是多少。

显然,分卷子的过程可以看成一个树形结构,问题转化为使 \(\sum_i dep_i \times cnt_i\) 最小,但是这无从下手。

注意到每一个叶子的卷子数量是一定的,正难则反(分类 --> 合并),即每次将某两堆的卷子 \(S_1,S_2\) 合并,付出 \(|S_1|+|S_2|\) 的代价,这是经典的 合并果子

哈夫曼树

一棵具有 n 个带权叶结点的二叉树,使得所有叶结点的带权路径长度之和(\(\sum _l v_ldep_l\))最小,这样的二叉树称为哈夫曼树。

Huffman 树的构建:

将 n 个带权的结点,看做 n 棵大小为 1 的树

每次选出两棵根结点权值最小的树,分别作为左右儿子合并成一棵树,并将根结点的权值设为左右儿子权值之和

哈夫曼编码

考虑这样一个问题:我们需要将一串字符转化为 01 字符串 ,并要求转化后的字符串总长度尽可能小

例如:E:0 , D:1 , C:00 , B:01 , A:10

显然我们可以从每个字母出现的频率下手,一种贪心策略是给频率大的字母配对长度尽可能小的 01串。

为了防止歧义(例如 001 可以是 EED、CD、EB),需要保证不能有一个串是另一个串的前缀,于是我们可以在 01trie树 上考虑

把字母当成叶子,以字母的频率为权值,构建一棵 Huffman 树。向左走为 0,向右走为 1,最终走到叶子形成的编码就是最优的。

贪心

对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,它所做出的仅是在某种意义上的局部最优解。关键是贪心策略的选择。必须注意的是,选择的贪心策略必须具备无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关,选择贪心策略时一定要观察其是否满足 无后效性

img

img

img

https://www.cnblogs.com/-citywall123/p/11184316.html

适用范围 :

最优子结构,无后效性

无后效性(贪心选择) : 所谓贪心选择是指同一规则,将原问题变成一个相似但规模更小的子问题,而后的每一步都是当前看似最佳的选择,且这种选择只依赖于已做出的选择,且不依赖于未做出的选择
最优子结构 : 执行算法时,每一次得到的结果虽然都是当前问题的最优解(即局部最优解),但只有满足全局最优解包含局部最优解时,才能保证最后得到的结果是最优解

证明方法 :
反证法和归纳法
反证法: 如果交换方案中的任意(相邻)两个元素,答案不会变得更好,可以推定目前的解已经是最优解了
归纳法: 执行算法过程了,先算出边界情况(k=1)的最优解 \(F_1\), 再证明对于任意的k,\(F_{k+1}\) 可以由 \(F_k\) 推出

反悔贪心 : 无论当前选择是否最优都接受,然后进行比较,如果选择之后不会更优,则反悔,舍弃这个选择,否则,正式接受,循环往复

1.最优装载问题

  • 有 \(n\) 个物体,第 \(i\) 个物体的重量为 \(w_i\) ,选择尽量多的物体,使得总重不超过 \(C\)

由于答案只和物体的数量有关,与重量无关,于是将重量按照从小到大排序,依次选择每个物品,满足最优子结构

贪心策略:每次装最轻的

  1. 部分背包问题
  • 有 \(n\) 个物品, 第 \(i\) 个物品的重量为 \(w_i\), 价值为 \(v_i\) , 要求在总重量不超过 \(C\) 的前提下,使得总价值尽可能大,每个物品可以只取走一部分,重量和价值按比例计算

贪心策略:每次选价值与重量比值最大的
由于可以选一部分,所以总重量恰好 \(C\) (或所有物品的重量和不超过 \(C\)),除了最后一个选的物品之外,其他物品要么不选,要么全部选,替换法可证

证:首先背包不能有剩余。若我们将一个单位价值小的装进背包而没装进单位价值大的,则我们显然可以用价值大的替换掉,不优。

  1. 选择不相交区间问题
  • 给定 \(n\) 个开区间 \((a_i,b_i)\), 选择尽量多的区间,使选择的区间之间没有公共点

按右端点升序排序,依次考虑,选择第一个与之前选的区间不冲突的区间,更新,决策包容性/数学归纳法

  1. 区间选点问题
  • 给定 \(n\) 个闭区间 \([a_i,b_i]\),选择尽量少的点,使得每个区间都包含至少有一个点

按右端点升序排序,依次考虑,对于当前区间,若选出点的集合不能覆盖她,则将区间末尾的点加入集合,决策包容性

  1. 区间覆盖问题

给定 \(n\) 个闭区间 \([a_i,b_i]\) ,选择尽量少的区间包含给定线段区间 \([s, t]\)

按左端点升序排序,依次处理每个区间,每次选择覆盖点 \(s\) 中右端点最大的一个区间,并将 \(s\) 更新为右端点,直到覆盖到 \(t\) 为止,决策包容性

  1. 流水作业调度问题
  • 有 \(n\) 个作业要在两台机器\(M_1,M_2\)上完成,每个作业必须先花 \(a_i\) 的时间在 \(M_1\) 完成,再花 \(b_i\) 的时间在 \(M_2\) 上完成

7.带限期和罚款的单位时间调度问题

  • 有 \(n\) 个任务,每个任务都需要 \(1\) 个时间单位执行,任务 \(i\) 的截止时间为 \(d_i\), 要求任务 \(i\) 必须在 \(d_i\) 之前完成,否则会导致 \(w_i\) 的罚款. 确定所有任务的执行顺序,使得罚款最小

将 \(w\) 降序排序,对于当前任务 \(i\) ,如果 \(d_i\) 之前没有排满,则找到最靠后的位置将这个任务加入集合. 否则,放弃处理此项任务,交换反证法易证

三分

三分法可以用来查找单峰函数的最大(小)值。

按照 lmid 和 rmid 是否在arg同侧分类

如果在同侧,舍弃较远点对应的区间(lmid),[l,r]->[lmid,r]
如果在异侧,与上一分类保持一致性

01分数规划 : 给定 \(a,b\), \(w_i \in\{0,1\}\) 最大/小化 \(\frac{\sum a_i\times w_i}{\sum b_i\times w_i}\) ,二分mid, \(\sum a_i-b_i\times mid > 0\) P4377

均值分数规划 : 最大/小化 \(\frac{\sum_{i=l}^ra_i}{r-l+1}\) ,即求平均值最大的子段,二分mid即可

树与图的遍历

深度优先遍历,就是在每个点 \(x\) 上面对多条分支时,任意选一条走下去,执行递归,直到回溯到点 \(x\) 后再考虑走向其他边。

void dfs(int u){
	vis[u]=1;
    for(auto v:e[u]){
		if(vis[v])continue;
        dfs(v);
    }
}

这段代码访问每个点和每条边恰好一次,复杂度为 \(O(N+M)\)

时间戳

按照上述深度优先遍历的过程,以每个点第一次被访问的顺序,给予这 \(N\) 个点 \(1-n\) 的整数标记,该标记就被称为时间戳,记为 dfn

树的dfs序

我们在对树执行深度优先遍历时,对于每个节点,在刚进入递归后以及即将回溯前各记录一次该点的编号,最后产生长度为 \(2N\) 的节点序列就称为树的 dfs 序。

DFS序 的性质:

每个节点 \(x\) 的编号在序列中恰好出现两次,设这两次出现的位置为 \(L[x],R[x]\) ,那么 \([L[x],R[x]]\) 就是以 \(x\) 为根的 DFS 序。我们在做一些与树的统计相关问题时,可以通过 DFS 序把子树统计转化为序列上的区间统计。

树的深度

树中各个点的深度是一种 自顶向下 的统计信息,在深度优先遍历的过程中结合自顶向下的递推。

树的重心

一种 自底向上 的统计信息,比如 子树大小,设节点 \(x\) 有 \(K\) 个儿子 \(y_1,y_2,..,y_k\) ,则 \(size_x=1+\sum_{1\le i\le k}size_{y_i}\)。

对于一个节点 \(x\),如果我们将它从树中删除,那么原来的一棵树可能会分成若干个连通分量,设 \(\mathrm{maxpart(x)}\) 表示删除 \(x\) 后产生子树中,最大的一棵的大小。\(\text{maxpart}\) 函数取得最小值的节点 \(p\) 就称为整棵树的重心。

图的连通块划分

从 \(x\) 开始一次遍历,就会访问 \(x\) 能够到达的所有点与边。因此,通过多次深度优先遍历,可以划分出无向图中的各个连通块,同理,对于一个森林,也可以划分出每棵树。

树与图的广度优先遍历

广度优先遍历需要使用一个队列实现,在遍历的过程中,不断从队头取出一个节点 \(x\) ,对于 \(x\) 面对的多条分支,把沿着每条分支到达的下一个节点插入队尾。重复执行上述操作直到队列为空。

我们在广度优先遍历的过程中顺便求出一个 \(d\) 数组,对于一棵树来讲,\(d[x]\) 就是节点 \(x\) 在树中的深度。对于一张图来讲,\(d[x]\) 被称为点 \(x\) 的层次(从起点 \(1\) 走到 \(x\) 需要的最少点数)。广度优先遍历时一种 按照层次顺序 进行访问的方法,它有如下两个重要性质:

  1. 在访问完所有第 \(i\) 层的节点后,才会开始访问第 \(i+1\) 层节点
  2. 任意时刻,队列中至多有两个层次的节点。若其中一部分属于第 \(i\) 层,则另一部分节点属于 \(i+1\) 层,并且所有第 \(i\) 层节点排在第 \(i+1\) 层节点之前。也就是说,**广度优先遍历队列中的元素关于层次满足 “ 两端性 ” 和 “ 单调性 ” **

拓扑排序

给定一张有向无环图,若一个由图中所有点构成的序列 \(A\) 满足:对于图中的每条边 \((x,y)\) ,\(x\) 在 \(A\) 中都出现在 \(y\) 之前,则称 \(A\) 是该有向无环图顶点的一个拓扑序,求解序列 \(A\) 的过程就称为拓扑排序。

拓扑排序过程思想十分简单,我们只需要不断选择图中入度为 \(0\) 的节点 \(x\) ,然后把 \(x\) 连向的点入度减 \(1\)。

拓扑排序可以判定 有向图是否存在环,我们对任意有向图执行上述过程,在完成后检查 \(A\) 的长度。若 \(A\) 的长度小于图中点的数量,则说明某些节点未被遍历,存在环。

ACwing164 可达性统计

从 \(x\) 出发能够到达的点,是从 \(x\) 的各个后继节点 \(y\) 出发能够到达的点的并集,再加上 \(x\) 自身。所以,在计算出一个点所有后继节点的 \(f\) 值之后,就可以计算出该点的 \(f\) 值。这启发我们可以先求出原图的一个拓扑序,然后按照 拓扑序倒序 进行计算——因为在拓扑序中,任意一条边 \((x,y)\) ,\(x\) 都排在 \(y\) 之前。

我们可以利用 状态压缩 的技巧,用一个 \(N\) 位二进制数存储每个 \(f(x)\) 。这样一来,对若干个集合求并,就相当于对二进制数做 按位或 运算。

\(N\) 位二进制数可以压缩成 $\frac{N}{32} +1 $ 个无符号 32 位整数 unsigend int 进行存储,也可以直接使用 STL 提供的 bitset。 \(O(N(N+M)/w)—O(N^2/32)\)

深度优先搜索

深度优先搜索,顾名思义,就是按照深度优先的顺序对 “问题状态空间” 进行搜索的方法。我们之前把一个问题的求解看做对问题状态空间的遍历与映射。从现在开始,我们可以进一步把 “问题空间” 类比为一张图,其中的状态类比节点,状态之间联系的可达性就用图中的边来表示,那么使用深度优先搜索算法求解问题,就相当于在一张图上进行深度优先遍历。

深度优先搜索与 “递归“ ,”栈“ ,密切相关。我们倾向于认为递归与递推是一种较为简单的遍历方式,除了搜索以外,还有许多算法都可以用递归实现。而深搜是一类包括遍历形式、状态记录与检索、剪枝优化等算法整体设计的统称。

我们先来定义过程中的 搜索树 结构。在对图进行深度优先遍历处于点 \(x\) 时,对于某些边 \((x,y)\) ,\(y\) 是一个尚未访问过的节点,程序从 \(x\) 成功进入了更深层的对 \(y\) 的递归;对于另外的一些边 \((x,y)\),\(y\) 已经被访问过了,从而程序继续考虑其他分支。我们称所有点(问题空间中的状态)与成功发生递归的边(访问两个状态之间的移动)构成的树为一棵搜索树。整个深搜算法就是基于该搜索树完成的——为了避免重复访问,我们对状态进行记录和检索;为了使程序更加高效,我们提前剪除搜索树上不可能是答案的子树和分支,不去进行遍历。

指数型,排列型,组合型枚举,都是深搜最简单的形式。与之相关的 子集和问题、全排列问题、N 皇后问题 都是可利用深搜解决的 NPC 问题。

Acwing165 小猫爬山

很像背包,可惜不是。我们以序列作为搜索的层次,依次尝试把每一只猫分配到缆车中:分配到一辆已经租用的车辆上,或者新租一辆缆车安置这只猫。于是我们关心的状态有:已经运送的猫有几只,已经租用的缆车有多少辆,以及每辆租用缆车搭载重量之和,这三个维度共同标识着一个 “ 节点 ” 。

优化搜索顺序

一个普遍的原则是:优先考虑决策少的元素,即越靠近根的树枝边越少,减少分叉的数量,这样不仅能减少整棵树和每棵子树的规模,还可以增加最优性剪枝的效率,及时判断。

回归本题,对于搜索树的每一层,我们要使越小层数的状态发生转移的选择数量尽可能小,显然重量越大选择缆车的个数越少,于是我们将重量按从大到小排序。优先搜索重量较大的小猫,减少搜索树 “分支” 的数量

第二个优化是对于每个猫,先去将它放到之前的缆车里,再新开一个。原因很简单,如果不这么做,将会遍历一个非常庞大的搜索树,影响效率。

可行性剪枝

如果猫的重量加上某个缆车重量和超过 \(W\),那么该方案不合法。

最优性剪枝

如果当前状态的缆车数量大于目前搜到的最优答案,那么当前分支就可以回溯了,这个点的搜索子树一定不优。

Acwing166 数独

首先,每一行、列、九宫格都要出现 \(1-9\),根据抽屉原理,这等价于往上面填 \(1-9\) ,使得每行、列、九宫格不会有重复的数字,即一个排列。

我们关心的状态就是数独的每个位置上填了什么数。在每个状态下,我们找出一个还没有被填的位置,检查有哪些合法的数字可以填,这些合法数字就构成了当前状态向下继续递归的分支,然后继续递归去填剩下的格子。

搜素边界有两种:一是如果所有位置都被填满,就找到了一个解;二是如果发现某个位置没有能填的合法数字,说明当前分支搜索失败,应该沿着递归该状态的那条边回溯到上一个状态,去尝试其他分支。

注意:在任意一个状态下,我们只需找出一个位置,考虑该位置填什么数,不需要枚举所有的位置和可填的数字向下递归(因为其他位置在更深的层次会被枚举到)。新手常犯的错误是混淆 ”层次“ 和 ”分支“,造成重复遍历若干棵覆盖同一状态空间的搜索树,致使搜索的复杂度大幅增长。

然而,数独问题的搜索树规模仍然很大,运用人类智慧,策略一定是先填上 “已经能够唯一确定的位置”,然后从那些填的比较满、选项比较少的位置进行突破。所以在搜索中我们也采取类似的策略:在每个状态下,从所有未填的位置里 选择 “ 能填的合法数字 ” 最少的位置 ,考虑该位置上填什么数,作为搜索的分支,而不是任意找出一个位置

在搜素程序中,影响程序效率的除了 搜索树的规模 (影响算法的时间复杂度),还有在每个状态上 记录、检索、更新的开销(影响程序运行的常数时间。我们可以用位运算代替桶执行 “ 对数独各个位置所填数字的记录 ” 以及 “ 可填性的检查与统计 ”。这就是我们所说的程序 “ 常数优化 “ 。具体地说,本题有以下优化方法:

  1. 对于每行、每列、每个九宫格,分别用一个一个 \(9\) 位二进制数保存哪些数字还可以填。
  2. 对于每个位置,把它所在行、列、九宫格的三个二进制数做 按位与(&)运算,就可以得到该位置能填哪些数,用 lowbit 运算就可以把能填的数字取出。
  3. 当一个位置填上了某个数后,把该位置所造行、列、九宫格记录的二进制数对应位置改为 \(0\),即更新当前状态,向下递归;回溯时改回 \(1\) 即可还原现场。

具体地,我们在每个状态维护一个未填点的序列,,每次将能填合法数字最少的位置与序列首元素交换,然后递归去填首元素后面的点,这是首元素与剩余元素独立,所以在剩余元素内部再交换是封闭的。

搜索算法的基本框架并不难,但如何减小搜索树的规模并快速遍历搜索树却是一门高深的学问。

遵循原则 : 正确性,准确性,高效性

剪枝

所谓剪枝,就是减小搜索树的规模、尽早排除搜索树中不必要分支的一种手段。形象地说,就好像减掉了搜索树的枝条。

  1. 优化搜索顺序

在一些搜索问题中,搜索树的各个层次、各个分支之间的顺序是不固定的。不同的搜索顺序会产生不同的搜索树形态,其规模大小也相差甚远。

  1. 排除等效冗余

在搜索过程中,如果我们能够判定从搜索树的当前节点沿着某几条不同分支到达的子树是等效的,那么只需要对其中一条分支进行搜索。

另外,我们一定要避免重叠、混淆 层次 与 分支 的概念,避免遍历若干棵覆盖同一状态空间的等效搜索树。

  1. 可行性剪枝

在搜索过程中,及时对当前状态进行检查,如果发现分支已经无法到达递归边界,就执行回溯。

某些题目条件的限制范围是一个区间,此时可行性剪枝也被称为 “ 上下界剪枝 ”

通过放缩、放宽条件限制。

  1. 最优性剪枝

在最优化问题的搜索过程中,如果当前花费的代价已经超过了当前搜到的最优解,那么无论采取多么优秀的策略到达递归边界,都不可能更新答案。此时可以停止对当前分支的搜索,执行回溯。

  1. 记忆化

可以记录每个状态的搜索结果,在重复遍历同一个状态时直接检索并返回。这就好比深度优先遍历一张图时标记一个点是否被访问过。

不过,我们发现,在 小猫爬山 和 数独 问题中,搜索算法遍历的状态空间其实是 树形,不会重复访问,所以不需要记录。

例:数的划分,生日蛋糕,小木棍

Acwing167 Sticks

经典题。

我们从小到大枚举木棒的长度 \(len\),\(len\) 必须是 \(sum\) 的约数。

我们面对的状态是:已经拼好的木棒个数,正在拼的木棒的当前长度,每个木棍的使用情况。在每个状态下,我们尝试用那些尚未使用的木棍拼上去,然后递归到新状态,递归边界就是成功拼好所有木棒。

  1. 优化搜索顺序:

将木棍长度从大到小排序,优先尝试较长木棍,决策较少。

  1. 排除等效冗余

(1)可以限制先后加入同一根木棒的木棍编号时递增的。因为木棍为 \((x,y),(y,x)\) 这是等效的,只需要搜索其中一种。

(2)对于当前木棒,如果当前分支递归失败,不再尝试向该木棒中拼接其他相同长度的木棍。反证法,替换可得一种方案。

(3)在尝试拼接该木棒中第一根木棍递归分支失败,那么直接判定当前分支失败,立即回溯。因为剩余的空木棒之间是等效的。反证。

(4)在尝试拼接该木棒中最后一根木棍递归分支失败,那么直接判定当前分支失败,立即回溯。可以用贪心解释:再用一根木棍拼完该木棒必然比再用若干根木棒拼完该木棒更好。也可以 反证。

(1)~ (4) 分别利用了 同一根木棒上木棍顺序的等效性 、等长木棍的等效性、空木棒的等效性、贪心。

Acwing168 生日蛋糕

搜索框架:从下往上搜索,枚举每层的半径和高度作为分支。

搜索面对的状态是:正在搜索第 \(dep\) 层,当前表面积 \(S\),当前体积 \(V\) ,上一层的高度和半径。

整个蛋糕的上表面积等于最底层的圆面积。可以在最上面的一层再累加进答案。这样只需要计算侧面积就行了。

  1. 优化搜索顺序(上下界剪枝)

枚举 \(R\in[M-dep,\min(\lfloor \sqrt {N-V}\rfloor,r[dep-1]+1)]\) ,再枚举 \(H\in [M-dep,\min(\lfloor (N-V)/R^2\rfloor,h[dep-1]+1)]\)

倒序枚举

  1. 可行性剪枝

取极限,预处理出从上往下的每一层半径和高度都取 \(1,2,...,i\) 时的侧面积和体积,如果当前状态加上这个最小体积超过了 \(N\) ,该分支递归失败,回溯。

  1. 最优性剪枝

(1)如果当前侧面积加上最小侧面积都大于目前答案,回溯。

(2)

\[n-v=\sum_{i=1}^{dep-1}r_i^2h_i ~, ~s=\sum_{i=1}^{dep-1}2r_ih_i \]

\[s=\frac{2}{r_{dep}}\sum_{i=1}^{dep-1}r_ir_{dep}h_i\ge \frac{2}{r_{dep}}(n-v) \]

通过放缩建立起了剩余体积和最小侧面积的不等关系。

这题告诉我们,在思考如何剪枝采取极限思想,假设该变量无穷小或无穷大会造成什么影响。

image-20220819202810676

迭代加深搜索(IDDFS)

DFS 在一些问题模中可能无限扩展,搜索树的深度可能会很深,节点规模很庞大。如果直接 DFS 的话每次选定一个分支,不断深入,直到到达递归边界才回溯。求最优/深度最低/步数最少解的状态时,问题的答案在某个较浅的节点上。

从小到大限制搜索的深度,假设目标状态的深度不超过某阈值。如果在当前搜索深度中搜不到答案的话,就把深度增加,重新进行一次搜索,反复迭代。因为每层节点数呈指数级增长,所以时间复杂度瓶颈在于深度最大的一次搜索。

当搜索树的规模随层次增长的很快时,并且我们能确保答案在一个较浅的节点时,就可以用 IDDFS。

IDDFS与BFS的对比

两种搜索树扩展的节点个数渐进相同。

  • 空间限制极强时
  • 时间限制内求较优解
  • 当状态的存储与表示比较麻烦/费时

Acwing170 Addition Chains

先考虑 DFS 该怎么做,依次搜索下一个点填什么数,每次枚举 \(i\) 和 \(j\) 。

优化搜索顺序:倒序枚举 \(i,j\),尽快逼近 \(n\),分支少。

(最优性剪枝:如果当前长度不小于目前答案,直接回溯。)

排除等效冗余:对于不同的 \(i,j\) ,\(X_i+X_j\) 相等的情况开个桶判掉。

每个点的分支个数是组合数级别的,搜索树规模十分庞大,但是 \(m\) 的值不会太大(易发现是 \(\le 10\) 的)。

广度优先搜索

如果把问题的状态空间类比为一张图,那么广度优先搜索就相当于在这张图上执行广度优先遍历。借助队列,我们可以得到从起始状态到每个状态的最少步数/代价。

Acwing172 Bloxorz

走地图 问题:给定一个矩形地图,控制一个物体在地图中按要求移动,求最少步数。

在这类问题中,地图的整体形态是固定不变的,只有少数个体或特征随着每一步的操作发生改变。将变化的部分提取为状态

广度优先搜索是逐层遍历搜索树的算法,所有状态按照入队先后顺序具有层次单调性。如果每一次扩展恰好对应一步,那么当一个状态第一次被访问时,就得到了最少步数。

该题中,变化的只有长方体的所在位置和放置形态,于是我们可以用一个三元组 \((x,y,dir)\) 来表示,其中 \(dir=0,1,2\) 分别表示竖着,横着,纵着,钦定 \((x,y)\) 为左上角的那个位置。

Acwing137 矩阵距离

Flood-Fill算法: 有一个 \(N\times M\) 的 01 矩阵( 0 是可通行点,1 是禁止点)和一个起点,每一步可以上下左右走。求从起点开始到矩阵上的每个点的最少步数。用 BFS 解决。

该题相当于 多个起点的 Flood-Fill ,我们把矩阵的每个点都看做是起点。对于每一个位置,求可以从任何起点出发,到达该位置的最少步数(也就是距离该位置最近点的距离)。

在这种有多个等价起始状态的问题中,只需在 BFS 开始之前,将所有点插入到队列中,根据 BFS 逐层扩展的性质,当点 \((x,y)\) 被第一次访问时,就相当于距离它最近的那个起点扩展到了它。

起始上述操作等价于建立超级源点 \(0\) ,向其他所有点连一条长度为 0 的边,这个思路在 Johnson、差分约束中都有体现。

\(O(NM)\)

Acwing174 Pushing Boxes

广搜变形

BFS 等价于在一张边权为 \(1\) 的图上执行广度优先遍历,求出了每个点相对于起点的最短距离,并得到了 队列中的状态层数满足两端性和单调性 的结论,每个状态在第一次被访问入队时,计算的步数即为所求。

双端队列 BFS

处理边权为 \(0,1\) 的图,得到每个点相对于起点的最短距离。

与 BFS 类似,只是在扩展节点时发生改变:如果这条分支时边权为 \(0\) 的边,就把新节点 在队头入队;如果是边权为 \(1\) 的边就在队尾入队。这样,任意时刻队列中的距离都满足 两端性和单调性,同一个点可能被更新(入队)多次,但是它被第一次被扩展(出队)时,最短距离已经确定了。

理解方式:依然考虑一棵最短路树,我们在扩展第 \(i\) 层点的时候,先把同层的点放到前半段继续扩展\(i+1\) 层的点放在后半段等扩展完第 \(i\) 层后再扩展。可以看做把权为 \(0\) 的边路径压缩,这样依然是一层层的遍历。

基于迭代思想的 BFS:SPFA

优先队列 DFS :DIjkstra

Acwing176 装满的油箱

第一眼思路是二分,但它没有前途。

对每组询问都做以下操作:我们可以使用二元组 \((u,fc)\) 记录每个状态,代表从起点到 \(u\) 剩余油量为 \(c\),的最少油价。

DP转移:\(dis_{u,c}\to dis_{v,c-w},dis_{u,c}+p_u\to dis_{u,c+1}\),转移形成的图有环,但一定不会绕环转移。于是这就可以 SPFA 了,知道所有状态收敛。

其实我们把每个状态当成一个点,转移的代价当成边,其实问题变成了从起点到每个点的最短路。由于边权非负,Dijkstra就行了,当一个状态从堆中取出时它的答案就确定了。

  • Trick1:我们在一个点加油时只需加上一点,因为加上更多的油会在别的状态更新到(一条链加若干条边),就像搜索没有在一层全搜完一样。

  • Trick2:当我们从堆中取出状态的点 \(u\) 是终点时这就是答案了。因为从堆中取出点的 \(dis\) 一定不降。虽然复杂度的上限是 \(10^8\) 的,但是如果这么做的话,边不会都被枚举到,代码也不会跑满。

双向 BFS

双向搜索可以避免在深层子树上浪费时间。在一些题目中,问题不但有 初态,还有 终态,并且从初态开始搜索产生的搜索树与从终态开始逆向搜索的搜索树都能覆盖整个状态空间(即 变换操作是可逆的)。我们可以采用 双向搜索——从初态和终态出发各搜索一半状态,产生两棵深度减半的搜索树,在中间交会,组合成答案。用 hash等方法将两者的搜索路径拼接。

双向 BFS 与双向搜索的思想基本相同。以最少步数为例,从起始状态、目标状态分别开始,两边轮流进行,每次各扩展一整层。如果两边各有一个状态在记录时重复了,说明这两个搜索过程相遇了,可以合并得出起点到终点的最少步数(因为最短路有最优子结构性质)。

Acwing177 噩梦

直接把男和女的位置作为状态是 \(4\) 维的,复杂度为 \(O(n^2m^2)\)

有两个起点,我们可以双向 BFS ,建立两个队列,分别从男/女的初始位置开始 BFS,两边轮流进行。这样每一轮都是一个时间点。

每一轮中,boy 扩展 3 层,girl 扩展 1 层,使用数组 d 记录每个位置对于 boy 和 girl 的可达性。

扩展的过程中,实时计算与 gost 的曼哈顿距离。若第一次出现位置 \((x,y)\) 能被两者到达则轮数就是两者最短会合之间。\(O(NM)\)

练习题

P1379 八数码难题

BFS 经典题

由于找的是最少步骤,且状态数不多,于是我们采取广度优先搜索。

我们用一个 \(9\) 进制数字存储状态,从初始状态出发,将转移到所有可能的状态加入队列,需要 map 或 hash表 判重,或者康托展开。

[zr联赛十连测day3] X子棋

乍一看像个不可做题,实际上仔细思考一下就是个傻逼模拟题。

先处理吃子:当我们在 \((x,y)\) 下上一个黑子后,我们需要将所有被围住的白子连通块删掉,其实很简单,由于落子后影响的只有 \((x+1,y),(x-1,y),(x,y+1),(x,y-1)\) 的所在连通块,所以只需检查从这四个连通块是否被围住即可。

再处理连着在一起的情况:由于下该点后影响的只是该点附近的连通块,从 \((x,y)\) 向两边扩展检查是否胜利即可。

Acwing 171 送礼物

广搜的优化技巧

双端队列bfs : 用于处理01最短路问题,对于新结点,对于边权为1的点放入队尾,边权为0的放入队首,这样任意时刻队列中的结点对应的距离值都具有两端性和单调性

解答树

以开始结点为根,每次向下分叉,一个点的每个儿子对应着下一步可能得到的结果。

注意,dfs 和 bfs 的解答树是一样的,只是两者的搜索顺序不同。dfs 搜出来的是步骤字典序最小解,bfs 搜出来的是步骤最小的解。

四阶数独

给出一个 \(4\times 4\) 的方格,每个格子只能填写 \([1,4]\) 的整数。问有多少种合法的填数方案,使得每行、每列、每个四等分的小正方形都恰好是一个排列。

由于行之间独立,不妨对于每一行都枚举一个排列,这样情况数为 \((4!)^4=331776\)

接着,我们可以用计数排序的思路,用数组记录每行、每列、每个小正方形每个数字的占用情况就行了。

P1219 八皇后

由于一行只能放下一个皇后,于是我们可以一行一行的放。

同时我们还需要记录每列,每两种对角线的占用情况,如果我们把对角线当成一个一次函数,则满足 \(x+y=c,x-y=d\),由于 \(x-y\) 可能小于 \(0\) ,所以我们还需要记录一个 偏移量 \(dt\)

迭代加深搜索(IDS)

是一种每次限定搜索层数的DFS,处理最优解问题

由于BFS基于队列完成,队列的空间复杂度很大,当状态比较多或无穷个时,BFS显出明显的劣势

规定层数后可配合最优性剪枝得到很好的效果

例:P1763:由于搜索树是一颗n叉树,第h层的复杂度大于前h-1层的复杂度,复杂度瓶颈集中在最后一层,故不会因层数不足以找到解而消耗大量的时间

meet in the middle

题目数据范围很小但又不能暴搜解决

将搜索对象分为两半,最后再将两半的答案合并,存储状态的答案需要哈希

可让指数级复杂度变为平方根级别

例:P2962,CF525E

双向同时搜索

从初始状态和目标状态同时开始深搜或广搜,每颗搜索树的期望树高为平方根级别

有两种实现方式:
1.将两个方向的状态放入一个队列,标记每个状态是哪个方向扩展到的
2.开两个队列,每次找扩展结点数小的方向交替扩展

例:P1379,P2324

字符串哈希

https://codeforces.com/blog/entry/60445

哈希的思想:定义一个把字符串映射到整数的函数 \(f\) ,这个 \(f\) 是哈希函数

可通过字符串哈希比较两个字符串是否相等

不妨考虑 ASCII 码,这是把一个字符映射到一个 \([0,128]\) 的整数,那我们将字符串的每一个字符看成一个整数时,再把当成一个 \(b(b>128)\) 进制数,是不是就能将字符串映射到整数域上了。

多项式滚动哈希 :

选取两个合适互质常数 \(b,h\) ,设 \(S=s_1s_2s_3...s_m\),则\(H(S)=\sum_{i=1}^m s_ib^{m-i} \pmod h\),本质上是一个 \(b\) 进制数

\(H(S,k+1)=H(S,k)\times b+ s_{k+1}\)

单次比较哈希冲突的概率为 \(\frac{1}{h}\)

n个哈希值均匀分布到值域上冲突概率约为 \(\frac{n^2}{2h}\)

进行 n 次比较错误的概率为 \(1-(1-\frac{1}{h})^n\)

元素个数不能超过 \(\sqrt h\)

注意任意字符不能映射成 0

双哈希 :

我们希望选取一个 \(10^{27}\) 级别的模数,可以极大的降低出错概率

选取两个模数 \(m1,m2\) ,根据中国剩余定理,等价于一个 \(m=m1\times m2\) 的模数,你甚至可以三哈希

选取模数 :

m1=1000000123,m2=2^64

注意不能只使用2^64模数

选取基数 :

\(max(a[i])<p<m1\)

子串哈希 : \(H(s[l...r])=H(S,r)-H(S,l-1)\times b^{r-l+1}\)

h一般取\(10^9+7,10^9+9\),自然溢出

  • 找到 \(S\) 所有与 \(T\) 相配的子串

计算 \(S\) 与 \(T\) 的前缀哈希,枚举 \(S\) 的开始位置,比较 \(S\) 中每个长度为 \(|T|\) 的子串与 \(T\) 的哈希值 ,\(O(n+m)\)

  • 求两个串的最长公共子串

计算两个串的前缀哈希,二分长度len,将一个串中所有长度为len的子串哈希值插入vector并排序,并在另一个串中查询,\(O(nlog(n)^2)\)

  • 将串的所有循环移位按字典序排序

复制 \(S\), 将所有开始位置插入到 vector, 比较两个串字典序大小通过二分比较哈希值实现, \(O(nlog(n)^2)\)

  • 求串的回文子串个数

计算正反哈希,枚举回文中心,二分回文半径,比较哈希值,\(O(nlog(n))\)

  • 求有多少个长度为 \(n\) 串的子串使得该子串是长度为 \(m\) 串的某一个循环移位

复制 \(m\) 串,将所有循环移位的哈希值插入到 vector 里并排序,在另一个串查询, \(O((n+m)log(n))\)

  • 求有多少个长度\(n\) 的串的后缀,使得后缀的无限复制与整个串的无限复制一样

将串翻转变前缀,计算前缀哈希,枚举前缀长度,显然,一个可行的循环节长度为 \(nm\) , 比较长度为 \(nm\) 的两个串的哈希值

\(H[0,m)*=(1+b+b^2+...+b^{(n-1)m})(S[0]*b^{m-1}+S[1]*b^{m-2}+...+S[m-1])\)

\(H[0,n)*=1+b+b^2+...+b^{(m-1)n})(S[0]*b^{n-1}+S[1]*b^{n-2}+...+S[n-1])\)

记几何级数的和为 \(sum(a,k)=1+a+a^2+...+a^{k-1}=\frac{a^k-1}{a-1}\)

由于 \(inv(a-1,mod)\) 当 \(mod\) 为 \(2^{64}\) 时不存在逆元,考虑另一种高妙的方法(我记得好像在那本书讲过?

  1. \(sum(a,2k)=(1+a)\times sum(a^2,k)\)
  2. \(sum(a,2k+1)=1+a\times sum(a,2k)\)

哈希表

一种高级的数据结构,能够高效的实现存储和查询,能与map作替换
哈希表的原理是构造合适的哈希函数实现键值到内存的映射,存入数组中
由于存在哈希冲突,最常见的方法是拉链法,即对每一个哈希值开一个邻接表来实现数组

引入

考虑这样一个问题,给定 \(N\) 个自然数,值域在 \([0,10^9]\) ,求出这 \(N\) 个自然数中有多少个不同的数。

显然,当值域为 \([0,10^7]\) 我们可以用计数排序的思路,记录每个值是否出现过。

如果值域为 \([0,10^9]\) 该怎么办呢?我们可以选取一个模数 \(mod\) ,定义一个大小为 \(mod\) 的数组,然后把每个数对 \(mod\) 取模。如果两个数对 \(mod\) 取模得到了相同的值,那么就认为两个数是相同的。

这样看来,我们虽然减少了空间的利用,但也牺牲了一定的正确性,可能会出现两个不同的数但取模之后得到了相同的结果,产生了哈希冲突。

怎么办呢?我们可以把 int 数组改成一个 vector<int> 的数组或者一个链表,然后取模后为同一个数的所有值都存在其对应的 vector 或者链表中。

判断一个数 x 是否存在时,遍历 x % mod 位置对应的链表中所有元素,看是否有 x 即可,这个表称为 哈希表

哈希函数的构造 :

构建一个哈希函数,把 \(x\) 弄成 \(key\)

  1. 数(取余法)

选择恰当的b,将key对b取模作为哈希值,即 \(H(key)=key \bmod b\)

b要选能存储下的且尽量大的质数(\(10^6+3,10^6+9,999979,999997\)),其实如果数是随机的话,没有必要找一个质数。

  1. 坐标 / DP状态 / 搜索状态: \(i\times n+j、(i\times n+j)\times m+k\) ,表达式越简单越好

  2. 字符串/数组

转换成 \(b\) 进制数(\(b=26,131,13131\))或者 类 $ b$ 进制数(就是每个位置再加一个权),转化成一个数字取中间的一段:hash 要支持区间减法。一般取b大于10的为基数,x转化为10进制数的值对 h取模作为哈希值,b与h是互质的

哈希函数可以复杂多样,只要能存储下且尽量避免哈希冲突即可

补充:对于取余法,期望插入查询步数为 \(O(\frac{N}{b})\)

\(n\) 位长度的哈希表发生碰撞的次数为 \(\sqrt n\)

  1. 图/树

找重心,最小表示法

哈希应用

《高级数据结构》P12

生日悖论

考虑这样一个问题:一个班级里至少有多少个人,才能使至少两个人生日相同的概率为 \(p\%\)

设一年有 \(n\) 天,一个班级有 \(k\) 个人,\(k\) 个人生日互不相同的事件为 A,则:

\[P(A)=\frac{n}{n}\times \frac{n-1}{n}\times ... \times \frac{n-k+1}{n} \]

又因为 \(P(\overline{A})=1-P(A)\ge p\),所以 \(P(A)\le 1-p\)

根据不等式 \(1+x\le e^x\) ,有

\[P(A)\le e^{-\frac{1}{n}}\times e^{-\frac{2}{n}}\times ...\times e^{-\frac{k-1}{n}}\le 1-p \]

\[e^{-\frac{k(k-1)}{2n}}\le 1-p \]

将 \(n=365,p=0.5\) 代入,解得 \(k=23\) ,说明只有 \(23\) 个人就能使至少两个人生日相同的概率达到 \(\frac{1}{2}\) ,这十分反直觉。如果是 \(70\) 个人的话,概率高达 \(99.9\% !\)

找整数

给定 n 个整数,m 个询问,每次询问一个正整数,询问该正整数是否在 n 个数中出现过,数的值域在 \([0,10^9]\),\(n,m\le 10^6\)

找矩形

给定 n 点,问这些点组成的最小矩形面积为多少,矩形的边平行于坐标轴。\(n\le 1000,x,y\le 40000\)

https://leetcode.cn/problems/minimum-area-rectangle/

我们枚举对角线的两个点,在哈希表里查剩下的两个点是否存在即可

找集合

有一个大小为 n 的集合,里面的元素都是正整数,如果某个子集是好的,那么就必须满足不存在这样的两个数:一个是另一个的 p 倍。给定 n 和 p,问最大的好的子集有多大。\(n\le 10^5,p\le 10^9\)

考虑将每个数写成 \(a\times p^b\) 的形式,则两个数有 p 关系当且仅当两个数的 a 相同且 b 相差 1。于是 a 之间独立,我们对每个 a 分别考虑。问题变成了:序列上有一些点,选的点不能相邻,最大化选点的数量。

排序后贪心,每次选最小的一定最优。用哈希表将每个 a 存下即可。

背单词

玲玲有 n 个想背的单词,每个单词的长度不超过 10,一篇文章由 m 个单词组成。给出这 m 个单词的出现顺序,她想在文章中找出连续的一段,其中包含最多的她想要背的单词(重复的只算一个)。并且在背诵的单词尽量多的情况下,还要使选出的文章尽量短。

\(n\le 10^5,m\le 10^5\)

我们先将每个单词计算哈希值,并按哈希值排序,枚举 m 个串,在单词序列中二分得到出现的位置,哈希离散化,并统计总单词个数。在这个文章中双指针找到最短满足条件的区间就行了。

排列哈希

排列的哈希主要运用的是 变进制数,变进制数能够实现全排列的哈希,并且变进制数与全排列之间形成 双射 ,不会形成冲突。

我们考虑这样的一种变进制数:第 1 位 逢 2 进 1,第 2 位 逢 3 进 1,......,第 n 位 逢 n+1 位进一,表示形式为:

\[K=a_0\times 0!+a_1\times 1!+a_2\times 2! + a_3\times 3!~ +...+ ~a_n \times n! \]

默认 a0=0,我们先来考查一下该变进制数的进位是否正确。

假设第 \(i\) 位 \(a_i=i+1\) ,需要进位,\(a_i\times i!=(i+1)\times i!=1\times (i+1)!\) ,即向高位进 1 正确,因此这是一种合法的计数方式。

\(n\) 位变进制数能表示的范围在 \([0,(n+1)!)\) ,共 \((n+1)!\) 个,如果我们能找到一种 “操作” 使得每个 \(n\) 位变进制数能与 每个 \(n+1\) 的排列一一对应,那我们就实现了 全排列的“数化”

考虑一个长度为 n+1 的序列 \(b_0<b_1<b_2<...<b_n\) ,对于它的任意一种排列 \(c_0,c_1,c_2,...,c_n\),求出其中第 \(i\) 个元素与 \(0-i-1\) 构成的逆序对数 \(d_i~~(1\le i\le n)\) ,则该排列的 逆序对序列 为 \(d_1,d_2,...,d_n~~~(0\le d_i\le i)\) ,于是我们每个排列都能变成一个变进制数。

由于 \(n\) 位变进制数只能表示 \((n+1)!\) 个不同的数,而 \(n+1\) 个元素的全排列刚好有这么多,且每一个排列都能表示一个变进制数。可以证明,任意两个排列产生的变进制数是不一样的,因为 逆序对序列与全排列是一一对应的 ,因此 全排列与变进制数是一一对应的

这其实就是 康托展开

P5043 【模板】树同构([BJOI2015]树的同构)

有根树的最小表示

树的括号序列转化:从树根开始执行一次 dfs,进入时加入左括号,回溯时加入右括号,可以构成一个长度为 2*n 的括号序列

在 dfs 的过程中,一个 u 有多个子节点,因此在不规定访问顺序的情况下会有不同的括号序列,我们把字典序最小的括号序列称作 有根树的最小表示

两棵树同构的充要条件是最小表示相同

我们设 \(f_u\) 为以 \(u\) 为根节点的最小表示,根据贪心,将 \(f_{v_1},f_{v_2},...,f_{v_m}\) 按字典序从小到大排序,\(f_u=(f_{v_1}f_{v_2}...f_{v_m})\)

最坏为 \(O(n^2)\)

无根树的同构

一种朴素的方法是:我们可以枚举一棵树的根 \(i\),并求出一个最小表示 \(ans_i\) ,令 \(ans(T)=min(ans_i)\) ,则两棵无根树同构的条件转化为了 \(ans(T1)=ans(T2)\)

对于树 \(T\),我们不妨设 \(ans(T)\) 在 \(minrt(T)\) 取到,即 \(ans(minrt(T1))=ans(minrt(T2))\) ,所以两棵无根树同构只需要以某一点为根的有根树同构就行了。

对于两棵无根树,我们只需要找到一对 “同位点”(即同构树中位置相同的点),并以这一对点为根求最小表示就行了。

通常我们选择的同位点是 重心,由于一棵树的重心至多只有两个,我们对每个重心都求一遍就行了。

https://www.luogu.com.cn/blog/codesonic/Mosalgorithm
https://www.cnblogs.com/ouuan/p/MoDuiTutorial.html

第二类斯特林数的行是bell数

loj6036

模拟赛
zhengruioi.com/problem/1575
zhengruioi.com/problem/1547
zhengruioi.com/problem/1536
https://www.luogu.com.cn/problem/T217752

zjr 7.8 讲课

https://atcoder.jp/contests/arc134/tasks/arc134_c

https://atcoder.jp/contests/abc240/tasks/abc240_g

https://atcoder.jp/contests/abc242/tasks/abc242_f

https://atcoder.jp/contests/abc237/tasks/abc237_f

https://codeforces.com/problemset/problem/1648/C

https://codeforces.com/problemset/problem/1696/E

CF1622E

P2216 理想的正方形

二维rmq
二维滑动窗口
二维st表

记 \(st[i][j][k][l]\) 为左上角为 \((i,j)\), 长为 \(2^k\), 宽为 \(2^l\) ,矩形的最值

先固定 \(j\) 令 \(l=0\) ,处理每一行的st表,再在列之间做做,相当于把每一行的一段区间看成一个点

P2034 选择数字

P3572 [POI2014]PTA-Little Bird



这篇关于好用的东西2的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程