几乎所有stl都缺氧,开O2跑飞快
vector
-
vector是表示可变大小数组的序列容器。
-
就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问
-
vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间,因此常数很大
食用方法
1.头文件
#include<vector>
2.声明,初始化
vector<int> vec; //声明一个int型vector vector<int> vec(5); //声明一个初始大小为5的int vector<int> vec(10, 1); //声明一个初始大小为10且值都是1的 vector<int> vec(tmp); //声明并用tmp初始化vec vector<int> tmp(vec.begin(), vec.begin() + 3); //用vec的第0个到第2个值初始化tmp int arr[5] = {1, 2, 3, 4, 5}; vector<int> vec(arr, arr + 5); //将arr数组的元素用于初始化vec //说明:当然不包括arr[4]元素,末尾指针都是指结束元素的下一个元素, //这个主要是为了和vec.end()指针统一。 vector<int> vec(&arr[1], &arr[4]); //将arr[1]~arr[4]范围内的元素作为vec的初始值
3.基本操作
(1). 容量
- 向量大小:
vec.size();
- 向量最大容量:
vec.max_size();
- 更改向量大小:
vec.resize();
- 向量真实大小:
vec.capacity();
- 向量判空:
vec.empty();
- 减少向量大小到满足元素所占存储空间的大小:
vec.shrink_to_fit();
(2). 修改
-
多个元素赋值:
vec.assign(); //类似于初始化时用数组进行赋值
-
末尾添加元素:
vec.push_back();
-
末尾删除元素:
vec.pop_back();
-
任意位置插入元素:
vec.insert();
-
在指定位置插入值为val的元素。
//在arr的头部插入值为10的元素 vector<int> arr; arr.insert(arr.begin(), 10);
-
在指定位置插入n个值为val的元素
//从arr的头部开始,连续插入3个值为10的元素 vector<int> arr; arr.insert(arr.begin(), 3, 10);
-
在指定位置插入区间[start, end]的所有元素
//从arr的头部开始,连续插入arrs区间[begin, end]的所有元素 vector<int> arr; vector<int> arrs = { 1, 2, 3, 4, 5 }; arr.insert(arr.begin(), arrs.begin(), arrs.end());
-
-
任意位置删除元素:
vec.erase();
-
交换两个向量的元素:
vec.swap();
-
清空向量元素:
vec.clear();
(3)迭代器
- 开始指针:
vec.begin();
- 末尾指针:
vec.end(); //指向最后一个元素的下一个位置
- 指向常量的开始指针:
vec.cbegin(); //意思就是不能通过这个指针来修改所指的内容,但还是可以通过其他方式修改的,而且指针也是可以移动的。
- 指向常量的末尾指针:
vec.cend();
(4)元素的访问
- 下标访问:
vec[1]; //并不会检查是否越界
- at方法访问:
vec.at(1); //以上两者的区别就是at会检查是否越界,是则抛出out of range异常,由于没有人闲的没事在竞赛代码里catch异常,所以和下标访问几乎没有区别
- 访问第一个元素:
vec.front();
- 访问最后一个元素:
vec.back();
- 返回一个指针:
int* p = vec.data(); //可行的原因在于vector在内存中就是一个连续存储的数组,所以可以返回一个指针指向这个数组。这是是C++11的特性。
(5)算法
- 遍历元素
vector<int>::iterator it; for (it = vec.begin(); it != vec.end(); it++) cout << *it << endl; //或者 for (size_t i = 0; i < (int)vec.size(); i++) { //注意,有些编译器的size方法不开O2是O(n)的 cout << vec.at(i) << endl; } //或者 for_each(vec.begin(), vec.end(), [](int value) { std::cout << value << " "; });
- 元素翻转
#include <algorithm> reverse(vec.begin(), vec.end());
- 元素排序
#include <algorithm> sort(vec.begin(), vec.end()); //采用的是从小到大的排序 //如果想从大到小排序,可以采用上面反转函数,也可以采用下面方法: bool Comp(const int& a, const int& b) { return a > b; } sort(vec.begin(), vec.end(), Comp);
(6)其他操作
-
预留空间
reverve()//并不会增大实际大小,也就是说并不会增加空元素。但是这样会显著增加插入效率
-
二维操作
-
定义
vector<vector<int>> arr; //定义一个空的二维vector vector<vector<int>> arr(5, vector<int>(3, 1)); //定义一个5行3列值全为1的二维vector
-
访问 正常访问即可
-
-
较为底层的原理
vector有两个指针,size()返回已使用,capacity()返回当前申请的内存大小。当size顶到capacity时,就会申请一块更大的内存,也就是扩容。如果当前内存块中没有足够的连续内存,那么它就会找一块更大的新内存然后迁移过去,这个操作的效率很低,我们要避免。
- 向量大小:
string
- string 类定义隐藏了字符串的数组性质,让我们可以像处理普通变量那样处理字符串。
- string有很多vector的特性
食用方法
1.头文件
#include<string>
2.声明,初始化
-
string(const char *s)
:将 string 对象初始化为 s 指向的字符串 -
string(size_type n,char c)
:创建一个包含 n 个元素的 string 对象,其中每个元素都被初始化为字符 c
string str(10,'a');
string(const string &str)
:将一个 string 对象初始化为 string 对象 str(复制构造函数)
string str1("hello!");
string str2(str1);
string()
:创建一个默认的 string 对象,长度为 0(默认构造函数)
string str; // 创建一个空的 string 对象
3.基本操作
string几乎具有所有vector的方法
一. string 对象的拼接和附加
在 C 语言中,使用 strcat、strncat 函数来进行字符串拼接操作。在 C++ 中也有多种方法来实现字符串拼接 和附加操作:
1.1 使用 + 操作符拼接两个字符串
string str1("hello ");
string str2("world!");
string str3 = str1 + str2;
1.2 使用 += 操作符在字符串后面附加内容
可以使用 += 来在一个 string 对象后面附加一个 string 对象、字符以及 C 风格的字符串:
1.3使用 string.append() 函数
可以使用 string.append() 函数来在一个 string 对象后面附加一个 string 对象或 C 风格的字符串:
string str1 = "hello,world!";
string str2 = "HELLO,WORLD!";
str1.append(str2);
str1.append("C string");
二. string 对象的比较
在 C 语言中,使用 strcmp、strncmp 函数来进行字符串的比较。在 C++ 中,由于将 string 对象声明为了简单变量,故而对字符串的比较操作十分简单了,直接使用关系运算符(==、!=、<、<=、>、>=)即可:
#include <string>
#include <iostream>
using namespace std;
int main()
{
string str1("hello");
string str2("hello");
if (str1 == str2)
cout << "str1 = str2" << endl;
else if (str1 < str2)
cout << "str1 < str2" << endl;
else
cout << "str1 > str2" << endl;
return 0;
}
当然,也可以使用类似 strcmp 的函数来进行 string 对象的比较,string 类提供的是 string.compare() 方法,函数原型如下:
int compare(const string&str) const;
int compare(size_t pos,size_t len,const string&str)const; // 参数 pos 为比较字符串中第一个字符的位置,len 为比较字符串的长度
int compare(size_t pos,size_t len,const string&str, size_t subpos,size_t sublen)const;
int compare(const char * s)const;
int compare(size_t pos,size_t len,const char * s)const;
int compare(size_t pos,size_t len,const char * s,size_t n)const;
compare 方法的返回值如下:
1)返回 0,表示相等;
2)返回结果小于 0,表示比较字符串中第一个不匹配的字符比源字符串小,或者所有字符都匹配但是比较字符串比源字符串短;
3)返回结果大于 0,表示比较字符串中第一个不匹配的字符比源字符串大,或者所有字符都匹配但是比较字符串比源字符串长。
三、使用 string.substr() 函数来获取子串
可以使用 string.substr() 函数来获取子串,string.substr() 函数的定义如下:
string substr(size_t pos = 0,size_t len = npos)const;
其中,pos 是子字符串的起始位置(索引,第一个字符的索引为 0),len 是子串的长度。这个函数的功能是:复制一个 string 对象中从 pos 处开始的 len 个字符到 string 对象 substr 中去,并返回 substr。
string str("Hello,World!");
string subStr = str.substr(3,5);
cout << subStr << endl;
这段代码的输出结果为:"lo,Wo"。
四、string 对象的查找操作
4.1 使用 string.find() 方法查找字符
find 方法的函数原型如下:
1)从字符串的 pos 位置开始(若不指定 pos 的值,则默认从索引 0 处开始),查找子字符串 str。如果找到,则返回该子字符串首次出现时其首字符的索引;否则,返回 string::npos:
size_type find (const string& str, size_type pos = 0) const;
2)从字符串的 pos 位置开始(若不指定 pos 的值,则默认从索引 0 处开始),查找子字符串 s。如果找到,则返回该子字符串首次出现时其首字符的索引;否则,返回 string::npos:
size_type find (const char *s, size_type pos = 0) const;
3)从字符串的 pos 位置开始(若不指定 pos 的值,则默认从索引 0 处开始),查找 s 的前 n 个字符组成的子字符串。如果找到,则返回该子字符串首次出现时其首字符的索引;否则,返回 string::npos:
size_type find (const char *s, size_type pos, size_type n);
4)从字符串的 pos 位置开始(若不指定 pos 的值,则默认从索引 0 处开始),查找字符 ch 。如果找到,则返回该字符首次出现的位置;否则,返回 string::npos:
size_type find (char ch, size_type pos = 0) const;
举个查找子字符串的例子(查找字符的代码与这一样,只需要将 find 函数的参数换成字符即可):
4.2 string.rfind()
string.rfind() 与 string.find() 方法类似,只是查找顺序不一样, string.rfind() 是从指定位置 pos (默认为字符串末尾)开始向前查找,直到字符串的首部,并返回第一次查找到匹配项时匹配项首字符的索引。换句话说,就是查找子字符串或字符最后一次出现的位置。还是以上面的程序为例,稍作修改:
#include <string>
#include <iostream>
using namespace std;
int main()
{
string str("cat,dog,cat,pig,little cat,hotdog,little pig,angry dog");
size_t catPos = str.rfind("cat",str.length()-1);
if (catPos == string::npos) {
printf("没有找到字符串\n");
return 0;
}
while (catPos != string::npos) {
cout << "在索引 " << catPos << " 处找到字符串" << endl;
catPos = str.rfind("cat", catPos - 1);
if (catPos == 0) {
cout << "在索引 " << catPos << " 处找到字符串" << endl;
break;
}
}
return 0;
}
4.3 string.find_first_of()
string.find_first_of() 方法在字符串中从指定位置开始向后(默认为索引 0 处)查找参数中任何一个字符首次出现的位置。举个例子说明:
#include <string>
#include <iostream>
using namespace std;
int main()
{
string str("cat,dog,cat,pig,little cat,hotdog,little pig,angry dog");
size_t pos = str.find_first_of("zywfgat");
if (pos == string::npos) {
printf("没有匹配到\n");
return 0;
}
else
cout << "在索引 " << pos << " 处匹配到" << endl;
return 0;
}
程序输出结果是:在索引 1 处匹配到。所查找的字符串 zywfgat 中,第一次出现在字符串 str 中的字符是 'a',该字符在 str 中的索引是 1.
4.4 string.find_last_of()
string.find_last_of() 方法在字符串中查找参数中任何一个字符最后一次出现的位置(也就是从指定位置开始往前查找,第一个出现的位置)。
4.5 string.find_first_not_of()
string.find_first_not_of() 方法在字符串中查找第一个不包含在参数中的字符。
4.6 string.find_last_not_of()
string.find_last_not_of() 方法在字符串中查找最后一个不包含在参数中的字符(从指定位置开始往前查找,第一个不包含在参数中的字符)。
五、string 对象的插入和删除操作
5.1 使用 string.insert() 进行插入操作
函数原型如下:
string&insert(size_t pos,const string&str); // 在位置 pos 处插入字符串 str
string&insert(size_t pos,const string&str,size_t subpos,size_t sublen); // 在位置 pos 处插入字符串 str 的从位置 subpos 处开始的 sublen 个字符
string&insert(size_t pos,const char * s); // 在位置 pos 处插入字符串 s
string&insert(size_t pos,const char * s,size_t n); // 在位置 pos 处插入字符串 s 的前 n 个字符
string&insert(size_t pos,size_t n,char c); // 在位置 pos 处插入 n 个字符 c
iterator insert (const_iterator p, size_t n, char c); // 在 p 处插入 n 个字符 c,并返回插入后迭代器的位置
iterator insert (const_iterator p, char c); // 在 p 处插入字符 c,并返回插入后迭代器的位置
5.2 使用 string.erase() 进行元素删除操作
函数原型如下:
string& erase (size_t pos = 0, size_t len = npos); // 删除从 pos 处开始的 n 个字符
iterator erase (const_iterator p); // 删除 p 处的一个字符,并返回删除后迭代器的位置
iterator erase (const_iterator first, const_iterator last); // 删除从 first 到 last 之间的字符,并返回删除后迭代器的位置
举个例子:
#include <string>
#include <iostream>
using namespace std;
int main()
{
string str("Hello,World!");
str.erase(5,6); // 删除从索引位置 5 开始的 6 个字符
cout << "str 为:" << str << endl;
return 0;
}
十、string 对象的一些其他操作
10.1 使用 getline() 函数来获取 string 输入
string str;
getline(cin,str); // 从输入流中读取一行数据到 str
**10.2 使用 empty() 函数判断字符串是否为空
10.3 使用 swap 函数交换两个字符串
str1.swap(str2);
4.其他
- 值得一提的是,string开O2之后find,insert方法非常快
*** 真的非常快 **
例子
给定字符串 S
和两个子串 S1
和 S2
。需要检测 S1
和 S2
是否都在 S
中出现,并且 S1
出现在 S2
左边,并且两者在 S
中互不交叉(即 S1
的右边界点在 S2
的左边界点的左侧)。计算满足上述条件的最大跨距(即,最大间隔距离:最右边的 S2
的起始点与最左边的 S1
的终止点之间的字符数目)。如果没有满足条件的 S1
和 S2
存在,则输出 -1。
输入:S = "abcd123ab888efghij45ef67kl"
, S1 = "ab"
, S2 = "ef"
输出:18
(满足条件的最大跨距为 18)
长度最大是100000
正解显然是 kmp 或者哈希
但是开 O2 之后,神奇的string能飞快跑过这道题
#include<bits/stdc++.h>
using namespace std;
int main(){
string s,s1,s2;
getline(cin,s,',');
getline(cin,s1,',');
cin >> s2;
int l = s.find(s1);
int len = s1.length();
int r = s.rfind(s2);
if(l >= r || l == -1 || r == -1) printf("-1\n");
else printf("%d\n",r - l - len);
return 0;
}
PB_DS
平衡树
-
提供了红黑树,Splay。
-
rbt的空间常数略大,Splay的常数非常大。所以一般用rbt
-
空间非常优秀
建立
#include <ext/pb_ds/assoc_container.hpp> // 因为 tree 定义在这里 所以需要包含这个头文件 #include <ext/pb_ds/tree_policy.hpp> using namespace __gnu_pbds; __gnu_pbds::tree<Key, Mapped, Cmp_Fn = std::less<Key>, Tag = rb_tree_tag, Node_Update = null_tree_node_update, Allocator = std::allocator<char>>
-
Key
: 储存的元素类型,如果想要存储多个相同的Key
元素,则需要使用类似于std::pair
和struct
的方法,并配合使用lower_bound
和upper_bound
成员函数进行查找 -
Mapped
: 映射规则(Mapped-Policy)类型,如果要指示关联容器是 集合,类似于存储元素在std::set
中,此处填入null_type
,低版本g++
此处为null_mapped_type
;如果要指示关联容器是 带值的集合,类似于存储元素在std::map
中,此处填入类似于std::map<Key, Value>
的Value
类型 -
Cmp_Fn
: 关键字比较函子,例如std::less<Key>
如果
Cmp_Fn
由默认的less<>
变为了greater<>
,其函数的行为会发生变化。lower_bound(x)
:查找第一个小于等于 x 的元素。upper_bound(x)
:查找第一个小于 x 的元素。之后的排名等操作就由从小到大变为了从大到小。
-
Tag: 选择使用何种底层数据结构类型,默认是 rb_tree_tag。__gnu_pbds
提供不同的三种平衡树,分别是:
rb_tree_tag
:红黑树,一般使用这个,后两者的性能一般不如红黑树splay_tree_tag
:splay 树ov_tree_tag
:有序向量树,只是一个由vector
实现的有序结构,类似于排序的vector
来实现平衡树,性能取决于数据想不想卡你
-
Node_Update
:用于更新节点的策略,默认使用null_node_update
,若要使用order_of_key
和find_by_order
方法,需要使用tree_order_statistics_node_update
-
Allocator
:空间分配器类型一般这么用:
tree<node,null_type,less<node>,rb_tree_tag,tree_order_statistics_node_update> tre;
该不会要背代码把
基础应用
方法:
insert(x)
:向树中插入一个元素x
,返回std::pair<point_iterator, bool>
,其中第一个元素代表插入位置的迭代器,第二个元素代表是否插入成功。erase(x)
:从树中删除一个元素/迭代器x
。如果x
是迭代器,则返回指向x
下一个的迭代器(如果x
是end()
则返回end()
);如果x
是Key
,则返回是否删除成功(如果不存在则删除失败)。order_of_key(x)
:返回严格小于x
的元素个数(以Cmp_Fn
作为比较逻辑),即从开始的排名。
find_by_order(x)
:返回Cmp_Fn
比较的排名所对应元素的迭代器。lower_bound(x)
:返回第一个不小于x
的元素所对应的迭代器(以Cmp_Fn
作为比较逻辑)。upper_bound(x)
:返回第一个严格大于x
的元素所对应的迭代器(以Cmp_Fn
作为比较逻辑)。join(x)
:将x
树并入当前树,x
树被清空(必须确保两树的 比较函数 和 元素类型 相同)。split(x,b)
:以Cmp_Fn
比较,小于等于x
的属于当前树,其余的属于b
树。empty()
:返回是否为空。size()
:返回大小。
-
进阶应用
难道pb_ds只能打板子?当然不是,例如平衡树板子,可以用一种很优雅的方式切掉,这里转载dalao Jimmy- LEEE的做法
迭代器
我们通过 lower_bound()
等获得的迭代器是指向值的。
但是如果我们要得到指向节点的迭代器怎么办?
auto itn=it.m_p_nd;
itn 就是指向节点的迭代器。
之后对树结构的操作就与 itn 相关。
自定义节点
首先我们定义节点类型 st
:
struct st
{
int num;
mutable int cnt;
bool operator<(const st &b) const {return num<b.num;}
};
num
为节点的值,cnt
为该值的数目。
这里采用了和珂朵莉树差不多的处理方式,用 multable
以便于修改 cnt
的值。
重载 operator<()
以用于平衡树的更新。
自定义 Node_Update
类
模板
然后我们可以写一个 Node_Update
类,这个类的模板长这样:
template<class Node_CItr,class Node_Itr,class Cmp_Fn,class _Alloc>
struct my_node_update {};
这样就定义了一个 my_node_update
类。
基本内容
但是这样的一个更新类还不能使用,需要再经过调整。
该类至少有以下内容:
template<class Node_CItr,class Node_Itr,class Cmp_Fn,class _Alloc>
struct my_node_update
{
typedef int metadata_type;
void operator()(Node_Itr it, Node_CItr end_it)
{
;
}
virtual Node_CItr node_begin() const = 0;
virtual Node_CItr node_end() const = 0;
};
typedef int metadata_type;
声明了节点维护的额外信息,在这里维护的是子树的大小,所以为 int
。如果有像 Treap 维护序列用到的懒标记这类也可以放在这里。
operator()
更新子树
operator()
是平衡树的 push_up
操作,如果我们要更新子树大小,就会调用 operator()
。
对于我们的操作,就像这样:
void operator()(Node_Itr it, Node_CItr end_it)
{
Node_Itr itl=it.get_l_child();
Node_Itr itr=it.get_r_child();
int l=0,r=0;
if(itl!=end_it) l=itl.get_metadata();
if(itr!=end_it) r=itr.get_metadata();
const_cast<int&>(it.get_metadata())=l+r+(*it)->cnt;
}
get_l_child()
返回该迭代器指向的节点的左儿子的迭代器。
get_r_child()
同理。
end_it
是结尾迭代器,如果指向某节点的迭代器 it 等于 end_it
,那么证明该节点不存在。
const_cast<int&>(it.get_metadata())
修改节点维护的额外信息。在这里就是左右子树大小之和加上该节点元素个数。
自定义操作
为了防止迭代器引用访问到非法内存,先定义一个 get()
函数,获取节点维护的额外信息。
int get(Node_CItr it) {return it==node_end()?0:it.get_metadata();}
查找排名
平衡树基本操作。
注意此时迭代器类型为 Node_CItr
。并且迭代器指向的是节点,节点指向的才是值。
int Rank(int x)
{
int ans=1;
Node_CItr it=node_begin();
while(it!=node_end())
{
Node_CItr itl=it.get_l_child();
Node_CItr itr=it.get_r_child();
if(x<=(*it)->num) it=itl;
else ans+=(*it)->cnt+get(itl), it=itr;
}
return ans;
}
由排名查找元素
平衡树基本操作。
注意事项同上。
int Find(int k)
{
Node_CItr it=node_begin();
while(it!=node_end())
{
Node_CItr itl=it.get_l_child();
Node_CItr itr=it.get_r_child();
int lsiz=get(itl);
if(k<=lsiz) it=itl;
else if(k<=lsiz+(*it)->cnt) return (*it)->num;
else {k-=lsiz+(*it)->cnt; it=itr;}
}
return -1;
}
自此,自定义 Node_Update
类已经完成了,该考虑其他部分了。
自定义 Tree
类
构建
我选择直接继承 Tree
类,然后再搞事情。
struct RBT:tree<st,null_type,less<st>,rb_tree_tag,my_node_update> {};
自定义函数
更新额外信息
最抽象和恶心的地方来了。
由于每次修改都需要重新统计子树大小,但是对 cnt
的修改不会触发 push_up()
,也就导致了下面这份代码挂了。
void Insert(int x, int v=1)
{
auto it=lower_bound({x, 0});
if(it==end()||it->num!=x) insert({x, 1});
else it->cnt+=v;
}
在 Insert()
过后父亲节点没有更新子树大小,所以排名相关的操作会挂掉。
每次 insert()
会调用 update_to_top()
操作来更新。
其实可以先 erase()
再 insert()
的,但是常数巨大
然后我上网找了很久找不到用法,文档里也没有...
然后自己试出来的。
void update(iterator x) {update_to_top(x.m_p_nd, (node_update*)this);}
这样就能正常更新子树大小了。
插入
能够插入任意数量的元素。
void Insert(int x, int v=1)
{
auto it=lower_bound({x, 0});
if(it==end()||it->num!=x) insert({x, 1});
else it->cnt+=v, update(it);
}
原来的方式插入 v 个值时间复杂度为 O(vlogn),空间复杂度为 O(v)。
现在时间复杂度为 O(logn),空间复杂度为 O(1)。
删除
与插入基本相同。
void Erase(int x, int v=1)
{
auto it=lower_bound({x, 0});
if(it==end()||it->num!=x) return;
else if(it->cnt<=v) erase(it);
else it->cnt-=v, update(it);
}
前驱
int Pre(int x)
{
auto it=lower_bound({x, 0}); it--;
return it->num;
}
后继
int Aft(int x)
{
auto it=upper_bound({x, 0});
return it->num;
}
其他
-
pb_ds很吃熟练度,所以需要很多练习
[ZJOI2006] 书架 pbds做法:[题解 P2596 【ZJOI2006]书架】 - 洛谷专栏
[TJOI2019] 甲苯先生的滚榜 : [题解 P5338 【TJOI2019]甲苯先生的滚榜】 - 洛谷专栏