티스토리 뷰

학업

카카오 Code Festival 본선 1~6번 풀이

기리이이이이인 2017. 9. 10. 02:15

9월 9일 진행된 카카오 코드페스티벌 본선 8문제 중 앞 6문제에 대한 풀이다. 

나중에 좀 더 다듬어서 예쁘게 쓰고 싶었는데 그런 마음 가졌다가 안쓴 글이 한두가지가 아니라서 일단 되는대로 쓴다.

생각나는대로 짠거라 코드가 길거나 더럽다 ㅜ...

어차피 곧 카카오 테크 블로그( tech.kakao.com )에 풀이 글이 올라온다고 하니 제대로 된 문제 설명과 풀이는 그쪽을 기다리면 될 듯!

예선 설명도 테크 블로그에 있다 : tech.kakao.com/2017/08/11/code-festival-round-1/


1번. 단체사진 찍기


문제 : 카카오 프렌즈 8명(마리?)이 한줄로 서서 단체 사진을 찍으려고 한다. 그런데 프렌즈간 미묘한 관계가 있어서 각자 다른 프렌즈과의 거리에 대해 요구사항이 있다. 이 때 요구사항을 만족하는 줄서기 방법이 몇가지인지 구하라. 각각의 프렌즈는 A, C, F, J, M, N, R, T의 대문자로 나타내며, 요구사항은 "A~B=3"의 꼴로 나타낸다. 이 문자열은 A라는 프렌즈와 B라는 프렌즈 사이에 있는 프렌즈가 정확히 3명이 되어야 한다는 것을 의미한다. 문자열의 두번째 문자는 항상 ~이고, 4번째 문자는 <, =, > 중 하나이며 마지막 숫자는 0~6이었던 것 같다. N은 최대 100? 1000? 정도였다.


풀이 : 그냥 단순하게 8! 가지 경우를 전부 검사해보면 된다. stl에 있는 next_permutation 함수를 쓰면 쉽게 할 수 있다. next_permutation 함수의 존재를 몰라도 재귀로 짜거나 8중 for문으로 8^8가지를 검사해도 맞을 수 있다.



2번. GPS


문제 : 어떤 택시가 움직인 경로가 있는데 오차가 생겨서 경로가 invalid 할 수 있다고 한다. 이 때 경로의 내용을 바꿔 valid한 경로로 만들기 위한 최소 수정 횟수를 구하는 문제이다. 경로의 시작점과 끝점은 바꿀 수 없다. 택시가 운행되는 도시의 정보는 정점 n개, 정점을 잇는 무방향 간선 m개로 이루어진 그래프 형식으로 주어진다. 운행 경로는 택시가 위치했던 정점의 번호가 순서대로 k개 주어진다. n<=200, m<=10000, k<=100이다.


풀이 : 다이내믹 프로그래밍으로 해결할 수 있다. 2차원 dp 테이블의 크기를 n * n으로 잡고 정의를 다음과 같이 두자.


dp[i][j] = 경로의 i번째 위치가 j번 도시가 되면서 i번째 위치까지의 경로가 valid하게 고쳐야 하는 최소 횟수


시작점은 바꿀 수 없기 때문에 base case는 'dp[0][j] = j가 원래 경로의 첫번째 정점이면 0, 그 외엔 INF' 가 된다.

이제 j번 도시가 연결된 도시를 a1, a2, ..., as라고 두면


dp[i][j] = min(dp[i-1][a1], ..., dp[i-1][as], dp[i-1][j]) + (origin[i] == j ? 0 : 1)


이 성립한다. i번째까지의 경로가 valid하기 위해선 i-1번째까지의 경로가 valid하면서 i-1번째 위치가 i번째 위치와 연결되어야 하기 때문이고, 주어진 경로에서 i번째 위치가 j면 그 상태로 두면 되고 아니면 한번 더 고쳐야 하기 때문이다. 택시가 가만히 있는 경우도 허용하기 때문에 dp[i-1][j]도 고려해준다. O(k(n+m))의 시간복잡도로 해결할 수 있다.



3번. 리틀 프렌즈 사천성


