프로그래밍/알고리즘

[알고리즘]그래프 알고리즘🌟🤔

다다면체 2024. 12. 23. 11:00
728x90
반응형
반응형

그래프 알고리즘은 복잡한 네트워크나 경로 문제를 해결하는 데 없어서는 안 될 필수 도구예요! 🚀 이번에는 그래프 탐색부터 최단 경로, 최소 신장 트리, 위상 정렬, 강결합 컴포넌트까지 하나씩 찬찬히 살펴보아요! 🌟


1. DFS와 BFS

그래프를 탐색할 때 사용하는 대표적인 방법 두 가지! 바로 DFS(깊이 우선 탐색)와 BFS(너비 우선 탐색)입니다. 🤔 그 차이를 알아볼까요?

1.1 DFS (깊이 우선 탐색)

  • 원리: 이름 그대로 "깊이"를 먼저 탐색해요. 한쪽으로 끝까지 파고들다가 막히면 돌아옵니다.
  • 특징: 재귀적으로 호출하거나 스택으로 구현합니다.
  • 활용: 경로 찾기, 퍼즐 풀이 등 깊이 중심의 문제 해결! 🌱
public void dfs(Node node, Set<Node> visited) {
    if (visited.contains(node)) return;
    visited.add(node);
    System.out.println(node.value);
    for (Node neighbor : node.neighbors) {
        dfs(neighbor, visited);
    }
}

1.2 BFS (너비 우선 탐색)

  • 원리: "너비"를 기준으로, 같은 레벨에 있는 노드를 차례로 탐색합니다.
  • 특징: 큐를 사용해서 구현합니다.
  • 활용: 최단 경로 찾기, 레벨 기반 탐색에 적합! 🚶‍♀️
public void bfs(Node start) {
    Queue<Node> queue = new LinkedList<>();
    Set<Node> visited = new HashSet<>();
    queue.add(start);
    visited.add(start);
    while (!queue.isEmpty()) {
        Node node = queue.poll();
        System.out.println(node.value);
        for (Node neighbor : node.neighbors) {
            if (!visited.contains(neighbor)) {
                queue.add(neighbor);
                visited.add(neighbor);
            }
        }
    }
}

2. 최단 경로 알고리즘

최단 경로를 찾는 건 그래프 문제에서 흔한 주제입니다. 🛤️ 여기선 대표적인 세 가지 알고리즘을 알아볼게요!

2.1 다익스트라 알고리즘

  • 원리: 특정 시작점에서 다른 모든 노드까지의 최단 거리를 계산합니다.
  • 특징: 가중치가 음수면 사용 불가. 힙을 사용하면 더 빠릅니다!
  • 시간 복잡도: O(V + E log V)
public void dijkstra(Graph graph, Node start) {
    PriorityQueue<Node> pq = new PriorityQueue<>();
    Map<Node, Integer> distances = new HashMap<>();
    for (Node node : graph.nodes) {
        distances.put(node, Integer.MAX_VALUE);
    }
    distances.put(start, 0);
    pq.add(start);

    while (!pq.isEmpty()) {
        Node current = pq.poll();
        for (Edge edge : current.edges) {
            Node neighbor = edge.to;
            int newDist = distances.get(current) + edge.weight;
            if (newDist < distances.get(neighbor)) {
                distances.put(neighbor, newDist);
                pq.add(neighbor);
            }
        }
    }
}

2.2 벨만-포드 알고리즘

  • 원리: 모든 간선을 반복적으로 확인하며 최단 경로를 갱신합니다.
  • 특징: 음수 가중치도 처리 가능!
  • 시간 복잡도: O(V * E)
public boolean bellmanFord(Graph graph, Node start) {
    Map<Node, Integer> distances = new HashMap<>();
    for (Node node : graph.nodes) {
        distances.put(node, Integer.MAX_VALUE);
    }
    distances.put(start, 0);

    for (int i = 0; i < graph.nodes.size() - 1; i++) {
        for (Edge edge : graph.edges) {
            Node u = edge.from;
            Node v = edge.to;
            int weight = edge.weight;
            if (distances.get(u) != Integer.MAX_VALUE && distances.get(u) + weight < distances.get(v)) {
                distances.put(v, distances.get(u) + weight);
            }
        }
    }

    for (Edge edge : graph.edges) {
        Node u = edge.from;
        Node v = edge.to;
        int weight = edge.weight;
        if (distances.get(u) != Integer.MAX_VALUE && distances.get(u) + weight < distances.get(v)) {
            return false; // 음수 사이클 존재
        }
    }
    return true;
}

