基础算法与数学总结 【第二部分 基础数据结构】

基础算法与数学总结 【第二部分 基础数据结构】

1 栈

别的就不介绍了,至于它可以转化表达式这个大家应该很清楚。

其实本来表达式就是递归定义,而前后缀表达式用从内到外的优先级避免了括号,方便了计算机的处理。

1.1 单调栈

单调栈是利用决策集合的有序性排除无用决策的一种策略。其思想是”抛弃无用,合并相同“。这种结构适合处理这种决策信息:

  • 决策信息取值范围一直在栈顶的方向扩大

  • 合法的决策序列满足单调性,每加入一条决策就可以抛弃不满足的部分

其实大概也就是”左/右边的最大/小值“这样的问题以及变式,用的不是很多。

2 队列

队列是先进先出的线性结构。

2.1 单调队列

其思想与单调栈很为相似,但是它可以在队头队尾进行操作

所以这意味着什么?

  1. 它可维护决策集合取值范围两边单调变化的情况

  2. 它维护决策的两维单调性:位置/最优性

  3. 它能够适应更多的情况

之前我们在”单调队列优化DP“那里总结了一段话:

记得有个博客总结的很好:“假如有的选手比你小还比你强,那么你就可以退役了”。

这句看似调侃,实际上摆明了单调队列优化的核心逻辑,如果某个决策集合取值范围单调滑动(滑动窗口),决策点显然越“靠前”就可以存活更长时间,如果这样的点比靠后的节点更新还要优,那么靠后的决策就是没有用的,因为它已经不可能再取代靠前面决策的地位。(无论从最优性还是合法性)

嗯,我认为总结到位了。

所以如果遇到某种描述,说明决策集合具有:

  • 范围单调变化

  • 求每一时刻的最优决策

  • 决策集合大部重叠,易于维护

这时可以使用单调队列优化。其实最明显的特征是”范围单调变化“,尤其是类似滑动窗口的描述。当然,单调队列可以搭配别的使用,例如ST表,稍后我们会看到这个应用的。

3 链表与邻接表

链表是支持以下操作的数据结构:

操作 插入 删除 查找 清空
时间复杂度 O(1)\mathcal{O}(1) O(1)\mathcal{O}(1) O(N)\mathcal{O}(N) O(N)\mathcal{O}(N)

注:假如你不想导致内存泄漏的话,清空就不能简单地删除头尾指针。

数组链表的一个常见实现是存储图的邻接表。同时,这也是实现Hash表的一个工具

4 Hash

Hash思想是将大范围、复杂的数据映射到易于处理的一个范围(通常一个正整数)的方法。就是设置一个代表。

额,当然,带来的后果就是有错误概率,当然一般只要模数选择恰当,通常不会出现过多的错误,如果要避免错误当然可以使用我们上面提到的链表,只是会增添时间复杂度,不过仍然可以接受。

但是如果你的Hash函数设计不当,就很有可能带来较高的Hash冲突概率。

4.1 字符串Hash

字符串Hash将字符串看作一个P(P=131/13331)进制数然后模上一个大数(通常 2642^{64}),随后按照mod的运算律可以对Hash值执行变换从而获得其他字符串的Hash值。

通常有以下两个常用运算:

  1. 现有字符串S,以及字符c,则在S后追加c的Hash值是 Hash(S+c)=(Hash(s)×P+value(c))modMHash(S+c)=(Hash(s)\times P+value(c))\mod M

  2. 现有字符串S,S+T,则T的Hash值是Hash(T)=(Hash(S+T)Hash(s)×Plength(T))modMHash(T)=(Hash(S+T)-Hash(s)\times P^{\operatorname {length}(T)})\mod M

通过这两个操作我们便可以处理出字符串的任意字串的Hash

5 字符串

字符串小节主要研究KMP模式匹配以及最小表示法。

之前对于KMP我有比较详细的笔记,但是并未叙述思想:关于KMP - haozexu

来,今天我就把坑填上。

要理解问什么会有 nextnext 数组,我们应该先从基础的暴力入手。暴力会这样匹配:

  1. 从前往后扫描文本串

  2. 对于每一个文本串中的位置,往后扫描判断其能否完全匹配模式串

  3. 如果失配立即退出,成功则记录答案,然后继续枚举

但是,如果对于文本串中的位置 TposT_{pos} ,如果扫描到模式串的 PposP_{pos} 位才失配了,那么我们已经知道中间 TposT_{pos}PposP_{pos} 是相同的,也知道如果往后推一位,肯定会有很多错位,也是不能匹配的。

暴力算法将这些信息视而不见,而KMP则加强了利用:

因为中间 TposT_{pos}PposP_{pos} 是相同的,就相当于自己匹配自己,如果知道失配之后应该从哪里可以重新开始,就是求对于自己的一个前缀,它的真前缀与真后缀能够匹配的最大长度X,如果知道这个长度,发生失配的时候就可以说当前其实匹配了X位。这个值X对于每一个前缀,即border,是它的next值。

请读者画个图,理解理解。

我们打暴力,不仅要得暴力分,而且应该注意暴力”为什么慢“,然后利用它丢失的信息。

当然这个也不是很常用,溜了溜了

6 Tire

Tire是一个多叉树结构,这种数据结构支持:

操作 初始化 插入 查询
时间复杂度 O(1)\mathcal{O}(1) O(length(S))\mathcal{O}(\operatorname {length}(S)) O(length(S))\mathcal{O}(\operatorname {length}(S))

