最近,出于巩固复习C++与数据结构、学习高级算法、准备PAT考试的需要,我开始照着网上的刷题教程,在力扣和PAT平台上逐题攻坚。以下是一些收获:
【1】map容器的使用
- make_pair返回pair对象,可以作为map容器的元素insert如下:
map.insert(make_pair(键,值));
- map的键值对访问:map[键]=值。其中,键为const修饰
- map的迭代器访问:
for(map<type1,type2>::iterator it=m.begin;it!=m.end();it++) { //对键进行操作(数值类型的键默认从小到大排序) it->first... //对值进行操作 it->second... }
- map.find返回迭代器,搜不到时返回map.end()
- map.count(键)返回int值,搜不到时返回0
- 删除键值对:map.erase(it)
【2】iterator容器的使用
- 1.迭代器基础语法
- 定义+初始化(以vector<int>为例):
vector<int>::iterator it=v.begin();
- 取值:*it或it->
- 迭代:it++(单向迭代器,即iterator,不支持++外的数值操作符,包括--)
- 迭代器化为下标:std::distance(it1,it2)
- 定义+初始化(以vector<int>为例):
- 迭代中删除元素
- 格式:it=it.erase(element)
- erase的返回值为下一个元素(注意,“下一个”被定义为从begin到end的下一个)
- 因此,it++不应该写在for里,避免删除后发生2次迭代
- 反向迭代器
定义+初始化+迭代(以vector<int>为例):
for(vector<int>::reverse_iterator it=v.rbegin();it!=v.rend();it++)
【3】sort的使用
- STL容器:
sort(stlType.begin(),stlType.end());//以默认方式对stlType容器内的元素排序
- 函数对象(以lambda表达式为例,也可以自定义cmp函数并传入sort中执行):
sort(stlType.begin(),stlType.end(),[需要引用的外部变量](参数)->返回值
{
函数体
});
//反向排序vector<int> v
sort(v.begin(),v.end(),[](int x,int y)->bool
{
return x>y;
});
【4】二分法的巩固学习
长期以来,我对二分法报有抽象而不具体的认识,只会用binary_search、lower_bound和upperbound接口。经过分析和讨论,我认为二分法的具体实现其实并无模板可记,但有清晰的规则可考。
- 二分法的写法由以下因素决定:
- 开闭区间选择
- 目标条件
- 是否需要对重复元素求边界
- 二分法的写法由以下部分组成:
- 初始区间
- 终止条件
- 区间缩小条件(arr[m]==num之类的)
- 区间缩小语句(l=m,r=m-1之类的)
- 返回值
- 对各个细节的归纳:
- 目标条件和有无重复如何影响二分法: 二分法的直接产物是特定的数值,至于>=、<=、<、>均为间接产物。也就是说,是目标条件的端点而非范围,参与决定了二分法的写法。注:针对有重数组的二分法可以得到特定数值的上下边界。详情见5
- 关于初始区间和终止条件的归纳: 全闭区间[0,arrSize-1],l<=r;左闭右开区间[0,arrSize),l<r。注:这两种我比较常用,其它的写法其实都可以,但是要能够熟练而且正确地写出。
- 关于区间缩小条件的归纳: 由1知,根据目标条件中隐含的端点即可。需要注意的是有重数组,详情见5
- 关于区间缩小语句的归纳:
- 全闭区间直接用+1、-1。
- 半开区间或全开区间,在开的半边“隐式缩小”区间即可。对于[l,r),即令l=m+1或r=m。
- 关于返回值的归纳:
- 无重复数组: 左闭右闭,返回m;左闭右开,返回l
- 有重复数组: 返回pos。pos的取值:下边界,则在>=时取m;上边界,则<=时取m。如此,pos会随着m改变而改变。实现见下述代码段
//针对有重数组arr,求小于0的最右侧元素下标 int test0(int* arr,int arrSize) { //左闭右闭 int l=0,r=arrSize-1; int pos; while(l<=r) { int m=l+((r-l)>>1); //此处的写法应该记忆,2点好处不言自明 //细节:这样的m始终取较小的值 if(arr[m]<0) { l=m+1; } else { r=m-1; pos=m; } } return pos; }
【5】自动机实现的KMP算法
(两者都是我很早就感兴趣的知识点,但一直懒得看,最近学了一下。可能实用性不高)
- 名词解释
- 模式pattern: 如AAAB
- 文本text: 如AAACAAAB/AAAAAAAB
- 需求:在text里找pattern,而且不用暴力法,要求在遍历pattern的时候“聪明点”
- 状态: pattern在使用中由一个字符转换到一个字符(不一定是前一个字符转换到后一个字符),则前后为各为一个状态;
- 状态推进: text的下一个字符仍然匹配pattern的下一个字符,由前一个字符转换到后一个字符;
- 状态回退: 下一个字符不匹配,若可以匹配影子状态,则在pattern中发生有限的回退。否则,发生完整回退;
- 影子状态: 最长的相同前缀位置。比如,在pattern字符串ABABC中,第二个A的影子是第一个A,第二个B的影子是第1个B,C没有影子。
- 转移过程: 根据text的下一个字符,转移到pattern的哪个位置,整个过程由二维数组dfa记录;
- dfa:自动机。其实就是记录(状态,情况)映射至(新状态)的数组。
- dfa[状态][text的下一个字符]
- =下一个状态
- ={pattern下一个字符
- {dfa[最近的影子状态位置][text的下一个字符]
- (值得记录的细节)dfa[patternSize][charSetSize]对应关系:0~null 1~pattern[0] ... patternSize-1~pattern[patternSize-2]
-
例图与原博客
https://pic3.zhimg.com/v2-2788562d1059d7b8ff71aa342e4ae036_r.jpgKMP 算法详解 - 知乎 (zhihu.com)
-
构建dfa数组的具体实现
-
双层循环的含义:外层i表示pattern的各个位置,内层j表示(状态,各个可能的字符)对应的状态转移
-
重要变量x(记录下一个位置的影子状态,规则也是推进和回退):在外层循环中更新,x=dfa[x][pattern[i]],即pattern[i+1]的影子状态=pattern[i]的影子状态遇到字符pattern[i+1]的转移后状态。x初始化为0,因为pattern[0]、dfa[1]的影子状态一定为null
-
内层循环:推进和回退
-
#include<iostream>
using namespace std;
class DFA_KMP
{
public:
string pattern;
int patternSize;
int charSetSize;
int** dfa;
DFA_KMP(string pattern,int charSetSize)
{
this->pattern=pattern;
this->patternSize=pattern.size();
this->charSetSize=charSetSize;
dfa=new int*[patternSize];
for(int i=0;i!=patternSize;i++)
{
dfa[i]=new int[charSetSize];
}
}
void dfaBuilder()
{
//i:0-a,1-b,2-a,3-b,4-c
//j:0-a,1-b,2-c
//dfa[0][]为"ababc"之前的空位的状态转移数组
for(int i=0;i!=charSetSize;i++)
{
if(i!=pattern[0]-97)dfa[0][i]=0;
else dfa[0][i]=1;
}
//影子状态位置
int x=0;
for(int i=1;i!=patternSize;i++)
{
for(int j=0;j!=charSetSize;j++)
{
//状态推进
if(pattern[i]-97==j)dfa[i][j]=i+1;
//状态回退
else dfa[i][j]=dfa[x][j];
}
//更新影子状态位置
x=dfa[x][pattern[i]-97];
//cout<<x<<'\n';
}
}
int matcher(string text)
{
int textSize=text.length();
int j=0;
for(int i=0;i!=textSize;i++)
{
j=dfa[j][text[i]-97];
if(j==patternSize)return i-patternSize+1;
}
return -1;
}
};
int main()
{
//只有a/b/c三个字母尝试进行KMP匹配,便于debug
DFA_KMP algOb("ababc",3);
algOb.dfaBuilder();
cout<<"构建状态转移数组\n";
for(int i=0;i!=algOb.patternSize;i++)
{
for(int j=0;j!=algOb.charSetSize;j++)
{
cout<<algOb.dfa[i][j]<<' ';
}
cout<<'\n';
}
cout<<"在text字符串下标为"<<algOb.matcher("abababc")<<"处发现模式字符串"<<'\n';
//任务:
//学会构造dfa数组
//学会更新影子状态
//完成完整的KMP
return 0;
}