DFS与BFS
DFS
与BFS
都是用来搜索的常用方法,但是两者有不同点,DFS
更适合层层深入的情况,但是DFS
遇到分支很多的时候往往非常慢甚至会迷路,而BFS
更适合找全局最优解,但是BFS
面对层次很深的情况很慢很慢,因此在平常使用时就要判断使用DFS
还是BFS
。
DFS
分治
分治法适用的情况
分治法所能解决的问题一般具有以下几个特征:
该问题的规模缩小到一定的程度就可以容易地解决
该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
利用该问题分解出的子问题的解可以合并为该问题的解;
该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;
第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;、
第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。
第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
分治法的基本步骤
分治法在每一层递归上都有三个步骤:
step1 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
step2 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
step3 合并:将各个子问题的解合并为原问题的解。
可使用分治法求解的一些经典问题
二分搜索
基本的二分搜索
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| int binarySearch(int[] nums, int target) { int left = 0; int right = nums.length - 1;
while(left <= right) { int mid = (right + left) / 2; if(nums[mid] == target) return mid; if (nums[mid] < target) left = mid + 1; if (nums[mid] > target) right = mid - 1; } return -1; }
|
寻找左侧边界的二分搜索
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| int left_bound(int[] nums, int target) { if (nums.length == 0) return -1; int left = 0; int right = nums.length;
while (left < right) { int mid = (left + right) / 2; if (nums[mid] == target) right = mid; if (nums[mid] < target) left = mid + 1; if (nums[mid] > target) right = mid; } return left; }
|
寻找右侧边界的二分查找
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int right_bound(int[] nums, int target) { if (nums.length == 0) return -1; int left = 0, right = nums.length;
while (left < right) { int mid = (left + right) / 2; if (nums[mid] == target) left = mid + 1; if (nums[mid] < target) left = mid + 1; if (nums[mid] > target) right = mid; } return left - 1;
|
大整数乘法
Strassen矩阵乘法
棋盘覆盖
合并排序
快速排序
线性时间选择
最接近点对问题
循环赛日程表
汉诺塔
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <iostream> #include <stdlib.h> using namespace std; static int count = -1; void move(char x,char y); void hanoi(int n,char one,char two,char three); int main() { int m; cout<<"请输入一共有多少个板子需要移动"; cin>>m; cout<<"以下是"<<m<<"个板子的移动方案:"<<endl; hanoi(m,'A','B','C'); system("pause"); return 0; } void hanoi(int n,char one,char two,char three) { if(n==1) move(one,three); else { hanoi(n-1,one,three,two); move(one,three); hanoi(n-1,two,one,three); } } void move(char x,char y) { count++; if( !(count%5) ) cout<<endl; cout<<x<<"移动至"<<y<<" "; }
|
并查集
基本信息
并查集是一种将拥有某种联系的一些节点归类的算法,其类似与人们的亲属关系,如果我们的祖先是同一个人,那么我们是同一类人,并查集也是这个意思,利用祖先的编号关系来区分或者合并两个节点。
详细知识
初始化
对于并查集,我们创建一个数组id[maxn]
来表示每一个节点的祖先节点,那么问题来了,没有祖先的,或者自己就是祖先,怎么办呢。
对于这种情况,我们将其祖先设置为他自己,因此第一步,先令每个人都是一个单独的节点,也就是每个人都没有祖先。
1 2 3 4
| int id[maxn]; inline void init(int n){ for(int i = 1;i <= n;i++) id[i] = i; }
|
查找祖先
初始化完成之后,那么对于两个点,怎么来找他们的祖先节点呢,这里使用find函数
1 2 3 4
| inline int find(int p){ if(p == id[p]) return p; return find(id[p]); }
|
在这里我们可以小小优化一下,一点小小的操作可以让这个查询操作时间减很多,那就是让他的父亲节点直接储存他的祖先
1 2 3 4
| inline int find(int p){ if(p == id[p]) return p; return id[p] = find(id[p]) }
|
合并
现在初始化完成了,查找祖先也有了,就只剩下有关系的节点合并了
1 2 3 4 5 6 7 8 9
| inline void funion(int p, int q){ int rootp = find(p); int rootq = find(q); if(rootp == rootq) return; id[rootp] = rootq; cnt--; }
|
模板题
P3367 【模板】并查集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #include<iostream> using namespace std; const int maxn = 1e4 + 5; int id[maxn]; inline void init(int n){ for(int i = 1;i <= n;i++) id[i] = i; } inline int find(int p){ if(p == id[p]) return p; return id[p] = find(id[p]); } inline void funion(int p, int q){ int rootp = find(p); int rootq = find(q); if(rootp == rootq) return; id[rootp] = rootq; } int main(void) { int n, m; cin>>n>>m; init(n); while(m--){ int z,x,y; cin>>z>>x>>y; if(z == 1){ funion(x, y); }else{ if(find(x) == find(y)) cout<<"Y"<<endl; else cout<<"N"<<endl; } } return 0; }
|
over!
排序
拓扑排序
在图论中,拓扑排序(Topological Sorting)是一个有向无环图
(DAG, Directed Acyclic Graph)的所有顶点的线性序列
。且该序列必须满足下面两个条件:
每个顶点出现且只出现一次。
若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。
有向无环图(DAG)才有拓扑排序,非DAG图没有拓扑排序一说。
百度百科定义:
对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序
(Topological Order)的序列,简称拓扑序列
。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序
。
转发一大佬的例子
这五种都是可以选择的方案,都是拓扑排序,只是选择的策略不同。因此,拓扑排序可以存在多个。
生成拓扑排序的方法一般为:
1.从图中选择入度(以其为终点的边的数量)为0的顶点,输出
2.删除顶点,删除以其为起点的边
3.重复以上步骤,直到无顶点
或当前所有顶点的入度都不为0
(有环)
简易版算法
1 2 3 4 5 6 7 8 9 10 11
| for(int i = 0;i < n;i++) for(int j = 0;j < n;j++){ if(in[j] == 0){ now = j; ans[cnt++] = j; in[j]--; break; } for(int j = 0;j < n;j++) if(a[now][j] != 0) in[j]--; } for(int i = 0;i < n;i++)cout<<ans[i]<<" ";
|
升级版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| queue<int>q; vector<int>edge[n]; for(int i = 0;i < n;i++) if(in[i] == 0) q.push(i); vector<int>ans; while(!q.empty()){ int p = q.front(); q.pop(); ans.push_bach(p); for(int i = 0;i < edge[p].size();i++){ int y = edge[p][i]; in[y]--; if(in[y] == 0)q.push(y); } } if(ans.size() == n) for(int i = 0;i < n;i++) cout<<ans[i]<<" "; else cout<<"WA!";
|
lca
基本信息
lca
是在树上面寻找两节点的最近共同祖先。
例如对于上面例子,13
与 12
的最近共同祖先是3。
那么怎么找呢,如果一个一个找,12 -> 9 -> 6 -> 3
,13 -> 10 -> 7 -> 3
,这明显不行,直接T飞,那么有没有方法可以一次找很多个呢。直接次方找不香么,算法复杂度直接降维打击O(lgN)
,这就是lca算法
。
详细知识
dfs初始化
那么怎么做到呢,因为要跳着找,因此第一步,需要找到每个节点的深度depth
。这里用的是链式前向星存图。
1 2 3 4 5 6 7
| int depth[maxn]; void dfs(int now, int fath){ depth[now] = depth[fath]+1; for(int i = head[now];i;i = edge[i].next){ if(edge[i].to != fa) dfs(edge[i].to, now). } }
|
然后在这里面可以找到每个节点的父亲节点,我们只用储存其中的部分父亲节点,因此开一个fa[maxn][21]
来储存当前节点的21个父亲,分别代表的是第2^1
,2^2
,2^3
,2^4
,…,2^21
个父亲节点。
此处我们先对数据进行一个优化,建立一个lg[maxn]
数组,来储存每个数字的lg值。要加一是因为lg 1 = 0
,但是很明显,1也是他的父亲。
1 2 3 4
| int lg[maxn]; for(int i = 1;i <= n;i++){ lg[i] = lg[i-1] + (1 << lg[i-1] == i); }
|
然后来计算fa
数组
1 2 3 4 5 6 7
| int fa[maxn][21]; void dfs(int now, int fath){ fa[now][0] = fath; for(int i = 1;i < lg[depth[now]];i++){ fa[now][i] = fa[fa[now][i-1]][i-1]; } }
|
这样就有了完整的dfs
代码来初始化数据。
1 2 3 4 5 6 7 8 9 10 11
| int fa[maxn][21], depth[maxn]; void dfs(int now, int fath){ fa[now][0] = fath; depth[now] = depth[fath] + 1; for(int i = 1;i < lg[depth[now]];i++){ fa[now][i] = fa[fa[now][i-1]][i-1]; } for(int i = head[now];i;i = edge[i].next){ if(edge[i].to != fath) dfs(edge[i].to, now); } }
|
lca主查询
先上代码吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| int lca(int x, int y){ if(depth[x] < depth[y]) swap(x, y); while(depth[x] > depth[y]){ x = fa[x][lg[depth[x] - depth[y]] - 1]; } if(x == y){ return x; } for(int k = lg[depth[x] - 1];k>=0;k--){ if(fa[x][k] != fa[y][k]){ x = fa[x][k]; y = fa[y][k]; } } return fa[x][0]; }
|
此处重点讲两处
1 2 3
| while(depth[x] > depth[y]){ x = fa[x][lg[depth[x] - depth[y]] - 1]; }
|
这里联系2进制来理解较好。2进制yyds。
对于两个节点A
,B
,深度分别为19
,2
,如果要让其跳到同一深度,很明显需要让A
跳到2
去。那么怎么跳呢。
我们来展开19(10011)
2(10)
,两者相减,(10001)
,就意味着A要跳这么远才能跳到2去。让我们来观察一下fa数组,fa数组分别储存的是每个次方的节点,因此fa[19][4]
其实储存的就是(10000)
号父亲节点,跳过去,这样就只剩下一个(1)
没有跳了,再跳一下,就相同了。
从这里可以发现,其实lg数组
储存的是每个数字二进制第一个1出现的位置。
1 2 3 4 5 6
| for(int k = lg[depth[x] - 1];k >= 0;k++){ if(fa[x][k] != fa[y][k]){ x = fa[x][k]; y = fa[y][k]; } }
|
这里是先跳大步然后跳小步,一定要保证不相同,这样就能一直跳,跳到最近公共祖先的下一个节点,不然虽然能跳到公共祖先,但是你不能保证这个节点是最近的。
这里主要说明一下在k次内是肯定可以找到的。lg找的是深度的第一个1的位置,但是如果每个k都跳的话是肯定会大于等于原本的深度的。因为如果每个k都跳,那么就跳了(111…11)步,这是肯定大于等于原来的深度,也就是肯定能找到。
至此,lca完毕。
模板题代码
P3379 【模板】最近公共祖先(LCA)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| #include<iostream> #include<cstdio> using namespace std; const int maxn = 5e5 + 5; struct edge{ int to; int next; }edge[maxn << 1]; int head[maxn], cnt; void add(int x, int y){ edge[++cnt].to = y; edge[cnt].next = head[x]; head[x] = cnt; } int depth[maxn], fa[maxn][21], lg[maxn]; void dfs(int now, int fath){ fa[now][0] = fath; depth[now] = depth[fath] + 1; for(int i = 1;i < lg[depth[now]];i++){ fa[now][i] = fa[fa[now][i-1]][i-1]; } for(int i = head[now];i;i = edge[i].next){ if(edge[i].to != fath) dfs(edge[i].to, now); } } int lca(int x, int y){ if(depth[x] < depth[y]) swap(x, y); while(depth[x] > depth[y]){ x = fa[x][lg[depth[x] - depth[y]] - 1]; } if(x == y) return x; for(int k = lg[depth[x] - 1];k >= 0;k--){ if(fa[x][k] != fa[y][k]){ x = fa[x][k]; y = fa[y][k]; } } return fa[x][0]; } int main(void){ int n, m, s; scanf("%d %d %d",&n, &m, &s); for(int i = 1;i < n;i++){ int x, y; scanf("%d %d",&x, &y); add(x, y); add(y, x); } for(int i = 1;i <= n;i++){ lg[i] = lg[i-1] + (1 << lg[i-1] == i); } dfs(s,0); for(int i = 1;i <= m;i++){ int x, y; scanf("%d %d",&x, &y); printf("%d\n",lca(x, y)); } return 0; }
|
st表
基本信息
st表是用来解决区间最大最小值的问题的,此问题如果利用常规思路,算法复杂度是O(N ^ 2),而使用st表的话,可以在O(NlgN)
预处理,O(1)
查询,快的不是一点半点。他和lca
其实是一个思路,对于每个数字,储存其后面2^n
范围内的最大值,然后直接查询即可。
详细知识
预处理
我们建立一个MAX[maxn][21]
数组来储存以当前数组为起点的,连续2^n
的范围内的最大值,其中MAX[maxn][0]
即为2 ^ 0 = 1
范围内的最大值,即数字本身。
然后使用动态规划来将MAX数组
处理。
1 2 3 4 5 6 7 8
| int MAX[maxn][21]; inline void init(int n){ for(int j = 1;j <= 21;j++){ for(int i = 1;i + (1 << j) - 1 < n;i++){ MAX[i][j] = max(MAX[i][j-1], MAX[i + (1 << (j-1))][j-1]); } } }
|
这个动态规划可以用下面这个图来解释
整段的最大值是左右两段其中之一的最大值。
查询
查询和上面的思路一样,因为我们已经找出来了以每个数字为起点,长度为2^n
的序列的最大值,因此我们只需要将整个区间分成两个区间的并集即可,为了保证两个小区间一定包含了全部区间,两个小区间需要尽量大,最好是都是2^n
长。
1 2 3 4 5 6 7 8
| int lg[maxn]; int query(int l, int r){ int m = lg[l - r + 1] - 1; return max(MAX[l][m], MAX[r - (1 << m) + 1][m]); } for(int i = 1;i < n;i++){ lg[i] = lg[i-1] + (1 << lg[i - 1] == i); }
|
区间长度为2 ^ m
长,这是[L,r]
内最长的次方数,也是最长的2^n
长度。
模板题代码
P3865 【模板】ST 表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include<iostream> #include<cstdio> #include<algorithm> using namespace std; const int maxn = 1e5 + 5; int f[maxn][21]; int lg[maxn]; inline void init(int n){ for(int i = 1;i <= n;i++){ lg[i] = lg[i-1] + (1 << lg[i-1] == i); } for(int j = 1;j <= 21;j++){ for(int i = 1;i + (1 << j) - 1 <= n;i++){ f[i][j] = max(f[i][j-1], f[i + (1 << (j - 1))][j-1]); } } } inline int query(int l, int r){ int mid = lg[r - l + 1] - 1; return max(f[l][mid], f[r - (1 << mid) + 1][mid]); } signed main(void){ int n, m; scanf("%d %d",&n, &m); for(int i = 1;i <= n;i++)scanf("%d",&f[i][0]); init(n); for(int i = 1;i <= m;i++){ int l, r; scanf("%d %d",&l, &r); printf("%d\n",query(l,r)); } return 0; }
|
over!
矩阵快速幂