八大板块算法讲解(自用)

1.搜索

DFS(深度优先搜索)

DFS

对于每一个点,枚举其连着的边,再到此点重复操作,直到无法继续遍历。

if(当前点所连边数=0){
	(按题目要求);
	返回;
}
for(0~当前点所连边条数)
	if(该边所连点不等于上各点) dfs(所连点,当前点);

IDDFS

与dfs不同的是,每次深搜都会有搜索的最大深度限制,如果没有找到解,那么就增大深度,再进行深搜,如此循环直到找到解为止,这样可以找到最浅层的解。

while(!dfs(cnt))
     cnt++;

BFS(广度优先搜索)

BFS

从起始节点开始,依次遍历当前节点的所有邻居节点,然后再依次遍历邻居节点的所有邻居节点,直到遍历到目标节点或者遍历完所有节点。

将起点推入队列中;
将起点标识为已走过;
while(队列非空){
  取队列首节点vt,并从队列中弹出;
  探索上面取出得节点的周围是否有没走过的节点vf,如果有将所有能走的vf的parents指向vt,并将vf加入队列(如果vf等于终点,说明探索完成,退出循环);
}

双向BFS

同时从两个方向BFS,一旦搜索到相同的值,意味着找到了一条联通起点和终点的最短路径。

剪枝

顾名思义,就是通过一些判断,砍掉搜索树上不必要的子树。这些子树可能是不可达的,也可能是可达但显然不是最优的,去掉它们对最终答案没有影响,所以我们称为“剪枝”。

把常用的剪枝分成以下两类。 1.可行性剪枝。 2.最优性剪枝。
用队列记录当前所连的点,分层重复此操作,知道所有点被访问过为止。

字符串

字符串哈希

如果我们要比较一个字符串, 我们不直接比较字符串, 而是比较它们对应映射的数字, 这样子就知道两个"子串"是否相等. 从而达到子串的 \(Hash\) 值的时间为 \(O(1)\), 进而可以利用"空间换时间"来节省时间复杂度。

#define base 233
unsigned long long hash(char s[]){
    long long ans=0,len=strlen(s);
    for(long long i=0;i<len;i++)
        ans=base*ans+(ull)s[i];
    return ans;
}

KMP

主旨是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()数组实现,数组本身包含了模式串的局部匹配信息。

inline void get_nxt(){ //nxt数组是从S[0到i-1]前子串的前缀后缀最大值
    int t1=0,t2;
    nxt[0]=t2=-1;
    while(t1<len2) 
        if(t2==-1||s2[t1]==s2[t2]) nxt[++t1]=++t2;
        else t2=nxt[t2];
}
inline void KMP(){
    int t1=0,t2=0;
    while(t1<len1){
        if(t2==-1 || s1[t1]==s2[t2])
            t1++,t2++;
        else t2=nxt[t2];
        if(t2==len2) printf("%d\n",t1-len2+1),t2=nxt[t2];
    }
}

字典树

先放一张图
image

  • 根节点没字符,其余每一个子节点都包含一个字符
  • 从根节点到某一节点。路径上经过的字符连接起来,就是该节点对应的字符串
  • 每个节点的所有子节点包含的字符都不相同
    用于统计,排序和保存大量的字符串(不仅限于字符串),经常被搜索引擎系统用于文本词频统计。

数论

同余

给定一个正整数 \(m\),如果两个整数 \(a\)\(b\) 满足 \(a-b\) 能够被 \(m\) 整除,即 \((a-b)\div m\) 得到一个整数,那么就称整数 \(a\)\(b\) 对模 \(m\) 同余,记作 \(a≡b(mod\ m)\)。对模 \(m\) 同余是整数的一个等价关系。
定理
1.同余式相加:若 \(a≡b(mod\ m)\)\(c≡d(mod\ m)\),则 \(a+c≡b+d(mod\ m)\)
2.同余式相乘:若 \(a≡b(mod\ m)\)\(c≡d(mod\ m)\),则 \(a\times c≡b\times d(mod\ m)\)
3.线性运算:如果 \(a≡b(mod\ m)\)\(c≡d(mod\ m)\),那么(1) \(a ± c ≡ b ± d (mod\ m)\);(2) \(a * c ≡ b * d (mod\ m)\)
4.除法:若 \(a\div c≡b\div c(mod\ m)\ \ c≠0\)\(a≡b(mod\ m\div gcd(c,m))\) 其中 \(gcd(c,m)\) 表示 \(c,m\) 的最大公约数。特殊地 , \(gcd(c,m)=1\)\(a≡b(mod\ m)\)
5.幂运算:如果 \(a≡b(mod\ m)\) 那么 \(a^n≡b^n(mod\ m)\)
6.若 \(a≡b(mod\ m)\)\(n|m\) ,则 \(a≡b(mod\ n)\)

