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即可。