您的位置:首页 > 大数据 > 人工智能

STL 教程

2017-08-31 12:17 323 查看


本人博客已经迁移到http://www.yichenxing.com/,欢迎来踩>_<,本文就是来自我的博客,我个人是博客刚刚开通的...


STL教程概述

也许你在解决 TopCoder 上的问题时已经用过 C++,可能你会发现别人在实现算法的时候,代码很简洁干练。又或许你是一个初级 C++ 程序员,那么你就更应该好好读一下这篇stl教程了。在本教程中,我会逐步向你介绍 C++ STL(Standard Template Library)的强大特性,这样你在各种 OJ 上挑战的时候或者在工作中,在算法实现上能节省很多时间。


1. stl教程之Containers

要学好 STL,首先要从 Containers 开始。当你要处理一系列的元素的时候,必然离不开某种特定的 container,在原生的 C 语言中,只有一种 container ——数组(array)。但是 array 只有基本的功能,而我们往往需要功能很强大的 container。比如以下功能:

1)往 container 中添加 string;

2)删除 container 中的 string;

3)查询 container 中是否有一个 string;

4)返回 container 中不同元素的个数;

5)遍历整个 container 获取里面的所有 string 列表。

当然你可以利用 array 自己来实现所有这些功能,但是这就要你花很多时间处理很多繁琐的细节。所以还是明智点,直接用 STL 吧。

在使用 STL 时,要先 #include 适当的头文件。例如你要用 stack,那么就应该在程序开头加上这句:
#include <stack>


STL 的各 Container 以及 algorithms 并定义在命令空间“std”中,并不是定义在全局的命名空间中,所以在 include 对应头文件后,还得加上这句:

using namespace std;

还有一点要注意的是,container 的类型都是模板参数类型,模板参数放在 <>(尖括号中),如定义int类型的 vector,如下:
vector<int> v1;


在定义嵌套的 container 的时候,尖括号间要加上适当的空格:
vector< vector<int> > v1; // 正确的定义方式
vector<vector<int>> v2; // 错误的定义方式,编译器会把 >> 和输出操作的 >> 混淆


2. stl教程之Vector

STL 中最简单的 container 是 vector,vector 只是简单的扩展了一下 array 的功能,它也是唯一个能向后兼容 C 标准的 container,因为它本身就是 array,只是多了一些特性而已。
vector<int> v(10);
for(int i = 0; i < 10; i++) {
v[i] = (i+1)*(i+1);
}
for(int i = 9; i > 0; i--) {
v[i] -= v[i-1];
}

vector<int> v;


在定义 v 的时候,就创建了一个空的 vector。

注意下面这行代码:
vector<int> v[10];


在这里,v 是一个长度为 10 的数组,每个数组元素是一个 vector,而不是 int。但是我们的本意是要定义一个 vector,其长度为 10,即存放 10 个整数的一个 vector。我们应该这样做
vector<int> v(10);


要用圆括号,而不是方括号。

要知道vector的长度,我们可以调用size函数:
int elements_count = v.size();


关于size函数,我们要注意两点:(1)size() 返回的是无符号整型,但是有时我们可能会定义一些宏,如 sz(C),这个宏用于返回 C 的长度,它返回的是 int 类型(有符号的);(2)在判断一个 container 是否为空的时候,最好不要用 v.size() 和 0 比较来判断,而是用 empty() 函数:
bool is_nonempty_notgood = (v.size() >= 0); // 最好不要这这样写
bool is_nonempty_ok = !v.empty(); // 应该这样写


因为并不是所有的 containers 的 size() 函数都是 O(1) 的,因为有的 container 在调用 size() 函数的时候,要做遍历操作等。

vecto r的另一个常用函数是 push_back(),该函数向 vector 尾部添加一个新的元素,并把 vector 的 size 增 1 :
 vector<int> v;for(int i = 1; i < 1000000; i *= 2) {
v.push_back(i);
}
int elements_count = v.size();


不用担心内存分配问题 —— 在分配内存的时候,vector 并不是逐一申请每个元素的内存的,每次在进行 push_back 的时候,如果空间不够大,它会一次性申请多个元素的空间。

如果你要调整 vector 的大小,可以调用 resize() 函数:
vector<int> v(20);
for(int i = 0; i < 20; i++) {
v[i] = i+1;
}
v.resize(25);
for(int i = 20; i < 25; i++) {
v[i] = i*2;
}


