B站左神算法课学习笔记(P8):贪心
一、前缀树
1、什么是前缀树?
2、前缀树的代码实现
(1)创建节点
(2)插入节点
(3)查询某个单词
(4)查询前缀
(5)删除某个单词
二、贪心算法
1、经典例题——安排会议:
2、严格证明贪心策略的艰难
3、例题:分金条(哈夫曼编码)
4、例题:项目利润
三、堆题目补充
1、数据流与中位数
四、N皇后问题
1、基础解法
2、优化
一、前缀树
1、什么是前缀树?
如下图,经典前缀树:信息储存在边上;对每个字符串中的字符,若有从原点出发相同的路径(字符),则复用;若没有,则新建。
2、前缀树的代码实现
(1)创建节点
节点一般需要携带更多信息,如下面代码所示:
另一个例子:
当如下前缀树存储 ["abc", "ab", "bc", "bck"] 这条信息,可以实现:
(1)查询特定字符串是否出现(查是否出现过"bc":依次查b-c,看c节点处e的值);
(2)查有多少个以"ab"开头的字符串(依次查a-b,看b节点处e的值)【哈希表做不到】;
tips:该查询代价极小,代价仅是字符串的长度!
(2)插入节点
代码实现:
而对于空串(""),root节点pass++,end++!
实际上,若字符种类太多,考虑到浪费空间,可以考虑使用哈希表的方式来存储路径信息:
HashMap<Char, Node> nexts;
进一步地,若希望路与路之间有序组织,而非像哈希表一样散列,可以使用有序表:
TreeMap<Char, Node> nexts;
(3)查询某个单词
思路:若输出词不为空,则拆为数组依次查看路径;若没有指定的某一条路径,则返回0;若成功遍历到指定路径的地步,则返回node.end。
(4)查询前缀
思路:(基本同上)若输出词不为空,则拆为数组依次查看路径;若没有指定的某一条路径,则返回0;若成功遍历到指定路径的地步,则返回node.pass。
(5)删除某个单词
思路:现搜索是否有该词,若有:沿途node.pass--;终点node.end--。
例:在下方前缀树中,删除一个"abc":
特殊情况:当删除一个单词后,部分节点的p值变为0,则需要把其后方的路径全部删除!
代码实现:
tips:中间删除时,java只需要将索引置空即可,jvm会自动释放内存;而c++则需手动遍历到每一个节点后依次释放!
二、贪心算法
定义:在某一标准下,优先考虑最满足标准的样本,最后考虑最不满足标准的样本,最终得到的一个答案的算法,叫做贪心算法。(即求局部最优解)
但是注意证明:局部最优解需推到整体最优解
1、经典例题——安排会议:
结合下图理解:
按照上图右侧的代码实现:(贪心的代码实现都很短,熟悉了很好做!!)
重点:别去纠结!贪心本质上是有技巧地 蒙 !
2、严格证明贪心策略的艰难
该部分以一个例题为例,详细说明严格证明贪心策略有多么艰难,以劝告各位,不要尝试证明!
Q:给定一组字符串,要求它们拼接起来后的字典序最小。
旧的贪心策略可以轻松举出反例:对于字符串组 ["b", "ba"],若按照旧贪心策略,则是"bba";但是其实最小的字典序应该是"bab",故旧贪心策略错误。
下面严格证明为什么新贪心策略正确:
(1)首先证明比较策略是有效的 -——> 看是否有传递性
有效,即,对于不同的原始数据,我们总能得到同样的排序结果;
下图是一个反例,因为形成了环,导致对于不同的原始数据可能得到不同的比较结果,所以是无效的比较策略:

开始证明传递性:


到此为止,已知我们的排序策略有效,可以排出一组唯一的序列出来。
(2)证明已排序的序列,交换任意两个元素后,字典序会变大
Case1:a、b相邻,可借助上述 a.b<=b.a 的结论:
Case2:a、b不相邻时,根据根据Case1的结论递推可知:
(3)使用数学归纳法继续证明交换3个数、交换4个数等等都会产生更大的字典序。
进而证明贪心策略有效!
所以:应该采用对数器,使用实验方式来验证!!!
3、例题:分金条(哈夫曼编码)
思路:采用小根堆实现:
代码实现:
4、例题:项目利润
题目理解:假设有如下5个项目,初始资金为1,所以只能按照下面的顺序做三个项目。虽然总共允许做四个项目,但是后面项目的资金要求无法满足,故做3个项目后停止。
解题思路:
建立两个堆:1、锁定的项目:根据项目所需花费建立小根堆,按照当前资金解锁项目;
2、解锁的项目:根据项目利润,将从上述堆中弹出的项目建立大根堆;
代码实现:



贪心策略小结:根据你确立的标准,依次考察每个样本。
三、堆题目补充
1、数据流与中位数
Q:若用户给你输入一个数据流,你需要维持一个数据结构,使得你可以随时取出所有数据流中的中位数。
为什么快:每次输入时,大根堆和小根堆的调整都是O(logN)水平!
四、N皇后问题
1、基础解法
代码实现:

上面代码可以结合下图(八皇后)理解:
下面时 isValild() 方法的具体实现:
2、优化
采用位运算,优化 record 中挨个查询!!


可结合下图理解:
