知识点整理

· · 个人记录

(长期持续更新)

一、基本算法

搜索

——基本所有题都可使用

深度优先搜索(dfs)

——大量应用于暴力

常用递归实现,伪代码:

void/*返回值*/dfs(参数){
    if(临界条件){
        return;
    }
    递归调用
}
广度优先搜索(bfs)

队列实现,注意重复入队和入队条件。

常用于拓扑排序。

迭代加深(ID)

本质上是dfs和bfs的结合

规定递归层数,超出就返回。

启发式搜索(A*)

智能算法,无法确定复杂度,速度很快。

启发式迭代加深搜索(IDA*)

顾名思义就是启发式搜索加迭代加深。

优化方法

排序

有大量排序方法,如插入排序,冒泡排序等。但多数因时间复杂度过高在OI中无人使用,下面只介绍两种常用的:

快速排序

原理是每次选一个元素,将小于它的放一边,大于它的放另一边,多次反复,直至有序。时间复杂度O(n\log n)

algorithm头文件里有sort函数可以实现,就没人手打了。而且你常数还不一定有人家优

归并排序

本质是二分。先分下去,再合并回来。有些特殊题目中比快排快。

桶排序

建值域数组,将已有元素映射,复杂度O(m) m为值域。

贪心

正确定义为利用局部最优解代替全局最优解。时间复杂度:O(n \log n)因为基本都要排序。

几乎最难的算法,正解往往不易想到且很难证明。

但是贪心骗分方法一般还是能想到的。毕竟贪心是人的本能

分治

满足小范围解和大范围解有一定规律性,且扩大范围不会影响本来小范围的解的题目可以分治。

常见使用:二分。

二分查找

对于一个有序序列进行的查找,因为有序,可以通过中间值来缩小查找范围,直至搜索到。

伪代码:

int l=最小位置,r=最大位置,mid;
while(l<=r){
    mid=(l+r)>>1;
    if(a[mid]==ans) break;
    if(a[mid]<ans) l=mid=mid+1;
    else r=mid=mid-1;
}
/*mid即最终答案*/

以上代码判断加不加等号,更新为mid还是mid\pm 1都需要具体情况具体判断。

二分答案

对于一个最优答案如果满足单调性,就可以进行二分搜索。常见词汇:最大值最小、最小值最大。

两种写法,主要是看所求的是所有相同情况中的最后一个还是最前一个。 核心是保证转移后范围包含结果

while(l<r){
    int mid=(l+r+1)>>1;
    if(check(mid)) l=mid;
    else r=mid-1;
}
while(l<r){
    int mid=(l+r)>>1;
    if(check(mid)) l=mid+1;
    else r=mid;
}

l为答案。

模拟

无需多讲,基本算法。

高精度

恶心东西。

基础为一位一压,可以改成四位一压、八位一压。

动态规划(Dynamic Programming,DP)

其实是一种解决问题思想:

通常由记忆化搜索和递推解决。

适用于之后决策不会影响之前决策(无后效性)。种类繁多,题目灵活,难度大。

可以增加维度来解决后效性的问题。

背包问题

经典dp问题。种类也很多。

描述:一个背包装物品,每种物品有一个价值和体积,在装得下的情况下求价值最大。

区间dp

每次合并的是一个区间,例:NOI1995石子合并

代码:

for(register int i=2;i<=n;++i){//枚举区间大小
    for(register int l=1;l<=len-i+1;++l){//枚举区间左端点
        int r=l+i-1;
        for(register int k=l;k<r;++k){//枚举中间合并节点
            f[l][r]=Max(f[l][r],f[l][k]+f[k+1][r]+sum[r]-sum[l-1]);//sum为前缀和数组
        }
    }
}
树形dp

利用树的节点关系进行dp。比较适合进行记忆化搜索。

DAGdp

在DAG上dp,常用拓扑排序实现。

期望dp