欧拉函数

定义:

对于一个正整数 \(n\),欧拉函数 \(φ(n)\) 表示小于等于 \(n\) 的正整数中与 \(n\) 互质的数的个数。
特殊的,当 \(n\) 为质数,\(φ(n)\ =\ n-1\)

费马小定理

有两个整数 \(n,m\),如果其互质,那么 \(n\)\(m-1\) 次幂对 \(m\) 取模与 \(1\) 恒等于。
如果要分数取模就要用乘法逆元,即 \(a\div b\) 对质数 \(p\) 取模就是 \(a\times b^{\ (p-2)\ }mod\ p\)

动态规划

背包

01背包

定义:\(f[i][v]\) 表示前 \(i\) 件物品恰放入一个容量为 v 的背包可以获得的最大价值。
转移方程:f[i][v]=max(f[i-1][v],f[i-1][v-c[i]]+w[i])。

for(1~n,i)
    for(V~0,v)
        f[v]=max{f[v],f[v-c[i]]+w[i]};

完全背包

定义:\(f[i][v]\) 表示前 \(i\) 种物品恰放入一个容量为 \(v\) 的背包的最大权值。
转移方程:f[i][v]=max(f[i][v-c[i]]+w[i],f[i-1][v]);

for(1~n,i)
    for(0~V,v)
        f[v]=max{f[v],f[v-c[i]]+w[i]};

混合背包

即先判断是哪种背包,再进行对应操作

for(1~n,i)
    if第i件物品是01背包
        for(V~0,v)
            f[v]=max{f[v],f[v-c[i]]+w[i]};
    else if第i件物品是完全背包
        for(0~V,v)
            f[v]=max{f[v],f[v-c[i]]+w[i]};

线性dp

精髓

主打的就是个随机应变,码前先想好状态和转移方程,其他的轻而易举。

例题1 最大连续子序列和

题目:给定一个序列:\(S_1,S_2,S_3…S_n\),有 \(i,j\) 使得 \(sum=S_i+S_{i+1}…+S_{j-1}+S_j\) 最大,求最大值。

思路:首先先明确好状态:我们设数组 \(dp[n]\) 当中存放的是以当前的 \(i\),即 \(dp[i]\) 是以 \(S_i\) 为结尾的最大连续子列的和。
那么对于每个 \(dp[i]\) 会有两种情况:

  • \(S_i\) 放进:dp[i]=dp[i-1]+S[i];
  • 不将 \(s_i\) 放进:dp[i]=S[i];
    显然,对于每个 \(i(1\sim n)\)\(dp[i]\) ,在两种情况中取大的,其次注意边界:dp[1]=S[1];

例题2 最长不下降子序列

题目:给定一个序列:\(S_1,S_2,S_3…S_n\),找到一个最长的子序列,可以是不连续的,使得其是不下降的序列(非递减的),求此序列的长度。

思路:\(dp[i]\) 表示的是当前点所在子序列的左端点编号,先展示一下核心代码:

for(int i=1;i<=n;i++){
	dp[i]=1;      //注意边界!
	
	for(int j=1;j<i;j++)
		if(S[i]>=S[j]&&dp[j]+1>dp[i]) dp[i]=dp[j]+1;    //从第一个开始依次看是否有比自己小的,有才可以加进去
}

为什么这道题是两层循环不像上题呢?举个例子:
一个序列 \(S\ ={1, 1, 4, 1, 1, 4 }\)。按照上一题:对于每一个 \(i\),只考虑 \(i-1\) 的话,答案是 \(3\),但注意题目是 可以是不连续的,这样的话答案是 \(5\),为什么错了呢?因为我们枚举到 \(S_4\) 的时候我们发现 \(S_4<S_3\),于是 dp[4]=4;,但因为不连续,所以我们可以从 \(1\sim i-1\) 枚举,所以故得出答案。


还有很多经典例题,随机应变是脱离不了的,但状态与转移方程式是绝对少不了的!

区间dp