문제 : m * n 격자가 있고 그 위에 여러 종류의 블록이 있다. 초기 상태에서 각 격자 칸은 비어 있거나, 장애물이 있거나, 서로 구별되는 특별한 블록이 있다. 특별한 블록은 A, B, ..., Z의 대문자 알파벳으로 나타내어 지고 각 블록은 0개거나 2개만 있다. 서로 같은 블록을 최대 한번만 90도로 꺾어서 연결할 수 있는 경로가 있으면 그 블록을 없앨 수 있다. 격자의 초기 상태가 주어질 때, 특별한 블록을 모두 없앨 수 있으면 없애는 순서 중 사전순으로 가장 앞선 문자열을 반환하고, 그럴 수 없으면 IMPOSSIBLE을 반환하라. m, n <= 100.


풀이 : 사라질 수 있는 쌍이 최대 26(=알파벳 대문자의 개수)개 이므로 그리디 + 브루트 포스로 하면 된다. 사전 순으로 가장 앞선 순서를 찾아야 하기 때문에 현재 상황이 있을 때 A부터 Z까지 순서대로 한 쌍씩 없앨 수 있는지 판단해보고 없앨 수 있다면 없앤 다음 A부터 찾는걸 다시 반복하면 된다. 그러다 더 이상 없애는게 불가능하면 남은 특별한 블록이 있는지 검사한 뒤 답을 반환하면 된다.


없앨 수 있는지 검사하는 방법은 다음과 같다. 이번에 없애고 싶은 블록의 위치가 (x1, y1), (x2, y2)라면 가능한 경로는 두가지 (x좌표를 먼저 움직인 뒤 y좌표를 움직이거나 y좌표를 먼저 움직인 뒤 x좌표를 움직이기) 이므로 그냥 O(n * m)에 판단 가능하다. 총 시간복잡도는 O(26*26*n*m). 대충 짜느라 코드가 꽤 길다.



4번. 튜브의 소개팅


문제 : 현재 m * n 격자 구조의 방에 파티가 열리고 있다. 튜브는 (0, 0)에서 출발해 (m-1, n-1)에 있는 소개팅 상대를 만나러 가려고 한다. 그런데 각 격자 위에는 장애물이 있거나 파티를 즐기고 있는 튜브의 친구가 있다. 장애물이 있는 칸은 지나갈 수 없고, 튜브의 친구가 있는 칸을 지나가려면 t초동안 친구와 수다를 떨어야 한다. 그런데 친구와 얘기를 나눈 시간이 합해서 s초를 넘어가면 튜브의 인내심이 버티지 못해 미친 오리로 변신하게 되고, 소개팅은 실패하게 된다. 파티장의 상황이 주어졌을 때, 튜브가 미친 오리로 변신하지 않고 갈 수 있는 경로 중 이동거리가 가장 짧은 경로를 찾고, 그 때의 경로 길이와 친구와 얘기를 나눈 총 시간을 각각 구하라. m, n <= 50. 파티장의 상황은 이차원 배열 time_map으로 주어지고 time_map[i][j]가 -1이면 장애물이 있는 것이고, 0 이상이면 (i, j) 칸에 친구가 있으며 그 칸을 지나기 위해선 time_map[i][j] 초만큼 수다를 떨어야 함을 의미한다.


풀이 : 가능한 경로의 길이가 최대 n * m이므로 일반적인 경로 탐색 알고리즘에서 cost를 시간으로 두고 모든 가능한 경로 길이에 대해 계산해보면 된다. 출발점부터 i, j까지 길이 d의 경로로 가는 값을 dist[d][i][j]라고 정의하고 bfs나 다익스트라를 통해 탐색하면 된다. 탐색이 끝난 후 0<=d<=n*m인 모든 d에 대해 dist[d][m-1][n-1]이 s를 넘으면 소개팅은 무조건 실패하고, 그렇지 않다면 그 중 가장 작은 d가 경로의 길이가 되고 dist[d][m-1][n-1]이 수다를 떠는 시간의 합이 될 것이다.



5번. 몸짱 트레이너 라이언의 고민


