算法基础图论
并查集
可以合并,可以查询联通关系的集合。
初始化1
pre[i] = i;
找根1
2
3
4int root(int x) {
if (pre[x] == x) return x;
return root(pre[x]);
}
合并1
2
3void merge(int u, int v) {
pre[root(u)] = root(v);
}
查询u,v是否联通1
2
3void query(int u, int v) {
root(u) == root(v);
}
路径压缩:
经过1
2
3void root(u) {
pre[u] = pre[u]==u ? u : root(pre[u]);
}
换根操作:
多出一个节点或一个连通块,可以将这个节点或连通块的根指向原来的连通块根,也可将原来的根指向新增的节点或连通块,前者称为按秩合并(小->大)。
1 |
|
Dijkstra单源最短路
单源:可以求任意一个点到某个点的最短路1
2
3
4
5
6graph LR
1-->|1|2
1-->|4|3
2-->|2|3
2-->|2|4
3-->|3|5
从1开始,数组d[i]存放1到i点距离的最小值。
初始化d[1]=0,d[i] = inf
1->2: d[2] = 1
1->3: d[3] = 4
2->3: d[3] = min(1+2, 4) = 3
2->4: d[4] = 1 + 2 = 3
3->5: d[5] = 3 + 3 = 6
贪心思想:每次找最近的点进行拓展和更新
dp:选取最优方案
每个点只拓展更新一次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
using namespace std;
using ll = long long;
const int N = 1e3 + 5;
// O(n^2)
struct Edge {
int x, w;
};
vector<Edge> g[N];
array<ll, N> d;
ll n, m;
void dijkstra(int st) {
d.fill(0x3f);
d[st] = 0;
bitset<N> vis; // 表示已经拓展过
for (int i=1;i<=n;++i) {
// 找出最小点,距离原点最近
int u = 1;
for (int j = 1; j <= n; ++j) {
if (vis[u] || (!vis[j] && d[j] < d[u]))
u = j;
}
vis[u] = 1; // 表示u已经拓展过
// d[u]已是最优的
for (auto &[v, w] : g[u]) {
if (!vis[v] && d[v] > d[u] + w)
d[v] = d[u] + w;
}
}
}
int main() {
cin >> n >> m;
for (ll i=1;i<=m;++i) {
int u, v, w;
cin >> u >> v >> w;
if (u !=v) g[u].push_back({v, w});
}
dijkstra(1);
// 判断是否能到达
cout << (d[n]>= 0x3f3f3f3f3f3f3f3f ? -1 : d[n]) << '\n';
}
最短路单调队列优化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
using namespace std;
using ll = long long;
const int N = 1e3 + 5;
struct Edge {
ll x, w;
bool operator<(const Edge &v) const {
// w小的优先
return w == v.w ? x < v.x: w > v.w;
}
};
vector<Edge> g[N];
array<ll, N> d;
ll n, m;
void dijkstra(int st) {
d.fill(0x3f3f3f3f3f3f3f3f);
d[st] = 0;
bitset<N> vis; // 表示已经拓展过
priority_queue<Edge> pq;
pq.push({st, d[st]});
while (pq.size()) {
int x = pq.top().x;pq.pop();
// 防止反复入队
if (vis[x])
continue;
vis[x] = 1;
for (auto &[y, w] : g[x]) {
if (!vis[y] && d[y] > d[x] + w) {
d[y] = d[x] + w;
pq.push({y, d[y]});
}
}
}
}
int main() {
cin >> n >> m;
for (ll i=1;i<=m;++i) {
int u, v, w;
cin >> u >> v >> w;
if (u !=v) g[u].push_back({v, w});
}
dijkstra(1);
// 判断是否能到达
cout << (d[n]>= 0x3f3f3f3f3f3f3f3f ? -1 : d[n]) << '\n';
}
Floyd多源最短路
1 | graph LR |
floyd由三重循环组成,第一层枚举中转点,第二层枚举入点,第三层枚举出点。
有以下图,虚线代表路径中有其他节点。1
2
3
4graph LR
i-.->j
i-.->k
k-.-j
那么从i到j的最短距离1
d[i][j]=min(d[i][j], d[i][k]+d[k][j])
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
using namespace std;
using ll = long long;
const int N = 300;
const ll inf = 4e18, p = 998244353;
ll d[N][N], n, m, q;
int main() {
memset(d, 0x3f, sizeof d);
cin >> n >> m >> q;
for (int i=1;i<=m;++i) {
ll u, v, w;
cin >> u >> v >> w;
d[u][v] = min(d[u][v], w);
}
// 排除自环 距离为0
for (int i=1;i<=n;++i) {
d[i][i]=0;
}
for (int k = 1; k <= n; ++k) {
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
d[i][j] = min(d[i][j], d[i][k]+d[k][j]);
}
}
}
while (q--) {
int u, v;
cin >> u >> v;
cout << (d[u][v] >= inf ? -1 : d[u][v]) << '\n';
}
}
分层图最短路
分层图图大小为k*n,一定要注意数据范围!!!
以洛谷 P4568 [JLOI2011] 飞行路线为例。
观察数据范围,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m >> k;
int s, t;
cin >> s >> t;
int a, b, c;
for (int i = 0; i < m; ++i) {
cin >> a >> b >> c;
g[a].push_back({b, c});
g[b].push_back({a, c});
for (int j = 1; j <= k; ++j) {
g[a+(j-1) * n].push_back({b+j*n, 0});
g[b+(j-1)* n].push_back({a+j*n, 0});
g[a + j * n].push_back({b + j * n, c});
g[b+j*n].push_back({a+j*n, c});
}
}
for (int i = 1; i <= k; ++i) {
g[t+(i-1)*n].push_back({t+i*n, 0});
}
dijkstra(s);
cout << d[t + k * n];
}
分层图的核心理念:
- 将点拆开,复制多层图,并利用特殊构造的边将各层相连的建图方法。
- 一般用于边或点有特殊限制问题(如重复经过次数,多种价值选择等)
- 需要保证拆开后的总点数规模可接受。
补充分层图加网络最大流问题
最小生成树(MST)
生成树:连通图中,找出一个节点最多的(n点),n-1条边的子连通图。
最小是指在生成树中找到权值和最小的
以下图为例:1
2
3
4
5
6
7graph LR
1---|2|2
1---|3|4
2---|3|3
2---|1|4
3---|4|5
4---|5|5则最小生成树(虚线连接)为:
1
2
3
4
5
6
7graph LR
1-.-|2|2
1---|3|4
2-.-|3|3
2-.-|1|4
3-.-|4|5
4---|5|5基于点的Prim算法(稠密图)
1
2
3
4
5
6
7graph LR
1---|1|2
1---|3|4
2---|2|3
2---|1|4
3---|5|5
4---|3|5
- 两个集合intree和outtree
- 选1号点作为起点放入intree,移出outtree(以下步骤同理)
- 循环n-1次,每次确定一条边,在所有点当中找离intree中的点距离最近的点,将其加入intree,更新所有点到intree任意一点的最短距离。
在权值均为正的情况下,使用邻接表只需考虑intree集合中的点直接连接的点。
如果使用邻接矩阵,非直接连接的点距离初始化为无穷大即可。
朴素 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
using namespace std;
using ll = long long;
const int N = 1e3 + 10;
const ll inf = 4e18, p = 998244353;
int a[N][N], d[N];
bitset<N> intree;
int main() {
int n, m;
cin >> n >> m;
memset(a, 0x3f, sizeof a);
memset(d, 0x3f, sizeof d);
for (int i = 1; i <= n; ++i) {
a[i][i] = 0;
}
for (int i = 1; i <= m; ++i) {
int u, v, w;
cin >> u >> v >> w;
// 排除无向图出现重边
a[u][v] = min(a[u][v], w);
a[v][u] = min(a[v][u], w);
}
ll ans = 0;
for (int i = 1; i <= n; ++i) {
int u = 1; // u就是距离intree点最近的点
for (int j = 1; j <= n; ++j) {
if (intree[u] || (!intree[j] && d[j] < d[u]))
u = j;
}
if (d[u] < 0x3f)
ans += d[u];
intree[u] = 1;
d[u] = 0;
for (int j = 1; j <= n; ++j) {
if (intree[j])
continue;
d[j] = min(d[j], a[u][j]);
}
}
cout << 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
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
60
61
using namespace std;
using ll = long long;
const int N = 2e5 + 10;
const ll inf = 4e18, p = 998244353;
struct Edge {
ll x, w;
bool operator<(const Edge &u) const {
return w ==u.w?x<u.x:w>u.w;
}
};
vector<Edge> g[N];
ll d[N];
bitset<N> intree;
int main() {
int n, m;
cin >> n >> m;
memset(d, 0x3f, sizeof d);
for (int i = 1; i <= m; ++i) {
ll u, v, w;
cin >> u >> v >> w;
g[u].push_back({v, w});
g[v].push_back({u, w});
}
ll ans = 0;
priority_queue<Edge> pq;
pq.push({1, 0});
d[1] = 0;
while (pq.size()) {
// auto [x, w] = pq.top();
int x = pq.top().x;
pq.pop();
if (intree[x])
continue;
intree[x] = 1;
// ans += w;
ans += d[x];
d[x] = 0;
for (auto &[y, w] : g[x]) {
if (!intree[y] && w < d[y]) {
d[y] = w;
pq.push({y, w});
}
}
}
for (int i=1;i<=n;++i) {
if (!intree[i]) ans = -1;
}
cout << ans << '\n';
}
基于边的Kruskal算法(稀疏图)
贪心、排序
- 给边从小到大排序
- 从小到大选边,若这条边的两个已经连通就跳过,若未连通就选上并连通。
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
using namespace std;
using ll = long long;
const int N = 1e5 + 10;
const ll inf = 4e18, p = 998244353;
ll d[N];
int pre[N];
int root(int x) {
return pre[x] = (pre[x] == x ? x : root(pre[x]));
}
struct Edge {
ll u, v, w;
bool operator<(const Edge &m) const {
return w == m.w ? (u==m.w?v<m.v:u<m.w) : w < m.w;
// return w<m.w;
}
};
int main() {
int n, m;
cin >> n >> m;
memset(d, 0x3f, sizeof d);
vector<Edge> es;
for (int i=1;i<=m;++i) {
ll u, v, w;cin >> u>> v>> w;
es.push_back({u, v, w});
}
sort(es.begin(), es.end());
ll ans = 0;
for (int i=1;i<=n;++i) {
pre[i] = i;
}
for (auto &[u, v, w] : es) {
if (root(u) == root(v))
continue;
ans += w;
pre[root(u)] = root(v);
}
for (int i=1;i<n;++i) {
if (root(i) != root(i+1)) ans = -1;
}
cout << ans;
}