Loading

Loading

图论基础复习(图存储、最小生成树、拓扑排序、最短路)

图论基础复习(图存储、最小生成树、拓扑排序、最短路)

一、图的基本性质

  • 对于有向图和无向图来说,所有顶点度数和=边数×2。

  • 对于有向图来说,所有顶点入度和=出度和=边数

  • n 个点的连通图至少需要 n-1 条边(链)

  • 连通分量:无向图中的极大连通子图

  • 在有向图中,如果任意两个点之间都有路径,那么该图为强连通图。n 个点至少要 n 条边才能是强连通图

  • 强连通分量:有向图的极大强连通子图

    image-20261004145859671

  • 生成树:在图中找出一棵树,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;
}
posted @ 2026-10-04 17:14  Miya555  阅读(9)  评论(0)    收藏  举报