문제 : 트레이너 라이언은 격자 칸 하나를 회원 한명이 사용할 수 있는 n * n 격자로 된 헬스장을 운영하고 있다.  그런데 최근 헬스장 회원들로부터 옆사람과 너무 가까워 헬스장 이용이 불편하다는 불만이 많이 제기되었다. 이에 따라 라이언은 회원 간 간격이 최대한 넓게 되도록 공간을 배정해주려고 한다. 각각의 회원이 헬스장을 이용하는 시간대가 주어질 때, 가능한 회원 간 간격의 최솟값의 최댓값을 구하라. 즉 모든 순간에 유지 가능한 간격의 최댓값을 구하라. 라이언이 헬스장 상황을 고려해 회원의 스케줄을 짜기 때문에 동시간대에 헬스장을 이용하는 회원의 수가 격자칸보다 많은 경우는 없다. 격자 칸의 거리는 상하좌우로 인접한 경우 1, 대각선으로 인접한 경우는 2로 계산한다(=택시기하학). 헬스장은 오전 10시부터 오후 10시까지만 운영하므로 어떤 회원이 헬스장을 이용하는 시간대 (t1, t2)는 600 <= t1 < t2 <= 1320이다. n <= 10이다.


풀이 : 결국 정답은 시간에 상관없이 헬스장에 동시간대에 존재할 수 있는 최대 회원 수에 의해 결정된다. 사람이 제일 많을 때 최적의 경우를 계산해두고 나면 나머지 시간대의 경우 그냥 그 중 아무칸에나 회원을 배치하면 되기 때문이다. 동시간대에 존재하는 최대 사람의 수는 간단한 스위핑으로 해결할 수 있다(solution 함수의 첫 7줄).


이제 n * n 격자에 k명의 사람을 배치할 때 유지 가능한 최대 간격을 구하는 문제를 풀면 된다. 좀 더 생각하기 쉽게 바꾸면, 간격 d와 격자 크기 n이 주어졌을 때 n * n 격자에 최소 d의 간격을 유지하며 사람을 얼마나 배치할 수 있는지를 구하면 된다.



먼저 맨 윗줄에 항상 사람을 배치해야 된다는 것은 생각하기 쉽다. 그렇지 않은 경우 그냥 모든 사람을 한칸 위로 올리면 똑같기 때문. 그런데 맨 왼쪽 위에 사람 한명을 배치한 뒤 배치 가능한 장소 중 (x, y) 값이 가장 작은 장소에 그리디하게 배치하게 하면 딱 한 경우(n = 8, d = 3, 즉 A와 B의 거리가 최소 3이므로 AㅁㅁB는 되고 AㅁB는 안되는 경우)가 생긴다. 


그런데 맨 윗줄에 첫 사람을 배치할 때 맨 왼쪽말고 다른 칸들도 전부 첫번째로 배치한 뒤 똑같은 그리디로 돌려보면 정답이 나온다. 정당성 증명은 헷갈려서 미루고 있는데 문제에서 주어진 n 제한 내에서는 항상 정답이 나온다. 아래는 아까 말한 반례 그림. 그리디 1이 무조건 맨 왼쪽 위에 배치하고 시작하는 것이고, 그리디 2는 맨 윗줄 중 2번째 칸을 먼저 채우고 그리디하게 채우는 방법의 결과이다.



n이 작은 것으로 보아 정해는 브루트 포스 + 적절한 커팅인 것 같다. 실제 대회에서는 그냥 브루트 포스를 돌린 결과를 기록해 둔 뒤 그대로 배열에 적어 제출하거나, 다른 경우는 위의 반례가 있는 그리디로 구한 뒤 반례 하나만 하드 코딩해 제출한 사람이 많은 듯. 

아래 코드에서 gap 값은 풀이의 d값에서 1을 뺀 것과 같은 의미를 가진다. foo에서 맨 윗줄의 어느 칸에 첫 사람을 배치할 지 결정하고 foo2에서 남은 칸을 그리디하게 채운다.



6번. 네오의 귀걸이


