PS/백준

[BOJ] 백준 17471번 : 게리멘더링 (JAVA)

제이온 (Jayon) 2020. 5. 26.

문제의 링크 : https://www.acmicpc.net/problem/17471

 

17471번: 게리맨더링

선거구를 [1, 4], [2, 3, 5, 6]으로 나누면 각 선거구의 인구는 9, 8이 된다. 인구 차이는 1이고, 이 값보다 더 작은 값으로 선거구를 나눌 수는 없다.

www.acmicpc.net

문제

백준시의 시장 최백준은 지난 몇 년간 게리맨더링을 통해서 자신의 당에게 유리하게 선거구를 획정했다. 견제할 권력이 없어진 최백준은 권력을 매우 부당하게 행사했고, 심지어는 시의 이름도 백준시로 변경했다. 이번 선거에서는 최대한 공평하게 선거구를 획정하려고 한다.

백준시는 N개의 구역으로 나누어져 있고, 구역은 1번부터 N번까지 번호가 매겨져 있다. 구역을 두 개의 선거구로 나눠야 하고, 각 구역은 두 선거구 중 하나에 포함되어야 한다. 선거구는 구역을 적어도 하나 포함해야 하고, 한 선거구에 포함되어 있는 구역은 모두 연결되어 있어야 한다. 구역 A에서 인접한 구역을 통해서 구역 B로 갈 수 있을 때, 두 구역은 연결되어 있다고 한다. 중간에 통하는 인접한 구역은 0개 이상이어야 하고, 모두 같은 선거구에 포함된 구역이어야 한다.

아래 그림은 6개의 구역이 있는 것이고, 인접한 구역은 선으로 연결되어 있다.

아래는 백준시를 두 선거구로 나눈 4가지 방법이며, 가능한 방법과 불가능한 방법에 대한 예시이다.

       

가능한 방법

[1, 3, 4]와 [2, 5, 6]으로 나누어져 있다.

가능한 방법

[1, 2, 3, 4, 6]과 [5]로 나누어져 있다.

불가능한 방법

[1, 2, 3, 4]와 [5, 6]으로 나누어져 있는데, 5와 6이 연결되어 있지 않다.

불가능한 방법

각 선거구는 적어도 하나의 구역을 포함해야 한다.

공평하게 선거구를 나누기 위해 두 선거구에 포함된 인구의 차이를 최소로 하려고 한다. 백준시의 정보가 주어졌을 때, 인구 차이의 최솟값을 구해보자.

 

풀이

조합과 BFS로 해결할 수 있는 문제였습니다. 조합을 사용하면, 특정 지역 중 몇 개를 뽑아서 A에 할당할 수 있기때문에 나머지 지역은 모두 B에 할당됩니다. 따라서 자연스럽게 두 가지 영역으로 나누는 것이 가능합니다.

또한, 6C2와 6C4는 같은 식이기때문에, 우리는 N / 2 까지만 조합을 돌리면서 모든 구역을 뽑은 후, 두 지역의 인구 수 차이를 구하면 됩니다.

BFS를 사용한 이유는 A 또는 B 지역에 할당된 선거구가 서로 연결되어있는지 확인하기 위함이었습니다. 저는 연결리스트를 이용해서 선거구의 연결된 정보를 저장했고, 이 리스트를 이용하기로 하였습니다.

BFS에 대한 자세한 설명은 아래 예제를 통해 다시 알아보겠습니다.

 

예제

문제에서 나온 첫 번째 예제를 보겠습니다. 이들의 연결 관계는 다음과 같을 것입니다.

1 - {2, 4}

2 - {1, 3, 5, 6}

3 - {2, 4}

4 - {1, 3}

5 - {2}

6 - {2}

 

그리고 조합 함수에서 A 지역에서는 {1, 4}, B 지역에서는 {2, 3, 5, 6}으로 분할했다고 가정합시다.

먼저, A 지역의 선거구는 모두 연결되었는지 확인해보겠습니다.

첫 단계는 count = 1로 초기화 하는 것입니다. 이는 연결 요소의 개수를 의미합니다.

visited와 queue를 사용하여 visited[1] = true, queue.offer(1) 처리를 해주겠습니다.

그리고 1의 연결 관계를 확인합니다. -> {2, 4}

1과 2는 연결되어있지만, A 지역에는 2가 없으므로 queue에 offer하지 않습니다.

1은 4와 연결되어있고, A 지역에는 4가 있으므로 queue에 offer를 합니다. 그리고 count + 1을 해 줍니다.

즉, visited과 A.contains를 이용하는 것이 핵심입니다.

다음으로, queue에서 poll한 4의 연결 관계를 확인합니다. -> {1, 3}

4는 1과 연결되어있지만, 이미 위에서 탐색했으므로 queue에 offer하지 않습니다.

4는 3과 연결되어있지만, A 지역에는 3이 없으므로 queue에 offer하지 않습니다.

탐색이 종료되면, count와 A.size()를 비교하여 동일하면 true, 아니면 false를 반환합니다.

위 과정에서 count는 2이고, A.size() = 2이므로 true입니다.

 

