夏令营讲课内容整理 Day 6 Part 1.

Day6讲了三个大部分的内容。
1.STL
2.初等数论
3.倍增
 
Part1主要与STL有关。
1.概述
STL的英文全名叫Standard Template Library,翻译成中文就叫标准模板库。
它有点类似于一个大型的工具箱,里面包含许多实用工具,可以拿过来直接用而大部分情况下无需去深入探究其内部原理。
不知道从什么时候开始,CCF不再限制选手使用STL,所以在OI赛事中STL被广泛应用。
它分为六个大部分:
1)容器 containers
2)迭代器 iterators
3)空间配置器 allocateor
4)配接器 adapters
5)算法 algorithms
6)仿函数 functors
在OI中应用非常广泛的是容器和算法,迭代器有可能会用到,另外三个不常见。
本篇随笔,我会简单谈一下迭代器,算法和容器方面的一些浅层知识。
我在学习STL的时候是基于实用主义的,之前我也谈到了,不需要过分的探求它的内部原理,只需要知道如何使用即可。
 
2.迭代器
迭代器是一种检查容器内元素并遍历元素的数据类型。它如同一个指针。
注意:C++当中的指针可以认为是一种迭代器,而迭代器不全是指针。
迭代器分为以下几种:
      • Input iterators 提供对数据的只读访问
      • Output iterators 提供对数据的只写访问
      • Forward iterators 提供读写操作,并可以向前驱动迭代器
      • Bidirectional iterators 提供读写操作,并可以双向(前,后)驱动迭代器
      • Random access iterators 提供读写操作,可以在数据中随机的移动
 
至于具体的迭代器实例,我会在后面提及。
 
3.算法
要想使用STL的算法,必须先加一个#include<algorithm>
顾名思义,你可以用这里面的函数完成很多操作。我们在之前的程序中也用到了STL库的算法,它会对我们编写程序带来极大的方便(只要你合理使用)。
 
3.1排序
(自从我会用这个进行排序之后,我就再也没写过人工快排,现在让我写人工快排我也不一定能写出来。。。)
STL提供的排序算法有很多,最常用也是最常见的是std::sort(RandomAccessIterator first,RandomAccessIterator last),它的时间复杂度是O(nlogn),没有返回值,内部实现类似快速排序,是一种不稳定的排序算法。
(正在看这篇随笔的读者朋友,我相信您知道什么是不稳定的排序算法)
还有一种稳定的排序算法,叫std::stable_sort(RandomAccessIterator first,RandomAccessIterator last),内部实现为归并排序,时间复杂度O(nlogn),没有返回值,不过我用的很少。
从参数名可以看出,这个函数出的参数是两个迭代器,同时可以理解为指向要排序的数组的指针。其中RandomAccessIterator first代表数组的首地址,RandomAccessIterator last代表数组的尾地址。通常的,数组名可以作为数组的首地址的指针,而数组的尾地址通常使用首地址+偏移量来代替。
(偏移量:相对于数组首地址偏移的长度,在这里应是数组长度)
应用:sort(a,a+n); 或 sort(a+1,a+n+1);
假设我们要排序的数组为a,则前者排序范围是a[0] ~ a[n-1],后者排序范围是a[1] ~ a[n]。
这两种方式按照实际情况选择使用。
 
3.1.1自定义比较器与基于struct(或class)的多关键字排序
一般的,STL的排序算法默认是按照整形的从小到大排序,但有的时候我们需要从大到小排序或者要排序的对象是一个自己写的struct(或 class),这样STL的排序算法就不太适合我们的需求了,我们应该稍加改进。
STL有一个默认的比较器,就是上文所述的按照整形从小到大。同时,STL支持传入一个自定义比较器(一般是函数)来作为一个新的比较规则,然后它可以按照新的比较规则进行排序。
这样的写法应该就是std::sort(RandomAccessIterator first,RandomAccessIterator last,Compare comp);
传入的comp应该自己实现,它应该是一个给定的struct或class的实例或者是一个返回值为bool或int的函数。
bool comp(int a,int b){
    return a > b;
}

 

