师徒恋大法好

原为PG所作,然而由于一些不可告人的原因,文章放到这儿了……

有了STL,不必再从头写大多的标准数据结构和算法,并且可获得非常高的性能。(如果没有氧气,最好不要用vector,deque,set,multiset,map,string)。

废话不多说,让我们一起看看STL到底有多好用。

1.vector

可变长度的数组。(可节省空间)

常用操作:

#include<vector>
vector<int>a;
a[i];//重载了[],返回数组的第i+1个元素(从0开始)
a.empty()//数组为空返回1,否则返回0
a.size()//返回数组元素个数
a.push_back(x)//尾部插入元素x
a.pop_back()//删除尾部元素
a.begin()//返回第一个元素迭代器
a.end()//返回最后一个元素迭代器

上面的操作时间复杂度均为O(1);(但常数大,比数组慢,只有吸氧才能和数组媲美)

下面的操作慎用:

a.erase(it1,it2)//删除迭代器it1到it2的元素(前闭后开)
a.erase(it)//删除迭代器it指向的元素
a.clear();//清空数组
a.insert(it,x)//在迭代器为it的位置加入x,其他后移
a.assign(b.begin(),b.end())//把vector b复制到vector a

这些操作时间复杂度为O(n),且常数较大。

vector支持algorithm库中的某些操作,如:

sort(a.begin(),a.end(),cmp);//排序,cmp是自定义函数。
lower_bound(a.begin(),a.end(),x);//返回数组中第一个大于等于x的元素的迭代器。
upper_bound(a.begin(),a.end(),x)//返回数组中第一个大于x的元素的迭代器。
max_element(a.begin(),a.end())//返回数组最大值
min_element(a.begin(),a.end())//返回数组最小值
random_shuffle(a.begin(),a.end())//随机打乱,在随机化(乱搞)算法中用到。
reverse(a.begin(),a.end())//数组反转

注意:没有氧气,慎用vector

下面通过几道例题感受vector的简便。

例题1:洛谷P1177【模板】快速排序

虽然可以用sort,但我们尝试使用vector来实现。

#include<bits/stdc++.h>
std::vector<int>a;
int main(){
	int n,x;
	scanf("%d",&n);
	for(register int i=0;i<n;++i)scanf("%d",&x),a.insert(upper_bound(a.begin(),a.end(),x),x);
	for(register int i=0;i<n;++i)printf("%d ",a[i]);
	return 0;
}

从上面可以看出,vector可以非常便捷地支持插排。

例题2:P3378 【模板】堆

虽然可以使用另一个容器priority_queue,但我们可以使用vector切了它。