다음으로, B 지역의 선거구 {2, 3, 5, 6} 는 모두 연결되었는지 확인해보겠습니다.

마찬가지로 count = 1로 초기화 합니다.

visited와 queue를 사용하여 visited[2] = true, queue.offer(2) 처리를 해주겠습니다.

그리고 2의 연결 관계를 확인합니다. -> {1, 3, 5, 6}

2는 1과 연결되어있지만, B 지역에는 1이 없으므로 offer하지 않습니다.

2는 3과 연결되어있고, B 지역에 3이 있으므로 offer하고, count + 1합니다.

5, 6도 2와 연결되어있고, B 지역에 존재하므로 모두 offer하고, count + 2를 합니다.

위와 같은 과정을 반복하면, count = 4가 되고 이것은 B.size()와 동일하다는 사실을 알 수 있습니다.

따라서, A와 B는 모두 정상적인 선거구라고 결론지을 수 있습니다.

 

아래는 위 풀이를 정리한 소스코드입니다.

소스코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;
import java.util.StringTokenizer;
 
class Position {
    int x;
    int peopleNum;
 
    Position(int x, int peopleNum) {
        this.x = x;
        this.peopleNum = peopleNum;
    }
}
 
public class Main {
    static int N;
    static Position[] positions; // 지역의 번호와 인구수를 담은 객체 배열.
    static ArrayList<ArrayList<Integer>> list; // 연결리스트
    static int ans = Integer.MAX_VALUE;
 
    public static void main(String[] args) throws NumberFormatException, IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st;
 
        N = Integer.parseInt(br.readLine());
 
        positions = new Position[N + 1];
        st = new StringTokenizer(br.readLine());
        for (int i = 1; i <= N; i++) {
            int peopleNum = Integer.parseInt(st.nextToken());
            positions[i] = new Position(i, peopleNum);
        }
 
        list = new ArrayList<>();
        for (int i = 0; i <= N; i++) {
            list.add(new ArrayList<>());
        }
 
        for (int i = 1; i <= N; i++) {
            st = new StringTokenizer(br.readLine());
            int n = Integer.parseInt(st.nextToken());
            for (int j = 0; j < n; j++) {
                int temp = Integer.parseInt(st.nextToken());
                list.get(i).add(temp);
            }
        }
        //////////// 입력 끝.
 
        ArrayList<Integer> A = new ArrayList<>();
        for (int i = 1; i <= N / 2; i++) {
            comb(1, N, i, A); // 조합을 통한 지역 분리.
        }
 
        // ans가 초기값 그대로라면, 게리멘더링에 실패한 것임.
        if (ans == Integer.MAX_VALUE) {
            ans = -1;
        }
 
        bw.write(ans + "\n");
        bw.flush();
        bw.close();
        br.close();
    }
 
    // 조합
    public static void comb(int start, int n, int r, ArrayList<Integer> A) {
        if (r == 0) {
            gerrymandering(A);
            return;
        }
 
        for (int i = start; i <= n; i++) {
            A.add(i);
            comb(i + 1, n, r - 1, A);
            A.remove(A.size() - 1);
        }
    }
 
    // 할당받은 지역의 인구수의 차이를 계산하는 함수.
    public static void gerrymandering(ArrayList<Integer> A) {
        // A 지역구가 잘 연결되어있는지 확인
        if(!isConnect(positions[A.get(0)].x, A, A.size())) {
            return;
        }
 
        ArrayList<Integer> B = new ArrayList<>();
        for (int i = 1; i <= N; i++) {
            if (A.contains(i)) {
                continue;
            }
            B.add(i);
        }
        
        // B 지역구가 잘 연결되어있는지 확인
        if(!isConnect(positions[B.get(0)].x, B, B.size())) {
            return;
        }
 
        int resultA = 0;
        
        // A 지역구 인구 계산
        for (int i = 0; i < A.size(); i++) {
            resultA += positions[A.get(i)].peopleNum;
        }
 
        int resultB = 0;
        
        // B 지역구 인구 계산
        for (int i = 0; i < B.size(); i++) {
            resultB += positions[B.get(i)].peopleNum;
        }
 
        int result = Math.abs(resultA - resultB);
        ans = Math.min(ans, result);
 
    }
    
    // 선거구가 모두 연결되어있는지 확인.
    public static boolean isConnect(int num, ArrayList<Integer> arr, int size) {
        boolean[] visited = new boolean[N + 1];
        visited[num] = true;
        Queue<Integer> q = new LinkedList<>();
        q.offer(num);
 
        int count = 1;
        while (!q.isEmpty()) {
            int start = q.poll();
 
            for (int i : list.get(start)) {
                // 이미 방문한 적이 없고, arr에 속해야 함.
                if (!visited[i] && arr.contains(i)) {
                    visited[i] = true;
                    count++;
                    q.offer(i);
                }
            }
        }
 
        if (count == size) {
            return true;
        }
        return false;
    }
 
}
cs

 

지적 혹은 조언 환영합니다! 언제든지 댓글로 남겨주세요.

댓글

추천 글