只需要这样写,就能让排序算法按照从小到大排序。
struct的多关键字排序:
strruct my_struct{
    int x,y;
};
my_struct a[maxn];
bool comp(const my_struct &a,const my_struct &b){
    return (a.x < b.x || (a.x==b.x && a.y < b.y));
}
sort(a,a+maxn,comp);

 

这样就可以让排序函数按照x值为第一关键字,y值为第二关键字进行从小到大的排序。
3.1.2 多关键字排序的运算符重载方法
对于多关键字排序,还可以使用运算符重载。
运算符重载类似于重新定义一个符号,而在排序时函数优先使用这个被重定义的比较规则。
struct my_struct{
    int x,y;
    bool operator<(const my_struct &r)const{
    return (x<r.x || (x==r.x && y<r.y));
    }
}
my_struct a[maxn];
sort(a,a+n);

 

 
 
3.2 min和max
std::min & std::max
返回二者中的较大值/较小值。
 
3.3 max_element & min_element
返回一段连续数组(或容器)中的一个最大值或最小值的指针(迭代器)
时间复杂度O(n),但我更喜欢手写。。
 
3.4lower_bound & upper_bound
在一个升序数组上进行二分查找可以用这两个函数。
std::lower_bound 返回第一个第一个大于等于查询值位置的迭代器
std::upper_bound 返回第一个大于等于查询值位置的迭代器
时间复杂度O(logn)。
示例程序:
 1 #include<iostream>
 2 #include<algorithm>
 3 using namespace std;
 4 int main(){
 5 int a[100],n;
 6 cin >> n;
 7     for (int i=1;i<=n;i++)
 8     cin >> a[i];
 9     int *first_bigger_or_equal_iterator = lower_bound(a+1,a+n+1,5);
10     cout << first_bigger_or_equal_iterator - a << endl;
11     return 0;
12 }

 

 
示例输入:5 2 3 5 7 8
示例输出:3
(如果是upper_bound,输出应该是4)
 
3.5 swap
交换两个对象,是的,什么类型都可以换,比自己写的swap不知道高到哪里去了。
template <class T>
void swap(T& a,T& b);
 
3.6去重
ForwardIterator unique(ForwardIterator first ,ForwardIterator last);
删掉相邻连续一段中的重复元素,返回一个指向去重后的最后一个位置的下一个位置的迭代器,时间复杂度O(n)。
经常被用在离散化操作上。
 
3.7排列
bool next_permutation(BidirectionalIterator first,BidirectionIterator last);
这个函数按照字典序生成当前段的下一个排列,如果生成成功则返回 true,否则返回false,时间复杂度O(n)。
bool prev_permutation(BidirectionalIterator first,BidirectionIterator last);
同上,只不过它生成上一个排列。
常用来枚举全排列,用它替代手写DFS
(当然手写全排列DFS不难写,所以有很多人仍喜欢用手写DFS而不是这个东西)
 
4.容器
容器就是各种封装好的数据结构,它分为三大类,序列容器,关联容器和其他容器。
同时容器还配备了许多实用的方法与函数,只要include了相应的头文件,我们就可以拿过来直接用。
 
4.1栈
序列容器。
STL栈,可以实现栈所有的基本功能。
需要#include<stack>
声明一个栈:stack<基类型,一般是int> s;
压入:s.push(x);
弹出:s.pop();
取栈顶但不弹出:s.top();
返回栈大小(元素个数):s.size();
判断栈空:s.empty();
 
4.2队列
序列容器。
需要#include<queue>
声明与基本操作和STL栈类似,把stack换成queue即可。
注意取队首操作,应该是q.front(),而不是q.top()(这是优先队列的取队首写法)。
 
