几乎所有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 和两个子串 S1S2。需要检测 S1S2 是否都在 S 中出现,并且 S1 出现在 S2 左边,并且两者在 S 中互不交叉(即 S1 的右边界点在 S2 的左边界点的左侧)。计算满足上述条件的最大跨距(即,最大间隔距离:最右边的 S2 的起始点与最左边的 S1 的终止点之间的字符数目)。如果没有满足条件的 S1S2 存在,则输出 -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::pairstruct 的方法,并配合使用 lower_boundupper_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_keyfind_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 下一个的迭代器(如果 xend() 则返回 end());如果 xKey,则返回是否删除成功(如果不存在则删除失败)。
    • order_of_key(x):返回严格小于 x 的元素个数(以 Cmp_Fn 作为比较逻辑),即从 0 开始的排名。
    • 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(vlog⁡n),空间复杂度为 O(v)。

现在时间复杂度为 O(log⁡n),空间复杂度为 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;
}

其他