这里主要记录一下在刷题过程中一些常用技巧
文章目录
STL容器
vector
vector<数据类型> 名称(长度,初值);
常用初始化
vector<int> s; //创建一个空vector
vector<int> s={8,2,3,1}; //直接给空vector初始化
vector<int> s(5); //创建一个vector,元素个数为5
vector<int> s(begin,end)//复制[begin,end)地址区间内元素到vector
常用方法
下面将以一个名为v
的vector介绍一下vector的常用用法
//可以v[i]下标访问
v.size();//返回求长度
v.push_back(元素名);//在末尾添加元素,必须是定义的元素,否则会报错
v.begin();//返回第一个元素的迭代器
v.end();//返回最后一个元素后面一个元素的迭代器
v.insert(v.begin()+i,5); //在v[i]处插入5,其余依次后移
v.insert(v.begin()+i,num,5); //在v[i]处插入num个5
v.insert(v.begin()+i,start,end); //在v[i]处插入区间[start, end)的所有元素
v.erase(v.begin()+2);//删除元素v[2]
v.clear(); //清空vector
v.front();v.back();//分别返回vector的首尾元素
for循环的写法
1、下标遍历
std::vector<int> s={8,2,3,1};
for(int i=0;i<v.size();i++){
std::cout << v[i] << endl;
}
2、迭代器遍历(迭代器相当于一个智能指针)
std::vector<int>::iterator iter;
for (iter = v.begin(); iter != v.end(); iter++) {
std::cout << (*iter) << endl;
}
3、基于范围的遍历(最简洁的遍历)
std::vector<int> s={8,2,3,1};
for(int i:v){ //遍历的是容器中的每个元素,类型要相符
std::cout << i << endl;
}
//对于类型非常复杂的元素,可以采用auto简化
for(auto i:v){ //遍历的是容器中的每个元素,类型要相符
std::cout << i << endl;
}
string
常用初始化
string s;//初始化字符串,空字符串
string s = "I am Yasuo"; //直接初始化
string s("I am Yasuo"); //直接初始化
string s = s1; //拷贝初始化,深拷贝字符串
string s(s1); //拷贝初始化,深拷贝字符串
string s4(10, 'a'); //s4存的字符串是aaaaaaaaaa
常用方法
输入输出流
cin >> s; //输入string只能用cin
cout << s << endl; //输出string只能用cout
getline(cin, s); //整行输入s
注意,使用cin
读入字符串时,遇到空白就停止读取。比如,输入"hello world",则 s= "hello"想把一个句子存下来,又不想创建多个string来存储,那就用getline
获取一整行内容。
但是在getline之前,如果使用过cin读取数据,最好在cin之后加一句cin.ignore()
,舍弃缓冲区中的换行符,避免getline读入为空。
cin >> N;
cin.ignore();
getline(cin,s);
我们经常会碰到这种情况,因为getline是为了避免因为空格造成分割而使用,但是往往一行里有多个词,我们还是需要按空格将其分割,这时候就需要使用sstream
头文件,这是c++11加入的方法。
getline(cin,key);
stringstream ss(key);
while (ss >> temp){ //按空格切割
index[temp].emplace(id);
}
//经典例题A1022
其他方法
//string对象可以通过下标访问每个字符s[i]
s.size();
s.length(); //两个都是求字符长度,没有本质区别
s = s1 + s2;//字符串拼接
//寻找子串
s.find("aa", 0); //返回的是子串位置。第二个参数是查找的起始下标,如果找不到,就返回string::npos(一个int型数)
//if(str1.find(str2) != string::npos) //在str1中包含str2
//切割子串
string s1 = "this is ok";
string s2 = s1.substr(2, 4); // s2 = "is i"
s2 = s1.substr(2); // s2 = "is is ok"
//删除子串
s1.erase(1, 3); //删除子串(1, 3)
s1.erase(5); //删除下标5及其后面的所有字符
//插入子串
s1.insert(2, "123"); //在下标 2 处插入字符串"123"
s1.insert(3, s2); //在下标 2 处插入 s2
s1.insert(3, 5, 'X'); //在下标 3 处插入 5 个 'X'
atoi(s.c_str()); //string先c_str转成char数组,再atoi转int
string经常会发生超时的问题,这种情况下用char数组替代。
经典题:PAT A1047
set
set<数据类型> 名称;
s.insert(元素);//向集合s里插入一个元素
s.emplace(元素);//也是插入元素,貌似比insert效率高
s.erase(元素);//删除集合s中的元素
s.size();//返回集合s的大小
s.begin();//返回集合中第一个元素的迭代器
s.end();//返回集合中最后一个元素后面一个元素的迭代器
s.find(元素);//返回集合s中的元素的迭代器,如果没有则返回的是s.end()
类似的还有无序STL unordered_set
map
map可以实现key和value之间的映射
map<key数据结构,value数据结构> 名称;
map<string,int> m;
m["hello"]=2;
m["world"]=3;
m.begin();//数据m的第一个数据的迭代器
m.end();//m的最后一个数据的下一个数据的迭代器
m.size();//m的数量
map<string,int>::iterator p;//迭代器的定义
m.find(key);//返回key映射的迭代器
p->first;//该迭代器位置的key键值
p->second;//该迭代器位置的value键值
m.erase(p);//按迭代器删除单个元素
m.erase(key);//删除key映射的元素
pair
pair可以看成是一个内部有两个元素的结构体,可以方便的实现将两个元素进行绑定。pair包含在#include<utility>
,另外头文件#include<map>
中包含头文件#include<utility>
。
//写法1和写法2完全等价
pair<string,int> p; //写法1
struct pair{ //写法2
string first;
int second;
}
//初始化两种
pair<string,int> p("haha",5);
make_pair("haha",5);
cout << p.first; //"haha",第一个元素
cout << p.second; //5,第二个元素
//两个pair类型数据可以比较大小,先比较first元素,相等再比较second
//直接使用比较运算符">,"<","=="之类的即可
//pair另一个最大的用处是作为map的键值对来进行插入
map<string,int> mp;
mp.insert(make_pair("haha1",5));
mp.insert(pair<string,int>("haha2",6));
stack
stack<int> s;
s.push(num);//将元素num压入栈中,放到栈顶
s.pop();//将栈顶元素pop,不返回该元素
s.top();//返回栈顶元素但不pop
s.size();//返回栈大小
s.empty();//判断栈是否为空,为空则返回true,否则返回false
queue
queue<int> q;
q.push(num);//将元素num从队尾压入队列中
q.pop();//将队列的第一个元素pop掉
q.front();//返回队列中的第一个元素
q.back();//返回队列中的最后一个元素
q.size();//返回队列的大小
q.empty();//判断队列是否为空,为空则返回true,否则返回false
这里有一点要注意,stack和queue使用top()、front()和pop()等方法前,必须用empty()判断队列是否为空,否则可能发生队空的错误。这里就有一个小技巧:
- 1、与判断:如果第一个条件不满足,立即退出-即不在对后续条件进行判断。依次类推。
- 2、或判断:如果第一个条件满足,立即退出-即不在对后续条件进行判断。依次类推。
看下面这种写法:
while(!s.empty() && s.top() == temp){
...
}
//因为栈空时不能用s.top(),因此必须判断,这种写法避免了分成两个if
//如果栈空,while直接退出,不会再判断栈顶元素是否与temp相等,避免了栈空引起的冲突
priority_queue
priority_queue是优先队列,其复杂度只有O(logn),其内部元素一定是按当前队列中优先级进行排序。每push或pop一个元素,其结构会动态调整。其内部实现为大小顶堆,在queue的头文件中.
创建方法
priority_queue<int> p;//最大值优先,是大顶堆一种简写方式
priority_queue<int,vector<int>,greater<int> >q1;
//greater<int>,最小值优先,小顶堆
priority_queue<int,vector<int>,less<int> >q2;
//less<int>,最大值优先,大顶堆
第一个是比较简单的,创建后是大顶堆,而如果想要创建小顶堆的话,需要按照第二种定义队列,容器一般为vector,less和greater决定其是大还是小。以上这种定义方法就可以解决
结构体内部元素优先级设置
通过在结构体内部设置一个友元函数来实现优先级设置,后面的operator对fruit类型的操作符“<”进行了重载,我们只需要重载“<”即可。
struct fruit{
string name;
int price;
friend bool operator< (fruit a, fruit b){
return a.price < b.price;
// 相当于less,这是大顶堆,最大值优先
// return a.price > b.price;
// 相当于great,这是小顶堆,最小值优先
}
} stu; //定义结构体变量
priority_queue<fruit> q; //其内部以价格高的水果优先。
常用方法
q.empty();//如果队列为为空,返回真
q.pop();//pop出第一个元素
q.push(元素);//压入一个元素
q.size();//返回队列大小
q.top();//返回队列中第一个元素
头文件 #include<algorithm>
reverse
根据翻转区间的地址,使用reverse将元素翻转
reverse(v.begin(),v.end());
sort
使用sort排序
sort(v.begin(),v.end(),cmp)
如果不传入cmp函数,仅sort(v.begin(),v.end())
,默认升序排序。
可以通过重写排序比较函数cmp进行不同的排序,如:
情况一:数组排列
int A[100];
bool cmp(int a,int b){
return a>b;//降序排列
} //可以理解成相邻两元素如果a>b,则为True,对应就是降序
sort(A,A+100,cmp);
如果是对vector这样的STL容器进行排序,传入的参数是vector的type类型参数
情况二:结构体排序 //很常用
struct node{
int num;
string s;
}
bool cmp(node a,node b){
if(a.num!=b.num)
return a.num>b.num;
else
return a.s>b.s;
}
注:比较方法也可以放在结构体中或类中定义。
使用标准库函数(需要#include<functional>
)
functional提供了一堆基于模板的比较函数对象,它们是:
equal_to | greater | less |
---|---|---|
not_equal_to | greater_equal | less_equal |
● 升序:sort(begin,end,less())
● 降序:sort(begin,end,greater())
sort(A,A+100,greater<int>());//降序排列
sort(A,A+100,less<int>());//升序排列
lower_bound和upper_bound
lower_bound
的功能是返回非递减序列内的第一个大于等于传入元素的位置,upper_bound
是返回非递减序列内的第一个大于传入元素的位置
两者都用的是二分的方法,时间复杂度O(logn)
,在线性查找方面速度很快,但传入的序列必须为有序序列(从小到大)
it1=lower_bound(v.begin(),v.end(),2);
//lower_bound(容器.begin(),容器.end(),待查找元素);
max、min、abs
略
swap
swap(x,y)
交换x和y的值
fill
可以把数组或容器对应范围某一区间赋相同的值。
fill(a,a+5,233); //a[0]~a[4]全部赋值为233
字符串hash
-
标准整型的hash是直接用整型的key作为下标,这样对于key和value的对应可以做到O(1)的查询
-
如果key是一个字符串,需要将这个字符串按一定的规则转换成整数,再用这个新生成的整数key
-
字符串hash常用于map、string等STL容器发生超时的时候。
-
如果字符串均由大写字母AZ构成,将AZ对应0~25,这样将26个大写字母对应到了二十六进制,再将二十六进制转换成十进制得到整数key。
-
转换后的整数为 2 6 l e n − 1 26^{len}-1 26len−1,len为字符串长度。
-
如果len过长可能会溢出,unsigned int为 0 0 0 ~ 2 32 − 1 2^{32}-1 232−1,而unsigned long long则对应 0 0 0 ~ 2 64 − 1 2^{64}-1 264−1,再长发生溢出就可能会发生哈希冲突,尽量避免出现问题。
-
如果还包含小写字母,则用五十二进制即可。
-
如果还包含数字,可以考虑用六十二进制,但如果字符串末尾是确定个数的数字,可以在将英文字母转换成整数后,再将末尾的数字拼接上去。如:“BCD4”->“731”+“4”->“7314”
-
经典题:PAT A1039
-
其实和map的实现方式类似,对于简单的问题开数组直接用字符串hash就可以得到比较好的解决,map反而会降低效率。
头文件 #include<iomanip>
是I/O流控制头文件,就像C里面的格式化输出一样
C++文件操作(ifstream、ofstream、fstream)
C++ 通过以下几个类支持文件的输入输出:
ofstream: 写操作(输出)的文件类 (由ostream引申而来)
ifstream: 读操作(输入)的文件类(由istream引申而来)
fstream: 可同时读写操作的文件类 (由iostream引申而来)
ofstream,open,close写入文件
#include<iostream>
#include<fstream>
using namespace std;
//通过ofstream的方式实现写入文件 open,close
int main()
{
ofstream fout; //ofstream输出文件
// ofstream fout("number.txt"); 自动创建一个文件
fout.open("../pose.txt");//打开文件
fout << "1234abcdef";//写入文件
fout.close();
}
fstream,fin从文件中读取文件并打印输出到屏幕
#include<iostream>
#include<fstream>
using namespace std;
//通过ifstream流读取文件,并将文件写入str中
int main()
{
ifstream fin("../pose.txt");//创建读取文件的流
char str[50] = { 0 };
fin >> str;//读取
fin.close();
cout << str;
cin.get();
}
按照行来读取数据
#include<iostream>
#include<fstream>
using namespace std;
//按照行来读取
int main()
{
//按照行来读取
ifstream fin("../pose.txt");
//读取4行数据
for (int i = 0; i < 4;i++)
{
char str[50] = { 0 };
fin.getline(str, 50);
cout << str << endl;
}
fin.close();
cin.get();
}
状态标志符的验证(Verification of state flags)
-
bad():如果在读写过程中出错,返回 true 。例如:当我们要对一个不是打开为写状态的文件进行写入时,或者我们要写入的设备没有剩余空间的时候。
-
fail():除了与bad() 同样的情况下会返回 true 以外,加上格式错误时也返回true ,例如当想要读入一个整数,而获得了一个字母的时候。
-
eof():如果读文件到达文件末尾,返回true。
-
good():这是最通用的:如果调用以上任何一个函数返回true 的话,此函数返回 false 。
要想重置以上成员函数所检查的状态标志,你可以使用成员函数clear(),没有参数。
while (!fin.eof()) {
double time, tx, ty, tz, qx, qy, qz, qw;
fin >> time >> tx >> ty >> tz >> qx >> qy >> qz >> qw;
Isometry3d Twr(Quaterniond(qw, qx, qy, qz));
Twr.pretranslate(Vector3d(tx, ty, tz));
poses.push_back(Twr);
}
DFS和BFS辨析
深度优先搜索DFS:
1.递归下去 2.回溯上来。
深度优先,则是以深度为准则,先一条路走到底,直到达到目标。这里称之为递归下去。
否则既没有达到目标又无路可走了,那么则退回到上一步的状态,走其他路。这便是回溯上来。
DFS特别适合处理:
带状态约束的问题,比如:
在一个给定形状的棋盘(形状可能是不规则的)上面摆放棋子,棋子没有区别。要求摆放时任意的两个棋子不能放在棋盘中的同一行或者同一列,请编程求解对于给定形状和大小的棋盘,摆放k个棋子的所有可行的摆放方案C。
搜索联通块问题,如后面的例子。
广度优先搜索BFS:
本质是采用队列的结构进行存储。特别适合解决各边权值相等的最短路径问题的问题!(因为每一层相当于走1步,如果在每层的BFS中发现了终点,那这一定是最短路径中的一条,其实相当于求的是最短边数)
下面看一个自己写完的经典例子:
给一个m*n的矩阵,矩阵中的元素为0或1。称位置(x,y)与其上下左右四个位置是相邻的。如果矩阵中有若干个1是相邻的,那么称这些1组成一个连通“块”。求给定矩阵中块的个数。
//注意一下,设置flag数组应该是判断有没有入过队,因为如果满足要求必定入过队
#include<iostream>
#include<cstdio>
#include<queue>
#include<map>
using namespace std;
int M[6][7] = {
{0,1,1,1,0,0,1},
{0,0,1,0,0,0,0},
{0,0,0,0,1,0,0},
{0,0,0,1,1,1,0},
{1,1,1,0,1,0,0},
{1,1,1,1,0,0,0}
};
bool flag[6][7]; //记录是否已经访问
int x[3] = {1,0,0}; //下右左的顺序
int y[3] = {0,1,-1};
int num = 0;
void BFS(pair<int,int> m){ //使用pair来记录坐标
queue<pair<int,int>> q;
q.push(m);
pair<int,int> s;
while(!q.empty()){ //BFS用的不是递归的思路
s = q.front();
flag[s.first][s.second] = true;//访问过了
for(int i=0;i<4;i++){
int a = s.first+x[i],b = s.second+y[i];
//没越界,数值是1,flag为false还没被访问过
if(a >= 0 && a <6 && b >=0 && b<7 && M[a][b] == 1 && !flag[a][b]){
q.push(make_pair(a,b));
flag[a][b] = true;//访问过了
}
}
printf("(%d,%d)\n",s.first+1, s.second+1);
q.pop();
}
}
//BFS一般用来搜索最短路径最好,DFS用来搜索能不能到达目的地
void DFS(pair<int,int> m){ //递归的思路进行深搜,先下,再右,再左,
//越界了,数值是0,flag为true已经访问过,有一条满足说明该次递归已经到头,可以返回上一阶段并开始下一次递归
if(m.first <0 || m.first >=6 || m.second <0 || m.second>=7){
return;
}
flag[m.first][m.second] = true;
for(int i=0;i<3;i++){ //依次递归遍历下右左
if(M[m.first+x[i]][m.second+y[i]] == 1 && flag[m.first+x[i]][m.second+y[i]] == false)
DFS(make_pair(m.first+x[i],m.second+y[i]));
}
printf("(%d,%d)\n",m.first+1, m.second+1);
}
int main(){
for(int i=0;i<6;i++){
for(int j=0;j<7;j++){
flag[i][j] = false;
}
}
for(int i=0;i<6;i++){
for(int j=0;j<7;j++){
if(flag[i][j] == false && M[i][j] == 1){
DFS(make_pair(i,j));
//BFS(make_pair(i,j));
num++;
}
}
}
printf("num:%d\n",num);
return 0;
}
这里我使用DFS和BFS两种方法来解决这个问题。
DFS采用的是递归的方法,本质是栈,因此函数的参数列表中一定要清楚的写出描述此时所处状态所需要的全部参数,因为递归的时候必须按状态搜索。这里因为只用记录xy坐标,因此我采用了pair来代替结构体。实际上把访问flag的信息直接也和坐标同时记录在一个结构体中不失为一种好方法。
DFS进入后,按回溯法标准,先判断return的条件。如果满足了条件,回溯到上一层。然后递归的调用DFS。这里因为要搜索3个方向,因此要调用3次DFS。即每有一个岔路口就需要调用一次。采用一种方向坐标数组x和y,使得遍历三个方向可以通过循环完成。
BFS采用的是队列的方法,即通过BFS一个函数内的循环进行搜索,而不是递归的思路,所以BFS中只需传入一个入口状态的参数即可。进入BFS后,先构造一个队列,将入口这个元素放入队列。然后进入循环直至队列为空,获得队首元素,搜索元素的三个方向,将符合要求的元素放入队列等待后续访问。完成一次循环时让队首元素出队。
简单的01背包问题
n件物品,重量w[i],价值c[i]。选若干物品放入容量V的背包,价值最大,求最大价值
#include<iostream>
#include<cstdio>
#include<queue>
#include<map>
using namespace std;
const int maxn = 30;
int n,v; //数量,总大价值,背包容量
int cost = 0;
int w[maxn],c[maxn];
void DFS(int index,int sumw,int sumc){ //因为w和c在主函数定义,因此用指针
if(index > n) return;
DFS(index+1,sumw,sumc); //不选第index件商品,这是岔路口的判断策略
if(sumw+w[index] <= v){ //在分支这里加入限制条件
if(sumc+c[index] > cost){
cost=sumc+c[index];
printf("cost:%d\n",cost);
}
DFS(index+1,sumw+w[index],sumc+c[index]);
}
}
struct node{
int index,sumw,sumc;
node(int i,int w,int c):index(i),sumw(w),sumc(c){}
};
void BFS(){ //因为w和c在主函数定义,因此用指针
queue<node> q;
q.push(node(1,0,0));
while(!q.empty()){
node s = q.front();
if(s.index > n) break; //得注意什么时候搜索结束
q.push(node(s.index+1,s.sumw,s.sumc));//先把岔路push进去
if(s.sumw + w[s.index] < v)
//push进岔路之前最好检查一下能不能进这个岔路,也就是剪枝
q.push(node(s.index+1,s.sumw+w[s.index],s.sumc+c[s.index]));
if(s.sumc+c[s.index] > cost){
cost = s.sumc+c[s.index];
printf("cost:%d\n",cost);
}
q.pop();
}
}
int main(){
cin >> n >> v;
for(int i=1;i<=n;i++){
cin >> w[i];
}
for(int i=1;i<=n;i++){
cin >> c[i];
}
//DFS(1,0,0);
BFS();
cout << cost << "\n";
return 0;
}
/*测试样例
5 8
3 5 1 2 2
4 5 2 1 3
*/
思路上还是经典的DFS和BFS的思路。特别需要注意在每次BFS和DFS进入某条岔路之前,一定要检查能不能进入。
DFS return回溯的条件应尽可能简单,因为很多不符合条件的直接进不来这条分支,所以回溯条件一般是index有没有到头。
BFS应该抽象成树的形式比较好理解层次遍历。访问某个节点时先把子节点push进去,再进行操作。