resize() 函数调整出来的 vector 会包含它原先有的那些元素,但是如果你把空间调小到不足以容纳原来那些元素的程序,那么末尾的元素会被舍弃。如果 resize() 的空间大于原先的长度,那么新创建的元素会被填充 0 。

注意如果你是在调用 resize() 函数后调用 push_back(),系统还是会分配新的空间来存放添加进来的元素:
vector<int> v(20);
for(int i = 0; i < 20; i++) {
v[i] = i+1;
}
v.resize(25);
for(int i = 20; i < 25; i++) {
v.push_back(i*2); // Writes to elements with indices [25..30), not [20..25) !
}


如上我们把 v 调大到 25,然后调用 push_back() 函数添加一些元素,但是新的元素是从下标为 25 开始的位置开始存放的,而不是 20 。

在清空 vector,就调用 clear() 函数,这个函数会使得 vector 包含的元素个数变成 0,而不是把元素的值变为 0 。

初始化 vector 的方式有很多种,也可以用已有的 vector 来初始化新声明的 vector:
 vector<int> v1;// ...
vector<int> v2 = v1;
vector<int> v3(v1);


如上初始化后, v2 和 v3 是一样的。

如果你要创建指定开度的 vector,可以如下调用:
vector<int> Data(1000);


如上定义了一个长度为 1000 的 vector,它所有元素的初值会被置为 0 。如果你不想用默认的初始值,你可以自己指定:
vector<string> names(20, “Unknown”);


如上定义了一个长度为 20 的 vector,存放 string 类型,所有元素初值为 "Unknown" 。

还可以定义多维的 vector,最简单的二维 vector 定义如下:
vector< vector<int> > Matrix;


我们也可以指定二维 vector 的长度:
int N, M;
// ...
vector< vector<int> > Matrix(N, vector<int>(M, -1));


如上,我们定义了一个 N*M 的矩阵,所有元素初值为 -1 。

现在我们知道 push_back() 函数总是会在 vector 的末尾添加元素,但是如果我们想在指定的位置添加元素的话,那么就得调用 insert() 函数了;对应的 erase() 函数用于删掉指定的元素,我们稍后介绍完 iterators 讲到它们。

要注意一点:如果你把 vector 当成参数直接传递给一个函数,那么该函数在接收的时候,实际上会创建一个这个 vector 的拷贝,通常这会花费很多时间和空间,而实际上往往不需要这么做:
void some_function(vector<int> v) { // 除非你确定要这样做,否则最好不要这样写
// ...
}


我们应该这样写(传递 vector 的引用)
void some_function(const vector<int>& v) { // OK
// ...
}


加上 const 限定,被调用函数就不能修改 vector 的内容。如果你想让被调用函数能修改原 vector 的内容,那么应该这样写:
int modify_vector(vector<int>& v) { // Correct
V[0]++;
}


3. stl教程之Pairs

在讲 iterators 前,我们先来简单讲一下 pairs,pairs 在 STL 中的应用非常广泛。pairs 的基本定义如下:
template<typename T1, typename T2> struct pair {
T1 first;
T2 second;
};


pair<int, int> 定义了一对整数,pair<string, pair<int, int> > 定义一个 string 和两个整数对,可如下访问其元素值:
pair<string, pair<int,int> > P;
string s = P.first; // 指前面的 string
int x = P.second.first; // 指后面的第一个 int
int y = P.second.second; // 指后面的第二个 int


pairs 最大的作用是它内部有一个比较函数,可用于元素间的比较。如果两个 pairs 的第一个元素不相等,那么它们的大小由第一个元素的比较决定;否则由第二个元素的比较决定。元素类型为 pairs 的 array 或 vector,可以非常方便的调用 STL 内部的排序函数来排序。

pairs 在关联容器(associative containers)中的使用很广泛,稍后我们会讲到。


4. stl教程之Iterators

在访问 containers 中的数据的时候,就得用到 Iterators 了。如下示例是一个反转数组元素的程序,我们先用类 C 的方式来实现(第一种方法):
void reverse_array_simple(int *A, int N) {
int first = 0, last = N-1;
While(first < last) {
swap(A[first], A[last]); // swap(a,b) is the STL function
first++; // Move first index forward
last--; // Move last index back
}
}