精髓

正所谓 区间 DP ,就是在区间上进行 \(DP\) 。区间 \(DP\) 以区间的长度划分阶段,记录两个端点的坐标,通过合并小区间的最优解来求出大区间的最优解。

例题1 石子合并

题目:\(n\) 堆石子摆成一个环,每次选相邻的 \(2\) 堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。求:将 \(n\) 堆石子合并成 \(1\) 堆的最小得分和最大得分。

思路:先考虑最小的分:先假设石子的摆放并不是环形,而是一条直线。首先,会想到要将第 \(l\) 堆石子和第 \(r\) 堆石子合并就要先将第 \(l\sim r\) 堆石子全部合并:设 \(dp[l][r]\) 为合并第 \(l\sim r\) 堆石子的最小的得分,假设区间 \(l\sim r\) 最后一次合并的两区间是 \(l\sim k-1\)\(k\sim r\) ,则有状态转移方程:dp[l][r]=dp[l][k-1]+dp[k][r];


树形DP

精髓

有些问题,我们从根节点出发,向子节点做深度优先搜索,对于树上的每个节点(除根节点外),由父节点的信息(父节点合并后的信息,除去该孩子的信息,就是其与孩子的信息)更新该节点的信息。

例题1 树上动态规划

题目:给出一个 \(n\) 个节点的树,找出一个节点为根,使得树上所有节点的深度之和最大。

思路:dfs一遍,由子节点信息得到整棵树以1为根节点时,以x为根的子树的节点数量(\(size[x]\))和整棵树以1为根节点时,以x为根的子树的所有节点的深度之和(\(f[x]\))。再dfs一遍,由父节点信息得到整棵树以x为根时所有节点的深度之和(\(ans[x]\)),求出最大值 \(O(n)\)

DP核心部分:

void dp(int u,int fa){
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa) continue;
		ans[v]=ans[u]+n-2*size[v];
		dp(v,u);
	}
}

例题2 树上01背包问题

题目:给出一棵有 \(n\) 个点的有根树,根节点的编号为1,初始的时候,树上所有边都没有被通,而每通一条边都需要一定的能量。每个点都只有 \(m\) 点能量,并且只能用来打通其和儿子之间的边,求最多有多少个点和根节点联通。

思路:\(dp[i]\):表示以 \(i\) 为根节点的子树最多能有多少个和 \(i\) 联通,那么可以把 \(u\) 的每个子节点 \(v\) 都看成一个物品,花费是打通 \(u,v\) 这条边的花费,而价值就是dp[v],所以求解每个点的dp值就成了一个01背包问题。

核心部分:

void dfs(int u,int fa){
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa)continue;
		dfs(v,u);
	}
    memset(f,0,sizeof(f));
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		int cost=e[i].cost;
		int val=dp[v];
		if(v==fa)continue;
		for(int j=m;j>=cost;j--){
			f[j]=max(f[j],f[j-cost]+val);
		}
	}
	dp[u]=f[m]+1;
}

图论

连通性

tanjan求强连通分量

首先我们需要两个变量:

  1. \(dfn_u\):深度优先搜索遍历时结点 \(u\) 被搜索的次序。
  2. \(low_u\):在 \(u\) 的子树中能够回溯到的最早的已经在栈中的结点。设以 \(u\) 为根的子树为 \(\textit{Subtree}_u\)\(\textit{low}_u\) 定义为以下结点的 \(\textit{dfn}\) 的最小值:\(\textit{Subtree}_u\) 中的结点;从 \(\textit{Subtree}_u\) 通过一条不在搜索树上的边能到达的结点。
    一个结点的子树内的所有节点的 \(dfn\) 都大于该结点的 \(dfn\);从根开始的一条路径上的 \(dfn\) 严格递增,\(low\) 严格非降。
    顺序按深度优先搜索算法依次搜索,每次搜索维护当前节点的 \(dfn\)\(low\),然后让此节点入栈,如果栈里已有此数,像小时候的拖拉机游戏:
    image
    像这样,我们就把2,3牌收入囊中,即弹出栈。
    最后当我们从节点 \(u->v\) 时,会有三种情况:
  3. v到过,且在栈中,我们就用 \(dfn_v\) 去更新 \(low_u\)
  4. v到过,不在栈中,这时说明已经处理过当前节点了,直接跳过。
  5. v没到过,继续对v作深度优先搜索,回溯时还要用 \(low_v\) 更新 \(low_u\),因为u能到v所以v能回溯到的节点u一定也行。
