蓝桥杯考纲和知识点总结
1.一些常用知识点
快速幂
快速幂很常用,要熟练
求 m^k mod p,时间复杂度 O(logk)。
int qmi(int m, int k, int p)
{
int res = 1 % p, t = m;
while (k)
{
if (k&1) res = res * t % p;
t = t * t % p;
k >>= 1;
}
return res;
}
卡特兰数
这个有时候会遇到,比如括号匹配数,求某种排列数量的题都可以带进来试试,求组合数的方法在6部分。
给定n个0和n个1,它们按照某种顺序排成长度为2n的序列,满足任意前缀中0的个数都不少于1的个数的序列的数量为: Cat(n) = C(2n, n) / (n + 1)
中位数
用对顶堆维护,前面大顶堆,后面小顶堆,大顶堆堆顶即中位数,来了一个数跟中位数比较,小的放大顶堆,大的放小顶堆,再维护两个堆的大小保证大顶堆的大小比小顶堆最多大1.
第k大的数
排序O(nlogn)
对顶堆O(nlonn)
类似快排的思想O(n)
逆序对:
归并排序O(nlogn)求出逆序对个数
void merge(int l,int mid,int r){
//合并a[l~mid]与a[mid+1~r]
//a是待排序数组,b是临时数组,cnt是逆序对个数
int i = l,j = mid + 1;
for(int k = l,k <= r;k++){
if(j > r || i <=mid && a[i] <= a[j]) b[k] = a[i++];
else b[k] = a[j++],cnt += mid-i+1;
}
for(int k = l;k <= r;k++) a[k] = b[k];
}
//还可以用树状数组求
位运算
求n的第k位数字: n >> k & 1
返回n的最后一位1:lowbit(n) = n & -n
2.基础算法
简单:枚举 倍增 离散化 差分 前缀和
中等:分治 贪心 Huffman编码 尺取法 二分 三分 ST算法
比较重要的是前缀和、差分算法,前缀和容易考到二维的情况。
前缀和
序列A某个下标区间的数的和 可表示为: sum(l,r) = S[r] - S[l-1]
二维前缀和:S[i,j] = S[i-1,j] + S[i,j-1] - S[i-1,j-1] + A[i,j]
差分
对于数列A,差分数列B: B[1]=A[1],B[i] = A[i] - A[i-1]
前缀和 与 差分 互为逆运算
差分数列的前缀和就是原数列A
把A的区间[l,r]加d,就是把差分B[l]+d,B[r+1]-d
差分可以应用在树上
二分
常规二分方法如下,不太好记,简单情况建议使用lower_bound(),也在下面介绍。
当问题的答案具有单调性,就可以通过二分法把求解转化为判定。
整数二分:
以下代码以l=r结束:
在升序a中找到>=x的最小一个:
while(l < r){
int mid = (l + r) >> 1;
if(a[mid] >= x) r = mid;else l = mid + 1;
}
return a[l];
在升序a中找到<=x的最大一个:
while(l < r){
int mid = (l + r + 1) >> 1;
if(a[mid] <= x) l = mid;else r = mid - 1;
}
return a[l];
#注:用右移运算>> 而不是整除/,因为前者是向下取整,后者是向0取整,在二分值域包含负数时后者不能正常取整。
#注:mid = (l+r) >> 1 不会取到r,mid = (l+r+1) >> 1 不会取到l,可以用这个性质处理无解的情况,把原区间[1,n]分别扩大为[1,n+1]和[0,n],如果最后二分终止于扩大后的下标,则无解。
#实域二分:
一般要保留k位小数时,eps=10^-(k+1)
while(l + 1e-5 < r){
double mid = (l + r) / 2;
if(calc(mid)) r = mid;else l = mid;
}
或 精度更高的:
for(int i = 0;i < 100;i++){
double mid = (l + r) / 2;
if(calc(mid)) r = mid; else l = mid;
}
#三分求单峰函数极值:
取 lmid 和 rmid
1. f(lmid) < f(rmid) 则lmid和rmid都在极大值左侧,或在极大值两侧。无论哪种情况极大值都在lmid右侧,可令 l = lmid;
2. f(lmid) > f(rmid) 同理,令 r = rmid;
如果取lmid与rmid为三等分点,那么定义域每次缩小1/3。
如果取lmid与rmid为二等分点两侧极其接近的地方,那么定义域每集近缩小1/2。
时间复杂度为log级别
#二分答案转化为判定:
把求最优的问题,转化为给定一个值mid,判定是否存在一个可行方案评分达到mid的问题。
lower_bound()
lower_bound() & upper_bound()
lower_bound() : 寻找≥x的第一个元素的位置
upper_bound() : 寻找>x的第一个元素的位置
lower_bound(abc.begin(),abc.end(),x)
upper_bound 同理
函数返回的是迭代器,如何转成下标索引呢?减去头迭代器即可,因为迭代器 - 迭代器=两个迭代器的距离
即 lower_bound(abc.begin(),abc.end(),x)-abc.begin()
以上就可以解决很多问题了,但是当我们的数组中使用的是自己定义的结构体,可以采用下述方法:
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
typedef struct Student{
int _id; //学号
int _num; //排名
Student(int id, int num)
:_id(id)
, _num(num)
{}
}Stu;
struct CompareV{
bool operator() (const Stu& s1, const Stu& s2)// 排名升序
{
return s1._num < s2._num;
}
};
int main()
{
vector<Stu> vS = { { 101, 34 }, { 103, 39 }, { 102, 35 } };
//结构体数组在此排序,排序后才能正常使用lower_bound
auto iter = lower_bound(vS.begin(), vS.end(), Stu(200,33), CompareV());
cout << iter - vS.begin() << endl; //我们就找到了按仿函数排序(找排名比33大的位置 就是0)
system("pause");
}
如上lower_bound()就可以帮助我们解决90%二分的问题了。
另外排序结构体:
bool cmp(Stu a,Stu b){
return a._num < b._num;
}
sort(vS.begin(),vS.end(),cmp);
3.搜索
简单:DFS、BFS 拓扑排序
中等:剪枝 记忆化 爬山算法 随机增量法 模拟退火
难:A* IDA*
深搜和宽搜可以说是蓝桥杯这种比赛的利器,后面的难题不会写可以考虑(我觉得大多数人都是不会最优解法的,或者时间不够,果断暴力+剪枝)
DFS
# 树与图的深度优先遍历,树的DFS序、深度和重心
void dfs(int x){
v[x] = 1;//记录点x被访问,v是visit
for(int i = head[x];i;i = next[i]){
int y = ver[i];
if(v[y]) continue;
dfs(y);
}
}
BFS
void bfs(){
memset(d,0,sizeof(d));
queue<int> q;
while(q.size() > 0){
int x = q.front();q.pop();
for(int i = head[x];i;i = next[i]){
int y = ver[i];
if(d[y]) continue;
d[y] = d[x] + 1;//深度
q.push(y);
}
}
}
拓扑排序
对于每条边(x,y),x在A中都出现在y之前。
思想:不断选择入度为0的节点x,并把x连向的点入度减1.
拓扑排序可以判定图中是否存在环。若拓扑序列长度小于节点个数则存在环。
void add(int x,int y){//在邻接表中添加一条有向边
ver[++tot] = y,next[tot] = head[x],head[x]=tot;
deg[y]++;
}
void topsort(){ //拓扑排序
queue<int> q;
for(int i = 1;i <= n;i++){
if(deg[i] == 0) q.push(i);
}
while(q.size()){
int x = q.front();q.pop();
a[++cnt] = x;
for(int i = head[x];i;i = next[i]){
int y = ver[i];
if(--deg[y] == 0) q.push(y);
}
}
}
int main(){
cin >> n >> m;//点数,边数
for(int i = 1;i <= m;i++){
int x,y;
cin >> x >> y;
add(x,y);
}
topsort();
for(int i = 1;i <= cnt;i++){
cout << a[i] <<" ";
}
}
#计算从每个点出发能到达的点数量,用拓扑排序。再用状压f[x]=N表示能到i(i位为1)。
剪枝
剪枝这一块还是比较难的,4、5最常用
1. 优化搜索顺序(树底要小)
2. 排除等效冗余(避免重叠、混淆层次与分支,避免遍历若干棵覆盖同一状态空间的等效搜索树)
3. 可行性剪枝(无法到达边界、未来预算)
4. 最优性剪枝(当前代价超过最优解)
5. 记忆化(记录是否访问过)
4.高级数据结构
简单:并查集 分块 字典树
中等:莫队算法(树上莫队) 树状数组 二叉搜索树
难:splay树 LCT树套树 猫树 CDQ分治 舞蹈树 左偏树 后缀平衡树 KDtree 线段树 可持久化线段树 treap树 替罪羊树 块状链表
并查集
并查集是一种可以动态维护若干个不重叠的集合,并支持合并与查询的数据结构。
#路径压缩与按秩合并
每次在只需get方法的同时,把访问过的每个节点都直接指向树根
int fa[SIZE];//1.定义并查集
for(int i = 1;i <= n;i++) fa[i] = i;//2.并查集初始化;
int get(int x){//3.并查集的get操作
if(x == fa[x]) return x;
return fa[x] = get(fa[x]);//路径压缩
}
void merge(int x,int y){//4.merge合并
fa[get(x)] = get(y);
}
#扩展域 与 边带权 的并查集
用d[x]保存节点x到父节点fa[x]之间的边权,每次路径压缩后更新d值。(边带权)
int get(int x){
if(x == fa[x]) return x;
int root = get(fa[x]);//递归计算集合代表
d[x] += d[fa[x]]; //维护d数组--对边权求和
return fa[x] = root;
}
void merge(int x,int y){
x = get(x),y = get(y);
fa[x] = y,d[x] = size[y];//size数组在树根记录集合大小
size[y] += size[x];
}
字典树
#字典树
用于实现字符串快速检索
#统计前缀个数、10^5个数两两异或最大值(每次尽量查询和当前位不同的)。
int trie[SIZE][26],tot=1;//初始化,假设字符串由小写字母组成
void insert(char* str){
int len = strlen(str),p = 1;
for(int k = 0;k < len;k++){
int ch = str[k] - 'a';
if(trie[p][ch] == 0) trie[p][ch] = ++ tot;
p = trie[p][ch];
}
end[p] = true;
}
bool search(char* str){//检索是否存在
int len = strlen(str),p = 1;
for(int k = 0;k < len;k++){
p = trie[p][str[k]-'a'];
if(p == 0) return false;
}
return end[p];
}
树上 x到y路径边权异或值,等于根到x路径异或值 异或 根到y路径异或值,因为两条路重叠的边恰好抵消掉了。
树状数组
和前缀和一样维护区间和,不同点在于,前缀和维护的是静态数组,树状数组支持单点修改。
1.每个内部节点c[x]保存以它为根节点的子树中所有叶节点的和。
2.每个内部节点c[x]的子节点个数等于lowbit(x)的位数。
3.除树根外,每个内部节点c[x]的父节点是c[x+lowbit(x)]。
4.树的深度为O(logN)。
树状数组有两个基本操作
第一个是查询前缀和
int ask(int x){//1~x的和
int ans = 0;
for(;x;x -= x&-x) ans += c[x];
return ans;
}
[l,r] : ask(r)-ask(l-1)
第二个是单点增加(同时维护前缀和)
void add(int x,int y){
for(;x<=N;x += x&-x) c[x] += y;
}
初始化:add(x,a[x])
#树状数组与逆序对
集合a,用t[val]表示val在a中出现的次数,则t[l,r]区间和表示a中在[l,r]区间的数有多少个。
树状数组维护前缀和允许在集合a修改。
利用树状数组求逆序对:
1.在序列a的数值范围上建立树状数组,初始化为全0。
2.倒序扫描给定的序列a,对于每个数a[i]:
在树状数组中查询前缀和[1,a[i]-1],累加到答案ans中。
执行"单点增加"操作,即把位置a[i]上数加1(相当于t[a[i]]++),同时维护t的前缀和,表示a[i]又出现1次。
3.ans即为所求
for(int i = n;i;i--){
ans += ask(a[i]-1);
add(a[i],1);
}
注:同理可以用树状数组分别求i位置前后又多少个数比i大(小)
#树状数组的扩展应用
可以把 区间增加+单点查询 变为树状数组擅长的 单点增加+区间增加
5.动态规划
简单:线性dp(01问题,背包九讲,最长公共子序列,最长递增子序列,编辑距离,最小划分,行走问题,矩阵最长递增路径,子集和问题,矩阵链乘法,布尔括号问题)
中等:区间dp,状压dp,树形dp,计数dp,概率dp
难:插头dp,基环树dp,dp优化(数据结构优化,单调队列优化,斜率优化,分治优化,四边形不等式优化)
6.数论
简单:余数,gcd,lcm,素数判定,埃氏筛
中等:整数拆分,ExGDC,欧拉筛(线性筛),威尔逊定理,原根,费马小定理,欧拉定理,欧拉函数,整除分块,同余,逆元,高斯消元,中国剩余定理,大步小步发BSGS,积性函数,莫比乌斯反演
素数
大多时候1就够用了,还好记一点。
1.Eratosthenes筛法
任意x的倍数 2x,3x,...,[N/x]*x标记为合数。
2和3都会把6标记为合数,实际上小于x^2的x的倍数在扫描更小的数时已经被标记过了。
对于每个数x,只需要从x^2开始,把x^2,(x+1)*x,...,[N/x]*x标记为合数即可。
void primes(int n){//时间复杂O(NloglogN)
memset(v,0,sizeof(v));
for(int i = 2;i <= n;i++){
if(v[i]) continue;
cout << i << endl;//i是质数
for(int j = i;j <= n/i;j++) v[i*j] = 1;
}
}
2.线性筛法
即使是优化后(从X^2开始),Eratosthenes筛法任然会重复标记合数。12=6*2,12=4*3.
线性筛法通过 从大到小积累质因子 的方式标记每个合数,即12 = 3*2*2一种产生方式。设数组v记录每个数的最小质因子。
1.依次考虑2~N之间的每一个数i。
2.若v[i] = i,说明i是质数,把它保存下来。
3.扫描不大于v[i] 的每一个质数p,令v[i*p]=p。也就是在i的基础上累积一个质因子p。因为p<=v[i],所以p就是合数i*p的最小质因子。
每个合数i*p只会被它的最小质因子p筛一次,时间复杂度为O(N)
int v[MAX_N],prime[MAX_N];
void prime(int n){
memset(v,0,sizeof(v));//存最小质因子
m = 0;//质数数量
for(int i = 2;i <= n;i++){
if(v[i] == 0){v[i] = i;prime[++m] = i;} //i是质数
//给当前的数i乘上一个质因子
for(int j = 1;j <=;j++){
//i 有比prime[j]更小的质因子,或者超出n的范围,停止循环
if(prime[j] > v[i] || prime[j] > n/i) break;
//prime[j]是合数i*prime[j]的最小质因子
v[i*prime[j]] = prime[j];
}
}
for(int i = 1;i <= m;i++) cout << pirme[i] << endl;
}
最大公约数、最小公倍数
#最大公约数
最大公约数gcd(a,b)
最小公倍数lcm(a,b)
定理:a*b = gcd(a,b) * lcm(a,b)
更相减损术:gcd(a,b) = gcd(b,a-b) = gcd(a,a-b)
gcd(2a,2b) = 2gcd(a,b)
欧几里得算法:gcd(a,b) = gcd(b,a % b)
int gcd(int a,int b){
reutrn b ? gcd(b,a%b) : a;
}
欧几里得算法时间复杂度为O(log(a+b))。
高精度不易取模,考虑更相减损术
约数个数和约数之和
这个要先分解质因数
p是质数
如果 N = p1^c1 * p2^c2 * ... *pk^ck
约数个数: (c1 + 1) * (c2 + 1) * ... * (ck + 1)
约数之和: (p1^0 + p1^1 + ... + p1^c1) * ... * (pk^0 + pk^1 + ... + pk^ck)
组合数
递推法求组合数
// c[a][b] 表示从a个苹果中选b个的方案数
for (int i = 0; i < N; i ++ )
for (int j = 0; j <= i; j ++ )
if (!j) c[i][j] = 1;
else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
通过预处理逆元的方式求组合数 (推荐)
首先预处理出所有阶乘取模的余数fact[N],以及所有阶乘取模的逆元infact[N]
如果取模的数是质数,可以用费马小定理求逆元
int qmi(int a, int k, int p) // 快速幂模板
{
int res = 1;
while (k)
{
if (k & 1) res = (LL)res * a % p;
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
// 预处理阶乘的余数和阶乘逆元的余数
fact[0] = infact[0] = 1;
for (int i = 1; i < N; i ++ )
{
fact[i] = (LL)fact[i - 1] * i % mod;
infact[i] = (LL)infact[i - 1] * qmi(i, mod - 2, mod) % mod;
}
C[a][b] = fact[a]*infact[b]*infact[a-b]
还有一种情况是,要阶乘的数比较大,我们可以求分母的质因数分解,再求分子的质因数分解,用质因数的个数相减,就出结果的质因数分解,进而求出结果。
7.字符串
简单:字符串处理 字符串Hash
中等:KMP,后缀树,后缀数组,Manacher回文算法,最小表示法
困难:AC自动机,后缀自动机,回文自动机
字符串Hash
字符串hash比较神奇,可以解决很多问题,比如求最长回文子串,马拉车算法也不好记,而字符串Hash也可以O(n)解决;
#字符串Hash
取一固定值P,把字符串看作P进制数,分配一个大于0的数值,代表每种字符。取一固定值M,求出该P进制数对M的余数作为Hash值。一般P=131,13331。M = 2^64,即直接使用unsigned long long存储Hash。溢出时直接取模。手动取模效率低。
F[i] = F[i-1] * 131 + (S[i] - 'a' + 1);
value[l,r] = F[r] - F[l-1] * 131^(r-l+1);//所以要维护131的阶乘p[i]
字符串hash O(nlongn) 求最长回文子串
枚举中心,二分出最长的 两边Hash值相等的子串
另外Manacher算法O(n)
重要:字符串Hash不用二分,也能O(n):每次探索len时赋值为当前的ans。char[N]比string快。
8.图论
简单:图的存储(矩阵,邻接表,链式前向星),最短路(BFS)
中等:最短路 最小生成树 拓扑排序二分图匹配 差分约束 无向图的连通性 有向图的连通性 强连通分量 割点 割边 缩点 桥 分数规划 2-SAT 树的直径的重心 LCA 树链剖分 树分块 虚树
困难:网络流*