我们还可以用指针来写(第二种方法):
void reverse_array(int *A, int N) {
int *first = A, *last = A+N-1;
while(first < last) {
Swap(*first, *last);
first++;
last--;
}
}


然后我们再来考虑一个问题:如何对一个双向链表进行反转操作呢?显然按上面的第一种方法是不可行的,因为它基于元素下标索引。而在双向链表中, 我们没法在 O(1) 的时间内取得所有结点的下标。但是第二种方法是可行的,因为它通过指针来操作元素:获取元素值、比较元素大小、指针自增、指针自减,Iterators 就实现这些基本功能。所有的 STL container 都可以通过 iterator 遍历。

Iterators 和指针很类似,它定义了如下基本操作:

1)获取一个 Iterators 的值:int x = *iter;

2)自增、自减 Iterators:iter++, iter--;

3)可以用 != < 等比较运算符对不同的 Iterators 进行比较;

4)Iterators 移动动指定的偏移位置,如 iter += 20(iter 前移 20 个元素的位置);

5)获取两个 Iterators 间的距离:int distance = iter2 - iter1;

但是和指针不同的是,Iterators 提供了更多的功能,它不仅可以操作 container,还可以进行范围检查、容器分析等。

Iterators 最大的优点是实现了代码重用:基于 Iterators 实现的算法,可以用在不同的 containers 中。

但是并不是所有的 Iterators 都提供了相同的功能,我们可以把 Iterators 大致分为 “普通的 Iterators” 和 “随机访问的 Iterators”。对于普通 Iterators,我们可以用 ==、!= 等比较运算符进行比较,也可以自增、自减,但是它们不可以相减,也不可以给普通 Iterators 增加指定的值。

基于 Iterators,前面的程序可以这样实现:
template<typename T> void reverse_array(T *first, T *last) {
if(first != last) {
while(true) {
swap(*first, *last);
first++;
if(first == last) {
break;
}
last--;
if(first == last) {
break;
}
}
}
}


这个程序和前面的程序的最大区别在于:不用 < 进行比较,而是用 == 。

通常 STL 的算法都会用两个 Iterators,一个 begin iterator、一个 end iterator,begin 指向 container 的第一个元素,end 指向 container 最后一个元素的下一个位置。每个 STL container 都有两个函数 begin()、end(),分别用于获取该 container 的 begin iterator 和 end iterator。

我们可以用 c.begin()==c.end() 来判断 container 是否为空,用 c.end()-c.begin() 来获取 container 的元素个数(和 c.size() 等效),当然并不是所有 container 都可以用这个相减操作。

STL 源码里的反转函数是这样的:
template<typename T> void reverse_array_stl_compliant(T *begin, T *end) {
// We should at first decrement 'end'
// But only for non-empty range
if(begin != end)
{
end--;
if(begin != end) {
while(true) {
swap(*begin, *end);
begin++;
If(begin == end) {
break;
}
end--;
if(begin == end) {
break;
}
}
}
}
}


注意这和 C++ 标准库的 std::reverse(T begin, T end) 函数是不一样的。

此外,因为模板的强大作用,STL 的 algorithms 和 functions 可以接收多种类型的 iterator:
 vector<int> v;// ...
vector<int> v2(v);
vector<int> v3(v.begin(), v.end()); // v3 equals to v2

int data[] = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31 };
vector<int> primes(data, data+(sizeof(data) / sizeof(data[0])));


最后一行代码通过 C 的原始数组创建一个 vector。

我们还可以这样来创建一个新的 vector:
 vector<int> v;// ...
vector<int> v2(v.begin(), v.begin() + (v.size()/2));


如上 v2 等于 v1 的前半段。

下面是 reverse() 函数的应用实例(只反转指定区域的元素):
int data[10] = { 1, 3, 5, 7, 9, 11, 13, 15, 17, 19 };
reverse(data+2, data+6); // the range { 5, 7, 9, 11 } is now { 11, 9, 7, 5 };


每个 container 都有两个函数 rbegin() 和 rend(),用于返回反向的 iterators,rbegin() 指向结尾、rend() 指向开头。在需要反向遍历 container 的时候,可用这两个函数。
 vector<int> v;vector<int> v2(v.rbegin()+(v.size()/2), v.rend());


上面用 v 的前半段来创建 v2,v2 的内容是 v 的前半段,但是元素顺序刚好相反。