int dfn[MAXN], tot = 0;
bool instack[MAXN];
int low[MAXN];
std::stack<int> stk;
void dfs(int u){
    dfn[u]=++tot;
    low[u]=dfn[u]; // 一开始low[u]是自己,有后向边再更新
    stk.push(u);
    instack[u]=true;
    for(int e=first[u];e;e=nxt[e]){
        int v=go[e];
        if(!dfn[v]){
            dfs(v);
            low[u]=min(low[u],low[v]); // 子节点更新了,我也要更新
            // 若子节点没更新,则min能够保证low[u] == dfn[u]
        }
        else if(instack[v]) // v访问过且在栈中,意味着u→v是后向边
            low[u]=min(low[u],dfn[v]); // 此处用min的原因是u→v可能是前向边,此时dfn[v]>dfn[u]
    }
    stk.pop();
    instack[u]=false;
}

讲到这,顺带讲下tarjan染色

if(low[u]==dfn[u]) // 是SCC中的第一个被访问的节点
    {
        co[u]=++col;
        while(stk.top()!=u) co[stk.top()]=col,instack[stk.top()]=false,stk.pop();
            // 染色,弹栈
        instack[u]=false;
        stk.pop(); // 最后把u弹出去
    }

板子

求割点

定义:对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点。
先想到枚举每一个点,我们将他删掉后再判连通性,显然这样复杂度不优,我们又会想到我们刚刚所讲的tarjan中的 \(dfn\)\(low\),我们依然dfs,如果当前节点 \(low_v \geq dfn_u\),即无法回到祖先,那么 \(u\) 为割点。
板子

求割边

定义:对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边。
和割点一样,只要改一处:\(low_v>dfn_u\) 就可以了。
板子

拓扑排序

暂无

欧拉图

定义:

  • 欧拉回路:通过图中每条边恰好一次的回路
  • 欧拉通路:通过图中每条边恰好一次的通路
  • 欧拉图:具有欧拉回路的图
  • 半欧拉图:具有欧拉通路但不具有欧拉回路的图
    Hierholzer求欧拉回路:
    从一条回路开始,每次任取一条目前回路中的点,将其替换为一条简单回路,以此寻找到一条欧拉回路。如果从路开始的话,就可以寻找到一条欧拉路。

最短路

floyd

直接贴了,没啥说的。。。

for (k = 1; k <= n; k++)
  for (x = 1; x <= n; x++)
    for (y = 1; y <= n; y++)
      f[k][x][y] = min(f[k - 1][x][y], f[k - 1][x][k] + f[k - 1][k][y]);

dijkstra

每次操作选择点 \(u\) 可以到达的代价最小的点,一般用单调队列优化,依次弹出单调队列中的第一个,然后用一个dis[v]数组表示1到u的最小距离,然后我们用式子dis[v]=dis[u]+w[v];,其中w数组只权值,这样就有一个 \(O(nlogn)\) 的算法求最短路了。
板子

#include<bits/stdc++.h>
using namespace std;
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > > q;
struct node{
	int to,w,nxt;
}e[200005];
int dis[100005],head[100005],n,m,s,cnt;
void add(int u,int v,int d){
    cnt++;
    e[cnt].w=d;
    e[cnt].to=v;
    e[cnt].nxt=head[u];
    head[u]=cnt;
}
void dij(){
	memset(dis,0x3f3f3f3f,sizeof(dis));
	dis[s]=0;
	q.push({0,s});
	while(!q.empty()){
		int d=q.top().first,u=q.top().second;
		q.pop();
		if(d!=dis[u]) continue;
		for(int i=head[u];i;i=e[i].nxt){
			int v=e[i].to;
			if(dis[u]+e[i].w<dis[v]){
				dis[v]=dis[u]+e[i].w;
				q.push({dis[v],v});
			}
		}
	}
}
int main(){
	cin>>n>>m>>s;
	for(int i=1;i<=m;i++){
		int u,v,d;
		cin>>u>>v>>d;
		add(u,v,d);
	}
	dij();
	for(int i=1;i<=n;i++)
		cout<<dis[i]<<" ";
	return 0;
}

但因为我们每次选的最小所以我们不能处理有负值的图。

SPFA

