알고리즘/프로그래머스

[Level 3] 등대 - Java

0woodev 2024. 9. 2. 14:07

https://school.programmers.co.kr/learn/courses/30/lessons/133500

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

문제 설명

더보기

인천 앞바다에는 1부터 n까지 서로 다른 번호가 매겨진 등대 n개가 존재합니다. 등대와 등대 사이를 오가는 뱃길이 n-1개 존재하여, 어느 등대에서 출발해도 다른 모든 등대까지 이동할 수 있습니다. 등대 관리자 윤성이는 전력을 아끼기 위하여, 이 중 몇 개의 등대만 켜 두려고 합니다. 하지만 등대를 아무렇게나 꺼버리면, 뱃길을 오가는 배들이 위험할 수 있습니다. 한 뱃길의 양쪽 끝 등대 중 적어도 하나는 켜져 있도록 등대를 켜 두어야 합니다.

예를 들어, 아래 그림과 같이 등대 8개와 7개의 뱃길들이 있다고 합시다. 이 경우 1번 등대와 5번 등대 두 개만 켜 두어도 모든 뱃길은 양쪽 끝 등대 중 하나가 켜져 있으므로, 배들은 안전하게 운항할 수 있습니다.

등대의 개수 n과 각 뱃길이 연결된 등대의 번호를 담은 이차원 배열 lighthouse가 매개변수로 주어집니다. 윤성이가 켜 두어야 하는 등대 개수의 최솟값을 return 하도록 solution 함수를 작성해주세요.


제한사항

  • 2 ≤ n ≤ 100,000
  • lighthouse의 길이 = n – 1
    • lighthouse 배열의 각 행 [a, b]는 a번 등대와 b번 등대가 뱃길로 연결되어 있다는 의미입니다.
      • 1 ≤ a ≠ b ≤ n
      • 모든 등대는 서로 다른 등대로 이동할 수 있는 뱃길이 존재하도록 입력이 주어집니다.

입출력 예


n lighthouse result
8 [[1, 2], [1, 3], [1, 4], [1, 5], [5, 6], [5, 7], [5, 8]] 2
10 [[4, 1], [5, 1], [5, 6], [7, 6], [1, 2], [1, 3], [6, 8], [2, 9], [9, 10]] 3

입출력 예 설명

입출력 예 #1

  • 본문에서 설명한 예시입니다.

입출력 예 #2

  • 뱃길은 아래 그림과 같이 연결되어 있습니다. 윤성이가 이중 1, 6, 9번 등대 3개만 켜 두어도 모든 뱃길은 양쪽 끝 등대 중 하나가 켜져 있게 되고, 이때의 등대 개수 3개가 최소가 됩니다.

 

 

 

 

 

문제 솔루션

DFS 를 통한 완전탐색
등대들을 `1`이 루트노드인 트리라고 생각해보자. 

이렇게 변경하고 나면, 어느 등대를 켤지 조금 더 잘 보인다.
리프노드가 최대한 많이 연결된 등대들을 밝히는 것이 유리하다는 것을 볼 수 있다.
 
따라서, 등대를 켜는 조건은 다음과 같다.
현재 등대의 자식 중 하나라도 꺼져있는 경우, 현재 등대의 불을 켠다. ( = 자식노드들이 모두 켜져있다면, 현재 등대는 키지 않아도 된다.)
 
시간복잡도
1. 인접리스트를 만드는 과정 : O(N)
2. DFS : O(V + E) = O (N + N - 1) = O(N)
 
총 시간복잡도: O(N)

 

전체 코드 

더보기
import java.util.*;

class Solution {
    public int solution(int n, int[][] edges) {
        boolean[] lights = new boolean[n + 1];
        boolean[] visited = new boolean[n + 1];
        List<Integer>[] graphs = new ArrayList[n + 1];
        
        lights[0] = false;
        visited[1] = true;
        
        // 인접리스트 graphs 생성
        for (int i = 1; i <= n; i++) {
            graphs[i] = new ArrayList<>();
        }
        
        for (int[] edge: edges) {
            graphs[edge[0]].add(edge[1]);
            graphs[edge[1]].add(edge[0]);
        }
       
        turnLight(1, 1, graphs, lights, visited);
        
        int count = 0;
        for (boolean lightOn: lights) {
            if (lightOn) count++;
        }
        
        return count;
    }
    
    // dfs 알고리즘 적용
    public void turnLight(int curr, int parent, 
    	List<Integer>[] graphs, boolean[] lights, boolean[] visited
    ) {
        // 무 방향 인접리스트 이므로, parent 가 포함되어 있다.
        // visited 로 체크 가능
        for (int vertex: graphs[curr]) {
            if (!visited[vertex]) {
                visited[vertex] = true;
                turnLight(vertex, curr, graphs, lights, visited);    
            }
        }
        
        // 부모들 제외한 자식 노드에 대해서 체크
        // 리프노드인 경우에는 lights 를 키지 않는 것으로 처리
        boolean hasAnyChildOff = false;
        for (int vertex: graphs[curr]) {
            if (vertex == parent) continue;
            if (!lights[vertex]) {
                hasAnyChildOff = true;
                break;
            }
        }
        
        if (hasAnyChildOff) {
            lights[curr] = true;
        }
    }
}