在创建 iterator 对象的时候,必须指定它的类型,类型指定可以通过在 container 后加上 ::iterator、::const_iterator、::reverse_iterator、::const_reverse_iterator 等来实现,如我们可以这样来遍历一个 vector:
 vector<int> v;
// ...

// Traverse all container, from begin() to end()
for(vector<int>::iterator it = v.begin(); it != v.end(); it++) {
*it++; // Increment the value iterator is pointing to
}


出于性能方面的考虑,建议用 != 代替 <,用 empty() 代替 size()!=0。

find() 函数可用于查找指定范围内是否有某个元素,如果存在,那么返回指向第一个指定元素的 iterator,如果不存在,那么返回值等于 end interator:
 vector<int> v;for(int i = 1; i < 100; i++) {
v.push_back(i*i);
}

if(find(v.begin(), v.end(), 49) != v.end()) {
// ...
}


如果想获取所找到的元素的下标,那么就用相减操作:
int i = (find(v.begin(), v.end(), 49) - v.begin();
if(i < v.size()) {
// ...
}


注意用到 STL 的 algorithms 的时候,要 #include 。

下面还有些常用的方法:
int data[5] = { 1, 5, 2, 4, 3 };
vector<int> X(data, data+5);
int v1 = *max_element(X.begin(), X.end()); // 返回 vector 中的最大值
int i1 = min_element(X.begin(), X.end()) – X.begin; // 返回 vector 中最小元素的下标

int v2 = *max_element(data, data+5); // 返回 array 中最大的元素
int i3 = min_element(data, data+5) – data; // 返回 array 中最小元素的下标


我们可以定义一个宏:
#define all(c) c.begin(), c.end()


然后有下面的排序方法:
vector<int> X;
// ...
sort(X.begin(), X.end()); // 升序排序
sort(all(X)); // 升序排序, 用 #define
sort(X.rbegin(), X.rend()); // 用 iterator 进行降序排序


5. Compiling STL Programs

首先我们来看一下一个经典的错误:
void f(const vector<int>& v) {
for(
vector<int>::iterator it = v.begin(); // 这里有错,看出来了吗
// ...
// ...
}


错误在于形参被 const 修饰了,但是我们获取的 iterator 不是 const 类型,应该改成
void f(const vector<int>& v) {
int r = 0;
// Traverse the vector using const_iterator
for(vector<int>::const_iterator it = v.begin(); it != v.end(); it++) {
r += (*it)*(*it);
}
return r;
}


然后我们来简单介绍一下一个 GNU C 标准里特有的扩展: typeof,typeof 用于自动推导后边 () 里的数据类型,如:

typeof(a+b) x=(a+b)

这行代码定义了一个变量 x,x 的类型和 a+b 的结果类型一样,这就是这里 typeof(a+b) 的作用。typeof 还可用于 container 的遍历:
#define tr(container, it) \
for(typeof(container.begin()) it = container.begin(); it != container.end(); it++)


因为 typeof 的自动类型推导功能,我们可以利用这个宏遍历任意类型的 container,如
void f(const vector<int>& v) {
int r = 0;
tr(v, it) {
r += (*it)*(*it);
}
return r;
}


对 vector 来说,可能这个宏定义的作用并不大,但是当我们要遍历复杂的数据类型的时候,就非常有用了,可以省掉很多代码。


6. stl教程之 vector 中的数据操作

我们可以用 insert() 函数向指定的位置插入一个新的元素:
 vector<int> v;// ...
v.insert(1, 42); // Insert value 42 after the first


这样,从 第二个元素(index=1)开始的所有元素都要向后移一个位置,以给新的元素腾出空间。如果我们要批量插入元素,这样调用这个函数效率就比较低了,因为插入一个元素,所有后面的元素都要身后移动。这里我们可以这样调用:
 vector<int> v;vector<int> v2;
// ..
// Shift all elements from second to last to the appropriate number of elements.
// Then copy the contents of v2 into v.
v.insert(1, all(v2));


把从第二个元素开始的所有元素都向后移动适当的位置,然后把 v2 中的所有内容填充到 v 移动元素后腾出的空间中。

同样的,erase() 函数也有两个类似的版本:
erase(iterator);
erase(begin iterator, end iterator);


第一个函数删除指定位置的元素,第二个删除指定区域的所有元素。


7. stl教程之String(字符串)

string 是 STL 中用于操作字符串的 container,它和 vector 是不一样的。主要的区别在于它们的字符串操作和内存管理方式策略。

String 有一个 substr() 函数,这个函数只需要通过下标值操作,不依赖于 iterators,如:
string s = "hello";
string
s1 = s.substr(0, 3), // "hel"
s2 = s.substr(1, 3), // "ell"
s3 = s.substr(0, s.length()-1), "hell"
s4 = s.substr(1); // "ello"


8. stl教程之Set(集合)

STL 的 Set(集合) 有以下基本性质或操作:

1)任意两元素都是不相等的;

2)可添加、删除元素;

