𝝅번째 알파카의 개발 낙서장

screen

[백준 / JAVA] 백준 알고리즘 1006번 습격자 초라기

posts

알고리즘

시리즈 톺아보기

백준 알고리즘

백준 알고리즘
count

습격자 초라기 🔗

랭크 사용 언어
image JAVA

🔗 전체 1006번 문제

조건 🔗

시간제한 메모리 제한
2초 512MB

문제 🔗

초라기는 한국의 비밀국방기지(원타곤)를 습격하라는 임무를 받은 특급요원이다. 원타곤의 건물은 도넛 형태이며, 초라기는 효율적인 타격 포인트를 정하기 위해 구역을 아래와 같이 두 개의 원 모양으로 나누었다. (그림의 숫자는 각 구역의 번호이다.)

image

초라기는 각각 명으로 구성된 특수소대를 다수 출동시켜 모든 구역에 침투시킬 예정이며, 각 구역 별로 적이 몇 명씩 배치되어 있는지는 초라기가 모두 알고 있다. 특수소대를 아래 조건에 따라 침투 시킬 수 있다.

  1. 한 특수소대는 침투한 구역 외에, 인접한 한 구역 더 침투할 수 있다. (같은 경계를 공유하고 있으면 인접 하다고 한다. 위 그림에서 1구역은 2, 8, 9 구역과 서로 인접한 상태다.) 즉, 한 특수소대는 한 개 혹은 두 개의 구역을 커버할 수 있다.
  2. 특수소대끼리는 아군인지 적인지 구분을 못 하기 때문에, 각 구역은 하나의 소대로만 커버해야 한다.
  3. 한 특수소대가 커버하는 구역의 적들의 합은 특수소대원 수 보다 작거나 같아야 한다.

이때 초라기는 원타곤의 모든 구역을 커버하기 위해 침투 시켜야 할 특수 소대의 최소 개수를 알고 싶어 한다.

입력 🔗

첫째 줄에 테스트 케이스의 개수 가 주어진다. 각 테스트 케이스는 다음과 같이 구성되어있다.

첫째 줄에는 (구역의 개수)/2 값 과 특수 소대원의 수 가 주어진다. (, ).

둘째 줄에는 번째 구역에 배치된 적의 수가 주어지고, 셋째 줄에는 번째 구역에 배치된 적의 수가 공백으로 구분되어 주어진다. (1 ≤ 각 구역에 배치된 최대 적의 수 ≤ 10000) 단, 한 구역에서 특수 소대원의 수보다 많은 적이 배치된 구역은 존재하지 않는다. (따라서, 각 구역에 배치된 최대 적의 수 ≤ )

출력 🔗

각 테스트케이스에 대해서 한 줄에 하나씩 원타곤의 모든 구역을 커버하기 위해 침투 시켜야 할 특수 소대의 최소 개수를 출력하시오.

케이스 🔗

예제 1 🔗

  • 입력

TC

01
18 100
270 60 55 43 57 60 44 50
358 40 47 90 45 52 80 40
  • 출력

TC

011

힌트 🔗

하나의 특수 소대로 인접한 두 영역을 커버할 수 있는 배치는 (2,10), (9,16), (4,5), (7,8), (13,14) 이다. 그리고 나머지 6개 구역은 각각 하나의 특수 소대로 커버할 수 있다. 그러므로 최소 11개 특수 소대를 침투시켜야 한다.

풀이 🔗

백준 알고리즘을 순서대로 푸는 나 같은 초심자들에게 힘의 차이를 느끼게 해주는 문제라고 한다. solved.ac에 의하면 문제 등급이 무려 PLATINUM III 수준. 지금까지 푼 문제 중 가장 높은 등급이 ACM Craft (GOLD III) 수준임을 감안하면 월등히 높은 수준의 문제. 실제로 풀면서도 혼자서는 도저히 방법이 안 떠오르는데다, 풀이를 봐도 이해가 잘 안 됐다.

해당 문제에 주어지는 구역은 원형이다. 문제를 쉽게 접근하기 위해선 이 원형을 임의로 잘라 직사각형 형태로 전개해야 한다. 즉, 풀 때는 직사각형 형태지만, 실제로는 원형이므로 직사각형의 잘린 양 끝부분까지 염두하여 계산을 해야한다. 난이도를 상승시키는 요인 중 하나.

케이스에 제시된 예제를 기준으로 구역을 사각형으로 도식화하면 아래와 같다.

image

위 사진과 같이 8x8 배열로 표현할 수 있다. 여기서 여기서 6번째 행까지 특수소대로 채울 수 있는 최소값은 어떻게 구할 수 있을까?

역으로 한번 생각해보자. 우리 특수소대는 너무나 유능해서 항상 최소의 팀만으로 목표 구역을 점령한다고 해보자. 작전 보고서엔 아래와 같이 점령한 구역을 표시하며, 이를 노란색 영역으로 마킹해서 보여준다.