常用求解法是设状态时,表示为差若干步到终点,而不是从起点过多少步。

顾名思义,求解期望的dp,常与DAG、树等联系起来,或是进行数学推导。

例:Cnoi2020线形生物

S_k为从1k的期望步数。T_kk的返祖边集合。

则有:

\Large \mathbb{E}[S_k]=\mathbb{E}[S_{k-1}]+1+\frac{\sum_{i\in T_{k-1}}(\mathbb{E}[S_k]-\mathbb{E}[S_i])}{|T_{k-1}|+1}

等号两边同乘\large |T_{k-1}|+1再移项得:

\Large \mathbb{E}[S_k]=(|T_{k-1}|+1)(\mathbb{E}[S_{k-1}]+1)-\sum_{i\in T_{k-1}}\mathbb{E}[S_i]

代码:

struct edge{//建图
    int end,nxt;
}e[1000010];
int head[1000010],cnt=0,tm[1000010];
ll s[1000010];
void adde(int x,int y){
    e[++cnt].end=y;
    e[cnt].nxt=head[x];
    head[x]=cnt;
    ++tm[x];
}

int main(){
    int id=qread(),n=qread(),m=qread();
    for(re int i=1;i<=m;++i){
        int x=qread(),y=qread();
        adde(x,y);
    }
    s[1]=0;
    for(re int i=2;i<=n+1;++i){//求解
        s[i]=((tm[i-1]+1)*(s[i-1]+1))%mod;
        for(re int j=head[i-1];j;j=e[j].nxt){
            s[i]=((s[i]-s[e[j].end])%mod+mod)%mod;
        }
        s[i]%=mod;
    }
    cout<<s[n+1]<<'\n';
    return 0;
}
状态压缩dp

当dp的某一组维度状态数都很小的时候就可以压成一个维度。称之为状态压缩。

实际上,状压在搜索中也有应用。

经典例题模型:二维平面放置物品,每个物品对于其他物品有限制,一维比较短,就可以将这一维压成一个数。

如:SCOI2005互不侵犯

可以将每一行压成一个二进制数,每一个二进制位的0/1代表这一格上有无国王。然后再考虑可行情况,枚举每一列求解即可。

代码:

int num[1<<9],s[1<<9],cnt=0;
ll a[20][201][1<<9];//a[i][k][j]表示前i行有k个国王当前行状态为j时的方案数
int main(){
    int n=qread(),m=qread();
    for(re int i=0;i<(1<<n);++i){
        if(i&(i<<1)) continue;
        int sum=0;
        for(re int j=0;j<n;++j)
            if(i&(1<<j)) sum++;
        num[++cnt]=sum;
        s[cnt]=i;
    }
    a[0][0][1]=1;
    for(re int i=1;i<=n;++i)
        for(re int j=1;j<=cnt;++j)
            for(re int k=0;k<=m;++k)
                if(k>=num[j])
                    for(re int l=1;l<=cnt;++l)
                        if(!(s[j]&s[l]||(s[j]<<1)&s[l]||(s[j]>>1)&s[l]))
                            a[i][k][j]+=a[i-1][k-num[j]][l];
    ll ans=0;
    for(re int i=1;i<=cnt;++i){
        ans+=a[n][m][i];
    }
    cout<<ans<<'\n';
    return 0;
}
插头dp

将路径覆盖类似问题转化为状态压缩的dp技巧。

数位dp

与数有关的dp,一位一位考虑,n 位数就从一些 n-1 位数推过来。

注意数的范围。

dp优化

建图相关详情数据结构部分。

图的遍历

dfs:dfs序

bfs:拓扑序

欧拉路
并查集

查询合并的算法,适用范围很广,实现方法本质是连边建树。

初始化f[x]=x;

代码:

int ff(int x){//核心
    return x==f[x]?x:f[x]=ff(f[x]);
}
void add(int x,int y){//合并
    f[ff(x)]=ff(y);
}
int check(int x,int y){//查询
    return ff(x)==ff(y);
}
最短路

