@(ACM模板)[图论]
图论知识点要求
必须会:
- 次短路&&路径数
- 生成树(看里面的过程)
- 最大流(主要是模板)
- 割(论文:最小割模型在信息竞赛里的应用)
- 二分图
- 树的分治,(点分治如重心分解,边分治如树链剖分)
了解,会套模板即可:
- K短路
- 度限制MST~end
- ZKW数组模拟
- 一般图匹配(NP,套模板)
- 各种回路
- 了解经典问题
General
- 注意下标是0-indexed的还是1-indexed的
- 看好是否有重边、自环
- 多组数据,vector要clear
- 在无向图中,存边的数组的大小要开边数的两倍
- unidirectional 和 one-way都是单向边
建图
使用vector
略
链式前向星
- 设图中n个点,m条边。在稀疏图( m≪n )中表示图,可以用邻接表。每个结点i有一个链表,保存从i出发的所有边。
- 方法:用数组模拟链表:
- 每条边编号
- first[u]保存结点u的第一条边的编号
- next[e]表示编号为e的边的下一条边的编号
- 注意每次插到链表的首部而非尾部,避免遍历
- 代码实现
typedef long long LL;
const int maxn = ?;
const int maxm = ?;
int n, m;
int head[maxn], next[maxm];
struct Edge
{
int to;
int dis;//LL
Edge(int to = 0, int dis = 0):to(to),dis(dis) {}
}edges[maxm];//无向图size为2*maxm
void build_graph()
{
cin >> n >> m;
memset(head, 0xff, sizeof head);
int u, v, w;
for(int i = 0; i < m; ++i)
{
scanf("%d%d%d", &u, &v, &w);
edges[i] = Edge(u, v, w);
//无向图要加两条
next[i] = head[u];
head[u] = i;
}
}
遍历u的所有边
for(int e = first[u]; ~e; e = next[e])
{
//...
}
最短路
1. Dijkstra算法
思想&步骤:
1. 初始化:d[s] = 0, 其他d为INF, 确定点集里只有s
2. 从未被确定的点的集合里找d最小的,加入确定的点集,并更新其他d
3. 重复2直到所有点都确定
注意:
- 不能处理负权。原因:Dijkstra贪心的找未确定点集里d最小的,若有负权可能之后找到d更小的。
- 此为使用队列优化的版本,复杂度 O(mlog(n)) ,其中 n 为点数,
- 基础版执行n次,每次遍历所有点,复杂度 O(n2)
基础版即使在 n2<mlog(n) 时也往往比队列版本慢,因为队列版本执行push操作的前提是能进行松弛操作,若这个式子不常常成立,则push操作会很少。
代码
注意:
- 若最短路是long long 的,下面的代码需要在相应注释的地方更改
- 若需记录路径,需要启用pa[]数组
- 注意点的下标是否从0开始
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int maxn = 1e5+5;//顶点数
const int maxm = 1e5+5;//边数
struct Dijstra//d[t] < inf代表有解
{
const int inf = 0x3f3f3f3f;
//const LL inf = 0x3f3f3f3f3f3f3f3f;
int n, m;
bitset<maxn> done;
int d[maxn];//LL
int head[maxn];
//int par[maxn];//记录路径
struct Edge
{
int to, nxt;
int dis;//LL
}e[maxm];
struct Node
{
int u;//结点编号
int dis;//LL
bool operator<(const Node &rhs) const
{
return dis > rhs.dis;
}
};
void init(int nn)
{
n = nn;
m = 0;
memset(head, 0xff, sizeof head);
}
void addEdge(int from, int to, int dis)//LL dis
{
e[m].to = to;
e[m].dis = dis;
e[m].nxt = head[from];
head[from] = m++;
}
void finda(int s)
{
priority_queue<Node> q;
for(int i = 0; i < n; ++i) d[i] = inf;//0~n-1,注意下标从哪里开始
d[s] = 0;
done.reset();
q.push((Node){s, 0});
while(!q.empty())
{
int u = q.top().u;
q.pop();
if(done[u] == true) continue;
done[u] = true;
for(int i = head[u]; ~i; i = e[i].nxt)
{
int v = e[i].to;
int dis = e[i].dis;//LL
if(d[v] > d[u] + dis)
{
d[v] = d[u] + dis;
q.push((Node){v, d[v]});
}
}
}
}
};
2. Bellman-Ford算法
思想&步骤
1. 初始化 所有顶点 d[i] = INF, 令d[s] = 0
2. 枚举每条边进行松弛,不能松弛时算法结束
3. 重复2,使2进行n-1次
memset(d, 0x3f, sizeof d);
d[s] = 0;
for(int times = 1; times < n; ++times)
{
for(int i = 0; i < m; ++i)
{
int x = u[i], y = v[i];
if(d[y] < d[x] + w[i]) d[y] = d[x] + w[i];
}
}
3 .SPFA算法
思想&步骤:
“松弛操作的连锁反应”
用queue存可用来松弛的点,每次用队首点进行松弛,然后将松弛到的&&不在queue中的点加入queue。
注意:
- 此为Bellman-Ford算法的队列优化版本
- 可以判断负环。因为任意一条最短路,所经过的点不会超过n,那么任何一点不可能超过(n-1)次被松弛。
- 最坏情况复杂度 O(nm) ,但实际中往往很快
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int maxm = 1e5 + 5;
const int maxn = 1e5 + 5;
struct SPFA
{
const int inf = 0x3f3f3f3f;
//const LL inf = 0x3f3f3f3f3f3f3f3f;
bool vis[maxn];
int c[maxn], head[maxn];//c为入队次数
int d[maxn];//LL
int n, m;
struct Edge
{
int to, nxt;
int dis;//LL
}e[maxm];
void init(int nn)
{
n = nn;
m = 0;
memset(head, 0xff, sizeof head);
}
void addEdge(int from, int to, int dis)//LL dis
{
e[m].to = to;
e[m].dis = dis;
e[m].nxt = head[from];
head[from] = m++;
}
queue<int> q;
bool finda(int s)//若存在负环返回true
{
memset(d, 0x3f, sizeof d);
d[s] = 0;
memset(vis, 0, sizeof vis);
memset(c, 0, sizeof c);
while(!q.empty()) q.pop();
q.push(s);
vis[s] = true;
c[s] = 1;
while(!q.empty())
{
int x = q.front();
q.pop();
vis[x] = false;
for(int i = head[x]; ~i; i = e[i].nxt)
{
int y = e[i].to;
int dis = e[i].dis;
if(d[y] > d[x] + dis)
{
d[y] = d[x] + dis;
if(!vis[y])
{
vis[y] = true;
++