4.3双端队列
序列容器。
声明使用 deque,允许在两端进行插入 删除操作,但是性能很差,不推荐使用,知道有这么个东西就好。
需要#include<deque>
常用操作:
入队首 push_front
弹出队首 pop_front
访问队首 front
入队尾 push_back
弹出队尾 pop_back
访问队尾 back
 
4.4 向量(不定长大数组)
序列容器。
需要#include<vector>
它的本质是一个不定长大数组,其占用空间的大小是动态的,申请空间操作是动态的。
当不知道要开多大的数组时,用vector可以节省空间。
vector中的数据在内存中占据的空间是连续的一段,每次不够时,就在别的地方再开两倍大内存空间,并将原来的数据进行转移。
但它的操作是非常慢的,不推荐使用。
 
4.5 对组
关联容器。
需要#include<uillity>
std::pair把两个数据类型打包成一个队组,用first和second访问值。
用途:代替只有两个成员变量的struct(说成偷懒也不足为过)
可以用来方便的表示一个二维平面的点,或者是一个区间。
有的人喜欢在写dijsktra的时候用vector和这个东西,我的代码风格不是这个流派的,所以不贴了。
 
4.6优先队列
序列容器。
要是不对时间过分苛求,这个东西就是个大神器,大神器,大神器。
需要#include<queue>,声明用priority_queue。
它名叫优先队列,却满足堆的所有性质,所以也能把这个当一个大根堆用(默认把比较大的数放在队列前面)。
要把这个东西和单调队列区分开,它们内部存储方式不太一样。
插入和删除的复杂度是O(logn),比较优秀。在绝大多数情况下可以代替手写堆。
要把它变成小根堆,一般俩方法。
1.这样写:priority_queue<int,vector<int>,greater<int> > q;
(注意后面那两个"<"要分开,要不编辑器会认为是流运算符"<<")
2.运算符重载,我没用过这个方法。。。
操作方式和一般容器类似。
 
4.7集合
关联容器。
需要#include<set>
std::set,内部用平衡树实现,保持内部元素有序,插入重复元素会自动忽略。
可以在O(logn)的时间内完成元素的插入,删除,查找等操作。
特色操作:
插入 insert
删除 erase
查找 find
查找个数(是否存在)count
二分查找 lower_bound,upper_bound
查询大小 size
判空 empty
返回第一个元素的迭代器 begin
返回最后一个元素的迭代器 end
清空(初始化)clear
还有一个multiset,与set类似,但可以存储相同的元素。
 
4.8映射
关联容器。
又是一个大神器。
需要#include<map>,其内部也用平衡树实现,保持内部元素的有序,同样的可以在O(logn)的时间内完成元素的插入,删除,查找等操作。
它内部的元素和“下标”之间有一个一一对应的关系,也就是键与值的关系,这个关系是不能重复的。每一个所谓的下标和键值由一个pair存储。
有时也用于离散化。
 
4.9字符串
序列容器。
挺好用,但用的不多。
需要#include<string>,存储的是一个变长字符串,每个字符都是char类型,支持下标访问,本质类似于vector<char>.
特色操作
取子串 substr
支持运算符"+"拼接两个string类,首尾连接。
支持运算符==,<等判断两个string类的大小关系。
 
4.10 bitset
其他容器。
用来管理一些二进制位,可以访问指定下标的bit,还能把它们作为一个整数进行统计。
NOIP难度可能用不到这个。
 
5.STL的局限性
STL虽然好用,但是在没有优化的情况下运行速度通常比等价的手写代码运行要慢,所以在一些题目当中会出现卡STL的现象。
(所以STL也叫Sometimes TLE Library
日常刷题用一下还是可以的,比赛时是否要用STL,请酌情选择。
 
posted @ 2017-08-24 20:23  ShawnZhou_Aether  阅读(310)  评论(2编辑  收藏  举报