image

즉, 노란색 영역은 최소의 특수소대팀이 투입된 것이며, 우리가 실제로 구현할 알고리즘의 결과물이기도 하다.

문제의 설정 상, 특수소대는 반드시 한 팀이 온전히 투입되어야 하며, 최소로 투입 가능한 인원 역시 한 팀이다. 그렇다면 위 사진에서 특수소대 한 팀이 커버할 수 있는 영역을 제외해보면 아래와 같이 세 케이스 , , 로 나눌 수 있다.

image

즉, 우리가 저 세 케이스에 대한 특수소대팀의 최소값을 계산할 수 있다면, 결과적으로 6번째 행 전체를 커버하는 특수소대팀의 수를 구할 수 있다. 이미 최소 인원이 나머지 구역을 점령한 상황에서, 투입할 수 있는 최소 인원인 한 팀만 투입할 수 있기 때문.

여러 블로그에서 위 세 그림을 많이 봤을 텐데, 뜬금없이 저런 그림이 등장함에는 이와같은 배경이 있는 것이다.

변수 🔗

알고리즘 설계에 사용할 변수는 아래와 같다.

  • : 케이스 수
  • : 구역의 행 수
  • : 구역별 적의 수
  • : 첫 번째 케이스의 특수소대 최소 투입 수
  • : 두 번째 케이스의 특수소대 최소 투입 수
  • : 세 번째 케이스의 특수소대 최소 투입 수

a 공식 🔗

image

첫번째 케이스로 의 최소값 공식을 설계하자.

위에서 했던 방식과 마찬가지로 추론하면 에서 한팀을 뺀 을 구해야 하며, 이는 의 조건에 따라 두 케이스로 나눌 수 있다.

일반적인 케이스 🔗

일반적으로 아래의 케이스가 해당된다.

image

이므로

한팀이 두개의 구역을 커버할 수 있을 경우 🔗

일 경우에 한해 아래와 같은 케이스가 해당된다.

image

이므로

일반적인 케이스와 비교했을 때, 더 작은 값이 가 된다.

일반화 🔗

케이스별로 구한 식의 일반화는 아래와 같다.

  • ->
  • ->

즉, 최종 일반식은 아래와 같다.

b 공식 🔗

image

의 최소값 공식을 설계하자.

에서 한팀을 뺀 을 구해야 하며, 이는 의 조건에 따라 두 케이스로 나눌 수 있다.

일반적인 케이스 🔗

일반적으로 아래의 케이스가 해당된다.

image

이므로

의 식과 동일하다.

한팀이 두개의 구역을 커버할 수 있을 경우 🔗

일 경우에 한해 아래와 같은 케이스가 해당된다.

image

이므로

일반적인 케이스와 비교했을 때, 더 작은 값이 가 된다.

일반화 🔗

케이스별로 구한 식의 일반화는 아래와 같다.

  • ->
  • ->

즉, 최종 일반식은 아래와 같다.

c 공식 🔗

image

의 최소값 공식을 설계하자. (는 4행까지 채워짐에 유의하자)

에서 한팀을 뺀 을 구해야 하며, 이는 의 조건에 따라 여러 케이스로 나눌 수 있다.

일반적인 케이스 🔗

일반적으로 아래의 케이스가 해당된다.

image

이므로

이므로

두 케이스 중 더 작은 케이스가 이므로 아래의 식으로 귀결된다.

한팀이 두개의 구역을 커버할 수 있을 경우 🔗

일 경우 아래와 같은 케이스가 해당된다.

image

일반적인 케이스와 비교했을 때, 더 작은 값이 가 된다.

한팀이 네개의 구역을 커버할 수 있을 경우 🔗

의 경우 한 가지 특이 케이스가 발생한다. , 의 경우 최소 투입인원인 1을 뺀 값만을 계산했다. 의 경우 직사각형이라는 특징 때문에 최대 4개 구역을 2팀이 점령할 수 있다.

이고 일 경우 아래와 같은 케이스가 해당된다.

image

모든 케이스와 비교했을 때, 더 작은 값이 가 된다.

일반화 🔗

케이스별로 구한 식의 일반화는 아래와 같다.

즉, 최종 일반식은 아래와 같다.

최종 일반식 🔗

구한 일반식을 정리하면 아래와 같다.

원형 구조 적용을 위한 초기값 지정하기 🔗

위 수식을 코드로 녹여내면 되지만, 완벽한 건 아니다. 왜냐하면 이 구역이 선형이 아닌 원형 구조이기 때문.

