hzwer收集课件笔记 - 算法基础 - Chapter1

C++模板与STL库介绍.ppt

链接:https://raw.githubusercontent.com/hzwer/shareOI/master/基础算法/C%2B%2B模板与STL库介绍.ppt

作为一个橙名玩家大部分都是会的,不过有几个新知识:

容器:可容纳各种数据类型的数据结构。
迭代器:可依次存取容器中元素的东西。
算法:用来操作容器中的元素的函数模板。例如,STL用sort来对一个vector中的数据进行排序,用find来搜索一个list中的对象。
函数本身与他们操作的数据的结构和类型无关,因此他们可以在从简单数组到高度复杂容器的任何数据结构上使用。

只不过性能就不一定保证了,比如用algorithm::lower_bound(set1.begin(), set1.end(), value),绝对暴毙。

容器分为三大类:
1.顺序容器:vector,deque,list;
2.关联容器:set,multiset,map,multimap;
3.容器适配器,stack,queue,priority_queue。

stack和queue确实是deque的适配器,是把deque堵住一端实现的,queue可以理解但是stack为什么要用deque实现呢?

是为了push和pop操作是个性能更平均的O(1)(因为vector在内存不足时大量复制发生卡顿,但是deque的卡顿不明显,而且deque回收内存更方便),并且已有的元素不发生内存地址的变化,可以使得指针仍然有效(但是迭代器可能会无效)。但为什么不用list做呢?这就不懂了,或许list性能比deque要低吧。

