แผนการเรียน
/

Graphs & DSU Foundations
การแทนกราฟและโครงสร้างเซตแยกจากกัน

Day 07 Morning: รากฐานของโครงสร้างข้อมูลความสัมพันธ์และอัลกอริทึมกลุ่มโหนด

สิ่งที่จะได้เรียนรู้

1
การแทนกราฟด้วย Adjacency Matrix และ List
2
นิยามพื้นฐาน Path, Cycle และ Connectivity
3
โครงสร้าง Disjoint Set Union (DSU) และการเพิ่มประสิทธิภาพ

แหล่งอ้างอิง: https://cses.fi/book/book.pdf

Graph คืออะไร ?

Graph เป็นโครงสร้างข้อมูลที่ใช้แทน ความสัมพันธ์ระหว่างสิ่งต่าง ๆ
V
Vertex (Node) คือจุดหรือสิ่งที่เราต้องการแทน
E
Edge คือเส้นที่เชื่อมระหว่าง Vertex

เขียนทางคณิตศาสตร์ได้ว่า G = (V, E)

ตัวอย่าง Graph ในโลกจริง

🗺
Road Network
เมือง = Vertex
ถนน = Edge
🌐
Internet Network
Router = Vertex
Connection = Edge
👥
Social Network
คน = Vertex
ความสัมพันธ์ = Edge
📦
Dependency Graph
Task = Vertex
Dependency = Edge

Graph ถูกใช้ใน Algorithm และ Competitive Programming จำนวนมาก

Directed vs Undirected Graph

Undirected Graph

  • Edge ไม่มีทิศทาง
  • A ↔ B ไปกลับได้
  • ใช้กับ network ทั่วไป

Directed Graph

  • Edge มีทิศทาง
  • A → B ไม่จำเป็นต้องย้อนกลับ
  • ใช้กับ dependency หรือ flow

Weighted Graph vs Unweighted Graph

Unweighted Graph

  • Edge ไม่มีค่าน้ำหนัก
  • ทุกเส้นมีค่าเท่ากัน
  • ใช้ใน BFS / connectivity

Weighted Graph

  • Edge มีค่า weight
  • เช่น ระยะทาง หรือ ค่าใช้จ่าย
  • ใช้กับ Shortest Path / MST

คำศัพท์พื้นฐานของ Graph

Path
เส้นทางจาก Vertex หนึ่งไปยังอีก Vertex ผ่าน Edge
Cycle
เส้นทางที่เริ่มต้นและจบที่ Vertex เดิม
Degree
จำนวน Edge ที่เชื่อมกับ Vertex
Connected Graph
ทุก Vertex สามารถเดินทางถึงกันได้

แนวคิดเหล่านี้จะถูกใช้ใน Graph Algorithms ต่อไป

ปัญหาในการเก็บข้อมูล Graph

เมื่อ Graph มีขนาดใหญ่ เราจะเก็บข้อมูลความสัมพันธ์อย่างไรให้มีประสิทธิภาพ?
Vertex
อาจมีจำนวนหลายพันหรือหลายแสน
Edge
ความสัมพันธ์ระหว่าง Vertex อาจมีจำนวนมาก
Memory
ถ้าเก็บข้อมูลไม่ดี โปรแกรมอาจใช้หน่วยความจำมากเกินไป

ดังนั้นเราจำเป็นต้องมี Graph Representation

ตัวอย่าง Input ของ Graph

Ubon Srisaket
Buriram Surin 
Buriram Yasothon
Surin Srisaket
Yasothon Ubon
5 4
1 3 
1 2
3 4
2 5

ความหมาย:

  • Vertex = 5
  • Edge = 4
  • มีเส้นเชื่อมระหว่าง Vertex ตามข้อมูล

คำถามคือ เราจะเก็บข้อมูลนี้ในโปรแกรมอย่างไร?

วิธีเก็บแบบพื้นฐาน (Naive Approach)

Idea
เก็บ Edge เป็น list ของคู่ (u, v)
Problem
การค้นหาว่า vertex เชื่อมกับใครทำได้ช้า
Example
ต้อง loop หา edge ทุกตัว

ทำให้ Algorithm บางตัวทำงานช้าลงมาก

วิธีเก็บ Graph ที่นิยมใช้

Adjacency Matrix

  • ใช้ Matrix ขนาด V × V
  • เช็ค edge ได้เร็ว
  • ใช้ memory มาก

Adjacency List

  • เก็บเพื่อนบ้านของแต่ละ vertex
  • ใช้ memory น้อยกว่า
  • นิยมใช้ใน competitive programming

เราจะเริ่มจากอะไรดี ?

ขั้นตอนต่อไปคือการเรียนรู้วิธีแทน Graph แต่ละแบบ
1
Adjacency Matrix
2
Adjacency List

ซึ่งจะเป็นพื้นฐานของ Graph Algorithms ทั้งหมด

Adjacency Matrix