지금까지 우리는 원리 이해 및 수식 도출의 편의를 위해 원타곤을 임의로 잘라 표타곤으로 전개하여 수식을 계산했다. 이러한 선형 구조는 시작점과 도착점이 있지만 원형은 순환 구조이므로 이에 맞춰 조건식을 작성해야 한다. 즉, 원형 구조에 호환되도록 일부 케이스에 초기값을 지정해야 최종적으로 원하는 알고리즘을 작성할 수 있다.

아래의 사진은 원타곤과 표타곤을 비교한 것이다.

image

이 처럼, 원형 구조는 끼리도 연결이 가능하지만, 선형 구조는 구조상 불가능하다. 때문에 이러한 케이스들의 초기값을 지정해줘야한다.

대충 감이 오겠지만, 걸친 모양에 따라 총 4가지 케이스가 존재한다.

걸치지 않을 경우 (기본) 🔗

image

혹은 과 같이 영역이 겹치지 않을 경우. 선형 구조에서도 적용 가능한 기본적인 케이스다. 의 모양과 연관지어 생각하면 아래와 같이 도식이 가능하다.

image

열의 윗 칸만 채우므로 만 점령한 상태이므로 1

열의 아래 칸만 채우므로 만 점령한 상태이므로 1

열을 채우는데, 은 논리상 불가능하므로 0

즉 초기값은 아래와 같다.

일 때의 초기값을 지정한다.

이 케이스일 경우 이 알고리즘의 답이 된다.

예를 들어, 일 경우 이 되므로 가장 적합한 최소값을 구할 수 있다.

image

윗 행만 걸칠 경우 🔗

image

을 점령할 경우. 원형 구조에서만 가능한 케이스다. 걸치지 않는 경우를 제외한 나머지 케이스는 전부 원형 구조에서만 가능한 케이스이니 참고할 것. 일 때는 영향을 받지 않아 걸치지 않는 경우와 동일하다. 걸치기 위해선 반드시 두 행 이상이 필요하기 때문에, 을 충족해야 한다.

image

연결된 부분을 체크패턴으로 하이라이팅 했다. 이 둘이 서로 연결되기 때문에, 다른 구역의 특수소대가 점령할 수 없다. 따라서 해당 부분은 초기값 계산 시 없는 영역으로 생각하면 된다. 이러한 특징을 감안하면 아래와 같이 초기값을 지정할 수 있다.

예제에서 이므로, 조건의 일반식은 이 된다.

일 때의 초기값을 추가로 지정한다.

이 케이스일 경우 이 알고리즘의 답이 된다. , 을 합쳐 이라고 생각하면 된다.

예를 들어, 일 경우 이 되므로 가장 적합한 최소값을 구할 수 있다.

image

아래 행만 걸칠 경우 🔗

image

을 점령할 경우. 세부 사항은 윗 행만 걸칠 경우와 동일하다.

image

예제에서 이므로, 조건의 일반식은 이 된다.

일 때의 초기값을 추가로 지정한다.

이 케이스일 경우 이 알고리즘의 답이 된다. , 을 합쳐 이라고 생각하면 된다.

예를 들어, 일 경우 이 되므로 가장 적합한 최소값을 구할 수 있다.

image

두 행 모두 걸칠 경우 🔗

image

, 을 점령할 경우. 세부 사항은 윗 행만 걸칠 경우와 동일하다.

image

예제에서 이므로, 조건의 일반식은 , 이 된다.

일 때의 초기값을 추가로 지정한다.

이 케이스일 경우 이 알고리즘의 답이 된다. , 을 합쳐 , , 을 합쳐 이라고 생각하면 된다.

예를 들어, 일 경우 이 되므로 가장 적합한 최소값을 구할 수 있다.

image

최종 케이스 🔗

  • 기본

  • 윗 행만 걸칠 경우

  • 아래 행만 걸칠 경우

  • 두 행 모두 걸칠 경우 ,

비로소 알고리즘을 구현하기 위한 모든 준비물이 갖춰졌다.

전체 소스 🔗

JAVA