2.3 플로이드-워셜 알고리즘

  • 원리: 모든 노드 쌍 간의 최단 경로를 계산합니다.
  • 특징: 동적 계획법 기반으로, 2차원 배열을 활용합니다.
  • 시간 복잡도: O(V³)
public void floydWarshall(int[][] graph) {
    int V = graph.length;
    int[][] dist = new int[V][V];

    for (int i = 0; i < V; i++) {
        for (int j = 0; j < V; j++) {
            dist[i][j] = graph[i][j];
        }
    }

    for (int k = 0; k < V; k++) {
        for (int i = 0; i < V; i++) {
            for (int j = 0; j < V; j++) {
                if (dist[i][k] + dist[k][j] < dist[i][j]) {
                    dist[i][j] = dist[i][k] + dist[k][j];
                }
            }
        }
    }
}

3. 최소 신장 트리 (MST)

모든 노드를 연결하는 최소 비용의 간선 집합을 찾는 문제입니다. 🌳

3.1 크루스칼 알고리즘

  • 원리: 간선을 가중치 오름차순으로 정렬 후, 사이클을 생성하지 않는 간선만 선택합니다.
  • 특징: 유니온-파인드(Union-Find)를 활용해 사이클을 검사합니다.
  • 시간 복잡도: O(E log E)
public int kruskal(Graph graph) {
    List<Edge> edges = new ArrayList<>(graph.edges);
    edges.sort(Comparator.comparingInt(edge -> edge.weight));
    UnionFind uf = new UnionFind(graph.nodes.size());
    int mstWeight = 0;

    for (Edge edge : edges) {
        if (uf.union(edge.from.id, edge.to.id)) {
            mstWeight += edge.weight;
        }
    }
    return mstWeight;
}

3.2 프림 알고리즘

  • 원리: 하나의 시작 노드에서 연결된 최소 가중치 간선을 선택하며 트리를 확장합니다.
  • 특징: 힙을 사용하면 효율성이 높아집니다.
  • 시간 복잡도: O(E log V)
public int prim(Graph graph) {
    PriorityQueue<Edge> pq = new PriorityQueue<>(Comparator.comparingInt(edge -> edge.weight));
    Set<Node> visited = new HashSet<>();
    int mstWeight = 0;

    Node start = graph.nodes.get(0);
    visited.add(start);
    pq.addAll(start.edges);

    while (!pq.isEmpty()) {
        Edge edge = pq.poll();
        if (!visited.contains(edge.to)) {
            visited.add(edge.to);
            mstWeight += edge.weight;
            pq.addAll(edge.to.edges);
        }
    }
    return mstWeight;
}

4. 위상 정렬

의존성이 있는 작업들을 순서대로 정리하려면 위상 정렬이 필요합니다! 📈

  • 원리: 진입 차수가 0인 노드부터 처리하며 정렬을 완성합니다.
  • 활용: 작업 스케줄링, 의존성 그래프 등.
public List<Node> topologicalSort(Graph graph) {
    List<Node> order = new ArrayList<>();
    Queue<Node> queue = new LinkedList<>();
    Map<Node, Integer> inDegree = new HashMap<>();

    for (Node node : graph.nodes) {
        inDegree.put(node, 0);
    }
    for (Node node : graph.nodes) {
        for (Node neighbor : node.neighbors) {
            inDegree.put(neighbor, inDegree.get(neighbor) + 1);
        }
    }

    for (Node node : inDegree.keySet()) {
        if (inDegree.get(node) == 0) {
            queue.add(node);
        }
    }

    while (!queue.isEmpty()) {
        Node current = queue.poll();
        order.add(current);
        for (Node neighbor : current.neighbors) {
            inDegree.put(neighbor, inDegree.get(neighbor) - 1);
            if (inDegree.get(neighbor) == 0) {
                queue.add(neighbor);
            }
        }
    }

    return order;
}

5. 강결합 컴포넌트 (SCC)

강결합 컴포넌트는 방향 그래프에서 서로 강하게 연결된 노드들의 집합을 뜻해요. 🎯

5.1 코사라주 알고리즘

  • 원리: 그래프를 뒤집은 후, 두 번의 DFS로 SCC를 찾습니다.
  • 특징: 간단한 구현으로도 정확한 결과를 얻을 수 있습니다.
import java.util.*;

public class KosarajuSCC {
    private int vertices;
    private List<List<Integer>> adjList = new ArrayList<>();
    private List<List<Integer>> reversedList = new ArrayList<>();
    private boolean[] visited;

    public KosarajuSCC(int vertices) {
        this.vertices = vertices;
        for (int i = 0; i < vertices; i++) {
            adjList.add(new ArrayList<>());
            reversedList.add(new ArrayList<>());
        }
        visited = new boolean[vertices];
    }