(参见https://blog.csdn.net/u012989012/article/details/84255664)

priority_queue默认使用的容器是vector,这个很好理解,heap本身就是用数组做的。

所有标准库容器共有的成员函数:
按字典序比较两个容器大小的运算符:<, <=, >, >=, ==, !=
empty:判断容器中是否有元素;
max_size:容器中最多能装多少元素;
size:容器中元素个数(注意这个一般是unsigned的,不要用这个-1或者与负数比较);
swap:交换两个容器的内容。

只在顺序容器和关联容器中有的成员函数:
begin:返回指向容器中第一个元素的迭代器;
end:返回指向容器中最后一个元素后面的位置的迭代器;
rbegin:返回指向容器中最后一个元素的迭代器;
rend:返回指向容器中第一个元素前面的位置的迭代器;
erase:从容器中删除一个或几个元素;
clear:从容器中删除所有元素。

注意rbegin和rend是不能放进sort里的(好像是这样)。
也就是说,容器适配器是不能够erase和clear的。

迭代器也有常迭代器,是只能读取不能修改的,例如:vector<int>::const_iterator it=vector1.begin()

只有顺序容器和关联容器才有迭代器。

迭代器的访问性能是不完全一样的,只有拥有随机访问迭代器的容器才能使用排序算法。

没有随机访问迭代器的容器,比如list还有关联容器,就不要使用排序算法了,也不要使用algorithm::lower_bound,不过关联容器可以用自带的代替。

STL 中的迭代器按功能由弱到强分为5种:
1.输入:Input iterators 提供对数据的只读访问。
1.输出:Output iterators 提供对数据的只写访问。
2.正向:Forward iterators 提供读写操作,并能一次一个地向前推进迭代器。
3.双向:Bidirectional iterators提供读写操作,并能一次一个地向前和向后移动。
4.随机访问:Random access iterators提供读写操作,并能在数据中随机移动。

编号大的迭代器拥有编号小的迭代器的所有功能,能当作编号小的迭代器使用。

所有迭代器:++p, p++
输入迭代器:*p, p = p1, p == p1, p!= p1
输出迭代器:*p, p = p1
正向迭代器:上面全部
双向迭代器:上面全部,--p, p--
随机访问迭代器:上面全部,以及:
p += i, p -= i
p + i:返回指向 p 后面的第i个元素的迭代器
p - i:返回指向 p 前面的第i个元素的迭代器
p[i]:p 后面的第i个元素的引用
p < p1, p <= p1, p > p1, p>= p1

注意只有随机访问迭代器可以比较大小,所以set一般用it != set1.end()来遍历,而vector可以用it < vector1.end()

容器,迭代器类别
vector,随机
deque,随机
list,双向
set/multiset,双向
map/multimap,双向
stack,不支持迭代器
queue,不支持迭代器
priority_queue,不支持迭代器

find查找失败返回查找区间的终点。

也就是说假如传入的区间终点不是end迭代器就不会是end。

所有顺序容器都支持以下成员函数:
front:返回容器中第一个元素的引用
back:返回容器中最后一个元素的引用
push_back:在容器末尾插入新元素
pop_back:删除容器末尾的元素

vector:最普通的容器,支持随机访问迭代器,所以支持所有的alorithm的算法

list自带一些特有的成员函数:
push_front:在容器前面插入新元素
pop_front:删除容器前面的元素
sort:排序(list自带的sort,注意list不支持alorithm::sort
remove:删除和指定值相等的所有元素
unique:删除所有和前一个元素相同的元素(也就是用来去重的话,建议先sort,不过这个不需要erase的吗?)
merge:合并两个链表,并清空被合并的那个链表
reverse:颠倒链表
splice:在指定位置前面插入另一链表中的一个或多个元素,并在另一链表中删除被插入的元素

deque容器:在vector的基础上支持在容器前面操作。

set容器:insert返回一个pair,这个pair的second表示是否insert成功。

C++的pb_ds库在OI中的应用_于纪平.pdf

链接:https://github.com/hzwer/shareOI/blob/master/基础算法/C%2B%2B的pb_ds库在OI中的应用_于纪平.pdf

__gnu_pbds::priority_queue

头文件:ext/pb_ds/priority_queue.hpp

与std::priority_queue基本相同,多了一个clear可以用。

template <typename Value_Type,
    typename Cmp_Fn = std::less <Value_Type>,
    typename Tag = pairing_heap_tag ,
    typename Allocator = std::allocator<char> 
>class priority_queue;

Tag:表示使用的堆的类型,可以是binary_heap_tag,binomial_heap_ta,rc_binomial_heap_tag,pairing_heap_tag,thin_heap_tag。

比std的更多功能:

可以使用begin和end获得iterator来遍历。
可以使用increase_key和decrease_key,插入和删除单个元素。
可以合并。

push:插入一个value,返回push成功后的迭代器。
modify:传入一个迭代器和一个value进行修改(修改完成后会自动调整)。
erase:删除一个迭代器。

join:传入另一个priority_queue的引用,那个合并到this里,然后那个会被清空(可合并堆)。

时间复杂度

共5种操作:push,pop,modify,erase,join。

pair_heap_tag:push和join为常数,其他为对数。
binary_heap_tag:只支持push和pop,为对数。
binomial_heap_tag:push为常数,其他为对数。
rc_binomial_heap_tag:push为常数,其他为对数。
thin_heap_tag:push为常数,不支持join,其他为对数。若只有increase_kay,则modify也为常数。

比起std::priority_queue的改进

合并的时候用pair_heap_tag?
Dijkstra的时候用thin_heap_tag?

是这样吗?

测试结论1:只进行push和pop,binary_heap_tag的效率与std近似,在没有开O2的时候比没有开O2的std快,其他的惨不忍睹。

测试结论2:进行join,binary_heap_tag的join显著优于其他,包括启发式合并的std,以及pairing_heap_tag。

测试结论3:极端情况(边巨多)堆优化Dijkstra,pairing_heap_tag和thin_heap_tag,优于其他堆,但不及线段树。(注意binary_heap_tag是不支持修改的,没有进行测试)

测试结论4:普通堆优化Dijkstra,pairing_heap比较好,但是还是不如线段树。

总结论:
不join的时候,线段树大法好。
join的时候,随机数据或者胆子大用binary_heap_tag自带的join(非启发式),时间虽然为对数但是真的小。但是这是比赛,用pairing_heap_tag或者binomial_heap_tag比较好,效率可以达到仅次于手动的左偏树。

也就是说,要么用可并堆,使用pairing_heap_tag,要么就直接离散化上线段树。

__gnu_pbds::tree

头文件:ext/pb_ds/assoc_container.hpp和/ext/pb_ds/tree_policy.hpp

使用方式和map差不多(传入两个参数key和value),不需要第二个参数的时候,传入null_type(或者null_mapped_type,当版本比较旧时)即可(变成set,连元素都不再是pair,只是第一个参数的类型)。

template <
    typename Key, 
    typename Mapped,
    typename Cmp_Fn = std::less <Key>,
    typename Tag = rb_tree_tag,
    template <
        typename Const_Node_Iterator, 
        typename Node_Iterator,
        typename Cmp_Fn_,
        typename Allocator_
    > class Node_Update = null_tree_node_update, 
    typename Allocator = std::allocator <char> 
>  class tree;

Tag:可以是rb_tree_tag,splay_tree_tag,ov_tree_tag之一。
Node Update:指定为tree_order_statistics_node_update,这个tree就获得了两个新的成员函数find_by_order和order_of_key,也就是名次树

find_by_order:找第order+1小的元素。(这个是从0开始还是从1开始的呢?)
order_of_key:询问有多少个比它小的。

join:把两棵值域不相交的树合并,也就是一棵完全比另一棵大,否则会抛出异常。
split:传入一个value和一个tree的引用other,把other清空然后把比value大的值移动到other。

自带的tree_order_statistics_node_update统计的是子树的size,可以修改为统计各种信息。

比如查询子树的value值之和,这个很复杂,维护前缀和感觉和手动数据结构一样麻烦,先留个

缺点:
1、不能交换左右子树,不能完全取代splay和无旋treap用来维护序列的地位。
2、打延迟标记很麻烦,失去了偷懒的意义。

时间复杂度

插入数据,然后遍历。

ov_tree_tag居然连插入然后遍历都可以TLE。
rb_tree_tag和splay_tree_tag分别和std的set和手动的splay性能近似。

插入数据,然后从中查询。

树的性能同上,但是都不如__gnu_pbds::cc_hash_table和__gnu_pbds::gp_hash_table(最快)。

__gnu_pbds::cc_hash_table和__gnu_pbds::gp_hash_table

头文件:ext/pb_ds/assoc_container.hpp和/ext/pb_ds/hash_policy.hpp

用法和unorder_map差不多,支持find和operator[]。

__gnu_cxx::rope

关联资料:
https://www.cnblogs.com/keshuqi/p/6257642.html
https://www.cnblogs.com/scx2015noip-as-php/p/rope.html

顾名思义,就是比string更强的字符“缆”,用起来非常像string,但是它可以从中间位置进行一些奇奇怪怪的操作,用来替代维护序列的平衡树。

正常操作
支持string的各种操作,包括用cin和cout输入输出。
插入/添加等:

push_back(x);     //在末尾添加x,可以是一个元素,也可以是string
insert(pos,x);    //在pos插入x,可以是一个元素,也可以是string
erase(pos,len);   //从pos开始删除len个
copy(pos,len,x);  //从pos开始,到pos+len为止用x代替
replace(pos,x);   //从pos开始换成x,可以是一个元素,也可以是string
substr(pos,len);  //提取pos开始len个
at(x)/[x];        //访问第x个元素

string &append(const string &s,int pos,int n);
                  //把字符串s中从pos开始的n个字符连接到当前字符串的结尾(没什么用)
a.append(b);      //直接把rope/string接在后面(没什么用)

可持久化操作

rope好用的关键是可持久化操作。

rope<int> *f[10005];
f[i]=new rope<int>(*f[i-1]);

这个操作是飞快的,与rope的size不成线性。
假如需要翻转序列,一般维护正反两个rope即可。

posted @ 2020-04-12 15:29  KisekiPurin2019  阅读(318)  评论(0编辑  收藏  举报