0import java.io.BufferedReader;
1import java.io.IOException;
2import java.io.InputStreamReader;
3
4/**
5 * 백준 전체 1006 문제 알고리즘 클래스
6 *
7 * @author RWB
8 * @see <a href="https://blog.itcode.dev/posts/2021/06/06/a1006">1006 풀이</a>
9 * @since 2021.06.06 Sun 22:44:45
10 */
11public class Main
12{
13 private static int N;
14 private static int W;
15
16 private static int[][] e;
17
18 private static int[] a;
19 private static int[] b;
20 private static int[] c;
21
22 /**
23 * 메인 함수
24 *
25 * @param args: [String[]] 매개변수
26 *
27 * @throws IOException 데이터 입출력 예외
28 */
29 public static void main(String[] args) throws IOException
30 {
31 BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
32
33 // 케이스 수
34 int T = Integer.parseInt(reader.readLine());
35
36 for (int i = 0; i < T; i++)
37 {
38 int result = 2147483647;
39
40 String[] temp = reader.readLine().split(" ");
41
42 // 행 수
43 N = Integer.parseInt(temp[0]);
44
45 // 특수소대원 수
46 W = Integer.parseInt(temp[1]);
47
48 // 구역별 적 배열
49 e = new int[2][N];
50
51 for (int j = 0; j < 2; j++)
52 {
53 temp = reader.readLine().split(" ");
54
55 for (int k = 0; k < N; k++)
56 {
57 e[j][k] = Integer.parseInt(temp[k]);
58 }
59 }
60
61 a = new int[N];
62 b = new int[N];
63 c = new int[N + 1];
64
65 a[0] = 1;
66 b[0] = 1;
67 c[0] = 0;
68
69 // 인덱스 0부터 시작
70 solve(0);
71
72 result = Math.min(result, c[N]);
73
74 // 두 행 이상일 경우
75 if (N > 1)
76 {
77 // 두 행 모두 걸칠 경우
78 if (e[0][0] + e[0][N - 1] <= W && e[1][0] + e[1][N - 1] <= W)
79 {
80 a[1] = 1;
81 b[1] = 1;
82 c[1] = 0;
83
84 // 인덱스 1부터 시작 (1까지 초기값이 있기 때문)
85 solve(1);
86
87 result = Math.min(result, c[N - 1] + 2);
88 }
89
90 // 윗 행만 걸칠 경우
91 if (e[0][0] + e[0][N - 1] <= W)
92 {
93 a[1] = 2;
94 b[1] = e[1][0] + e[1][1] > W ? 2 : 1;
95 c[1] = 1;
96
97 // 인덱스 1부터 시작 (1까지 초기값이 있기 때문)
98 solve(1);
99
100 result = Math.min(result, b[N - 1] + 1);
101 }
102
103 // 아래 행만 걸칠 경우
104 if (e[1][0] + e[1][N - 1] <= W)
105 {
106 a[1] = e[0][0] + e[0][1] > W ? 2 : 1;
107 b[1] = 2;
108 c[1] = 1;
109
110 // 인덱스 1부터 시작 (1까지 초기값이 있기 때문)
111 solve(1);
112
113 result = Math.min(result, a[N - 1] + 1);
114 }
115 }
116
117 System.out.println(result);
118 }
119
120 reader.close();
121 }
122
123 /**
124 * 알고리즘 함수
125 *
126 * @param num: [int] 시작 인덱스
127 */
128 private static void solve(int num)
129 {
130 for (int i = num; i < N; i++)
131 {
132 c[i + 1] = Math.min(a[i] + 1, b[i] + 1);
133
134 // c팀이 인접한 두 개의 구역을 점령할 수 있을 경우
135 if (e[0][i] + e[1][i] <= W)
136 {
137 c[i + 1] = Math.min(c[i + 1], c[i] + 1);
138 }
139
140 // c팀이 인접한 두개의 구역 2개를 점령할 수 있을 경우
141 if (i > 0 && e[0][i - 1] + e[0][i] <= W && e[1][i - 1] + e[1][i] <= W)
142 {
143 c[i + 1] = Math.min(c[i + 1], c[i - 1] + 2);
144 }
145
146 // a, b팀의 인덱스 보정 (c팀은 인덱스가 하나 더 많음)
147 if (i < N - 1)
148 {
149 a[i + 1] = c[i + 1] + 1;
150 b[i + 1] = c[i + 1] + 1;
151
152 // a팀이 인접한 두 개의 구역을 점령할 수 있을 경우
153 if (e[0][i] + e[0][i + 1] <= W)
154 {
155 a[i + 1] = Math.min(a[i + 1], b[i] + 1);
156 }
157
158 // b팀이 인접한 두 개의 구역을 점령할 수 있을 경우
159 if (e[1][i] + e[1][i + 1] <= W)
160 {
161 b[i + 1] = Math.min(b[i + 1], a[i] + 1);
162 }
163 }
164 }
165 }
166}

분류 🔗

  • 다이나믹 프로그래밍

여담 🔗

6월 1일부터 풀기 시작해서 이 문제를 완전히 이해하는데 근 일주일 가까이 걸렸다. 나름의 풀이를 작성해야하는데, 남의 풀이가 아닌 내 풀이를 작성하기 위해선 해당 문제를 온전히 이해할 필요가 있었다. 내가 다른 사람들의 풀이를 보면서 이해하지 못해서 내 스스로 생각하고 이해한 걸 나름대로 녹여냈다. 다른 사람이 내 글을 보고 이 어려운 문제를 쉽게 이해할 수 있었으면 좋겠다.

이해하고 봐도 다소 난해한데, 이걸 원리부터 코드까지 이끌어내어 풀어내는 사람은 정말 대단한 거 같다. 아님 내가 실력이 없는건가.

참고 🔗