    public void addEdge(int from, int to) {
        adjList.get(from).add(to);
        reversedList.get(to).add(from);
    }

    private void dfs(int node, Stack<Integer> stack) {
        visited[node] = true;
        for (int neighbor : adjList.get(node)) {
            if (!visited[neighbor]) {
                dfs(neighbor, stack);
            }
        }
        stack.push(node);
    }

    private void dfsReversed(int node, List<Integer> scc) {
        visited[node] = true;
        scc.add(node);
        for (int neighbor : reversedList.get(node)) {
            if (!visited[neighbor]) {
                dfsReversed(neighbor, scc);
            }
        }
    }

    public List<List<Integer>> findSCCs() {
        Stack<Integer> stack = new Stack<>();
        Arrays.fill(visited, false);

        // Step 1: Perform DFS and fill the stack
        for (int i = 0; i < vertices; i++) {
            if (!visited[i]) {
                dfs(i, stack);
            }
        }

        // Step 2: Reverse DFS on the reversed graph
        Arrays.fill(visited, false);
        List<List<Integer>> sccList = new ArrayList<>();
        while (!stack.isEmpty()) {
            int node = stack.pop();
            if (!visited[node]) {
                List<Integer> scc = new ArrayList<>();
                dfsReversed(node, scc);
                sccList.add(scc);
            }
        }

        return sccList;
    }

    public static void main(String[] args) {
        KosarajuSCC graph = new KosarajuSCC(5);
        graph.addEdge(0, 1);
        graph.addEdge(1, 2);
        graph.addEdge(2, 0);
        graph.addEdge(1, 3);
        graph.addEdge(3, 4);

        List<List<Integer>> sccs = graph.findSCCs();
        System.out.println("Strongly Connected Components:");
        for (List<Integer> scc : sccs) {
            System.out.println(scc);
        }
    }
}

5.2 타잔 알고리즘

  • 원리: DFS 방문 순서와 Low-Link 값을 이용해 SCC를 탐색합니다.
  • 특징: 단일 DFS로 효율적인 SCC 탐색이 가능!
  • 시간 복잡도: O(V + E)
import java.util.*;

public class TarjanSCC {
    private int vertices;
    private List<List<Integer>> adjList = new ArrayList<>();
    private int[] ids, low;
    private boolean[] onStack;
    private Stack<Integer> stack;
    private int id = 0;
    private List<List<Integer>> sccList = new ArrayList<>();

    public TarjanSCC(int vertices) {
        this.vertices = vertices;
        for (int i = 0; i < vertices; i++) {
            adjList.add(new ArrayList<>());
        }
        ids = new int[vertices];
        low = new int[vertices];
        onStack = new boolean[vertices];
        stack = new Stack<>();
        Arrays.fill(ids, -1);
    }

    public void addEdge(int from, int to) {
        adjList.get(from).add(to);
    }

    private void dfs(int at) {
        stack.push(at);
        onStack[at] = true;
        ids[at] = low[at] = id++;

        for (int to : adjList.get(at)) {
            if (ids[to] == -1) {
                dfs(to);
                low[at] = Math.min(low[at], low[to]);
            } else if (onStack[to]) {
                low[at] = Math.min(low[at], ids[to]);
            }
        }

        // On root node, start SCC
        if (ids[at] == low[at]) {
            List<Integer> scc = new ArrayList<>();
            while (true) {
                int node = stack.pop();
                onStack[node] = false;
                scc.add(node);
                if (node == at) break;
            }
            sccList.add(scc);
        }
    }

    public List<List<Integer>> findSCCs() {
        for (int i = 0; i < vertices; i++) {
            if (ids[i] == -1) {
                dfs(i);
            }
        }
        return sccList;
    }

    public static void main(String[] args) {
        TarjanSCC graph = new TarjanSCC(5);
        graph.addEdge(0, 1);
        graph.addEdge(1, 2);
        graph.addEdge(2, 0);
        graph.addEdge(1, 3);
        graph.addEdge(3, 4);

        List<List<Integer>> sccs = graph.findSCCs();
        System.out.println("Strongly Connected Components:");
        for (List<Integer> scc : sccs) {
            System.out.println(scc);
        }
    }
}

결론

그래프 알고리즘은 알고 보면 그렇게 어렵지 않아요! 💡 차근차근 개념을 이해하고, 코드를 따라 작성하다 보면 자신감이 붙을 거예요. 😊 DFS, BFS, MST, 위상 정렬은 코딩 테스트와 실무에서 정말 많이 쓰이니 꼭 익혀두세요! 🙌

728x90
반응형