所以这篇随笔就带大家走进博大精深的C++STL,系统讲解各种STL容器及其用法、作用。在学习STL的时候认真体会STL语法及功能,提升自己在算法竞赛及程序设计中解题、码代码的能力。
话不多说,现在开始:
本篇随笔简单介绍一下C++STL中vector容器的使用方法和常见的使用技巧。vector容器是C++STL的一种比较基本的容器。我们在学习这个容器的时候,不仅要学到这个容器具体的使用方法,更要从中体会C++STL的概念。
vector在英文中是矢量的意思。如果学过高中数学必修四的平面向量或者高中物理必修一的第一节课对其会有一个直观的认识。但是在STL中,vector和物理、几何等东西没有任何关系。
我们知道,一个数组必须要有固定的长度,在开一个数组的时候,这个长度也就被静态地确定下来了。但是vector却是数组的“加强版”,对于一组数据来讲,你往vector里存多少数据,vector的长度就有多大。也就是说,我们可以将其理解为一个“变长数组”。
事实上,vector的实现方式是基于倍增思想的:假如vector的实际长度为n,m为vector当前的最大长度,那么在加入一个元素的时候,先看一下,假如当前的n=m,则再动态申请一个2m大小的内存。反之,在删除的时候,如果n≥m2,则再释放一半的内存。
vector容器存放在模板库:#include里,使用前需要先开这个库。
vector容器的声明遵循C++STL的一般声明原则:
容器类型<变量类型> 名称
例:
#include
vector<int> vec;
vector<char> vec;
vector<pair<int,int> > vec;
vector<node> vec;
struct node{...};
vector容器的使用方法大致如下表所示:
用法 | 作用 |
---|---|
vec.begin(),vec.end() | 返回vector的首、尾迭代器 |
vec.front(),vec.back() | 返回vector的首、尾元素 |
vec.push_back() | 从vector末尾加入一个元素 |
vec.pop_back() | 从vector末尾删除一个元素 |
vec.size() | 返回vector当前的长度(大小) |
vec.empty() | 返回vector是否为空,1为空、0不为空 |
vec.clear() | 清空vector |
除了上面说过的那些之外,我们的vector容器是支持随机访问的,即可以像数组一样用[]来取值。请记住,不是所有的STL容器都有这个性质!在STL的学习过程中,一定要清楚各个容器之间的异同!
本篇随笔简单介绍一下C++STL中queue容器的使用方法和常见的使用技巧。
queue在英文中是队列的意思。队列是一种基本的数据结构。而C++STL中的队列就是把这种数据结构模板化了。我们可以在脑中想象买票时人们站的排队队列。我们发现,在一个队列中,只可以从队首离开,从队尾进来。即一个先进先出的数据结构。
queue容器存放在模板库:#include里,使用前需要先开这个库。
queue容器的声明遵循C++STL的一般声明原则:
容器类型<变量类型> 名称
例:
#include
queue<int> q;
queue<char> q;
queue<pair<int,int> > q;
queue<node> q;
struct node{...};
queue容器的使用方法大致如下表所示:
用法 | 作用 |
---|---|
q.front(),q.back() | 返回queue的首、尾元素 |
q.push() | 从queue末尾加入一个元素 |
q.size() | 返回queue当前的长度(大小) |
q.pop() | 从queue末尾删除一个元素 |
q.empty() | 返回queue是否为空,1为空、0不为空 |
注意,虽然vector和queue是两种最基本的STL容器,但请记住它们两个不是完全一样的。就从使用方法来讲:
queue不支持随机访问,即不能像数组一样地任意取值。并且,queue并不支持全部的vector的内置函数。比如queue不可以用clear()函数清空,清空queue必须一个一个弹出。同样,queue也并不支持遍历,无论是数组型遍历还是迭代器型遍历统统不支持,所以没begin(),end();函数,使用的时候一定要清楚异同!
本篇随笔简单介绍一下C++STL中stack容器的使用方法和常见的使用技巧。
stack在英文中是栈的意思。栈是一种基本的数据结构。而C++STL中的栈就是把这种数据结构模板化了。
栈的示意图如下:这是一个先进后出的数据结构。这非常重要!!
事实上,stack容器并不是一种标准的数据结构,它其实是一个容器适配器,里面还可以存其他的STL容器。但那种使用方法过于高深而且不是很常用,所以在此不与介绍。请有兴趣的读者自行查询资料。
stack容器存放在模板库:#include里,使用前需要先开这个库。
stack容器的声明遵循C++STL的一般声明原则:
容器类型<变量类型> 名称
例:
#include
stack<int> st;
stack<char> st;
stack<pair<int,int> > st;
stack<node> st;
struct node{...};
stack容器的使用方法大致如下表所示:
用法 | 作用 |
---|---|
st.top() | 返回stack的栈顶元素 |
st.push() | 从stack栈顶加入一个元素 |
st.size() | 返回stack当前的长度(大小) |
st.pop() | 从stack栈顶弹出一个元素 |
st.empty() | 返回stack是否为空,1为空、0不为空 |
本篇随笔简单讲解一下C++STL中string容器的使用方法及技巧。
其实string并不是STL的一种容器,但是由于它的使用方法等等和STL容器很像,所以就把它当作STL容器一样介绍。
其实string容器就是个字符串,这通过它的英文译名就能看得出来。但是对于字符串以及字符串的相关操作,可能读者还是对普通的C/C++的#include
,#include
库更熟悉一些。我丝毫不否认这些传统字符操作的经典性和实用性,但是由于它们函数定义的局限,有些时候对于一些特殊的读入、输出、遍历等要求,它的操作并不如string容器好用。
比如,要求读入一群中间可能带空格的字符串,如果用传统方式进行读入,可能就会很麻烦,但是如果使用string的话,一个读入函数就可以完全搞定。
string容器的使用方法及与传统字符读入的对比
一张图解决问题。
本篇随笔简单介绍一下C++STL中priorityqueue容器的使用方法和常见的使用技巧。
priorityqueue在英文中是优先队列的意思。
队列是一种基本的数据结构。其实现的基本示意图如下所示:
而C++STL中的优先队列就是在这个队列的基础上,把其中的元素加以排序。其内部实现是一个二叉堆。所以优先队列其实就是把堆模板化,将所有入队的元素排成具有单调性的一队,方便我们调用。
priorityqueue容器存放在模板库:#include
里,使用前需要先开这个库。
这里需要注意的是,优先队列的声明与一般STL模板的声明方式并不一样。事实上,我认为其是C++STL中最难声明的一个容器。
大根堆就是把大的元素放在堆顶的堆。优先队列默认实现的就是大根堆,所以大根堆的声明不需要任何花花肠子,直接按C++STL的声明规则声明即可。
#include
priority_queue<int> q;
priority_queue<string> q;
priority_queue<pair<int,int> > q;
C++中的int,string等类型可以直接比较大小,所以不用我们多操心,优先队列自然会帮我们实现。但是如果是我们自己定义的结构体,就需要进行重载运算符了。关于重载运算符的讲解,请参考:
一、重载运算符的用途
这是一个比较哲学的问题:我们为什么要重载运算符?
理由就是,我们C++语言中已经给出的运算符(包括算数运算符和逻辑运算符)只是针对C++语言中已经给定的数据类型进行运算,假如我们想要对我们的自定义数据类型进行运算的话,则需要重载运算符,我们可以把重载运算符理解成对已有的运算符的一种重新定义。
比如:
double a,b,c;
a=1/3;
b=1/2;
c=a+b;
printf("%lf",c);
这段程序输出的肯定不是两个分数相加的结果。
这时候我们就可以重载运算符+。
二、重载运算符的实现
语法格式如下(非常重要)
<返回类型> operator <运算符符号>(<参数>)
{
<定义>;
}
这里我们举一个例子。
在优先队列(priority_queue)中,存储的元素较大的会被放到堆顶。如果存的是int或者string等类型还好办(因为他们本身就可以互相比较大小),如果是我们自定义的结构体类型,那就需要重载<运算符。
比如:
struct node
{
int id;
double x,y;
}//定义结构体
bool operator <(const node &a,const node &b)
{
return a.x<b.x && a.y<b.y;
}//重载运算符“<”
注:这里的结构体保存了一个整型变量id,两个长浮点变量x,y,表示坐标。
这里的重载运算符先比横坐标后比纵坐标。
三、重载运算符的注意事项。
以下运算符不可重载:
关系运算符"."
成员指针运算符".*"
作用域运算符"::"
sizeof运算符
三目运算符"?:"
重载运算符限制在C++语言中已有的运算符范围内的允许重载的运算符之中,不能创建新的运算符。
运算符重载实质上是函数重载。
大根堆是把大的元素放堆顶,小根堆就是把小的元素放到堆顶。
实现小根堆有两种方式:
第一种是比较巧妙的,因为优先队列默认实现的是大根堆,所以我们可以把元素取反放进去,因为负数的绝对值越小越大,那么绝对值较小的元素就会被放在前面,我们在取出的时候再取个反,就瞒天过海地用大根堆实现了小根堆。
第二种:
小根堆有自己的声明方式,我们记住即可(我也说不明白道理):
priority_queue<int,vector<int>,greater<int> >q;
注意,当我们声明的时候碰到两个"<“或者”>"放在一起的时候,一定要记得在中间加一个空格。这样编译器才不会把两个连在一起的符号判断成位运算的左移/右移。
priorityqueue容器的使用方法大致如下表所示:
用法 | 作用 |
---|---|
q.top() | 返回priority_queue的首元素 |
q.push() | 向priority_queue中加入一个元素 |
q.size() | 返回priority_queue当前的长度(大小) |
q.pop() | 从priority_queue末尾删除一个元素 |
q.empty() | 返回priority_queue是否为空,1为空、0不为空 |
注意:priority_queue取出队首元素是使用top,而不是front,这点一定要注意!!
本篇随笔简单介绍一下C++STL中deque容器的使用方法及常见使用技巧。
deque的意义是:双端队列。队列是我们常用而且必须需要掌握的数据结构。C++STL中的确有模拟队列的模板:#include中的queue和priority_queue。队列的性质是先进先出,即从队尾入队,从队首出队。而deque的特点则是双端进出,即处于双端队列中的元素既可以从队首进/出队,也可以从队尾进/出队。
即:deque是一个支持在两端高效插入、删除元素的线性容器。
deque模板存储在C++STL的#include
中。
因为deque容器真的和queue容器大体相同,其使用方式也大体一致。下面把deque容器的使用方式以列表的方式放在下面:
用法 | 作用 |
---|---|
q.begin(),q.end() | 返回deque的首、尾迭代器 |
q.front(),q.back() | 返回deque的首、尾元素 |
q.push_back() | 从队尾入队一个元素 |
q.push_front() | 从队头入队一个元素 |
q.pop_back() | 从队尾出队一个元素 |
q.pop_front() | 从队头出队一个元素 |
q.clear() | 清空队列 |
除了这些用法之外,deque比queue更优秀的一个性质是它支持随机访问,即可以像数组下标一样取出其中的一个元素。
即:q[i]
。
deque的一些用途
由于本蒟蒻水平有限,暂时想不出deque应用的一些实例。但有一点是肯定的:deque容器可以被应用到SPFA算法的SLF优化。其具体应用方式可见:
信息学奥林匹克竞赛中图论部分的求最短路算法SPFA的两种优化方式。学习这两种优化算法需要有SPFA朴素算法的学习经验。在本随笔中SPFA朴素算法的相关知识将不予赘述。
No.1 SLF优化(Small Label First)
顾名思义,这种优化采用的方式是把较小元素提前。
就像dijkstra算法的堆优化一样。我们在求解最短路算法的时候是采取对图的遍历,每次求最小边的一个过程,为了寻找最小边,我们需要枚举每一条出边,如果我们一上来就找到这个边,那当然是非常爽的。一次找一次爽,一直找一直爽。所以我们采用了这种优化方式。
具体实现方式是把原来的队列变成双端队列,如果新入队的元素比队首元素还要小,就加入到队首,否则排到队尾。
模板如下:
void spfa()
{
memset(dist,0x3f,sizeof(dist));
memset(v,0,sizeof(v));
deque<int> q;
q.push_back(1);
v[1]=1;
dist[1]=0;
while(!q.empty())
{
int x=q.front();
q.pop_front();
v[x]=0;
for(int i=head[x];i;i=nxt[i])
{
int y=to[i];
if(dist[y]>dist[x]+val[i])
{
dist[y]>dist[x]+val[i];
if(v[y]==0)
{
if(dist[y]<=dist[q.pront()])
q.push_front(y);
else
q.push_back(y);
v[y]=1;
}
}
}
}
}
No.2 LLL优化(Large Label Last)
顾名思义,它的策略是把大的元素放到后面。
你会说,这不跟上面的一样么?
不不不,这个优化针对的是出队元素。它的实现过程是:对于每个出队元素,比较它的dist[]和队列中dist的平均值,如果它的dist[]更大,将它弹出放到队尾。以此类推,直至dist[x]小于其平均值。
模板:
void spfa()
{
memset(dis, 0x3f, sizeof(dis));
queue<int> q;
q.push(1);
v[1] = 1;
dist[1] = 0;
cnt = 1;
while(!Q.empty())
{
int x = q.front();
while (dis[x]*cnt > sum)
{
q.pop();
q.push(x);
x = q.front();
}
q.pop();
cnt--;
sum -= dist[x];
v[x] = 0;
for (int i = head[x]; i ; i=nxt[i])
{
int y=to[i];
if (dist[y] > dist[x] + val[i])
{
dist[y] = dist[x] + val[i];
if (v[y]==0)
{
q.push(y);
sum += dist[y];
cnt++;
}
}
}
}
}
重点来了!!
No.3 SLF+LLL同时优化!
听名字就很高级。
是的,的确很高级,不仅高级,而且快。
我就直接上模板了。
void spfa()
{
memset(dist, 0x3f, sizeof(dist));
memset(v,0,sizeof(v));
deque<int> q;
q.push_back(1);
v[1] = 1;
dist[1] = 0;
cnt = 1;
while (!q.empty())
{
int x = q.front();
while (cnt*dist[x] > sum)
{
q.pop_back();
q.push_back(x);
x = q.front();
}
q.pop_front();
cnt--;
sum -= dist[x];
v[x] = 0;
for (int i = head[x]; i ; i=nxt[i])
{
int y=to[i];
if (dist[y] > dist[x] + val[i])
{
dist[y] = dist[x] + val[i];
if (!v[y])
{
if (dist[y] <= dist[q.front()])
q.push_front(y);
else
q.push_back(y);
v[y] = 1;
sum += dist[y];
cnt++;
}
}
}
}
}
本篇随笔简单介绍一下C++STL中set容器的使用方法及常见使用技巧。
set在英文中的意义是:集合。set容器也的确“人如其名”,实现了这个集合的功用。
高中数学必修一集合那章,关于集合的性质,给出了三个概念:无序性、互异性、确定性。
那么,set容器的功用就是维护一个集合,其中的元素满足互异性。
我们可以将其理解为一个数组。这个数组的元素是两两不同的。
这个两两不同是指,如果这个set容器中已经包含了一个元素i,那么无论我们后续再往里假如多少个i,这个set中还是只有一个元素i,而不会出现一堆i的情况。这就为我们提供了很多方便。
但是,需要额外说明的是,刚刚说集合是有无序性的,但是set中的元素是默认排好序**(按升序排列)**的。(稍微说一句,set容器自动有序和快速添加、删除的性质是由其内部实现:红黑树(平衡树的一种)。这个东西过于高深我不会,所以不予过多介绍,有兴趣的小伙伴可以自行浏览相关内容。)
set容器的声明和大部分C++STL容器一样,都是:容器名<变量类型> 名称的结构。前提需要开#include库。如:
#include
set<int> s;
set<char> s;
set<pair<int,int> > s;
set<node> s;
struct node{...};
其实,C++STL容器的使用方式都是差不多的。我们完全可以举一反三地去类比。与bitset重定义了许多奇形怪状新的函数之外,其他都是大致相同的。
s.empty(); //empty()函数返回当前集合是否为空,是返回1,否则返回0.
s.size(); //size()函数返回当前集合的元素个数。
s.clear(); //clear()函数清空当前集合。
s.insert(k); //insert(k)函数表示向集合中加入元素k。
s.find(k); //find(k)函数返回集合中指向元素k的迭代器。如果不存在这个元素,就返回s.end(),这个性质可以用来判断集合中有没有这个元素。
s.erase(k);
//erase(k)函数表示删除集合中元素k。
//这也反映了set容器的强大之处,指哪打哪,说删谁就删谁,完全省略了遍历、查找、复制、还原等繁琐操作。
//更不用像链表那种数据结构那么毒瘤。直接一个函数,用O(logn)的复杂度解决问题。
s.begin(),s.end();
//begin()函数和end()函数返回集合的首尾迭代器。
//注意是迭代器。我们可以把迭代器理解为数组的下标。但其实迭代器是一种指针。
//这里需要注意的是,由于计算机区间“前闭后开”的结构,begin()函数返回的指针指向的的确是集合的第一个元素。
//但end()返回的指针却指向了集合最后一个元素后面一个元素。
其他好用的函数
下面介绍一些不是很常用,但是很好用的set容器的内置函数
s.lower_bound(),s.upper_bound();
熟悉algorithm库和二分、离散化的小伙伴会对这两个函数比较熟悉。其实这两个函数比较常用。但是对于set集合来讲就不是很常用。其中lower_bound返回集合中第一个大于等于关键字的元素。upper_bound返回集合中第一个严格大于关键字的元素。
s.equal_range();
这个东西返回一个pair(内置二元组),分别表示第一个大于等于关键字的元素,第一个严格大于关键字的元素,也就是把前面的两个函数和在一起。如果有一个元素找不到的话,就会返回s.end()。
本篇随笔简单介绍一下C++STL中multiset容器的使用方法及常见使用技巧。
set在英文中的意义是:集合。而multi−前缀则表示:多重的。所以multiset容器就叫做:有序多重集合。
multiset的很多性质和使用方式和set容器差不了多少。而multiset容器在概念上与set容器不同的地方就是:set的元素互不相同,而multiset的元素可以允许相同。
所以,关于一些multiset容器和set容器的相同点,本篇博客就不加以赘述了。
与set容器不太一样的地方:
s.erase(k);
erase(k)函数在set容器中表示删除集合中元素k。但在multiset容器中表示删除所有等于k的元素。
时间复杂度变成了O(tot+logn),其中tot表示要删除的元素的个数。
那么,会存在一种情况,我只想删除这些元素中的一个元素,怎么办呢?
可以妙用一下:
if((it=s.find(a))!=s.end())
s.erase(it);
if中的条件语句表示定义了一个指向一个a元素迭代器,如果这个迭代器不等于s.end(),就说明这个元素的确存在,就可以直接删除这个迭代器指向的元素了。
s.count(k);
count(k)函数返回集合中元素k的个数。set容器中并不存在这种操作。这是multiset独有的。
本篇随笔讲解C++STL中bitset容器的用法及常见使用技巧。
bitset容器其实就是个01串。可以被看作是一个bool数组。它比bool数组更优秀的优点是:节约空间,节约时间,支持基本的位运算。在bitset容器中,8位占一个字节,相比于bool数组4位一个字节的空间利用率要高很多。同时,n位的bitset在执行一次位运算的复杂度可以被看作是n/32,这都是bool数组所没有的优秀性质。
bitset容器包含在C++自带的bitset库中。
#include
因为bitset容器就是装01串的,所以不用在< >中装数据类型,这和一般的STL容器不太一样。< >中装01串的位数。
如:(声明一个105位的bitset)
bitset<100000> s;
1、常用的操作函数
和其他的STL容器一样,对bitset的很多操作也是由自带函数来实现的。下面,我们来介绍一下bitset的一些常用函数及其使用方法。
count()函数
count,数数的意思。它的作用是数出1的个数。即s.count()返回s中有多少个1.
s.count();
any()/none()函数
any,任何的意思。none,啥也没有的意思。这两个函数是在检查bitset容器中全0的情况。
如果,bitset中全都为0,那么s.any()返回false,s.none()返回true。
反之,假如bitset中至少有一个1,即哪怕有一个1,那么s.any()返回true,s.none()返回false.
s.any();
s.none();
set()函数
set()函数的作用是把bitset全部置为1.
特别地,set()函数里面可以传参数。set(u,v)的意思是把bitset中的第u位变成v,v∈0/1。
s.set();
s.set(u,v);
reset()函数
与set()函数相对地,reset()函数将bitset的所有位置为0。而reset()函数只传一个参数,表示把这一位改成0。
s.reset();
s.reset(k);
flip()函数
flip()函数与前两个函数不同,它的作用是将整个bitset容器按位取反。同上,其传进的参数表示把其中一位取反。
s.flip();
s.flip(k);
2、位运算操作在bitset中的实现
bitset的作用就是帮助我们方便地实现位运算的相关操作。它当然支持位运算的一些操作内容。我们在编写程序的时候对数进行的二进制运算均可以用在bitset函数上。
比如:
~:按位取反
&:按位与
|:按位或
^:按位异或
<< >>:左/右移
==/!=:比较两个bitset是否相等。
另外,bitset容器还支持直接取值和直接赋值的操作:具体操作方式如下:
s[3]=1;
s[5]=0;
这里要注意:在bitset容器中,最低位为0。这与我们的数组实现仍然有区别。
bitset可以高效率地对01串,01矩阵等等只含0/1的题目进行处理。其中支持的许多操作对我们处理数据非常有帮助。如果碰到一道0/1题,使用bitset或许是不错的选择。
位运算是很多算法优化的基础和实现的条件,极其重要。理解位运算对于一些算法及其优化有着非常重要的意义。本篇随笔讲解位运算的一些基本原理和常用的使用技巧。
注:本篇随笔的所有“运算”均指二进制下的运算,请大家自行理解。
1、与(&)运算
(1)运算法则
两个二进制数进行与&运算,如果对应位都为1则结果为1,否则为0.
(2)技巧及用途
与运算常常用于二进制下的取位操作。想要知道二进制下的某位是否是1,就&上这个位数对应的十进制数。假如返回的是这个十进制数本身,则这个位的确是1,反之就是0.
比如:
我们要取第三位是否为1,我们只需要与&上第三位(二进制表示为100)对应的二进制数4,如果返回值为4,就代表第三位为1,反之就是0.
最常用的是取二进制下的最末位,即a&1。这样的技巧可以用于判断奇偶,根据二进制常识,尾数为1则为奇数,反之为偶数。
2、或(|)运算
(1)运算法则
两个二进制数进行或|运算,如果对应位有一个为1,结果就为1.只有在两个数的对应位置都是0的时候,结果才为0.
(2)技巧及用途
或运算常用于二进制特定位的赋值。想把哪个位强行变成1,就用这个数|上这个位数对应的二进制数。
还是上面那个例子,我们想让00000的第三位变成1.即十进制变4,我们直接|上4就可以。
当然,不同于&运算,我们很少用|运算进行任意位赋值。通常来讲,我们只使用a|1把a的最后一位强行变成1,其实质意义是把原数加一。或者使用a|1-1再把它变为0.这个技巧通常用于把它变成它最接近的偶数。
3、异或(^)(xor)运算
(1)运算法则
两个二进制数进行异或(^)运算,如果对应位相同,不管是0或者是1,都返回1,反之返回0.
(2)技巧及用途
其实没啥用途…
好吧,我介绍一个性质:一个数经过两次异或之后等于原数。
(很好理解)
4、非(~)运算
(1)运算法则
把给定二进制数全部取反。
(2)技巧及用途
其实没什么运算上的用途,本蒟蒻曾看见一些大佬用这个运算判断输入是否为0…
大约长这个样子:
while(~scanf("%d",&n))
5、左移(<<)运算
(1)运算法则
a<
(2)技巧及用途
根据二进制的常识,我们会发现,二进制第k位上的数就等于2k。(从0开始计位)
比如,二进制下的100就是2k=2=4。
所以我们发现,左移运算a<
左移运算最常用的技巧就是用来代替×2的整数次幂的乘法运算。因为我们普遍认为,位运算是要比四则运算加减乘除及模运算更快一些的运算。
6、右移(>>)运算
(1)运算法则
a>>b就是把a的二进制位向右移动b位,溢出的舍去。
(2)技巧及用途
类比于左移运算,我们发现右移运算就是把a除以2的整数次幂。这就是右移运算的用途——优化除法运算。
这里需要特殊说明的是,右移算法可以用在数学知识中的求最大公约数的程序块上。因为mod运算的效率慢的出奇,所以我们可以用右移运算来进行除以2的操作。据说可以提高百分之60的效率。
7、位运算优先级
位运算的优先级是我们在处理位运算的时候常常要考虑的问题,诚然,我们可以用括号强制位运算的顺序,但是,我们还是应该学会位运算的优先级(这应该是常识)。
位运算的优先级如下:
按位反(~)>位移运算(<<,>>)>按位与(&)>按位异或(^)>按位或(|)
附:位运算在状压DP的用法
众所周知,状压DP就是把状态压缩成一个01串(其实就是一个二进制数),用以减少DP数组的维数。但是我们在DP的时候就要按照01串来进行状态的转移。所以位运算是状压DP的基础知识和必备知识。所以我在本篇随笔的末尾还附上了状压DP中比较常用的操作及其二进制实现的方式。
正文:(本文中的a表示十进制下的整数)
1、获得第i位的数字:(a>>i)&1 或者 a&(1<
很好理解,我们知道可以用&1来提取最后一位的数,那么我们现在要提取第i位数,就直接把第i位数变成最后一位即可(直接右移)。或者,我们可以直接&上1左移i位,也能达到我们的目的。
2、设置第i位为1:a=a|(1<
我们知道强制赋值用|运算,所以就直接强制|上第i位即可。
3、设置第i位为0:a=a&(~(1<
这里比较难以理解。其实很简单,我们知道非~运算是按位取反,(1<
4、把第i位取反:a=a^(1<
1左移i位之后再进行异或,我们就会发现,如果原数第i位是0,一异或就变成1,否则变成0。
5、取出一个数的最后一个1:a&(-a)
本篇随笔简单讲解一下C++STL中的map容器的使用方法和使用技巧。
map的英语释义是“地图”,但map容器可和地图没什么关系。map是“映射容器”,其存储的两个变量构成了一个键值到元素的映射关系。
比如下图:
我们可以根据键值快速地找到这个映射出的数据。
map容器的内部实现是一棵红黑树(平衡树的一种),因为比较复杂而且与理解并无多大关系,所以不予介绍,有兴趣的读者可以自己查阅相关的资料。
map容器存在于STL模板库#include中。使用的时候需要先开这个库。
比如:
#include
map<int,char> mp;
这就建立了一个从一个整型变量到一个字符型变量的映射。
因为map容器和set容器都是使用红黑树作为内部结构实现的。所以其用法比较相似。但由于二者用途大有不同,所以其用途还有微妙的差别。对于初学者来讲,其更容易涉及到的应该是vector容器、queue容器等,但是对于大佬们,经常用个set、map,没事再用bitset压一压状态这都是家常便饭。
其实,C++STL容器的使用方式都是差不多的。我们完全可以举一反三地去类比。与bitset重定义了许多奇形怪状新的函数之外,其他都是大致相同的。
常规操作
如其他C++STL容器一样,map支持基本相同的基本操作:
比如清空操作,函数clear(),返回容器大小size(),返回首尾迭代器begin(),end()等。
插入操作
map容器的插入操作大约有两种方法,第一种是类似于数组类型,可以把键值作为数组下标对map进行直接赋值:
mp[1]='a';
当然,也可以使用insert()函数进行插入:
mp.insert(map<int,char>::value_type(5,'d'));
删除操作
可以直接用erase()函数进行删除,如:
mp.erase('b');
遍历操作
和其他容器差不多,map也是使用迭代器实现遍历的。如果我们要在遍历的时候查询键值(即前面的那个),可以用it->first来查询,那么,当然也可以用it->second查询对应值(后面那个)
查找操作
查找操作类比set的查找操作。但是map中查找的都是键值。
比如:
mp.find(1);
即查找键值为1的元素。
map和pair的关系
我们发现,map和C++内置二元组pair特别相似。那是不是map就是pair呢?(当然不是)
那么map和pair又有什么关系呢?
@JZYShruraK大佬
首先,map构建的关系是映射,也就是说,如果我们想查询一个键值,那么只会返回唯一的一个对应值。但是如果使用pair的话,不仅不支持O(log)级别的查找,也不支持知一求一,因为pair的第一维可以有很多一样的,也就是说,可能会造成一个键值对应n多个对应值的情况。这显然不符合映射的概念。