3)可获取元素的个数;

4)可查询集合中是否有指定元素(可在 O(logN) 时间内完成)。

先来看个简单的例子:
set<int> s;

for(int i = 1; i <= 100; i++) {
s.insert(i); // Insert 100 elements, [1..100]
}

s.insert(42); // does nothing, 42 already exists in set

for(int i = 2; i <= 100; i += 2) {
s.erase(i); // Erase even values
}

int n = int(s.size()); // n will be 50


Set 是没有 push_back() 函数的,因为它的元素不需要顺序,也正是这个原因,也不可通过下标来访问集合中的元素。遍历 Set 的唯一方法是用 iterators:
// Calculate the sum of elements in set
set<int> S;
// ...
int r = 0;
for(set<int>::const_iterator it = S.begin(); it != S.end(); it++) {
r += *it;
}


利用宏进行遍历会更好些,想象一下如果有定义 set< pair<string, pair< int, vector > >,我们怎么定义这玩意呢?所以还是用宏吧:
set< pair<string, pair< int, vector<int> > > SS;
int total = 0;
tr(SS, it) {
total += it->second.first;
}


注意 it->second.first,因为 it 是个 iterator,所以我们得取到它的值,才能进行操作。这句代码也可以这样写 (*it).second.first 。

要查找集合中是否指定元素,可用 find() 函数,但是注意了,STL 的全局 algorithm 里面也有个 find() 函数,也可以用于 Set 的查找操作,但是它的复杂度是 O(N)。而 Set 本身的 find() 方法,是为 Set 量身定做的,其算法复杂度为 O(logN),类似的 multiset、multimap、hash_map、hash_set 中也有类似方法。
set<int> s;
// ...
if(s.find(42) != s.end()) {
// 42 presents in set
}
else {
// 42 not presents in set
}


另一个复杂度为 O(logN) 的函数是 count() 函数:
if(s.count(42) != 0) {
// …
}


或者
if(s.count(42)) {
// …
}


但是,我个人倾向于用宏来实现和 count() 函数一样的功能,用于 Set 中元素的 count,因为要 count 的元素要么存在,要么不存在于 Set 中,没有数量这个说法:
#define present(container, element) (container.find(element) != container.end())
#define cpresent(container, element) (find(all(container),element) != container.end())


all(c) 展开为 c.begin(), c.end(前面我们定义过了)。

present 用于一般的 container,cpresent 用于 vector。

从集合中删除元素,可以用 erase() 函数:
set<int> s;
// …
s.insert(54);
s.erase(29);


erase() 也可以指定区间:
set<int> s;
// ..
set<int>::iterator it1, it2;
it1 = s.find(10);
it2 = s.find(100);
// Will work if it1 and it2 are valid iterators, i.e. values 10 and 100 present in set.
s.erase(it1, it2); // Note that 10 will be deleted, but 100 will remain in the container


同样的,其构造函数也可以用指定的区间作为参数:
int data[5] = { 5, 1, 4, 2, 3 };
set<int> S(data, data+5);


还可以利用 Set 把 vector 中重复的元素去掉并排序:
 vector<int> v;// …
set<int> s(all(v));
vector<int> v2(all(s));


这样,v2 会包含 v 中所有的元素,且按升序排序,且没有重复的元素。Set 中所有可比较的元素都可以排序。


9. stl教程之Map(映射)

Map 的简单示例如下:
map<string, int> M;
M["Top"] = 1;
M["Coder"] = 2;
M["SRM"] = 10;

int x = M["Top"] + M["Coder"];

if(M.find("SRM") != M.end()) {
M.erase(M.find("SRM")); // or even M.erase("SRM")
}


