数据结构 学习 图 2025年6月14日 12点57分

发布于:2025-06-15 ⋅ 阅读:(15) ⋅ 点赞:(0)

搜索算法

深度优先搜索

一种用于遍历或搜索树或图的算法。它沿着树的深度遍历树的节点,尽可能深的搜索树的分支。

DFS核心思想

  • 深度优先:尽可能深地搜索树的分支

  • 回溯思想:当节点v的所在边都已被探寻过,搜索将回溯到发现节点v的那条边的起始节点

  • 递归实现:通常用递归方式自然地实现DFS

  • void dfs(Node* node, vector<bool>& visited) {
        // 标记当前节点为已访问
        visited[node->val] = true;
        cout << node->val << " ";
        
        // 访问所有相邻节点
        for (Node* neighbor : node->neighbors) {
            if (!visited[neighbor->val]) {
                dfs(neighbor, visited);
            }
        }
    }
    
    递归实现

    经典运用

  • //图的连通分量
    int countComponents(int n, vector<vector<int>>& edges) {
        vector<vector<int>> graph(n);
        vector<bool> visited(n, false);
        int components = 0;
        
        // 构建邻接表
        for (auto& edge : edges) {
            graph[edge[0]].push_back(edge[1]);
            graph[edge[1]].push_back(edge[0]);
        }
        
        for (int i = 0; i < n; ++i) {
            if (!visited[i]) {
                components++;
                dfs(i, graph, visited);
            }
        }
        
        return components;
    }
    
    void dfs(int node, vector<vector<int>>& graph, vector<bool>& visited) {
        visited[node] = true;
        for (int neighbor : graph[node]) {
            if (!visited[neighbor]) {
                dfs(neighbor, graph, visited);
            }
        }
    }

    DFS是解决许多算法问题的强大工具,掌握其核心思想和各种优化技巧对算法能力提升至关重要。

广度优先搜索

一种用于遍历或搜索树或图的算法。它从根节点开始,先访问所有相邻节点,然后再依次访问这些节点的相邻节点,以此类推。

BFS核心思想

  • 层级遍历:按照距离起始节点的层级依次访问

  • 队列结构:使用队列来存储待访问节点

  • 最短路径:在无权图中能找到最短路径

#include <queue>
#include <vector>

using namespace std;

//标准实现 使用队列
void bfs(Node* start) {
    if (!start) return;
    
    queue<Node*> q;
    vector<bool> visited(NODES_SIZE, false);
    
    q.push(start);
    visited[start->val] = true;
    
    while (!q.empty()) {
        Node* current = q.front();
        q.pop();
        cout << current->val << " "; // 处理当前节点
        
        // 访问所有相邻节点
        for (Node* neighbor : current->neighbors) {
            if (!visited[neighbor->val]) {
                visited[neighbor->val] = true;
                q.push(neighbor);
            }
        }
    }
}

最短路径(无权图)

int shortestPath(Node* start, Node* end) {
    if (!start || !end) return -1;
    if (start == end) return 0;
    
    queue<Node*> q;
    unordered_map<Node*, int> distance;
    
    q.push(start);
    distance[start] = 0;
    
    while (!q.empty()) {
        Node* current = q.front();
        q.pop();
        
        for (Node* neighbor : current->neighbors) {
            if (!distance.count(neighbor)) {
                distance[neighbor] = distance[current] + 1;
                
                if (neighbor == end) {
                    return distance[neighbor];
                }
                
                q.push(neighbor);
            }
        }
    }
    
    return -1; // 不可达
}

 

注意事项

  1. 访问标记:必须在入队时标记,而非出队时

  2. 队列大小:处理层级时需要先保存当前队列大小

  3. 边界检查:网格类问题注意边界条件

  4. 状态表示:复杂状态需要设计良好的哈希函数

BFS是解决最短路径问题和层级遍历问题的利器,掌握其核心思想和各种变种对算法能力提升至关重要。

图的概念

有向图 (强连通分量)

强连通分量是有向图中的一个重要概念,指有向图中任意两个顶点都互相可达的最大子图。理解强连通分量对于分析有向图的结构至关重要。