문제 : n개의 2차원 점이 주어지고 점들의 x나 y좌표는 겹치지 않는다. 또한 x좌표가 가장 작은 점과 y좌표가 가장 큰 점은 같고, x좌표가 가장 큰 점과 y좌표가 가장 작은 점도 같다. 각각을 S, E로 부른다. 네오는 각 점을 왼쪽 위나 오른쪽 아래 꼭지점으로 가지는 x/y축에 평행한 직사각형들의 순서쌍으로 귀걸이를 만드려고 한다. 첫 직사각형의 왼쪽 위 점은 S, 마지막 직사각형의 오른쪽 아래 점은 E여야 하고 순서에서 A 직사각형 뒤에 B 직사각형이 오면 A의 오른쪽 아래 꼭지점과 B의 왼쪽 위 꼭지점은 동일해야 한다.




각 점에는 특성값 k가 있는데, 이는 해당 점이 직사각형의 오른쪽 아래 점이 될 경우 그 직사각형의 가로+세로 길이(둘레의 절반)가 k가 되어야 함을 의미한다.


n개의 점이 주어졌을 때 만들 수 있는 서로 다른 귀걸이의 개수를 구하라. 또한 초기 상태에서 어떤 점을 더하거나 원래 있던 점을 제거했을 때 만들 수 있는 서로 다른 귀걸이의 개수를 구하라.


n<=100000, 점 추가/삭제 쿼리 n개. 각 쿼리는 누적되지 않는다. 즉 초기 상태에 대해 실행해야 한다. 점을 추가하는 경우 점의 좌표와 특성값이 주어지고, 점을 제거하는 경우 좌표만 주어진다. 좌표값은 -10억 이상 10억 이하이다. 가능한 개수가 매우 클 수 있으므로 10억 7로 나눈 나머지를 출력하라.


풀이 : i번째 점에 대해 S에서부터 출발해 해당 점이 오른쪽 아래 꼭지점이 되는 직사각형 순서쌍의 수를 dp[i], 해당 점에서 출발해 E에서 끝나는 직사각형 순서쌍의 수를 dp2[i]라고 하자. 이 값들을 구할 수 있다면 해당 점을 지나는 답이 되는 직사각형 순서쌍의 수는 dp[i] * dp2[i]로 쉽게 계산할 수 있을 것이다. 전체 순서쌍의 수는 S나 E에 대해서 같은 방식으로 계산해서 구할 수 있다.


이제 해당 테이블들의 점화식을 생각해보자. 만약 i번째 점에 대해 이번 점이 직사각형의 오른쪽 아래 꼭지점이 될 때 왼쪽 위 꼭지점이 될 수 있는 다른 점이 p_1, p_2, ..., p_s고, 왼쪽 위 꼭지점이 될 때 아래쪽 꼭지점이 될 수 있는 다른 점이 q_1, q_2, ..., q_t라면 다음 점화식이 성립한다. 그냥 dp[i]의 경우 S로부터 그 점까지 내려올 수 있는 모든 경우의 수를 구한 것이고, dp2[i]의 경우 E로부터 그 점까지 올라올 수 있는 모든 경우의 수를 구한 것이다.


dp[i] = dp[p_1] + dp[p_2] + ... + dp[p_s]

dp2[i] = dp2[q_1] + dp2[q_2] + ... + dp2[qt_]


이제 가능한 p와 q의 집합을 모두 구하고 각 집합의 dp / dp2 값의 부분합을 빠르게 구하면 문제를 풀 수 있다. 이 때 k값의 의미를 잘 생각해보면 각 점이 속한 x - y = a 꼴의 대각선이 중요함을 알 수 있다. 어떤 점의 좌표가 (a, b)이고 특성값이 k라고 하자. 그럼 그 점이 직사각형의 오른쪽 아래 꼭지점이 된다면 왼쪽 위 꼭지점이 될 수 있는 다른 점의 좌표는 항상 x - y = a - b - k인 직선 위에 있기 때문이다. 또 그 직선위에서도 가능한 후보는 한정된 x좌표에만 존재한다(아래 그림의 오렌지색 선분).


