NOIp 图论算法专题总结 (1):最短路、最小生成树、最近公共祖先
系列索引:
最短路
Floyd
基本思路:枚举所有点与点的中点,如果从中点走最短,更新两点间距离值。时间复杂度 \(O(V^3 )\)。
int n, m, f[N][N];
memset(f, 0x3f, sizeof(f));
for (int i=1, a, b, w; i<=m; i++) {
scanf("%d%d%d", &a, &b, &w);
if (f[a][b]>w) f[a][b]=w, f[b][a]=w; //去重边
}
for (int k=1; k<=n; k++)
for (int i=1; i<=n; i++)
for (int j=1; j<=n; j++)
f[i][j]=min(f[i][j], f[i][k]+f[k][j]);
Dijkstra (堆优化)
create vertex set \(Q\)
for each vertex \(v\) in Graph:
$d(v)← \infty \(
  \)\text{prev}(v) ←$ NULL
add \(v\) to \(Q\)
\(d(\text{source}) ← 0\)
while \(Q\) is not empty:
\(u ←\) vertex in \(Q\) with min \(d(u)\)
remove \(u\) from \(Q\)
for each neighbor \(v\) of \(u\):
\(alt ← d(u) + \text{length}(u,v)\)
if \(alt < d(v)\):
$d(v)← alt \(
      \)\text{prev}(v) ← u $