其实 Map 和 Set 很相像,只是 Map 包含的不是 values,而是 <key, value>,即键值对。Map 保证了 key 的唯一性,而且还支持 [] 运算符。我们可以用前面定义过的 tr() 宏轻松实现对 map 的遍历。Map 的 iterator 是标准的 std::pair 类型,所以取值语句为 it->second:
map<string, int> M;
// …
int r = 0;
tr(M, it) {
r += it->second;
}


注意在遍历的时候,不要修改任何元素的 key,因为这样会破坏其实体完整性。

Map 的 map::find() 函数和 map::operator[] 有个很大的区别是:map::find() 不会改变 map 的内容,而 operator[] 会在元素不存在的时候,创建一个对应的元素。如果你不想往 map 中添加元素,那就不要用 operator[] 。如果形参是被 const 修饰的 map,那么 operator[] 也是不可用的:
void f(const map<string, int>& M) {
if(M["the meaning"] == 42) { // Error! Cannot use [] on const map objects!
}
if(M.find("the meaning") != M.end() && M.find("the meaning")->second == 42) { // Correct
cout << "Don't Panic!" << endl;
}
}


10. Notice on Map and Set

在底层实现上,map 和 set 都是基于红黑树的,底层的实现细节,我们不用太过关心。我们要记住的是:map 和 set 本身就以升序的顺序存储元素,在遍历时,map 和 set 总以升序的顺序遍历,所以说最好不要在遍历的过程中修改 map 或 set 的 key。在算法实现上,map 和 set 是有序的这个特点非常有用。

还有重要的一点:map 和 set 的 iterators 也可用 ++ 和 -- 运算符。如下,假设 set 中有一个元素 42,且它的前驱和后续都存在,那么下面的代码就有效:
set<int> S;
// ...
set<int>::iterator it = S.find(42);
set<int>::iterator it1 = it, it2 = it;
it1--;
it2++;
int a = *it1, b = *it2;


这样我们就得到 42 的左右邻居结点了。


11. More on algorithms

STL 的 algorithms 都声明在 #include 头文件中,其中最基本的四个函数:

1)min(a, b),求两者中的小者;

2)max(a, b),求两者中的大者;

3)swap(a, b),交互两个元素的值。

4)sort(begin, end),用于对指定区域进行升序排序,但是 sort() 函数只能对 random access iterators 进行排序,所以并不是所有的 containers 都支持 sort() 函数。注意 set 和 map 都是有序的,不需要再用 sort() 进行排序了。

还有两个重要的函数 next_permutation(begin, end) 和 prev_permutation(begin, end),分别用于返回指定内的元素的下一个排列和上一个排列,对 next_permutation() 函数,如果已经是最后一个排列, 则返回 false;对 prev_permutation() 函数,如果已是最前面一个排列,则返回 false。但是注意,在调用 next_permutation() 函数前,要先保证 container 已经排好序了:
 vector<int> v;
for(int i = 0; i < 10; i++) {
v.push_back(i);
}

do {
Solve(..., v);
} while(next_permutation(all(v));


12. stl教程之String Streams

C++ 有两个 istringstream 和 ostringstream 用于处理 input、output 流,它们都定义在 #include

1)istringstream 用于 standard input 读取数据:
 void f(const string& s) {

// Construct an object to parse strings
istringstream is(s);

// Vector to store data
vector<int> v;
// Read integer while possible and add it to the vector
int tmp;
while(is >> tmp) {
v.push_back(tmp);
}
}


2)ostringstream 用于格式化 output:
string f(const vector<int>& v) {

// Constucvt an object to do formatted output
ostringstream os;

// Copy all elements from vector<int> to string stream as text
tr(v, it) {
os << ' ' << *it;
}

// Get string from string stream
string s = os.str();

// Remove first space character
if(!s.empty()) { // Beware of empty string here
s = s.substr(1);
}

return s;
}


13. stl教程总结

我总结了一些常用宏,可用在 TopCoder 中:
typedef vector<int> vi;
typedef vector<vi> vvi;
typedef pair<int,int> ii;
#define sz(a) int((a).size())
#define pb push_back
#defile all(c) (c).begin(),(c).end()
#define tr(c,i) for(typeof((c).begin() i = (c).begin(); i != (c).end(); i++)
#define present(c,x) ((c).find(x) != (c).end())
#define cpresent(c,x) (find(all(c),x) != (c).end())
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息