基本概念

1. 强连通分量定义

  • 强连通:在有向图中,如果从顶点u到v有一条路径,且从v到u也有一条路径,则称u和v强连通

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

2. 相关性质

  • 每个顶点属于且仅属于一个强连通分量

  • 将每个强连通分量缩为一个顶点,得到的有向图是一个有向无环图(DAG)

  • 应用场景:编译器优化、社交网络分析、电路设计等

// Kosaraju算法(两次DFS) 实现
#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>

using namespace std;

class Graph {
    int V;
    vector<vector<int>> adj;
    vector<vector<int>> revAdj;
    
    void fillOrder(int v, vector<bool>& visited, stack<int>& st) {
        visited[v] = true;
        for (int u : adj[v]) {
            if (!visited[u]) {
                fillOrder(u, visited, st);
            }
        }
        st.push(v);
    }
    
    void DFSUtil(int v, vector<bool>& visited, vector<int>& component) {
        visited[v] = true;
        component.push_back(v);
        
        for (int u : revAdj[v]) {
            if (!visited[u]) {
                DFSUtil(u, visited, component);
            }
        }
    }
    
public:
    Graph(int V) : V(V), adj(V), revAdj(V) {}
    
    void addEdge(int v, int w) {
        adj[v].push_back(w);
        revAdj[w].push_back(v);
    }
    
    vector<vector<int>> findSCCs() {
        stack<int> st;
        vector<bool> visited(V, false);
        
        // 第一次DFS,填充栈
        for (int i = 0; i < V; i++) {
            if (!visited[i]) {
                fillOrder(i, visited, st);
            }
        }
        
        // 重置visited数组
        fill(visited.begin(), visited.end(), false);
        
        vector<vector<int>> sccs;
        
        // 第二次DFS,按照栈的顺序处理逆图
        while (!st.empty()) {
            int v = st.top();
            st.pop();
            
            if (!visited[v]) {
                vector<int> component;
                DFSUtil(v, visited, component);
                sccs.push_back(component);
            }
        }
        
        return sccs;
    }
};
// Tarjan算法(一次DFS)
#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>

using namespace std;

class Graph {
    int V;
    vector<vector<int>> adj;
    
    void tarjanSCCUtil(int u, vector<int>& disc, vector<int>& low, 
                      stack<int>& st, vector<bool>& inStack, 
                      vector<vector<int>>& sccs, int& time) {
        disc[u] = low[u] = ++time;
        st.push(u);
        inStack[u] = true;
        
        for (int v : adj[u]) {
            if (disc[v] == -1) { // v未被访问
                tarjanSCCUtil(v, disc, low, st, inStack, sccs, time);
                low[u] = min(low[u], low[v]);
            } else if (inStack[v]) { // v在栈中
                low[u] = min(low[u], disc[v]);
            }
        }
        
        // 发现强连通分量
        if (low[u] == disc[u]) {
            vector<int> component;
            while (st.top() != u) {
                int v = st.top();
                component.push_back(v);
                inStack[v] = false;
                st.pop();
            }
            component.push_back(u);
            inStack[u] = false;
            st.pop();
            sccs.push_back(component);
        }
    }
    
public:
    Graph(int V) : V(V), adj(V) {}
    
    void addEdge(int v, int w) {
        adj[v].push_back(w);
    }
    
    vector<vector<int>> tarjanSCC() {
        vector<int> disc(V, -1), low(V, -1);
        vector<bool> inStack(V, false);
        stack<int> st;
        vector<vector<int>> sccs;
        int time = 0;
        
        for (int i = 0; i < V; i++) {
            if (disc[i] == -1) {
                tarjanSCCUtil(i, disc, low, st, inStack, sccs, time);
            }
        }
        
        return sccs;
    }
};

实际应用: 有向图的缩点(将SCC缩为单个顶点)