วิธีแทน Graph โดยใช้ Matrix ขนาด V × V
  • Row = Vertex ต้นทาง
  • Column = Vertex ปลายทาง
  • ถ้ามี edge → ค่า = 1
  • ถ้าไม่มี edge → ค่า = 0

เหมาะกับ Graph ที่มี Edge จำนวนมาก

ตัวอย่าง Graph

//Vertices = {"Annie","Bennie","Cody","Danny","Eddy"}
Vertices = {1,2,3,4,5}

Edges
1 - 2
1 - 3
2 - 3
4 - 5

เราจะเปลี่ยน Graph นี้ให้เป็น Matrix

Matrix Representation

    1 2 3 4
1 [ 0 1 1 0 ]
2 [ 1 0 0 1 ]
3 [ 1 0 0 1 ]
4 [ 0 1 1 0 ]

ค่า 1 หมายถึง มี Edge เชื่อมกัน

คุณสมบัติของ Adjacency Matrix

Symmetric สำหรับ Undirected Graph
Diagonal ส่วนใหญ่จะเป็น 0
Check Edge ตรวจสอบ edge ได้ใน O(1)

C++ Implementation

const int MAXN = 100;

int adj[MAXN][MAXN];

int main() {

    int n, m;
    cin >> n >> m;

    for(int i = 0; i < m; i++){
        int u, v;
        cin >> u >> v;

        adj[u][v] = 1;
        adj[v][u] = 1;
    }
}

Time & Space Complexity

  • Memory = O(V²)
  • Check edge = O(1)
  • Iterate neighbors = O(V)

ถ้า V ใหญ่มาก Matrix จะใช้ memory มาก

ควรใช้ Adjacency Matrix เมื่อไร ?

Dense Graph
Graph ที่มี edge จำนวนมาก
Fast Edge Check
ต้องตรวจสอบ edge บ่อย
Small Graph
จำนวน vertex ไม่ใหญ่มาก

ต่อไปเราจะเรียนรู้วิธีที่นิยมใช้มากกว่าใน Competitive Programming

Adjacency List

วิธีแทน Graph โดยเก็บ เพื่อนบ้านของแต่ละ Vertex
  • แต่ละ Vertex จะมี list ของ Vertex ที่เชื่อมต่อ
  • ใช้โครงสร้างข้อมูล เช่น vector หรือ list
  • เป็นวิธีที่นิยมใช้ใน Competitive Programming

ตัวอย่าง Graph

Vertices = {1,2,3,4}

Edges
1 - 2
1 - 3
2 - 4
3 - 4

เราจะเก็บ Graph นี้ในรูปแบบ Adjacency List

Adjacency List Representation

1 : 2 3
2 : 1 4
3 : 1 4
4 : 2 3

แถวแต่ละแถวคือ Vertex และเพื่อนบ้าน

แนวคิดของ Adjacency List

Vertex 1 → [2,3]
Vertex 2 → [1,4]
Vertex 3 → [1,4]
Vertex 4 → [2,3]

แต่ละ Vertex จะเก็บเฉพาะ Edge ที่มีอยู่จริง

C++ Implementation

const int MAXN = 100;

vector adj[MAXN];

int main(){

    int n, m;
    cin >> n >> m;

    for(int i = 0; i < m; i++){

        int u, v;
        cin >> u >> v;

        adj[u].push_back(v);
        adj[v].push_back(u);

    }
}

การวนดูเพื่อนบ้าน

for(int v : adj[u]){
    cout << v << endl;
}

ใช้สำหรับ algorithm เช่น DFS / BFS

Time & Space Complexity

  • Memory = O(V + E)
  • Iterate neighbors = O(degree)
  • เหมาะกับ Sparse Graph

Matrix vs List

  • Adjacency Matrix → Memory = O(V²)
  • Adjacency List → Memory = O(V + E)
  • List เหมาะกับ Competitive Programming มากกว่า

ดังนั้น Algorithm ส่วนใหญ่จะใช้ Adjacency List

Graph Connectivity Problem

ถ้าเรามี Graph ขนาดใหญ่ เราจะรู้ได้อย่างไรว่า
  • Vertex u และ v อยู่ในกลุ่มเดียวกันหรือไม่?
  • Graph มีกี่กลุ่ม?
  • การเชื่อม edge ใหม่จะทำให้เกิด cycle หรือไม่?

ปัญหาเหล่านี้สามารถแก้ได้ด้วย Disjoint Set Union

Disjoint Set คืออะไร ?

โครงสร้างข้อมูลสำหรับจัดการ กลุ่มของสมาชิก
  • สมาชิกแต่ละตัวอยู่ใน set ใด set หนึ่ง
  • set แต่ละชุดไม่ทับซ้อนกัน
  • สามารถรวม set เข้าด้วยกันได้

ตัวอย่าง Disjoint Sets

Set 1 : {1,2,3}
Set 2 : {4,5}
Set 3 : {6}