tarjan算法

一类算法的总称,用dfs实现,引入时间戳概念。

二分图最大匹配

二分图:两个点集,同一集合间没有边,不同集合有边相连。

二分图最大匹配:将左边的点按边匹配右边的点,求最大匹配数。

网络流

不想打了,也不太会。

树论

其实也算图论,但分出来排版舒服。

树的直径

指树上最长一条链。可以dfs求。

树的重心

指距离其他点距离和最小的点。

最近公共祖先(lca)
最小生成树
树链剖分

字符串算法

字符串哈希

将每一位字符当做一个27进制数位(或更高),求出一个大数再对大质数取模,得到结果即为这个字符串所对应的哈希值。

可能会重复出错,所以可以进行双哈希之类的操作。

Trie树

实际为数据结构,但因为字符串算法较为独立,分在这里。

KMP算法

字符串匹配算法。在一个字符串上匹配一个字符串。

manacher算法

寻找回文串算法。

AC自动机

可自动AC的算法

字符串匹配算法,在一个字符串上匹配多个字符串。

二、数据结构

基础数据结构,先进后出。

单调栈

栈中元素单调。解决最优问题。

队列

基础数据结构,先进先出。

单调队列

队中元素单调。常用来解决滑动窗口、区间最优问题。

优先队列(堆)

实际是堆,常使用STL的priority_queue,见STL部分。

前缀和

区间查询数据结构,修改复杂度O(n)所以一般不修改。

递推实现:\large sum_i=sum_{i-1}+a_i,则查询l-r区间和即为\large sum_r-sum_{l-1}

查询复杂度:O(1)

差分

区间修改单点查询数据结构,差分数组\large d_i=a_i-a_{i-1}

l-r位置的值加上x,只需要\large d_l+x\large d_{r+1}-x就行了。复杂度O(1)

实际上前缀和和差分的修改和查询复杂度都不是均摊的,这就引出了下面的数据结构。

树状数组

单点修改区间查询数据结构,满足区间可减性题目可用,常数小,代码短。

修改:O(\log n)

查询:O(\log n)

可以利用差分变成单点查询区间修改。

代码:

int lwbit(int x){//求一个数二进制中最右边的1的值,核心代码。
    return x&(~x+1);
}
void update(int p,int x){//将p位置加上x
    while(p<=n){
        t[p]+=x;
        p+=lwbit(p);
    }
}
int srch(int p){//查询到p的前缀
    int sum=0;
    while(p>0){
        sum+=t[p];
        p-=lwbit(p);
    }
    return sum;
}
//求l到r区间即求srch(r)-srch(l-1)

线段树

支持多种操作的强大数据结构,修改和查询均为O(\log n)但常数较大,代码较长。一般递归实现。

一般为单点修改区间查询,可以利用差分变成单点查询区间修改。

具体而言,实现的功能取决于存储什么量和下放。

模板(以加乘为例):