基本思路:更新每个点到原点的最短路径;寻找最短路径点进行下一次循环;循环次数达到 n - 1 次说明每个点到原点的最短路已成,停止程序。时间复杂度 \(O(E\log{E})\)。
struct node {
int u, dis;
bool operator < (const node &n) const {return dis>n.dis; }
} t;
priority_queue<node> q;
int d[N];
bool v[N];
memset(d, 0x3f, sizeof(d));
d[s]=0, t.u=s; q.push(t);
while (!q.empty()) {
t=q.top(), q.pop();
if (v[t.u]) continue; v[t.u]=true;
for (int i=head[t.u]; i; i=nex[i])
if (t.dis+w[i]<d[to[i]])
d[to[i]]=t.dis+w[i], q.push((node){to[i],d[to[i]]});
}
使用 Fibonacci 堆:时间复杂度 \(O(E+V\log{V})\)。
SPFA (Bellman-Ford 队列优化)
BFS-SPFA:
基本思路:更新每个点到原点的最短路径,保证「路径可变得更小的点」在队列中;队列空说明每个点到原点的最短路已成,停止程序。时间复杂度稀疏图 \(O(kE), k\approx 2\),最坏 \(O(VE)\)。
int d[N], pre[N], enq[N];
bool inq[N];
queue<int> q;
memset(d, 0x3f, sizeof(d));
q.push(s); d[s]=0; inq[s]=true; enq[s]++;
while (!q.empty()) {
int a=q.front(); q.pop();
inq[a]=false;
for (int b=head[a]; b; b=nex[b])
if (d[a]+w[b]<d[to[b]]) {
d[to[b]]=d[a] + w[b];
pre[to[b]]=a; // *输出路径
if (!inq[to[b]]) {
q.push(to[b]);
enq[to[b]]++; if (enq[to[b]]>=n) {printf("负环!\n"); return; } // *判断负环
inq[to[b]]=true;
}
}
}
DFS-SPFA:
bool flag=false;
void spfa(int u) {
ins[u]=true;
for (int i=head[u]; i; i=nex[i])
if (dis[u]+w[i]<dis[to[i]]) {
dis[to[i]]=dis[u]+w[i];
if (!ins[to[i]]) spfa(to[i]);
else {flag=true; return; } // 负环!
}
ins[u]=false;
}
// 贪心优化
bool spfa_init(int u) {
for (int i=head[u]; i; i=nex[i])
if (dis[u]+w[i]<dis[to[i]]) {
dis[to[i]]=dis[u]+w[i];
spfa_init(to[i]);
return true;
}
return false;
}
for (int i=1; i<=n; i++) while (spfa_init(i)); //贪心求较优解
for (int i=1; i<=n; i++) spfa(i);
关于 SPFA:
“它死了。” (NOI2018 D1T1 出题人 )
考虑使用堆优化 SPFA:Dijkspfa!
把队列改成堆(例如优先队列)。注意到堆不会随着编号所对应的权值大小的改变而改变,存在很多冗余状态,此时应主动把堆顶冗余状态删除。
while (!q.empty() && q.top().dis > d[q.top().u]) q.pop(); if (q.empty()) break;
注意,即使加入堆优化,SPFA 还是 SPFA,并不会变成 Dijkstra。(Dijkstra 比较贪心所以无法处理负权回路;SPFA 因为方便卡常,已经死了。)另外,对于最小费用最大流,还有一种实现 Dijkstra 处理负权回路的方式,参见 本系列 (3)。
01 BFS:双端队列,0 加入队列前端,1 加入队列末端。时间复杂度 \(O(m )\)。
\(\sum w_i \le W\) BFS:桶 + 链表 代替堆。时间复杂度 \(O(m+W )\)。
差分约束
形如 \(x_i-x_j\ge -c_k\) 或 \(x_i+c_k\ge x_j\) 可以看作从 \(i\) 向 \(j\) 连一条长度为 \(c_k\) 的边,求最短路;存在负环则无解。
同理,形如 \(x_i-x_j\le -c_k\) 或 \(x_i+c_k\le x_j\) 也可以看作从 \(i\) 向 \(j\) 连一条长度为 \(c_k\) 的边,求最长路;存在正环则无解。
判断差分约束系统是否成立:以根节点出发遍历全图,不出现负环。如果图不联通,从超级源向每个节点引边权为 0 的边。
标准做法是使用 SPFA,但显然要用 Dijkstra。
\(k\) 短路
\(n\) 个点,\(m\) 条边,每条给出有向边并带有权值,给出 start,end 和 \(k\),求 s~t 所有路径中的第 \(k\) 短路。
\]
\(f(x)\) 就是路径的长度;\(g(x)\) 是估价函数,我们选择 \(x\)~end 的最短路径;\(h(x)\) 是实际长度,start~\(x\) 的总路径长。
那么我们优先访问 \(f(x)\) 更加小的点:因为它更可能成为最短,再第二短,再……如果 end 被访问了 \(k\) 次了,那么目前得到的值 \(f(x)\) 就是 \(k\) 短路的长度。求 \(k\) 短路,要先求出最短路、次短路、第三短路、……、第 \((k-1)\) 短路,然后访问到第 \(k\) 短路。
预处理 \(g(x)\):将所有边反向,然后求 end 到所有点的单源最短路径(Dijkstra)。
A* 启发式搜索。可以做成 BFS-A*,节点直接按照 \(f(x)\) 序访问。
最小生成树
Prim
基本思路:记录 \(f[i]\) 表示当前由已经选出来的最小生成树到 \(i\) 点的最小边是多少;每次就是找出还没有加进最小生成树的点中最小边最小的点,加进最小生成树,更新联结的点 \(f\) 值。时间复杂度 \(O(V^2 )\)。
Kruskal
sort \(E\)
\(MST←\varnothing\)
let each point be independent connected component
for each edge \(u\) in \(E\)
if \(x_u\) and \(y_u\) on difference connected component
add \(u\) to \(MST\)
union \(x_u,y_u\)
基本思路:按边长度从小到大排序,循环添加「不成环」的边;边数达到 \(n - 1\) 说明最小生成树已成,停止程序。时间复杂度 \(O(E\log{E})\)。
const int N = 1000 + 3, M = 20000 + 3;
int dset[N], n, m;
struct edge {
int x, y, w;
bool operator < (const edge &a) const { return w < a.w; }
} edges[M];
int find(int x) { return (dset[x]==-1) ? x : dset[x]=find(dset[x]); }
void join(int x, int y) { if (find(x)!=find(y)) dset[find(x)]=find(y); }
int Kruskal() {
memset(dset, -1, sizeof(dset));
sort(edges+1, edges+m+1);
int cnt=0, tot=0;
for (int i=1; i<=m; i++) //循环所有已从小到大排序的边
if (find(edges[i].x)!=find(edges[i].y)) { // (因为已经排序,所以必为最小)
join(edges[i].x, edges[i].y); // 相当于把边(u,v)加入最小生成树。
tot += edges[i].w;
cnt++;
if (cnt==n-1) break; // 说明最小生成树已经生成
}
return tot;
}
Borůvka
基本思路:用定点数组记录每个子树的最近邻居。对于每一条边进行处理:如果这条边连成的两个顶点同属于一个集合,则不处理,否则检测这条边连接的两个子树,如果是连接这两个子树的最小边,则更新 (合并)。时间复杂度平均 \(O(V+E)\),最坏 \(O((V+E)\log V)\)。(显然比 Kruskal 快)
struct node {int x, y, w; } edge[M];
int d[N]; // 各子树的最小连外边的权值
int e[N]; // 各子树的最小连外边的索引
bool v[M]; // 防止边重复统计
int fa[N];
int find(int x) {return x==fa[x] ? x : (fa[x]=find(fa[x])); }
void join(int x, int y) {fa[find(x)]=find(y); }
int Boruvka() {
int tot=0;
for (int i=1; i<=n; ++i) fa[i]=i;
while (true) {
int cur=0;
for (int i=1; i<=n; ++i) d[i]=inf;
for (int i=1; i<=m; ++i) {
int a=find(edge[i].x), b=find(edge[i].y), c=edge[i].w;
if (a==b) continue;
cur++;
if (c<d[a] || c==d[a] && i<e[a]) d[a]=c, e[a]=i;
if (c<d[b] || c==d[b] && i<e[b]) d[b]=c, e[b]=i;
}
if (cur==0) break;
for (int i=1; i<=n; ++i) if (d[i]!=inf && !v[e[i]]) {
join(edge[e[i]].x, edge[e[i]].y), tot+=edge[e[i]].w;
v[e[i]]=true;
}
}
return tot;
}
最近公共祖先
倍增求 LCA
基本思路:先把比较深的点跳到和比较浅的点同样高,然后两个点分别往上跳一格(可以看做同时往上跳),直到跳到相同的点为止。 记录 \(p[i][j]\),表示从 \(i\) 号点往上跳 \(2^j\) 步到达哪个点。初始情况:\(p[i][0]\) 就是 \(i\) 点树上的父亲。即只记录一部分 \(j\)。如果要从 \(i\) 号点往上跳 \(k\) 步,就把 \(k\) 在二进制下分解成几个 2 的次幂,利用 \(p\) 就可以一次多跳几步。\(p\) 数组可以在预处理的时候顺便完成。\(p[i][j]=p\big[~p[i][j-1]~\big]\big[j-1\big]\)。时间复杂度预处理 \(O(n\log{n})\),询问 \(O(q\log{n})\)。
int p[N][logN], dep[N];
void dfs(int x) {
for (int i=head[x]; i; i=nex[i]) if (to[i]!=p[x][0])
dep[to[i]]=dep[x]+1, p[to[i]][0]=x, dfs(to[i]);
}
inline void init() {
dfs(s);
for (int j=1; (1<<j)<=n; j++)
for (int i=1; i<=n; i++)
p[i][j]=p[p[i][j-1]][j-1];
}
inline int lca(int x, int y) {
if (dep[x] > dep[y]) swap(x, y);
int f = dep[y] - dep[x];
for (int i=0; (1<<i)<=f; i++) if ((1<<i) & f) y=p[y][i];
if (x==y) return x;
for (int i=log2(n); i>=0; --i) if (p[x][i]!=p[y][i]) x=p[x][i], y=p[y][i];
return p[x][0];
}
DFS 序 + ST 表求 LCA
基本思路:欧拉序上的 RMQ。DFS 一遍求出欧拉序、每个点深度;对于一个固定的序列欧拉序,多次询问区间内最小的数及其位置。记 \(\text{RMQ}[i][j]\) 表示从 \(i\) 开始,长度为 \(2^j\) 区间内最小的数是多少,\(\text{RMQ}[i][j]=\min\{\text{RMQ}[i][j-1],\text{RMQ}[i+2^{j-1}][j-1]\}\);为了求位置,再记个 \(\text{MinPos}[i][j]\)。 利用预处理的信息取两个长度均为 2 的次幂的区间使得其能覆盖 \([x,y]\)。
int v[N], d[N], mpos[N], dfn[N<<1], fn, RMQ[N<<1][log(N<<1)];
void dfs(int x, int t) {
v[x]=1, d[x]=t, mpos[x]=fn, dfn[fn++]=x;
for (int i=head[x]; i; i=nex[i]) if (!v[to[i]])
dfs(to[i], t+1), dfn[fn++]=x;
}
inline void lca_init() {
dfs(s, 0);
for (int i=0; i<fn; ++i) RMQ[i][0]=dfn[i];
for (int j=1; (1<<j)<=fn; ++j)
for (int i=0; i+(1<<j)-1<fn; ++i)
if (d[RMQ[i][j-1]]<=d[RMQ[i+(1<<j-1)][j-1]]) RMQ[i][j]=RMQ[i][j-1];
else RMQ[i][j]=RMQ[i+(1<<j-1)][j-1];
}
inline int lca(int x, int y) {
x=mpos[x], y=mpos[y];
if (x>y) swap(x, y);
int k=log2(y-x+1);
if (d[RMQ[x][k]]<=d[RMQ[y-(1<<k)+1][k]]) return RMQ[x][k];
else return RMQ[y-(1<<k)+1][k];
}
树链剖分求 LCA
基本思路:选出每个点最“重”的儿子,就是子树大小最大的那个儿子,并将这条边标为“重边”,重边连成“重链”。 我们设 \(x\) 的重链链顶为 \(\text{Top}[x]\)。求 \(\text{LCA}(u,v)\):注意一个点只有一个重儿子,所以 \(u\) 和 \(v\) 往祖先的两条路径,至少一条是从 \(\text{LCA}\) 出来的轻边。每次看看 \(\text{Top}[u]\) 和 \(\text{Top}[v]\) 哪个深度更大,如果是 \(u\),就把 \(u\) 跳到 \(\text{Fa}[\text{Top}[u]]\)。直到两个点在同一条重链上,\(\text{LCA}\) 就是此时深度比较小的点。 时间复杂度 \(=\) 重链条数 \(O(\log{n})\)。
int son[N], fa[N], dep[N], siz[N], top[N];
inline int lca(int x, int y) {
while (top[x]!=top[y]) {
if (dep[top[x]] < dep[top[y]]) swap(x, y);
x=fa[top[x]];
}
if (dep[x]>dep[y]) swap(x, y);
return x;
}
void dfs1(int x, int f, int d) {
dep[x]=d, fa[x]=f, siz[x]=1;
int heavy=-1;
for (rint i=head[x]; i; i=nex[i]) {
int &y=to[i]; if (y==f) continue;
dfs1(y, x, d+1);
siz[x]+=siz[y];
if (siz[y]>heavy) son[x]=y, heavy=siz[y];
}
}
void dfs2(int x, int tp) {
top[x]=tp;
if (!son[x]) return;
dfs2(son[x], tp);
for (rint i=head[x]; i; i=nex[i]) {
int &y=to[i]; if (y==fa[x] || y==son[x]) continue;
dfs2(y, y);
}
}
dfs1(root, 0, 1);
dfs2(root, root);
NOIp 图论算法专题总结 (1):最短路、最小生成树、最近公共祖先的更多相关文章
- NOIp 图论算法专题总结 (2)
系列索引: NOIp 图论算法专题总结 (1) NOIp 图论算法专题总结 (2) NOIp 图论算法专题总结 (3) 树链剖分 https://oi-wiki.org/graph/heavy-lig ...
- NOIp 图论算法专题总结 (3):网络流 & 二分图 简明讲义
系列索引: NOIp 图论算法专题总结 (1) NOIp 图论算法专题总结 (2) NOIp 图论算法专题总结 (3) 网络流 概念 1 容量网络(capacity network)是一个有向图,图的 ...
- 图论算法(二)最短路算法:Floyd算法!
最短路算法(一) 最短路算法有三种形态:Floyd算法,Shortset Path Fast Algorithm(SPFA)算法,Dijkstra算法. 我个人打算分三次把这三个算法介绍完. (毕竟写 ...
- 图论算法(三) 最短路SPFA算法
我可能要退役了…… 退役之前,写一篇和我一样悲惨的算法:SPFA 最短路算法(二)SPFA算法 Part 1:SPFA算法是什么 其实呢,SPFA算法只是在天朝大陆OIers的称呼,它的正统名字叫做: ...
- LCA(最近公共祖先)离线算法Tarjan+并查集
本文来自:http://www.cnblogs.com/Findxiaoxun/p/3428516.html 写得很好,一看就懂了. 在这里就复制了一份. LCA问题: 给出一棵有根树T,对于任意两个 ...
- 算法专题 | 10行代码实现的最短路算法——Bellman-ford与SPFA
今天是算法数据结构专题的第33篇文章,我们一起来聊聊最短路问题. 最短路问题也属于图论算法之一,解决的是在一张有向图当中点与点之间的最短距离问题.最短路算法有很多,比较常用的有bellman-ford ...
- 图论算法-最小费用最大流模板【EK;Dinic】
图论算法-最小费用最大流模板[EK;Dinic] EK模板 const int inf=1000000000; int n,m,s,t; struct node{int v,w,c;}; vector ...
- 图论算法(一)存图与STL第六弹——vector容器
图论算法(一)存图 我发现我的博客阅读量贼低,问小伙伴们,ta们都说这些博客太长了QAQ! 今天来个短亿点的(也短不了多少……) 进入正题,图论究竟是什么? 图论就是给你一张图,让你在这张图上进行各种 ...
- 图论算法-网络最大流【EK;Dinic】
图论算法-网络最大流模板[EK;Dinic] EK模板 每次找出增广后残量网络中的最小残量增加流量 const int inf=1e9; int n,m,s,t; struct node{int v, ...
随机推荐
- C#—Nhibernate使用教程
本篇文章,让我们一起来探索Nhibernate.首先我们去搜索Nhibernate下载地址,如下链接所示.该版本可能是最新版,我下载的4.0.4.GA.其中GA意思我没搞清楚.不过应该不重要.http ...
- 学习《Oracle PL/SQL 实例讲解 原书第5版》----创建账户
通过readme.pdf创建student账户. 以下用sys账户登录时都是sysdba. 一.PL/SQL 登录oracle. SYS/123 AS SYSDBA 账户名:sys:密码:123:作 ...
- 使用K近邻算法改进约会网站的配对效果
1 定义数据集导入函数 import numpy as np """ 函数说明:打开并解析文件,对数据进行分类:1 代表不喜欢,2 代表魅力一般,3 代表极具魅力 Par ...
- html5 WebSocket的Js实例教程
详细解读一个简单+ ,附带完整的javascript websocket实例源码,以及实例代码效果演示页面,并对本实例的核心代码进行了深入解读. 从WebSocket通讯三个阶段(打开握手.数据传递. ...
- C++学习笔记(七)--共用体、枚举、typedef
1.共用体 union其定义与结构体类似:union 类型名{ 成员表列;};声明变量的方法也类似: a. union 类型名{ b. union { c.类型名 变量名; 成员 ...
- Spring MVC-学习笔记(3)参数绑定注解、HttpMessageConverter<T>信息转换、jackson、fastjson、XML
1.参数绑定注解 1>@RequestParam: 用于将指定的请求参数赋值给方法中的指定参数.支持的属性: 2>@PathVariable:可以方便的获得URL中的动态参数,只支持一个属 ...
- Linux 查看日志文件
1. tail命令:从文本文件的尾部开始查看,用于显示文本文件的末尾几行 tail -n filename 指定需要显示多少行 tail -f filename 实时 ...
- 一个阿里云apache服务器配置两个或多个域名forLinux
一个阿里云apache服务器配置两个或多个域名for Linux: 默认已经配置好了阿里云提供的一键web安装,可以参考:http://www.42iot.com/?id=8 修改/alidata/s ...
- Linux基础命令一(补充)
echo ls ls–l ---- ll cd / 根目录 cd ~ cd - 返回上一个目录 env ip addr 显示物理网络地址,缩写:ip a /etc/init.d/network ...
- latex算法步骤如何去掉序号
想去掉latex算法步骤前面的序号,如下 我想去掉每个算法步骤前面的数字序号,1,2,3,因为我已经写了step.我们只需要引用a lgorithmic这个包就可以了,代码如下: \usepackag ...