แต่ละ element จะมี representative

Operations ใน DSU

  • Find(x) → หาตัวแทนของ set
  • Union(a,b) → รวมสอง set เข้าด้วยกัน

ใช้ตรวจสอบว่า element อยู่ใน set เดียวกันหรือไม่

การแทน DSU ด้วย Tree

1
|
2
|
3

Root ของ tree คือ representative ของ set

Initialization

const int MAXN = 100005;

int parent[MAXN];

void make_set(int n){

    for(int i=1;i<=n;i++)
        parent[i] = i;

}

เริ่มต้นให้ทุก node เป็น root ของตัวเอง

ใช้หา root ของ set

Find Operation

int find(int x){

    if(parent[x] == x)
        return x;

    return find(parent[x]);

}

ใช้หา root ของ set

Union Operation

void unite(int a, int b){

    a = find(a);
    b = find(b);

    if(a != b)
        parent[b] = a;

}

ใช้รวมสอง set ให้เป็น set เดียวกัน

ปัญหาของ DSU แบบพื้นฐาน

ถ้าเราใช้ Union แบบธรรมดา Tree อาจมีความลึกมาก
1
|
2
|
3
|
4
|
5

การทำ find() อาจใช้เวลาถึง O(N)

วิธีแก้ปัญหา

  • Path Compression
  • Union by Rank / Size

เทคนิคเหล่านี้ทำให้ DSU ทำงานได้เร็วมาก

Path Compression

ปรับโครงสร้าง Tree ระหว่างการ Find
ก่อน Find

1
|
2
|
3
|
4

หลังจาก Find จะทำให้ node ชี้ไปที่ root โดยตรง

Path Compression Implementation

int find(int x){

    if(parent[x] != x)
        parent[x] = find(parent[x]);

    return parent[x];

}

ทำให้ Tree มีความลึกน้อยลง

Union by Rank

รวม Tree โดยให้ Tree ที่เล็กกว่าชี้ไปหา Tree ที่ใหญ่กว่า
  • ลดความสูงของ Tree
  • ทำให้ find() เร็วขึ้น

Union by Rank Implementation

int parent[MAXN];
int rank[MAXN];

void unite(int a, int b){

    a = find(a);
    b = find(b);

    if(a != b){

        if(rank[a] < rank[b])
            swap(a,b);

        parent[b] = a;

        if(rank[a] == rank[b])
            rank[a]++;

    }

}

Complexity หลัง Optimization

  • Find ≈ O(α(N))
  • Union ≈ O(α(N))
  • α(N) คือ Inverse Ackermann Function

ซึ่งถือว่า เกือบ O(1) ในทางปฏิบัติ

ตัวอย่างข้อมูลนำเข้า (dsu01.in)

6 3
1 2
2 3
4 5

N = 6 (จำนวน node)

M = 3 (จำนวน union operations)

operations:

union(1,2)
union(2,3)
union(4,5)

ผลลัพธ์หลังทำ Union

Set 1 : {1,2,3}
Set 2 : {4,5}
Set 3 : {6}

node ที่มี root เดียวกัน จะอยู่ใน set เดียวกัน

ตัวอย่างโปรแกรม DSU แบบสมบูรณ์

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;

int parent[MAXN];
int rankv[MAXN];

int find_set(int x){

    if(parent[x] != x)
        parent[x] = find_set(parent[x]);

    return parent[x];
}

void unite(int a,int b){

    a = find_set(a);
    b = find_set(b);

    if(a != b){

        if(rankv[a] < rankv[b])
            swap(a,b);

        parent[b] = a;

        if(rankv[a] == rankv[b])
            rankv[a]++;
    }
}

int main(){

    int N,M;
    cin >> N >> M;

    for(int i=1;i<=N;i++){
        parent[i] = i;
        rankv[i] = 0;
    }

    for(int i=0;i<M;i++){
        int a,b;
        cin >> a >> b;
        unite(a,b);
    }

    map<int, vector<int>> groups;

    for(int i=1;i<=N;i++){
        int r = find_set(i);
        groups[r].push_back(i);
    }

    int id = 1;

    for(auto g : groups){

        cout << "Set " << id++ << " : ";

        for(int x : g.second)
            cout << x << " ";

        cout << endl;
    }

}

Visual Tree of DSU

        1
       / \
      2   3
     /
    4
   /
  5

โครงสร้างของ DSU สามารถมองเป็น ต้นไม้ (Tree) โดย root คือ representative ของ set

Before Path Compression

find(5)

Traversal Path

5 → 4 → 2 → 1

ถ้า tree ลึกมาก การทำ find() จะใช้เวลามากขึ้น

After Path Compression

        1
     / / | \
    2 3  4  5

หลังจากเรียก find() node ทุกตัวจะชี้ตรงไปที่ root

ทำให้การค้นหาครั้งต่อไปเร็วขึ้นมาก