Graph buildCondensedGraph(Graph& g, vector<vector<int>>& sccs) {
    // 创建顶点映射:原始顶点 -> 缩点后的顶点编号
    vector<int> vertexToComponent(g.V);
    for (int i = 0; i < sccs.size(); i++) {
        for (int v : sccs[i]) {
            vertexToComponent[v] = i;
        }
    }
    
    // 构建缩点后的图
    Graph condensed(sccs.size());
    unordered_set<string> edges; // 避免重复边
    
    for (int u = 0; u < g.V; u++) {
        for (int v : g.adj[u]) {
            int compU = vertexToComponent[u];
            int compV = vertexToComponent[v];
            if (compU != compV) {
                string edge = to_string(compU) + "-" + to_string(compV);
                if (!edges.count(edge)) {
                    condensed.addEdge(compU, compV);
                    edges.insert(edge);
                }
            }
        }
    }
    
    return condensed;
}

强连通分量分析是图算法中的重要工具,掌握Kosaraju和Tarjan这两种经典算法,能够有效解决许多有向图相关问题。

无向图 (双连通分量)

双连通分量是无向图中的一个重要概念,指没有"关节点"(割点)的最大连通子图。理解双连通分量对于分析网络可靠性、电路设计等问题至关重要。

基本概念

1. 双连通分量定义

  • 关节点(割点):删除该顶点会增加图的连通分量数量

  • 桥(割边):删除该边会增加图的连通分量数量

  • 双连通分量:不含关节点的极大连通子图

2. 相关性质

  • 任意两个顶点之间至少存在两条不相交的路径

  • 双连通分量之间通过关节点连接

  • 应用场景:网络容错分析、交通网络规划、电路板设计

// Tarjan算法求双连通分量
#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>

using namespace std;

class Graph {
    int V;
    vector<vector<int>> adj;
    
    void BCCUtil(int u, vector<int>& disc, vector<int>& low, 
                stack<pair<int, int>>& st, vector<bool>& isAP,
                vector<vector<pair<int, int>>>& bccs, int& time, int parent = -1) {
        int children = 0;
        disc[u] = low[u] = ++time;
        
        for (int v : adj[u]) {
            if (disc[v] == -1) { // v未被访问
                children++;
                st.push({u, v});
                
                BCCUtil(v, disc, low, st, isAP, bccs, time, u);
                
                low[u] = min(low[u], low[v]);
                
                // u是关节点(两种情况)
                if ((parent == -1 && children > 1) || (parent != -1 && low[v] >= disc[u])) {
                    isAP[u] = true;
                    // 输出双连通分量
                    vector<pair<int, int>> component;
                    while (st.top() != make_pair(u, v)) {
                        component.push_back(st.top());
                        st.pop();
                    }
                    component.push_back(st.top());
                    st.pop();
                    bccs.push_back(component);
                }
            } 
            else if (v != parent && disc[v] < disc[u]) { // 处理回边
                low[u] = min(low[u], disc[v]);
                st.push({u, v});
            }
        }
    }
    
public:
    Graph(int V) : V(V), adj(V) {}
    
    void addEdge(int v, int w) {
        adj[v].push_back(w);
        adj[w].push_back(v);
    }
    
    pair<vector<bool>, vector<vector<pair<int, int>>>> findBCCs() {
        vector<int> disc(V, -1), low(V, -1);
        vector<bool> isAP(V, false);
        stack<pair<int, int>> st;
        vector<vector<pair<int, int>>> bccs;
        int time = 0;
        
        for (int i = 0; i < V; i++) {
            if (disc[i] == -1) {
                BCCUtil(i, disc, low, st, isAP, bccs, time);
                
                // 处理剩余边
                vector<pair<int, int>> component;
                bool hasEdge = false;
                while (!st.empty()) {
                    hasEdge = true;
                    component.push_back(st.top());
                    st.pop();
                }
                if (hasEdge) {
                    bccs.push_back(component);
                }
            }
        }
        
        return {isAP, bccs};
    }
};

//查找桥(割边)算法
vector<pair<int, int>> findBridges(Graph& g) {
    vector<int> disc(g.V, -1), low(g.V, -1);
    vector<pair<int, int>> bridges;
    int time = 0;
    
    for (int i = 0; i < g.V; i++) {
        if (disc[i] == -1) {
            bridgeUtil(i, -1, disc, low, bridges, time, g);
        }
    }
    
    return bridges;
}

