图论基础复习(图存储、最小生成树、拓扑排序、最短路)
图论基础复习(图存储、最小生成树、拓扑排序、最短路)
一、图的基本性质
-
对于有向图和无向图来说,所有顶点度数和=边数×2。
-
对于有向图来说,所有顶点入度和=出度和=边数
-
n 个点的连通图至少需要 n-1 条边(链)
-
连通分量:无向图中的极大连通子图
-
在有向图中,如果任意两个点之间都有路径,那么该图为强连通图。n 个点至少要 n 条边才能是强连通图
-
强连通分量:有向图的极大强连通子图
-
生成树:在图中找出一棵树,n个点 n-1条边,且不存在环,连通所有点。也就是连通图包含所有顶点的一个极小连通子图。
二、图的DFS
对于非连通无向图,执行几次深度优先遍历,图中就有几个连通分量。
普通的DFS
#include<bits/stdc++.h>
using namespace std;
int n, m;
vector<vector<int>> g;
vector<int> visited;
void dfs(int u) {
// 1. 标记 u 已访问
visited[u]=1;
cout<<u<<endl;
// 2. 枚举 u 的每一个邻居 v
for (int v : g[u]) {
if(!visited[v])
{
dfs(v);
}
// 3. 若 v 未访问,则继续 dfs(v)
}
}
int main() {
cin >> n >> m;
// 4. 按 n 初始化 g 和 visited
g.resize(n + 1);
visited.assign(n + 1, 0);
for (int i = 0; i < m; i++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
// 5. 无向图如何建边?
}
// 6. 从 1 号点开始遍历
dfs(1);
return 0;
}
dfs求连通分量
#include<bits/stdc++.h>
using namespace std;
int n, m;
vector<vector<int>> g;
vector<int> visited;
void dfs(int u) {
// 1. 标记 u 已访问
visited[u]=1;
cout<<u<<endl;
// 2. 枚举 u 的每一个邻居 v
for (int v : g[u]) {
if(!visited[v])
{
dfs(v);
}
// 3. 若 v 未访问,则继续 dfs(v)
}
}
int main() {
cin >> n >> m;
// 4. 按 n 初始化 g 和 visited
g.resize(n + 1);
visited.assign(n + 1, 0);
for (int i = 0; i < m; i++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
// 5. 无向图如何建边?
}
// 6. 从 1 号点开始遍历
dfs(1);
int cnt=1;
for(int i=2;i<=n;i++)
{
if(!visited[i])
{
cnt++;
dfs(i);
}
}
cout<<"总共有:"<<cnt<<"个连通块";
return 0;
}
三、图的BFS
BFS 的思路与DFS的不同之处在于,DFS每条路都在疯狂的调用自己,而 BFS每个连通分量只调用一次,在函数中通过队列来遍历。
#include<bits/stdc++.h>
using namespace std;
int n, m;
vector<vector<int>> g;
vector<int> visited;
void dfs(int u) {
// 1. 标记 u 已访问
visited[u]=1;
cout<<u<<endl;
// 2. 枚举 u 的每一个邻居 v
for (int v : g[u]) {
if(!visited[v])
{
dfs(v);
}
// 3. 若 v 未访问,则继续 dfs(v)
}
}
void bfs(int s){
queue<int>q;
visited[s]=1;
q.push(s);
cout<<s<<endl;
while(!q.empty())
{
int u=q.front();
q.pop();
for(int v:g[u])
{
if(!visited[v])
{
cout<<v<<endl;
visited[v]=1;
q.push(v);
//bfs(v);
}
}
}
}
int main() {
cin >> n >> m;
// 4. 按 n 初始化 g 和 visited
g.resize(n + 1);
visited.assign(n + 1, 0);
for (int i = 0; i < m; i++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
// 5. 无向图如何建边?
}
// 6. 从 1 号点开始遍历
bfs(1);
int cnt=1;
for(int i=2;i<=n;i++)
{
if(!visited[i])
{
cnt++;
bfs(i);
}
}
cout<<"总共有:"<<cnt<<"个连通块";
return 0;
}
四、最小生成树
“怎么花最小的成本连通所有点?”
在无向图中求一棵树,且这棵树的边权和最小。
求最小生成树的算法有Prim算法和Kruskal算法
Prim算法(加点)
从某个点出发,选择距离最短的点拓展,再选择这两个点能到达的,距离最短的点进行拓展...
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = 4e18;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<vector<pair<int, ll>>> g(n + 1);
for (int i = 1; i <= m; i++) {
int u, v;
ll w;
cin >> u >> v >> w;
g[u].push_back({v, w});
g[v].push_back({u, w});
}
vector<ll> dist(n + 1, INF);
vector<int> vis(n + 1, 0);
// {当前接入边权, 点编号},小根堆
priority_queue<
pair<ll, int>,
vector<pair<ll, int>>,
greater<pair<ll, int>>
> q;
dist[1] = 0;
q.push({0, 1});
ll ans = 0;
int cnt = 0;
while (!q.empty()) {
auto [w, u] = q.top();
q.pop();
if (vis[u]) continue;
vis[u] = 1;
ans += w;
cnt++;
for (auto [v, cost] : g[u]) {
if (!vis[v] && cost < dist[v]) {
dist[v] = cost;
q.push({dist[v], v});
}
}
}
if (cnt == n) {
cout << ans << '\n';
} else {
cout << "orz\n"; // 图不连通
}
return 0;
}
Kruskal算法(加边)
每次加边,要看两个端点是否在同一个连通分量里,如果不在则加边。
维护是否在同一个连通分量,使用并查集进行维护即可。
#include<bits/stdc++.h>
using namespace std;
int n,m;
const int N=400010;
struct p{
int u,v,w;
}e[N];
int fa[N];
int find(int u)
{
if(fa[u]==u) return u;
return fa[u]=find(fa[u]);
}
int cmp(p a,p b)
{
return a.w<b.w;
}
int ans,cnt;
void kruskal()
{
sort(e+1,e+1+m,cmp);
for(int i=1;i<=m;i++)
{
int a=find(e[i].u);
int b=find(e[i].v);
if(a!=b)
{
fa[b]=a;
cnt++;
ans+=e[i].w;
}
}
}
int a,b,c;
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=m;i++)
{
cin>>e[i].u>>e[i].v>>e[i].w;
}
kruskal();
if(cnt==n-1)
{
cout<<ans;
}else cout<<"orz";
return 0;
}
五、最短路
dijskra
普通版 \(O(n^2+m)\)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = 4e18;
int main() {
int n, m;
cin >> n >> m;
vector<vector<ll>> w(n + 1, vector<ll>(n + 1, INF));
for (int i = 1; i <= m; i++) {
int u, v;
ll cost;
cin >> u >> v >> cost;
w[u][v] = min(w[u][v], cost); // 重边只保留最短边
// 若是无向图,打开这一行:
// w[v][u] = min(w[v][u], cost);
}
vector<ll> dist(n + 1, INF);
vector<int> vis(n + 1, 0);
dist[1] = 0;
for (int i = 1; i <= n; i++) {
int u = -1;
// 找尚未确定、dist 最小的点
for (int j = 1; j <= n; j++) {
if (!vis[j] && (u == -1 || dist[j] < dist[u])) {
u = j;
}
}
if (u == -1 || dist[u] == INF) break;
vis[u] = 1;
// 用 u 松弛所有点
for (int v = 1; v <= n; v++) {
if (w[u][v] != INF) {
dist[v] = min(dist[v], dist[u] + w[u][v]);
}
}
}
for (int i = 1; i <= n; i++) {
cout << dist[i] << " ";
}
}
堆优化版 \(O(m\log n)\)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = 4e18;
int main() {
int n, m;
cin >> n >> m;
vector<vector<pair<int, ll>>> g(n + 1);
for (int i = 1; i <= m; i++) {
int u, v;
ll w;
cin >> u >> v >> w;
g[u].push_back({v, w});
// 若是无向图,打开这一行:
// g[v].push_back({u, w});
}
vector<ll> dist(n + 1, INF);
// {当前距离, 节点编号}
priority_queue<
pair<ll, int>,
vector<pair<ll, int>>,
greater<pair<ll, int>>
> q;
dist[1] = 0;
q.push({0, 1});
while (!q.empty()) {
auto [d, u] = q.top();
q.pop();
// 堆中旧状态,跳过
if (d != dist[u]) continue;
for (auto [v, w] : g[u]) {
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
q.push({dist[v], v});
}
}
}
for (int i = 1; i <= n; i++) {
cout << dist[i] << " ";
}
}
Floyd
\(O(n^3)\)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = 4e18;
int main() {
int n, m;
cin >> n >> m;
vector<vector<ll>> dis(n + 1, vector<ll>(n + 1, INF));
for (int i = 1; i <= n; i++) {
dis[i][i] = 0;
}
for (int i = 1; i <= m; i++) {
int u, v;
ll w;
cin >> u >> v >> w;
dis[u][v] = min(dis[u][v], w); // 重边取最小
// 无向图则加:
// dis[v][u] = min(dis[v][u], w);
}
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (dis[i][k] != INF && dis[k][j] != INF) {
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
}
}
}
// 若 dis[i][i] < 0,说明存在负环
for (int i = 1; i <= n; i++) {
if (dis[i][i] < 0) {
cout << "negative cycle\n";
}
}
// 例:输出 1 到所有点的距离
for (int i = 1; i <= n; i++) {
if (dis[1][i] == INF) cout << "INF ";
else cout << dis[1][i] << " ";
}
return 0;
}
六、拓扑排序
对有向无环图的节点进行排序。
方法是每次删除入度为0的点连接的边即可。
时间复杂度是 \(O(n+m)\)
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
vector<vector<int>> g(n + 1);
vector<int> in(n + 1, 0);
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v; // u 必须在 v 前面:u -> v
g[u].push_back(v);
in[v]++;
}
queue<int> q;
// 所有入度为 0 的点可以最先做
for (int i = 1; i <= n; i++) {
if (in[i] == 0) {
q.push(i);
}
}
vector<int> topo;
while (!q.empty()) {
int u = q.front();
q.pop();
topo.push_back(u);
for (int v : g[u]) {
in[v]--;
if (in[v] == 0) {
q.push(v);
}
}
}
if ((int)topo.size() != n) {
cout << "有环,不能拓扑排序\n";
} else {
for (int x : topo) {
cout << x << " ";
}
cout << '\n';
}
return 0;
}

浙公网安备 33010602011771号