마찬가지로 그 점이 직사각형의 왼쪽 위 꼭지점이 된다면 오른쪽 아래 꼭지점이 될 수 있는 다른 점은 좌표와 특성값 (c, d), s가 a - b = c - d - s를 만족하는 점들이다. 이 특성을 이용해 위의 점화식을 계산할 수 있다.


먼저 핵심 아이디어는 같은 n^2 풀이를 소개한 뒤 nlogn 풀이로 발전시키자.


전처리를 다음과 같이 하자. long long을 key로 가지고 vector<int>를 value로 가지는 map 2개를 각각 p2l, p2l2라고 두자. p2l[a]는 x-y값이 a인 점들의 인덱스를 x좌표 기준으로 정렬한 벡터로, p2l2[a]는 x-y-k값이 a인 점들의 인덱스를 x좌표 기준으로 정렬한 벡터이다. 이제 p2l을 통해 dp 테이블을, p2l2를 통해 dp2 테이블을 계산할 수 있다. 좌표가 (a, b)고 특성값이 k인 i번째 점에 대해 p2l[a - b - k] 벡터에 들어있는 인덱스를 다 순회하며, 그 점이 자신의 왼쪽 위에 있는 경우에만 dp 값을 더해준다. 메모이제이션을 사용하면 O(N^2)으로 갱신할 수 있다. dp2도 p2l2[a-b] 벡터를 순회하며 같은 방식으로 채울 수 있다.


초기 상태에서 답이 ans라고 하자. 그 뒤 점을 추가하는 쿼리가 들어오는 경우 같은 방식으로 그 점에 대한 dp, dp2 값을 계산하고 그 둘을 곱한걸 ans에 더해주면 된다. 점을 제거하는 쿼리가 들어오는 경우 그냥 저장해둔 그 점의 dp, dp2 값을 불러와서 그 둘을 곱한걸 ans에서 빼주면 된다. 아래는 이대로 구현한 O(N^2) 코드이다.



위의 방식에선 dp나 dp2의 부분합을 구하기 위해 p2l[a-b-k] 벡터나 p2l2[a-b] 벡터를 전부 순회하는데 O(N)의 시간이 걸려 시간복잡도가 커졌는데, 이분탐색과 세그먼트트리(또는 bit)를 사용하면 이 부분을 O(logN)으로 줄일 수 있다. 위의 그림처럼 목표 대각선 위의 점 중 일정 x좌표 구간에서의 합만 구하면 되기 때문에 이분 탐색으로 구간을 찾고, 누적합이나 세그먼트트리 등을 사용해 구간합을 계산하면 된다. x-y값이 작은 점들부터 계산을 시작하면 dp의 경우 누적합만 사용하면 되지만 dp2는 세그먼트트리가 필요하다. 세그트리나 bit 안쓰는 풀이도 있지만 시간복잡도도 같고 구현 난이도도 그렇게 높진 않기 때문에 그냥 세그트리를 썼다.


아래 코드에서 linelu가 p2l과 같은 역할을 하고 linerb가 p2l2와 같은 역할을 한다. map 대신 가능한 모든 x-y, x-y-k 값을 lineVal이라는 배열에 정렬해 넣어 그 배열의 인덱스를 찾아 썼다. rbSum과 luSum2는 필요없는 값이다. 코드 길이가 길긴 한데 중복되는 부분이 많아 실제로는 100줄 정도 적게 짰다.



7번의 경우 그래프이론개론을 듣는 친구가 수업시간에 배운거고 아는 사람은 30분 정도면 짠다더라. 지금 머리가 안돌아가서 생각이 안된다(핑계).


8번은 그냥 너무 어렵다 ㅜㅜ 낸 사람도 신기하고 푼 사람도 신기하다


'학업' 카테고리의 다른 글

Batch Normalization  (0) 2018.01.23
codeforces polygon 사용법  (0) 2017.10.23
이 때까지 공부하는데 사용한 교재 목록 정리  (0) 2017.03.23
C++ 기법 모음  (0) 2016.09.18
Anagram 구하기(철자바꾸기) 알고리즘  (0) 2016.03.06
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
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
글 보관함