二叉树
例题
1、已知中、后序遍历求先序
代码逻辑
- 后序遍历的最后一个字符一定是当下二叉树的根结点,先序遍历中根节点首先被遍历,所以每找到一个根节点就输出。在中序遍历中,根结点前面的是左子树,后面的是右子树,根据后序遍历的最后一个字符可以确定根,从而根据中序遍历确定左子树,从而根据左子树的大小从后序遍历中取出左子树,然后递归求解子树,右子树同理用递归求解
核心代码
1 2 3 4 5 6 7 8 9
| void forw(string s1, string s2) { if(!s1.size()) return ; char root = s2[s2.size()-1]; int p = s1.find(root); std::cout<<root; forw(s1.substr(0,p),s2.substr(0,p)); forw(s1.substr(p+1,std::string::npos),s2.substr(p,s2.size()-p-1)); }
|
2、已知先、后序遍历,求中序
代码逻辑
- 考虑一棵树只有一个根结点和一个结点,那么它子结点为左和子结点为右结点时,得出的先、后序遍历不变
- 如果树有一棵子树满足上述条件,那么子树有 2 种中序遍历,整棵树也就有 2 种中序遍历,根据乘法原理,如果树上有 k 个结点只有一个子结点,那么整棵树有 2^k^种中序遍历
- 如果一个结点只有一个子结点,那么先序遍历时,子结点一定在它的后一位,后序遍历时,子结点一定在它的前一位
核心代码
1 2 3 4 5 6 7 8 9 10
| pow = 0 for(int i=0;i<=s1.length()-2;i++) { for(int j=1;j<=s2.length()-1;j++) { if( s1[i]==s2[j] && s1[i+1]==s2[j-1] ) pow++; } } ans = (1<<pow)
|
树的直径
概述
边具有权重,如果以权重代表距离,那么整棵树距离最远的两个结点的距离就是树的直径
代码逻辑
- 以任意结点(比如 1 结点)为起点,dfs 求出各结点到 1 结点的距离,找到距离 1 结点最远的 far 结点
- 以 far 结点为起点,dfs 求结点到 far 结点的距离,找到最大距离,最大距离就是树的直径
核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void dfs( int cur, int fath) { if(dist[cur] > D) { D = dist[cur]; far = cur; } for( int ver : edge[cur]) { if(ver==fat) continue; dist[ver] = dist[cur] + 1; dfs(ver,cur); } }
|
例题
1、城市规划
问题描述
n 个城市有 n-1 条边,边权都视作 1,以 k 个城市组成核心城市,核心城市之间可以不经过普通城市相互联通,求其他城市到核心城市的最大距离的最小值
代码逻辑
- 先求直径,dfs 找到以任意结点为起点,离它最远的结点 far,然后用 dfs 求离 far 最远的结点,第二份 dfs 相比前一份 dfs 多一行代码,每次递归求子结点 dfs 之前先令 fa[ver] = cur,留下回溯数组方便找直径中点
- 直径中点作为核心城市的核心,其他所有结点都具有两个距离值,一个是自己到核心的距离,一个是自己联通的子结点距离核心最远的距离。为了让普通城市离核心城市群尽可能近,必须把自己到核心距离与子结点到核心最远距离的差值前 k-1 大的归为核心城市
核心代码_求直径留回溯
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void dfs( int cur, int fat) { if(dist[cur] > D) { D = dist[cur]; far = cur; } for (int ver : edge[cur]) { if(ver==fat) continue; dist[ver] = dist[cur] + 1; fa[ver] = cur; dfs(ver,cur); } }
|
核心代码_回溯找直径中点
1 2 3
| center = far; for(int i=1;i<=( D + 1 )/2;i++) center = fa[center];
|
核心代码_核心距离、最深距离
1 2 3 4 5 6 7 8 9 10 11 12
| void dfs( int cur, int fat) { dist_max[cur] = dist[cur]; for( int ver : edge[cur]) { if(ver==fat) continue; dist[ver] = dist[cur] + 1; dfs(ver,cur); dist_max[cur] = dist_max[ver] > dist_max[cur] ? dist_max[ver] : dist_max[cur]; } }
|
最小生成树
Kruskal
例题
N 个结点的图有 M 条无向有权边,判断能否生成一棵树,如果能则求出最小权重和
代码逻辑
- 将边从小到大遍历,如果边的两个结点不在同一个并查集中,就合并它们,合并的权重代价就是这条边的权重
核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| int Kruskal() { for(int i=1;i<=m;i++) { int root1 = FindRoot(edge[i].u), root2 = FindRoot(edge[i].v); if(root1==root2) continue; root[root1] = root[root2]; ans += edge[i].w; cnt++; if(cnt==n-1) return ans; } return -1; }
|
Prime
代码逻辑
- 起始让 1 结点入树,每当有结点入树就更新和它相连且未入树的结点到树的距离,每次找距离树最近的结点收入树中,无法用边联通的结点的距离视作 inf
- visited 判断结点是否已经入树
核心代码
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
| bool prime() { for(int i=2;i<=n;i++) dist[i] = inf; for(edge ver : e[1]) dist[ver.to] = dist[ver.to] < ver.weight ? dist[ver.to] : ver.weight; int now = 1, tot = 0; while(++tot<n) { int min = inf; visited[now] = true;
for(int i=1;i<=n;i++) { if(!visited[i] && min>dist[i]) { min = dist[i]; now = i; } } if(min==inf) return false; ans+=min;
for(edge ver : e[now]) { if(!visited[ver.to] && dist[ver.to]>ver.weight) dist[ver.to] = ver.weight; } } return true; }
|
树上结点最近祖先(LCA)
例题
问题描述
n 个结点的树,s 为根,查询任意两个结点的最近祖先
代码逻辑
- 先将两结点提到同一深度,然后一起向上回溯,回溯数组存 2 的幂次祖先,fa
now,i表示从 now 向前回溯 2^i^得到的祖先
- 无论是提到同一高度还是一起回溯,都按照 2 的幂次为单位向前跳,可以降低时间开销
- 一起回溯时,从大到小遍历所有 2 的幂次级别的祖先结点,若不相同就跳。如果最近公共祖先是 x 结点,再向上看,两个结点上面的全部祖先结点肯定都相同
核心代码_预处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void dfs(int now, int fath) { depth[now] = depth[fath] + 1; fa[now][0] = fath; for(int i=1;i<= (int)( log(depth[now])/log(2) );i++) fa[now][i] = fa[fa[now][i-1]][i-1];
for(int ver : edge[now]) { if(ver!=fath) dfs(ver,now); } return ; }
|
核心代码_LCA 主程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| int LCA(int a, int b) { if(depth[a]<depth[b]) Swap(a,b);
while(depth[a]>depth[b]) a = fa[a][log_2[depth[a]-depth[b]]-1]; if(a==b) return a;
for(int i=log_2[depth[a]]-1;i>=0;i--) { if(fa[a][i]!=fa[b][i]) a = fa[a][i], b = fa[b][i]; } return fa[a][0]; }
|
单源最短路径(Dijkstra)
概述
- Dijkstra只能处理无负权图的单源最短路径问题
例题
1、有向有权图单源最短距离
问题描述
n 个结点 m 条边,以 s 为源点,求其它所有结点到它的最短距离
代码逻辑
- 辅助数据结构:优先队列 元素属性:队列中元素具有标号和距源点距离两个属性 排序方式:距离源点小的排前
- 先让源点入队,每次循环从队列中弹出元素,更新它子结点到源点的距离,将被修改的子结点入队
- 代码是广度优先搜索的逻辑,所以可能会出现同一个结点被修改多次,所以它会多次入队,由于是优先队列,只有第一次出队表示的才是它离源点的最短距离,所以用 visited 数组记录结点是否出过队
核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| void Dijkstra() { q.push( (node) {s,0} ); while(!q.empty()) { int ind = q.top().ind; q.pop(); if(visited[ind]) continue; for( edge ver : e[ind] ) { if( dist[ver.to] > dist[ind] + ver.w ) { dist[ver.to] = dist[ind] + ver.w; q.push( (node) { ver.to,dist[ver.to] } ); } } } }
|
2、无向无权图单源最短路
问题描述
n 个结点 m 条边的无向无权图(边权都视作 1)以 1 为源点,求抵达任意点的最短路径的条数
代码逻辑
- 先初始化 dist
1为 0,其它 dist 都为 inf,ans1为 1,其它 ans 为 0
- 利用优先队列,使到源点距离小的排前,但优先队列默认小的排后,所以传值时,传入距离的相反数
- 处理当下结点的子结点时,如果子结点 dist 大于当下结点 dist 再+1,那么说明从当下结点去到子结点才是去它的最短路径,赋值式更新子结的 ans,但如果子结点的 dist 原本就和当下结点的 dist 再+1 相等,由于每个结点只会 visit 一次,所以子结点的 dist 被另外一个父结点更新为了当下结点的 dist 再+1,由排列组合加法原理知子结点的 ans 应当加上当下结点的 ans
核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| void Dijkstra() { std::priority_queue< std::pair<int,int> > q; q.push( std::make_pair(0,1) ); while(q.size()) { int pos = q.top().second; q.pop(); if(visited[pos]) continue; visited[pos] = true; for(int ver : edge[pos]) { if(dist[ver] > dist[pos] + 1) { dist[ver] = dist[pos] + 1; ans[ver] = ans[pos]; q.push( std::make_pair(-dist[ver],ver) ); } else if(dist[ver] == dist[pos] + 1) ans[ver] = ( ans[ver] + ans[pos] ) % mod; } } }
|
图上最小环
例题
1、生日
题目描述
现在有 n 个人,起始时每个人只知道自己的生日,每个人都有一个信息传递对象,这个传递对象可以是自己,每一轮都会把自己知道的所有生日告诉传递对象,最少多少轮的时候会有人从别人口中听到自己的生日?
代码逻辑
- 这是一个找最小环的问题,因为每个人只有一个信息传递对象,所以环都是独立的
- 初始化每个人的传递对象为自己,每输入一个传递对象就判断能否构成环,可以的话就更新答案,不行的话说明更新传递对象
- FindEnd 找到直线传递路径的终点,如果成环了,那么成环的结点的 nxt 不会被更新,所以它会导致 FindEnd 返回,也就是说,成环的结点就是直线传递路径的终点
核心代码_寻找直线传递路径终点
1 2 3 4 5 6 7
| int FindEnd(int x) { cnt++; if(x==nxt[x]) return x; return FindEnd(nxt[x]); }
|
核心代码_寻找最小环
1 2 3 4 5 6 7 8 9 10 11 12 13
| for(int i=1;i<=n;i++) nxt[i] = i; ans = inf; for(int i=1;i<=n;i++) { int x; cin>>x; cnt = 0; if(FindEnd(x)==i) ans = min(ans,cnt); else nxt[i] = x; }
|
拓扑结构
概述
拓扑结构关注结点之间的依赖关系
例题
1、工序时间
问题描述
有 n 项事件待做,输入 n 行,表示事件的标号和花费时间和需要的准备工作的标号,工人无限,可以无限制并行完成任务,输出完成所有事件的最小时间
代码逻辑
- 利用入度数组 in 存下事件的前驱结点个数,如果入度为 0 了,就说明这件事情可以开始做了
- ans 数组存完成各事件的最小时间,最终的答案为数组的最大值
- 入度为 0 的事件入队列,每次把出队列的对象的子结点的入度减 1,且更新它的 ans,同一个事件有多个前驱结点,ans 会被更新多次,必须取最大值,如果子结点的入度被减为 1,那么它入队列
核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| for(int i=1;i<=n;i++) { if(in[i]) { q.push(i); ans[i] = t[i]; } } while(!q.empty()) { int temp = q.top(); q.pop(); for(int ver : edge[temp]) { ans[ver] = max( ans[ver], ans[temp] + t[ver] ); in[ver]--; if(in[ver]==0) q.push(ver); } }
|
2、火车站分级
问题描述
有 n 个站点(编号 1 到 n)的火车站,每个站点都有一个级别,火车站内走过了 m 躺火车,这些火车都遵循一个原则:如果在某一个站停了车,那么后面凡是级别大于等于这个站的站点都要停车,现在已知 m 躺火车停过的站,求火车站的站点至少有几种级别
代码逻辑
- 这是一个用拓扑排序分层次的问题
- 火车停过的站之间没有停下的站的级别必然小于停过的站的级别,用 greater
i,j表示 i 号站点级别是否大于 j 号站点,用 out 数组记录出度,outi存的是比 i 号站点级别小的站点的个数
- 每一层级可能有多个站点,但是答案只关注层级的个数,每次循环消去一个层级,把答案加 1
核心代码
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
| int cnt,ans=-1; do { ans++; cnt = 0; for(int i=1;i<=n;i++) { if(out[i]==0 && !deleted[i]) { lowest[++cnt] = i; deleted[i] = true; } } for(int i=1;i<=cnt;i++) { for(int j=1;j<=n;j++) { if(greater[j][lowest[i]]) { greater[j][lowest[i]] = false; out[j]--; } } } }while(cnt);
|