#include<bits/stdc++.h>
std::vector<int>a;
int main(){
	int n,k,x;
	scanf("%d",&n);
	for(register int i=1;i<=n;++i){
		scanf("%d",&k);
		if(k==1){
			scanf("%d",&x);
			a.insert(upper_bound(a.begin(),a.end(),x),x);
		}
		else if(k==2)printf("%d
",a[0]);
		else a.erase(a.begin());
	}
	return 0;
}

从上面两道例题可以看出,vector的操作也不一定慢(玄学n方过百万),但是最好注意程序的常数,能开O2尽量开

例题3:洛谷P3369 【模板】普通平衡树

此题是上述操作的综合,不要以为数组很菜,它能支(A)持(K)平(I)衡(O)树(I)的操作呢。(骚的一批)

#include<bits/stdc++.h>
std::vector<int>a;
int main(){
	int n,opt,x;
	scanf("%d",&n);
	for(register int i=1;i<=n;++i){
		scanf("%d%d",&opt,&x);
		if(opt==1)a.insert(upper_bound(a.begin(),a.end(),x),x);
		else if(opt==2)a.erase(lower_bound(a.begin(),a.end(),x));
		else if(opt==3)printf("%d
",lower_bound(a.begin(),a.end(),x)-a.begin()+1);
		else if(opt==4)printf("%d
",a[x-1]);
		else if(opt==5)printf("%d
",*(lower_bound(a.begin(),a.end(),x)-1));
		else printf("%d
",*(upper_bound(a.begin(),a.end(),x)));
	}
	return 0;
}

从第一个容器就能看出师徒恋的方便了吧

2.stack

常用操作:

#include<stack>
stack<int>st;
st.push(x)//向栈顶加入x,O(1)
st.pop()//栈顶出栈,O(1)
st.top()//返回栈顶,O(1)

看出来了吗?stack的所有操作vector都能支持。

但栈的思想很重要,后缀表达式,tarjan强连通分量算法以及单调栈等都需要用到。

这里给大家推荐一道单调栈例题,感受一下单调栈

例题:洛谷SP1805 HISTOGRA - Largest Rectangle in a Histogram

如果矩形从左到右单调递增,答案是以每个矩形的高度为高度,从当前矩形到右边界为宽度的矩形的面积的最大值

如果下一个比上一个矮,那么这块矩形和之前的矩形构成较大面积时,新矩形高度不可能超过此矩形高度,所以可以把比此矩形高的矩形删掉,用宽度不变,高度为此矩形高度的矩形替代

简单说,我们维护一个栈,保存高度单调递增的矩形,然后扫描每个矩形,如果比栈顶矩形高,进栈,否则栈顶出栈直至栈空或栈顶矩形比当前矩形矮(简单吧)

#include<bits/stdc++.h>
long long n,p,a[100001],w[100001],ans,kd;
std::stack<long long>st;
int main(){
	while(scanf("%lld",&n)&&n){
		for(register long long i=1;i<=n;++i)scanf("%lld",&a[i]);
		a[n+1]=0;st.push(0);ans=0;//注意栈为空时.top()会出错,a[n+1]=0避免结束后栈中还有矩形
		for(register long long i=1;i<=n+1;++i)
			if(a[i]>st.top())st.push(a[i]),w[st.size()]=1;//比栈顶矩形高
			else{//否则更新答案
				kd=0;
				while(st.top()>a[i])kd+=w[st.size()],ans=std::max(ans,kd*st.top()),st.pop();
				st.push(a[i]);w[st.size()]=kd+1;
			}
		printf("%lld
",ans);
	}
	return 0;
}

这就是单调栈,O(n),借助单调性,能及时排除不可能选项,保持高度有效性和秩序性。

3.queue

循环队列(可节省空间)

常用操作:

#include<queue>
queue<int>q;
q.push(x)//队尾加入x
q.pop()//队首出队
q.front()//返回队首
q.back()//返回队尾
q.empty()//队列为空返回1否则返回0
q.size()//返回队列大小

循环队列的操作都是O(1)的,并且常数较小,使用方便。

循环队列一般用于广搜,树和图的广度优先遍历,拓扑排序和SPFA等算法中。

树和图的广度优先遍历:

inline void bfs(){
	memset(d,0,sizeof(d));//d数组记录点在树中的深度或点在图中的层次。
	queue<int>q;
	q.push(1);d[1]=1;
	whiel(!q.empty()){
		int x=q.front();q.pop();
		for(register int i=head[x];i;i=edge[i].next){//链式前向星存边
			if(d[edge[i].to])continue;
			d[edge[i].to]=d[x]+1;q.push(edge[i].to);
		}
	}
}

广搜在骗分博客中会详讲。此处略

拓扑排序:

#include<bits/stdc++.h>
int x,n,m,deg[1000001],head[1000001],u,v,a[1000001],cnt;
struct Edge{int next,to;}edge[1000001];
inline void topsort(){
	queue<int>q;
	for(register int i=1;i<=n;++i)if(!deg[i])q.push(i);
	while(!q.empty()){
		int x=q.front();q.pop();a[++cnt]=x;
		for(register int i=head[x];i;i=edge[i].next)if(--deg[edge[i].to])q.push(edge[i].to);
	}
}
int main(){
	scanf("%d%d",&n,&m);
	for(register int i=1;i<=m;++i){scanf("%d%d",&u,&v);edge[i].next=head[u];edge[i].to=v;head[u]=i;++deg[v];}
	//链式前向星存边并统计入度
	topsort();
	for(register int i=1;i<=cnt;++i)printf("%d ",a[i]);
	return 0;
}

洛谷P3371 【模板】单源最短路径(弱化版)

关于SPFA 它死了

#include<bits/stdc++.h>
int n,m,k,x,head[1000001],dis[1000001],vis[1000001],u,v,w;
struct Edge{int next,to,w;}edge[1000001];
inline void spfa(int k){
	for(register int i=1;i<=n;++i)dis[i]=0x7fffffff,vis[i]=0;
	std::queue<int>q;q.push(k);dis[k]=0;vis[k]=1;
	while(!q.empty()){
		x=q.front();q.pop();vis[x]=0;
		for(register int i=head[x];i;i=edge[i].next)
			if(dis[edge[i].to]>dis[x]+edge[i].w){
				dis[edge[i].to]=dis[x]+edge[i].w;
				if(!vis[edge[i].to])vis[edge[i].to]=1,q.push(edge[i].to);
			}
	}
}
int main(){
	scanf("%d%d%d",&n,&m,&k);
	for(register int i=1;i<=m;++i){scanf("%d%d%d",&u,&v,&w);edge[i].next=head[u];edge[i].to=v;edge[i].w=w;head[u]=i;}
	spfa(k);
	for(register int i=1;i<=n;++i)printf("%d ",dis[i]);
	return 0;
}

4.deque

双端队列deque==vector+queue,即能高效从队列两端进行操作的队列,包含所有vector支持的操作,还支持:

#include<deque>
deque<int>dq;
dq.push_front(x)//从队首插入,O(1)
dq.pop_front()//队首出队,O(1)

有了vector和queue,为什么还要deque呢?

因为有单调栈就要有单调队列啦(雾

双端队列可以支持单调队列的操作

例题:洛谷 P1886 滑动窗口

用两个双端队列维护区间最值

#include<bits/stdc++.h>
int x,n,m,cnt=1,k;
std::deque<std::pair<int,int> >q,q1;//q维护单调递减序列,q1维护单调递增序列
int ans[2][1000001];
int main(){
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++){
        scanf("%d",&x);
        while(!q.empty()&&x>=q.back().second)q.pop_back();//保持单调性
        while(!q1.empty()&&x<=q1.back().second)q1.pop_back();
        q.push_back(std::make_pair(i,x));q1.push_back(std::make_pair(i,x));
        while(i-k>=q.front().first)q.pop_front();//保证序列在区间内
        while(i-k>=q1.front().first)q1.pop_front();
        if(i>=k)ans[0][cnt]=q.front().second,ans[1][cnt++]=q1.front().second;
    }
    for(int i=1;i<cnt;i++)printf("%d ",ans[1][i]);
    puts("");
    for(int i=1;i<cnt;i++)printf("%d ",ans[0][i]);
    return 0;
}

单调栈和单调队列都是挖掘题目中的单调性,及时排除不可能,能保持序列的高度有效性和秩序性,能解决一些高级数据结构(如线段树)才能解决的问题,值得我们思考

注意,deque和vector比较相似,最好降低常数,没有氧气,慎用deque

因为NOIP所以不讲单调队列优化多重背包

5.pair

这就是上面代码中出现的pair(没什么好讲的

二元组,尖括号内为自己定义的类型,用.first()访问第一元,用.second()访问第二元,重载了<,第一元优先级高于第二元

主要用于STL其他容器的存储类型,如上面程序中双端队列的类型为一个二元组,第一元表示序号,第二元表示大小

6.priority_queue

优先队列,通俗讲就是没素质,插队

操作:

#include<queue>
priority_queue<int>q;//定义大根堆,即大的插队
priority_queue<int,vector<int>,greater<int> >q;//定义小根堆,即小的插队
q.push(x)//将x插入堆,O(logn)
q.pop()//堆顶出堆,O(logn)
q.top()//返回堆顶,O(1)

看完上面的操作,你想到了什么呢?对,没错,它能动态维护序列的最值

堆能优化贪心,dijkstra,prim等算法

但需要注意优先队列存储类型必须重载<。优先队列不支持erase,这让我们很蛋疼,解决方案为在删除时,在堆外记录(例如记录元素最新值),用于鉴别过时元素,在堆顶取出最值时,再检查是不是过时的,若是,取下一个

例题1:洛谷 P1090 合并果子

思路就是每次取出两个最小值合并,用堆维护

#include<bits/stdc++.h>
int n,x,ans,a,b;
std::priority_queue<int,std::vector<int>,std::greater<int> >q;
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d",&x),q.push(x);
    while(q.size()>=2)a=q.top(),q.pop(),b=q.top(),q.pop(),ans+=a+b,q.push(a+b);
    printf("%d
",ans);
    return 0;
}

这就是堆优化贪心

例题2:洛谷P4779 【模板】单源最短路径(标准版)

堆优化dijkstra

#include<bits/stdc++.h>
int x,y,n,m,k,u,v,w,head[1000001],dis[1000001],vis[1000001];
struct Edge{int next,to,w;}edge[1000001];
inline void dijkstra(int k){
	for(register int i=1;i<=n;++i)dis[i]=0x7fffffff,vis[i]=0;//初始化
	std::priority_queue<std::pair<int,int>,std::vector<std::pair<int,int> >,std::greater<std::pair<int,int> > >q;q.push(std::make_pair(0,k));dis[k]=0;
	while(!q.empty()){
		x=q.top().second;q.pop();
		if(vis[x])continue;
		vis[x]=1;
		for(register int i=head[x];i;i=edge[i].next)if(dis[edge[i].to]>dis[x]+edge[i].w)dis[edge[i].to]=dis[x]+edge[i].w,q.push(std::make_pair(dis[edge[i].to],edge[i].to));
	}
}
int main(){
	scanf("%d%d%d",&n,&m,&k);
	for(register int i=1;i<=m;++i)scanf("%d%d%d",&u,&v,&w),edge[i].next=head[u],edge[i].to=v,edge[i].w=w,head[u]=i;
	dijkstra(k);
	for(register int i=1;i<=n;++i)printf("%d ",dis[i]);
	return 0;
}

毒瘤压行

这就是堆优化dijkstra,O((n+m)logn)比某个死了的算法好多了

7.string

字符串string,很多人不是很会使用它,其实它能在水掉很多字符串的题

基本操作:

#include<string>
#include<sstream>//stringstream头文件
string s,s1;
stringstream ss(s);
ss>>s;
char c;
cin>>s//输入
[],+,+=,>,<,>=,<=,==,!=,= //重载[],从0开始编号,+连接两个字符串,比较运算符从第一个字符开始比较,=赋值
getline(cin,s)//输入一行
s.push_back(c)//字符串尾端加入字符c
s.append(c)//字符串尾端加入字符c
s.append(n,c)//在字符串尾端加入n个字符c
s.insert(it,c)//在迭代器it处插入c
s.substr(l,r)//返回l到r的字符串(前闭后开)
s.find(s1)//搜索字符串,返回第一次找到的位置,若没找到返回-1
s.empty()//是否为空,为空返回1,否则返回0
s.erase(it)//清除迭代器it指向的元素
s.erase(it1,it2)//清除迭代器it1到it2之间的元素(前闭后开)
s.replace(a,len,str)//从a开始len个用str替换

常用操作就是这些了,find在一定程度上可以替代KMP,+和substr可以构造字符串只要不是模板题或数据卡你,就算时间复杂度是错的,也能在你不会写时骗到更多的分,且调试难度极低,纯模拟至少能拿到分,只要你坚信,n^2过百万,暴力碾标算,根据常数的优秀以及NOIP的水数据就有可能得到AC的好成绩

你肯定会好奇stringstream是什么,让我来告诉你

试想一道题因为找不到这样的题,输入若干个整数,求和

如果你会快读,应该能写出来,但是容易写错,也有点难理解,这里提供一种解决方案,使用stringstream,很容易理解

#include<bits/stdc++.h>
string s;
int sum,x;
int main(){
	getline(std::cin,s);
	stringstream ss(s);
	while(ss>>x)sum+=x;
	printf("%d",sum);
	return 0;
}

现在理解stringstream了吧,它能将读入的字符串变成其他类型的变量,或一行字符串变成空格分隔的多个字符串。但请注意,string很慢,stringstream更慢,所以还是那句话,没有氧气,慎用

综上,有效地利用各种字符串处理函数和模拟,能使你的NOIP分数提高一个档次

8.set和multiset

集合和可重集合,内部实现是一棵红黑树呵呵,但是常数较大使得它的效率较低

基本操作:

#include<set>
set<int>a;
multiset<int>b;
a.size(x)//返回大小,O(1)
a.empty()//返回是否为空,空返回1,否则返回0
a.clear()//清空,O(nlogn)
a.begin()//返回最小元素迭代器
a.end()//返回最大元素迭代器
a.insert(x)//插入x
a.find(x)//查找x,若存在返回指向该元素的迭代器,若不存在返回a.end(),O(logn)
a.lower_bound(x)//返回第一个大于等于x的元素的迭代器,O(logn)
a.upper_bound(x)//返回第一个大于x的元素的迭代器,O(logn)
a.erase(it)//删除迭代器it指向的元素,O(logn)
a.erase(x)//删除所有等于x的元素,O(k+logn),k为等于x的元素个数
if((it=s.find(x))!=s.end())s.erase(it);//可以只删除一个等于x的元素
a.count(x)//返回元素个数,O(k+logn),k为等于x的元素个数

set能排序并去重,能方便地进行离散化等操作,还能用它实现珂朵莉树。但注意,set,multiset和map的迭代器的只支持自增或自减且自增或自减的时间复杂度是O(logn)的,所以在使用时一定要注意

例题1:洛谷 UVA10815 Andy's First Dictionary

由于string重载了<,所以把非字母字符转化为空格,再用stringstream得到单词丢进set,排序且去重

#include<bits/stdc++.h>
std::set<std::string>zd;
std::string s,dc;
int main(){
	while(std::cin>>s){
		for(register int i=0;i<s.size();++i)
			if(s[i]>='a'&&s[i]<='z'||s[i]>='A'&&s[i]<='Z')s[i]=tolower(s[i]);
			else s[i]=' ';
		std::stringstream ss(s);
		while(ss>>dc)zd.insert(dc);
	}
	for(std::set<std::string>::iterator it=zd.begin();it!=zd.end();++it)std::cout<<*it<<std::endl;
	return 0;
}

从上面的例题就能看出set排序加去重的方便了

例题2:洛谷 UVA136 Ugly Numbers

用优先队列保存所有已生成的丑数,每次取最小x,生成2x,3x,5x插入堆中,注意一个丑数有多种生成方式,所以可以用set判断是否已生成过

#include<bits/stdc++.h>
std::priority_queue<long long,std::vector<long long>,std::greater<long long> >q;
std::set<long long>s;
long long a[3]={2,3,5},x,t;
int main(){
	q.push(1);s.insert(1);
	for(register int i=1;;++i){
		x=q.top();q.pop();
		if(i==1500){printf("The 1500'th ugly number is %d.
",x);break;}
		for(register int j=0;j<3;++j){
			t=x*a[j];
			if(!s.count(t))s.insert(t),q.push(t);
		}
	}
	return 0;
}

从上可以看出学会同时运用多个容器,融会贯通,能解决更多的题目。

9.map

map映射,它的内部实现也是一棵红黑树呵呵,同样,也因为常数较大使得效率较低

基本操作

#include<map>
map<key,value>a;//建立一个key到value的映射,如map<string,int>a
a.size()//返回大小,O(1)
a.empty()//返回是否为空,空返回1,否则返回0,O(1)
a.clear()//清空,O(nlogn)
a.begin()//返回第一个元素的迭代器,O(1)
a.end()//返回最后一个元素的迭代器,O(1)
a.insert(pair<string,int>)//参数必须是pair,O(logn)
a.erase(pair<string,int>)//参数必须是pair或迭代器,O(logn)
a.erase(it)//O(logn)
a.find(x)//查找x,若存在返回指向key为x的二元组的迭代器,O(logn)
a[x]//重载了[],O(logn),需要注意,若x不存在,则会自动新建一个二元组(x,0或NULL),强烈建议用[]之前,先用find()检查存在性

map可不得了,它可以一定程度上代替hash表

例题1:洛谷 P3370 【模板】字符串哈希

用map不光能解决这道题,还能统计单词的出现次数

#include<bits/stdc++.h>
std::map<std::string,int>a;
int n;
std::string s;
int main(){
	scanf("%d",&n);
	while(n--){
		std::cin>>s;
		if(a.find(s)==a.end())++a[s];//统计单词的出现次数
	}
	printf("%d",a.size());
	return 0;
}

可以方便的代替hash表,但常数大,有时需要注意

例题2:洛谷 UVA156 Ananagrams

把每个单词初始化,全部转化成小写在排序,然后丢进map统计

#include<bits/stdc++.h>
std::map<std::string,int>mp;
std::vector<std::string>a;
std::string s;
int n;
inline std::string init(const std::string &s){//初始化
	std::string ans=s;
	for(register int i=0;i<ans.size();++i)ans[i]=tolower(ans[i]);
	sort(ans.begin(),ans.end());
	return ans;
}
int main(){
	while(std::cin>>s&&s[0]!='#'){
		a.push_back(s);std::string s1=init(s);
		if(!mp.count(s1))mp[s1]=0;
		mp[s1]++;
	}
	std::vector<std::string>ans;
	for(register int i=0;i<a.size();++i)if(mp[init(a[i])]==1)ans.push_back(a[i]);
	sort(ans.begin(),ans.end());
	for(register int i=0;i<ans.size();++i)std::cout<<ans[i]<<std::endl;
	return 0;
}

此例说明,没有良好的代码设计,是无法发挥STL的威力的,如果没有想到初始化,很难用map简化代码

10.bitset

bitset可看做多位二进制数,每8位占用一个字节,相当于状压的bool数组,支持基本位运算,n位bitset执行一次位运算的复杂度可视为n/32,效率较高

#include<bitset>
bitset<1000001>s;//尖括号中写位数
~s//按位取反
&,|,^//按位与或,异或
<<,>> //左移右移
==,!= //比较是否相等
s[k]//表示第k位,从0开始
s.count()//返回多少位为1,O(n)
s.any()//若所有位为0返回0,否则返回1
s.none()//若所有位为0返回1,否则返回0
s.set()//所有位变为1
s.set(k,v)//把第k位变为v,即s[k]=v
s.reset()//把所有位变为0,
s.reset(k)//把第k为变为0,即s[k]=0
s.flip()//把所有位取反,即s=~s
s.flip(k)//把第k位取反,即s[k]^=1

例题

11.algorithm

STL的算法库,提供了

int a[1000001];
reverse(a+1,a+n+1);//翻转1~n
int m=unique(a+1,a+n+1)-a-1;//m为去重后的元素个数
random_shuffle(a+1,a+n+1);//随机打乱
do{
}next_permutation(a+1,a+n+1);//下一个排列,当不存在排名更靠后的排列返回0,否则返回1
sort(a+1,a+n+1);//快排
inline void cmp(const int &a,const int &b){return ...}
sort(a+1,a+n+1,cmp)//cmp自定义快排,返回值为1即交换
lower_bound(a+1,a+n+1,x)//返回第一个大于等于x的元素下标
upper_bound(a+1,a+n+1,x)//返回第一个大于x的元素下标

上述就是STL库中的常用的函数,其中a数组可用vector,deque,string等容器替换

最后,祝大家NOIP2018 rp++

原文地址:https://www.cnblogs.com/wendster/p/stlwin.html