#define ls p<<1
#define rs p<<1|1
//宏定义,加大可读性
struct Tree{
    int val;//值
    int lta,ltm;//lazytag
}t[100010<<2];//注意要开4倍空间
void pushup(int p){//上放
    t[p].val=t[ls].val+t[rs].val;
}
void pushdown(int u,int v,int p){//下放,重点
    t[ls].ltm*=t[p].ltm;
    t[rs].ltm*=t[p].ltm;
    t[ls].lta*=t[p].ltm;
    t[rs].lta*=t[p].ltm;
    t[ls].val*=t[p].ltm;
    t[rs].val*=t[p].ltm;
    t[p].ltm=1;
    int mid=(u+v)>>1;
    t[ls].val+=(mid-u+1)*t[p].lta;
    t[rs].val+=(v-mid)*t[p].lta;
    t[ls].lta+=t[p].lta;
    t[rs].lta+=t[p].lta;
    t[p].lta=0;
}
void build(int u,int v,int p){//建树
    t[p].ltm=1;
    if(u==v){
        t[p].val=a[u];
        return;
    }
    int mid=(u+v)>>1;
    build(u,mid,ls);
    build(mid+1,v,rs);
    pushup(p);
}
void upda(int l,int r,int u,int v,int p,int x){//加
    if(l<=u&&r>=v){
        t[p].val+=(v-u+1)*x;
        t[p].lta+=x;
        return;
    }
    int mid=(u+v)>>1;
    pushdown(u,v,p);
    if(l<=mid) upda(l,r,u,mid,ls,x);
    if(r>mid) upda(l,r,mid+1,v,rs,x);
    pushup(p);
}
void updm(int l,int r,int u,int v,int p,int x){//乘
    if(l<=u&&r>=v){
        t[p].val*=x;
        t[p].lta*=x;
        t[p].ltm*=x;
        return;
    }
    int mid=(u+v)>>1;
    pushdown(u,v,p);
    if(l<=mid) updm(l,r,u,mid,ls,x);
    if(r>mid) updm(l,r,mid+1,v,rs,x);
    pushup(p);
}
int srch(int l,int r,int u,int v,int p){//查询
    if(l<=u&&r>=v){
        return t[p].val;
    }
    int mid=(u+v)>>1;
    int ans=0;
    pushdown(u,v,p);
    if(l<=mid) ans+=srch(l,r,u,mid,ls);
    if(r>mid) ans+=srch(l,r,mid+1,v,rs);
    return ans;
}

建图法

邻接矩阵

常用于floyd。

链式前向星

最常用。

ST表

倍增思想的应用。只能求区间最值等可重区间的量的数据结构。

完全可以被线段树代替,但是代码短。

代码:

平衡树

分为两大类(基本上):splay和treap

splay

常用双旋splay,长,但能维护的东西多,是Link-Cut Tree 的基础。

treap

代码相对短,同时满足二分查找树和堆的性质。

非旋 treap(fhq treap) 代码短,挺好学的,将旋转操作改为使用分裂与合并解决。

STL

c++大杀器。

一般使用STL时都会使用已经封装好的数据类型。

sort

用法:sort(.begin,.end,比较函数)

快排,速度很快。

nth_element

用法 :nth_element(开始位置,目标位置,结束位置后一个,比较函数)

将结束位置到开始位置排列后在目标位置的元素放到目标位置,复杂度O(n)

priority_queue

实际上是堆,但是比一般手打堆快。(内部是红黑树)

默认是大根堆。改成小根堆:priority_queue<type,vector<type>,greater<type> >q;

成员函数:

map

支持c++14后,可以直接使用unordered_map,常数很小,基本等于O(1)。但空间占用大,头文件:unordered_map。

映射,map<type,type>。常数大,但方便使用。

可以直接用类似数组的方法访问。

成员函数:

set/multiset

也可使用unordered_set

只保存关键字,集合,有序存储。

set去重,multset不去重。

pair

两个量组成的数据单位。

pair<type,type> name定义。

成员变量:

pair先按第一个值排序,再按第二个值排序。

bitset

卡空间的神。还支持位运算。

直接当一个二进制数或只有0/1的数组使就行。

三、数学

主要偏向数论、组合、线性代数。

数论

经常出结论题,一般只有100分和50分暴力两档。

只讨论整数。

整除与同余
快速幂
ll qpow(ll x,int k){
    ll sum=1;
    while(k){
        if(k&1) sum*=x;
        x*=x;
        k>>1;
    }
    return sum;
}
质数(素数)
最大公约数(gcd)和最小公倍数(lcm)

这就引出了:

逆元

组合计数

前置知识
  1. 阶乘(\large!):\large n!=n\cdot (n-1)\cdot(n-2)\cdots2\cdot1
排列数和组合数