接上回:虽然dij有优秀的复杂度,但他不能处理有负值的图,这时我们就要用一个比较玄学的算法了:
关于SPFA:他死了
为什么会死?首先你要知道SPFA的过程:

  1. 建立一个队列,初始时队列里只有起始点,在建立一个表格记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)
  2. 执行松弛操作,用队列里有的点去刷新起始点到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。
  3. 重复执行直到队列为空。
    在稠密图中他会可能退化到 \(n^2\)

生成树

我们定义无向连通图的最小生成树为边权和最小的生成树。
Prim算法:
贪心策略:每次选择到下一个点权最小的边,来个例子:
image
image
真正实现其实跟楼上dij很像,只是有一个地方存在差别,就是dis[]数组记录的含义是不一样的,dij算法中的是源点到其余顶点的最短距离,而这里的是顶点v到集合S之间的距离。

线性代数

单位矩阵

就像数字 \(1\times x=x\) 一样,设一个矩阵A和一个单位矩阵B,那么 \(A\times B=A\),一句话:一个矩阵乘以另一个矩阵仍是这个矩阵,那么另一个矩阵就是单位矩阵。
求法:暴力枚举每一个点,当当前横排号与竖排号相等时标号为1,否则为0。

矩阵乘法

首先我们要了解矩阵乘法的方法:设矩阵A乘以矩阵B得到矩阵C,就是A阵的第i行和B阵的第j列的元素两两相乘再相加,举个例子:
image
本例中,结果矩阵第2行第3列的元素值为49,它通过下列计算而得:4×4+3×11=49
求的话枚举即可。

快速幂

快速幂,顾名思义是快速计算指数的值的例如 \(2^{1000000000}\),如果一次一次 \(\times 2\) 的话显然会T,拿 \(2^{10}\) 举例子:
\(4=2^2\)
\(16=4^2=2^4\)
\(64=8^2=4^4=2^8\)
……
\(2^n=4^{\tfrac{n}{2}}=8^{\tfrac{n}{4}}=……\)
这样复杂度是不是就低了很多?
那就贴板子了:

long long fpow(long long a,long long b){//a是底数,b是指数 
	long long ans=1;
	while(b){//当指数不为0时执行
		if(b%2==0){//指数为偶数时,指数除以2,底数乘以2
			b/=2;
			a*=a; 
		}else{//指数为奇数时,分离指数,ans乘以底数
			ans*=a; 
			b--;
		}
	} 
	return ans;
}

这是精简:

long long fpow(long long a,long long b){
	long long ans=1;
	while(b){
		if(b&1)ans*=a;
		b>>=1;
		a*=a;
	} 
	return ans;
}

数据结构

栈(stack)

栈是一种先进后出的数据结构,用一张图表示:
image
基本操作:

代码 含义
stk.push() 压栈,增加元素
stk.pop() 移除栈顶元素
stk.top() 取得栈顶元素(但不删除)
stk.empty() 检测栈内是否为空,空为真
stk.size() 返回stack内元素的个数

队列(queue)

队列

队列是一种先进先出的数据结构,用一张图表示:
image
基本操作:

代码 含义
q.front() 返回队首元素
q.back() 返回队尾元素
q.push() 尾部添加一个元素副本 进队
q.pop() 删除第一个元素
q.size() 返回队列中元素个数,返回值类型unsigned int
q.empty() 判断是否为空,队列为空,返回true

优先队列(priority_queue)

队列中的元素按照一定的优先级进行排序和访问。
priority_queue<int,vector<int>, greater<int> > pq; 降序。
priority_queue<int, vector<int>, less<int> >pq; 升序。
操作跟普通队列几乎一样。

ST表

前置芝士:倍增
顾名思义,倍增就是不停地翻倍。它能够使线性的处理转化为对数级的处理,大大地优化时间复杂度。
ST表就是运用了倍增思想的一个数据结构,主要解决区间最大值/最小值查询,\(O(nlogn)\) 预处理,\(O(1)\) 查询,但缺点是不能修改。
ST表使用的是一个二维数组 \(f[i][j]\) 来表示区间 \([i,\ i+2^j-1]\) 的答案,就拿求最大值举例子。
显然的 \(f[i][0]=a_i\),根据定义,我们有转移方程式 \(f[i][j]=max(f[i][j-1],f[2+i^{j-1}][j-1])\)

未完待续~~~

posted @ 2023-09-10 15:25  hn小曜仔  阅读(24)  评论(0编辑  收藏  举报