「程式碼隨想錄演算法訓練營」第四十八天 | 圖論 part6

云雀AC了一整天發表於2024-08-27

目錄
  • 108. 冗餘連線
  • 109. 冗餘連線II

108. 冗餘連線

題目連結:https://kamacoder.com/problempage.php?pid=1181
文章講解:https://www.programmercarl.com/kamacoder/0108.冗餘連線.html
題目狀態:看題解

思路:

構建並查集,然後透過並查集來判斷節點,若當前這對節點(s, t)在同一個集合中,那麼輸出這對節點即可達到題目要求,否則,就將s和t合併到同一個集合中。

程式碼:

#include <iostream>
#include <vector>
using namespace std;
int n; // 節點數量
vector<int> father(1001, 0); // 按照節點大小範圍定義陣列

// 並查集初始化
void init() {
    for (int i = 0; i <= n; ++i) {
        father[i] = i;
    }
}
// 並查集裡尋根的過程
int find(int u) {
    return u == father[u] ? u : father[u] = find(father[u]);
}
// 判斷 u 和 v是否找到同一個根
bool isSame(int u, int v) {
    u = find(u);
    v = find(v);
    return u == v;
}
// 將v->u 這條邊加入並查集
void join(int u, int v) {
    u = find(u); // 尋找u的根
    v = find(v); // 尋找v的根
    if (u == v) return ; // 如果發現根相同,則說明在一個集合,不用兩個節點相連直接返回
    father[v] = u;
}

int main() {
    int s, t;
    cin >> n;
    init();
    for (int i = 0; i < n; i++) {
        cin >> s >> t;
        if (isSame(s, t)) {
            cout << s << " " << t << endl;
            return 0;
        } else {
            join(s, t);
        }
    }
}

109. 冗餘連線II

題目連結:https://kamacoder.com/problempage.php?pid=1182
文章講解:https://www.programmercarl.com/kamacoder/0109.冗餘連線II.html
題目狀態:看題解

思路:

函式

  1. init():

    • 初始化並查集,每個節點的父節點指向自身。
  2. find(int u):

    • 遞迴查詢節點 u 的根節點,並進行路徑壓縮。
  3. join(int u, int v):

    • 合併兩個節點所在的集合。
  4. same(int u, int v):

    • 判斷兩個節點是否在同一個集合中。
  5. getRemoveEdge(const vector<vector<int>>& edges):

    • 遍歷所有邊,找到構成有向環的邊並輸出。
  6. isTreeAfterRemoveEdge(const vector<vector<int>>& edges, int deleteEdge):

    • 刪除指定邊後檢查圖是否為樹。

main() 函式

  • 讀取節點數 n 和邊。
  • 計算每個節點的入度。
  • 找到入度為 2 的節點對應的邊,優先刪除最後出現的一條邊。
  • 如果刪除後形成樹,輸出該邊。
  • 如果沒有入度為 2 的節點,呼叫 getRemoveEdge 找到並輸出構成環的邊。

邏輯流程

  1. 處理入度為 2 的情況:

    • 找出入度為 2 的節點,嘗試刪除對應的邊,檢查是否能形成樹。
  2. 處理無入度為 2 的情況:

    • 如果沒有入度為 2 的節點,必然存在一個環,找到並刪除構成環的邊。

程式碼:

#include <iostream>
#include <vector>
using namespace std;
int n;
vector<int> father (1001, 0);
// 並查集初始化
void init() {
    for (int i = 1; i <= n; ++i) {
        father[i] = i;
    }
}
// 並查集裡尋根的過程
int find(int u) {
    return u == father[u] ? u : father[u] = find(father[u]);
}
// 將v->u 這條邊加入並查集
void join(int u, int v) {
    u = find(u);
    v = find(v);
    if (u == v) return ;
    father[v] = u;
}
// 判斷 u 和 v是否找到同一個根
bool same(int u, int v) {
    u = find(u);
    v = find(v);
    return u == v;
}

// 在有向圖裡找到刪除的那條邊,使其變成樹
void getRemoveEdge(const vector<vector<int>>& edges) {
    init(); // 初始化並查集
    for (int i = 0; i < n; i++) { // 遍歷所有的邊
        if (same(edges[i][0], edges[i][1])) { // 構成有向環了,就是要刪除的邊
            cout << edges[i][0] << " " << edges[i][1];
            return;
        } else {
            join(edges[i][0], edges[i][1]);
        }
    }
}

// 刪一條邊之後判斷是不是樹
bool isTreeAfterRemoveEdge(const vector<vector<int>>& edges, int deleteEdge) {
    init(); // 初始化並查集
    for (int i = 0; i < n; i++) {
        if (i == deleteEdge) continue;
        if (same(edges[i][0], edges[i][1])) { // 構成有向環了,一定不是樹
            return false;
        }
        join(edges[i][0], edges[i][1]);
    }
    return true;
}

int main() {
    int s, t;
    vector<vector<int>> edges;
    cin >> n;
    vector<int> inDegree(n + 1, 0); // 記錄節點入度
    for (int i = 0; i < n; i++) {
        cin >> s >> t;
        inDegree[t]++;
        edges.push_back({s, t});
    }

    vector<int> vec; // 記錄入度為2的邊(如果有的話就兩條邊)
    // 找入度為2的節點所對應的邊,注意要倒序,因為優先刪除最後出現的一條邊
    for (int i = n - 1; i >= 0; i--) {
        if (inDegree[edges[i][1]] == 2) {
            vec.push_back(i);
        }
    }
    // 情況一、情況二
    if (vec.size() > 0) {
        // 放在vec裡的邊已經按照倒敘放的,所以這裡就優先刪vec[0]這條邊
        if (isTreeAfterRemoveEdge(edges, vec[0])) {
            cout << edges[vec[0]][0] << " " << edges[vec[0]][1];
        } else {
            cout << edges[vec[1]][0] << " " << edges[vec[1]][1];
        }
        return 0;
    }

    // 處理情況三
    // 明確沒有入度為2的情況,那麼一定有有向環,找到構成環的邊返回就可以了
    getRemoveEdge(edges);
}

相關文章