void bridgeUtil(int u, int parent, vector<int>& disc, vector<int>& low,
               vector<pair<int, int>>& bridges, int& time, Graph& g) {
    disc[u] = low[u] = ++time;
    
    for (int v : g.adj[u]) {
        if (disc[v] == -1) { // v未被访问
            bridgeUtil(v, u, disc, low, bridges, time, g);
            low[u] = min(low[u], low[v]);
            
            // 发现桥
            if (low[v] > disc[u]) {
                bridges.push_back({u, v});
            }
        } 
        else if (v != parent) { // 处理回边
            low[u] = min(low[u], disc[v]);
        }
    }
}

 无向图的双连通分量分析是图论中的重要工具,掌握Tarjan算法及其变种能够有效解决网络可靠性、关键节点识别等问题。理解算法的核心思想和实现细节对解决实际问题至关重要

        回路

欧拉回路

欧拉回路和欧拉路径是图论中的重要概念,分别指图中经过每条边恰好一次并回到起点的回路,和经过每条边恰好一次的路径。

基本概念

1. 定义

  • 欧拉回路:图中经过每条边恰好一次并回到起点的闭合路径

  • 欧拉路径:图中经过每条边恰好一次的路径(不一定闭合)

  • 欧拉图:存在欧拉回路的图

  • 半欧拉图:存在欧拉路径但不存在欧拉回路的图

2. 判定条件

对于无向图:
类型 连通性 顶点度数条件
欧拉回路 连通 所有顶点度数为偶数
欧拉路径 连通 恰好两个顶点度数为奇数(起点和终点)
对于有向图:
类型 连通性 顶点度数条件
欧拉回路 强连通 每个顶点入度等于出度
欧拉路径 单向连通 一个顶点出度=入度+1(起点),一个顶点入度=出度+1(终点),其余入度=出度

算法实现

//Hierholzer算法(寻找欧拉回路/路径)
#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>

using namespace std;

class Graph {
    int V;
    vector<vector<int>> adj;
    
public:
    Graph(int V) : V(V), adj(V) {}
    
    void addEdge(int u, int v) {
        adj[u].push_back(v);
    }
    
    void removeEdge(int u, int v) {
        auto it = find(adj[u].begin(), adj[u].end(), v);
        if (it != adj[u].end()) {
            adj[u].erase(it);
        }
    }
    
    vector<int> findEulerianCircuit() {
        vector<int> circuit;
        stack<int> currPath;
        int currVertex = 0; // 可以选择任意顶点作为起点
        
        currPath.push(currVertex);
        
        while (!currPath.empty()) {
            if (!adj[currVertex].empty()) {
                currPath.push(currVertex);
                int nextVertex = adj[currVertex].back();
                adj[currVertex].pop_back();
                currVertex = nextVertex;
            } else {
                circuit.push_back(currVertex);
                currVertex = currPath.top();
                currPath.pop();
            }
        }
        
        reverse(circuit.begin(), circuit.end());
        return circuit;
    }
};

欧拉回路和路径在DNA测序、网络路由、物流配送等领域有广泛应用。掌握Hierholzer算法及其实现细节,能够有效解决许多实际问题。理解欧拉图的性质和判定条件是应用这些算法的基础。 

哈米尔顿回路 

哈密尔顿回路是图论中的一个重要概念,指经过图中每个顶点恰好一次并最终回到起点的闭合路径。与欧拉回路(经过每条边一次)不同,哈密尔顿回路关注的是顶点的遍历。

基本概念

1. 定义

  • 哈密尔顿路径:经过图中每个顶点恰好一次的路径

  • 哈密尔顿回路:闭合的哈密尔顿路径(起点=终点)

  • 哈密尔顿图:包含哈密尔顿回路的图

2. 判定条件

哈密尔顿回路的判定是NP完全问题,没有已知的多项式时间算法。但有一些充分条件和必要条件:

充分条件:
  • Dirac定理:对于n≥3的简单图,若每个顶点度数≥n/2,则是哈密尔顿图

  • Ore定理:对于n≥3的简单图,若任意两个不相邻顶点u,v满足deg(u)+deg(v)≥n,则是哈密尔顿图

必要条件:
  • 图必须是连通的

  • 没有度数为1的顶点

  • 删除任意k个顶点后,剩余子图的连通分量不超过k个

算法实现

//回溯法(基础实现)
#include <iostream>
#include <vector>

using namespace std;

class Graph {
    int V;
    vector<vector<int>> adj;
    
    bool hamCycleUtil(vector<int>& path, int pos) {
        if (pos == V) {
            // 检查最后一个顶点是否与第一个顶点相连
            return adj[path[pos-1]][path[0]] == 1;
        }
        
        for (int v = 1; v < V; v++) {
            if (isSafe(v, path, pos)) {
                path[pos] = v;
                
                if (hamCycleUtil(path, pos+1))
                    return true;
                
                path[pos] = -1; // 回溯
            }
        }
        return false;
    }
    
    bool isSafe(int v, vector<int>& path, int pos) {
        // 检查当前顶点是否与上一个顶点相连
        if (adj[path[pos-1]][v] == 0)
            return false;
            
        // 检查是否已经包含在路径中
        for (int i = 0; i < pos; i++)
            if (path[i] == v)
                return false;
                
        return true;
    }
    
public:
    Graph(int V) : V(V), adj(V, vector<int>(V, 0)) {}
    
    void addEdge(int u, int v) {
        adj[u][v] = 1;
        adj[v][u] = 1;
    }
    
    bool hamCycle() {
        vector<int> path(V, -1);
        path[0] = 0; // 从顶点0开始
        
        if (!hamCycleUtil(path, 1)) {
            cout << "不存在哈密尔顿回路" << endl;
            return false;
        }
        
        printSolution(path);
        return true;
    }
    
    void printSolution(vector<int>& path) {
        cout << "哈密尔顿回路: ";
        for (int i = 0; i < V; i++)
            cout << path[i] << " ";
        cout << path[0] << endl;
    }
};

哈密尔顿回路问题在运筹学、电路设计、生物信息学等领域有重要应用。虽然它是NP难问题,但通过合理的算法选择和优化技巧,可以有效地解决中小规模的实际问题。

并查集

并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。它支持两种基本操作:

  • 查找(Find):确定元素属于哪个子集

  • 合并(Union):将两个子集合并成一个集合

基本概念

1. 核心操作

操作 功能描述
makeSet(x) 创建仅包含x的新集合
find(x) 返回x所在集合的代表元素
union(x, y) 合并x和y所在的集合

2. 关键思想

  • 代表元:每个集合选择一个元素作为代表

  • 树形结构:用树表示集合,根节点为代表元

  • 路径压缩:优化查找操作

  • 按秩合并:优化合并操作

//基础实现(带路径压缩和按秩合并)
class DSU {
private:
    vector<int> parent;
    vector<int> rank; // 秩(树高度的上界)
    
public:
    DSU(int n) {
        parent.resize(n);
        rank.resize(n, 0);
        // 初始化每个元素为自己的父节点
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
    }
    
    // 查找根节点(带路径压缩)
    int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]); // 路径压缩
        }
        return parent[x];
    }
    
    // 合并两个集合(按秩合并)
    void unionSets(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);
        
        if (rootX == rootY) return; // 已在同一集合
        
        // 按秩合并
        if (rank[rootX] < rank[rootY]) {
            parent[rootX] = rootY;
        } else if (rank[rootX] > rank[rootY]) {
            parent[rootY] = rootX;
        } else {
            parent[rootY] = rootX;
            rank[rootX]++;
        }
    }
};

 并查集是解决动态连通性问题的利器,在社交网络分析、图像处理、网络连接等领域有广泛应用。掌握其核心思想和优化技巧,能够高效解决许多实际问题。


网站公告

今日签到

点亮在社区的每一天
去签到