这其实还是跟BST挺像的,都是通过划分数据,逐位给数据分类,避免重复遍历,实现优化。

7 二叉堆

二叉堆是一棵满足”堆性质“的完全二叉树。

大根/小根堆性质是指:对于每一个节点,其上面的值都大于/小于其子节点。

注意到这个性质无关左右子树的位置,所以它很容易在堆结构发生变更时进行维护。

也因为上面的原因,这个结构可以被排布成为一个完全二叉树,这是它可以优化的原因

支持的操作
操作 调整 插入 删除堆顶 删除任意 查询最值
时间复杂度 O(logN)\mathcal{O}(\log N) O(logN)\mathcal{O}(\log N) O(logN)\mathcal{O}(\log N) O(logN)\mathcal{O}(\log N) O(1)\mathcal{O}(1)

以及Huffman树,一种常常用于数据编码的结构(好吧几乎只在初赛里考)

Huffman树的构建基于贪心,策略是优先将权值最小的点置于树底。

8 例题

拆开原代数式,问题化为RMQ问题。

可以解决问题的分支有很多:分治、线段树、平衡树、ST、单调栈

想到维护一个值可以贡献的范围,考虑一个值在哪个范围内的区间都可以产生贡献:

以最大值为例,它能作用的区间一直到左/右边的边界为止。

所以使用单调栈求这个边界。

单调队列的基本应用。这个问题完全符合我们上面说的适用特征。

星战这个题首先很难看出满足的特征就是一个出度都为1

然后看出来了也很难找到这样惊世骇俗的判断方式。

其实这种方法也可以叫哈希,也是构造了一种代表/映射,来代表原本的情况。

题解有一段话:

这就是哈希的原理了:判定时构造一个必要不充分条件,但这个“不充分”实际上有非常大非常大的概率充分,以至于不充分性可以忽略不计,从而达到充要判定的效果。读者应当熟悉的字符串哈希,也是同样的原理。

所以如果考虑要判别某一种条件,就要考虑如果满足这个条件,还有没有什么其他东西可以推出——以及如果满足这个推出的条件,有多大可能满足目标条件。

本题的哈希,主要是通过扩大影响的方法实现的,要知道2=1+1还是2=1+0,就可以扩大1和0的影响,使得他们不同。比如我们给1赋权值114000,0赋权值514,那么1+1=228000,1+0=114514。如果要判断是不是由两个1加起来的就可以判断它是不是114000的倍数嘛。

通过本题,我们认识了别样的哈希:它既可以把一些不易处理的数据映射成为一个代表,又可以将原来的情况映射到另外的随机值来区分它们。

这个题目不好考虑,因为字符串循环节有可能是不完整的。

现在考虑手动模拟,先从简单部分:循环节完整开始,随后再削减末尾的字符。

原串: abcabcabc

对比: abcabcab

如果c不是残缺的,那么容易发现这样的串有这样的特质:任意3的倍数长度的前后缀都是相等的。考虑残缺的串是不是有这样的特质:不论字符串是不是残缺的,其内部总含有循环节,而残缺的部分也肯定是循环节的前缀,所以能够匹配3的倍数加2长度的子串。

由于匹配一整个串没有意义,考虑少匹配一点,只匹配最长真前后缀,由于最小循环节的不可再拆分性,少匹配的必然就是一个最小循环节。而求最长前后缀,这就是KMP的适用领域了。

当然,能够讲通思路的人必然是技巧高超的人,因为他能够克服有知和无知的信息障碍。我做不到这样高深的地步。

手动模拟不管到了什么地方都是重要的,因为它代表了人类主动求知的精神,而不是消极等待灵感出现。

考虑随便拿两个点看看,一般路径问题都和LCA有关系,这个题也是不例外。

注意到异或运算自己与自己异或为0,所以一条路径的异或和其实可以直接用两个点到根的异或和再异或来代替,上面LCA到根的部分就消掉了。

如果是这样,那么考虑直接用这个到根的异或和代替这个节点。

然后就是选哪两个异或的问题。如果逐位考虑任意两个数,显然应该每一位都要尽可能不同。

然而每次都重新扫描其实浪费了一些信息:每个二进制位仅仅有0/1两个取值,说不定大部分数这一位都是相等的,然而我们还是要扫描它。

然后我们就想到这样的、给数据逐位分类的数据结构:Tire。

首先应该考虑到有贪心策略:由于每一头奶牛的贡献都是1,选择优惠完了价值最低的k头,然后如果还有闲钱就再买。

但是,有一点是正确的,那就是现在选择的k头一定在答案中,因为我们放弃这头牛就会省下一张优惠券,要应用这张优惠券在其他的牛上虽然都达到了一样的贡献,但是都没有用在这头牛上省,所以是不优的。

所以每一刻,我们都可以看作在做这样的操作:

  • 用原价买价格最低的奶牛

  • 将已经用过的优惠券给另一头奶牛,同时要保留这一只奶牛

综上我们维护三个堆,维护已选集合中:P-C最小的,维护未选集合中P、C最小的。

有的时候贪心算法是错误的,但是如果我们可以通过调整之前决策来触达最优解,这就是反悔贪心。

posted @ 2023-07-21 08:08  haozexu  阅读(12)  评论(0)    收藏  举报  来源