Overview
3시간 5문제 셋입니다. 앞쪽 2문제는 난이도순, 나머지 3문제는 난이도 상관 없이 섞어놓은 것 같습니다.
체감 난이도는 1 < 2 < 4 < 5 < 3 순입니다.
2번 문제는 KOI 고등부 1번, 3번 문제는 KOI 고등부 4번, 4번 문제는 KOI 중등부 3번, 5번 문제는 KOI 중등부 3번 정도의 난이도인 것 같습니다.
코드는 글 하단에 한 번에 올리겠습니다.
문제 지문 : https://hsin.hr/coci/contest1_tasks.pdf
Task 1. Patkice (50점)
문제
N by M 격자가 주어지고, o에서 출발해 x로 가려고 합니다.
처음 이동방향은 마음대로 정할 수 있고, 그 다음부터는 < > ^ v에 따라 이동 방향이 결정 됩니다. x로 도달할 수 있는지 판별하는 문제입니다.
풀이
단순 구현 문제입니다.
Task 2. Bajka (70점)
문제
길이가 300 이하인 두 문자열 S와 T가 주어집니다. (두 문자열의 길이는 서로 다를 수 있습니다.)
S의 임의의 위치에서 시작해 적절히 이동을 해서, 최소 거리만큼 이동해 T를 만드는 것이 목표입니다. 3가지 방법으로 이동할 수 있습니다.
- 왼쪽으로 한 칸 이동하고 그 칸에 있는 문자를 T에 추가한다. 이때 이동 거리는 1이다.
- 오른쪽으로 한 칸 이동하고 그 칸에 있는 문자를 T에 추가한다. 이때 이동 거리는 1이다.
- 현재 칸과 동일한 문자가 적혀있는 칸으로 이동한다. x에서 y로 이동한다고 하면 이동 거리는 $\vert x - y \vert$ 이다.
문자열을 만들 수 있는 경우에는 최소 거리를 출력하고, 그렇지 않은 경우에는 -1을 출력하는 문제입니다.
풀이
D(i, j) = T의 i번째 글자까지 완성했고, 현재 S의 j번째 위치에 있을 때 최소 이동 거리
로 DP를 정의하면 O(N^2M)에 문제를 풀 수 있습니다.
Task 3. 3D Histogram (110점)
문제
3차원 히스토그램에서 가장 부피가 큰 직육면체의 부피를 구하는 문제입니다.
N ≤ 2000인 경우를 해결하면 20점을 받을 수 있고, N ≤ 2e5인 경우를 해결하면 90점을 추가로 받을 수 있습니다.
풀이
2차원의 경우는 매우 유명한 문제이고, 모노톤 스택을 이용한 풀이와 분할 정복을 이용한 풀이가 있습니다. 모노톤 스택을 이용하는 것은 어려울 것 같으니 분할 정복 풀이를 생각해봅시다.
구간 [st, ed]에 대해 중점 md를 지나는 경우를 모두 처리한 뒤, 구간 [st, md-1]과 [md+1, ed]에 대해 재귀적으로 처리할 것입니다.
두 함수 f(i, j)와 g(i, j)를 아래와 같이 정의합시다.
$f(i, j) = \min_{i \leq k \leq j} (a_k), \ g(i, j) = \min_{i \leq k \leq j} (b_k)$
f(st, ed) * g(st, ed) * (ed - st + 1)을 최대화 하는 것이 이 문제의 목표입니다.
구간의 양 끝점 st, ed와 중간 지점 md에 대해, 4가지 경우로 분류할 수 있습니다.
- f(st, md) ≤ f(md, ed) && g(st, md) ≤ g(md, ed)
- f(st, md) ≤ f(md, ed) && g(st, md) ≥ g(md, ed)
- f(st, md) ≥ f(md, ed) && g(st, md) ≥ g(md, ed)
- f(st, md) ≥ f(md, ed) && g(st, md) ≤ g(md, ed)
1, 2만 처리하면 적절히 뒤집고 swap하는 것으로 3, 4번을 똑같이 처리할 수 있습니다. 1, 2에 대한 풀이를 알아봅시다.
1번 케이스는 투포인터로 O(ed - st + 1)만에 쉽게 해결할 수 있습니다.
각 시작점 s에 대해, f(s, e) = f(s, m)이고 g(s, e) = g(s, m)인 e의 최댓값을 찾아서 최대 부피를 갱신해주면 됩니다. (아래 코드에서 calc1 함수를 참고하세요.)
2번 케이스는 복잡합니다.
f(s, m) * g(m, e) * (e - s + 1)을 최대화 하는 것이 목적입니다.
식을 좀 변형하면, f(s, m) * (-s + 1) * g(m, e) + f(s, m) * e * g(m, e)가 됩니다.
f(s, m)으로 묶어주면, f(s, m) * { g(m, e) * (-s + 1) + e * g(m, e) }가 됩니다.
중괄호 안쪽의 식은 g(m, e)를 기울기, e*g(m, e)를 y절편으로 하는 일차함수라고 생각할 수 있습니다. 일차함수의 최댓값을 구하는 것은 Convex Hull Trick을 이용해 빠르게 구할 수 있으니, 이 성질을 이용해서 풀이를 찾아봅시다.
먼저 1번 케이스처럼 각 시작점 s에 대해, f(s, e) = f(s, m)이고 g(s, e) = g(s, m)이 되도록 하는 e의 구간을 각각 전처리해줍시다. O(ed - st + 1) 시간에 가능합니다. 이 구간을 l(s), r(s)라고 합시다.
각 시작점 s에 대해, l(s)부터 r(s)번째 직선들 중에서 (-l + 1) 시점에서의 최댓값을 구하면 됩니다. 이는 세그먼트 트리의 각 정점에서 CHT를 관리하는 것으로 처리할 수 있습니다.
세그먼트 트리의 각 정점에 저장되는 직선의 기울기와 쿼리로 주어지는 x 좌표인 (-l + 1) 모두 단조성이 있으니 CHT를 선형에 처리할 수 있습니다.
$T(N) = 2T(N/2) + O(N \log N)$이므로 $O(N \log^2 N)$에 문제를 풀 수 있습니다.
Task 4. Papričice (110점)
문제
트리가 주어집니다. 트리의 간선을 2개 제거하면 3개의 컴포넌트로 나누어질텐데, 가장 큰 컴포넌트와 가장 작은 컴포넌트의 차이를 최소화하는 것이 이 문제의 목표입니다.
N ≤ 200인 경우를 해결하면 15점, N ≤ 2000인 경우를 해결하면 35점, N ≤ 2e5인 경우를 해결하면 60점을 받아 총 110점 만점입니다.
풀이
풀이 설명과 구현의 편의를 위해 임의의 정점을 루트로 잡아서 rooted tree로 만듭시다.
제거하는 두 간선의 양 끝에 달린 정점 중 깊이 있는 정점을 각각 x, y라고 합시다.
(1) x, y가 조상 관계인 경우와 (2) 그렇지 않은 경우, 2가지로 나누어서 생각합시다.
1번 케이스를 먼저 봅시다. 일반성을 잃지 않고, x가 y의 조상이라고 합시다. 각 컴포넌트의 크기는 S[y], S[x] - S[y], N - S[x]가 됩니다.
2번 케이스에서 각 컴포넌트의 크기는 S[x], S[y], N - S[x] - S[y]가 됩니다.
각 y에 대해서 적절한 x를 찾아 세 컴포넌트의 크기가 최대한 같아지도록 만들면 됩니다.
1번 케이스는 S[x]가 (N+S[y])/2에 가까워야하고, 2번 케이스는 S[x]가 (N-S[y])/2에 가까워야합니다.
DFS를 돌면서, std::set에 lower_bound를 적당히 사용해주면 $O(N \log N)$만에 정답을 구할 수 있습니다.
“정답이 D 이하일 것이다.” 라고 파라메트릭 서치를 하면서 Persistent Segment Tree를 쓰면 $O(N \log^2 N)$, Merge Sort Tree를 쓰면 $O(N \log^3 N)$인데, 두 풀이 모두 TLE가 납니다. 개인적으로는 N ≤ 50000인 서브태스크가 있었으면 더 좋았을 것 같습니다.
이 코드도 아래에 같이 올리겠습니다.
Task 5. Tenis (110점)
문제
N명의 선수가 풀리그 방식으로 테니스 경기를 합니다. 테니스 코트는 3가지 종류가 있고, 각 코트마다 참가자들의 순위가 주어집니다. 두 선수가 어떤 코트에서 경기를 한다면, 그 코트에서 순위가 높은 선수가 무조건 이깁니다.
우리는 코트마다 참가자들의 순위가 주어졌을 때, 각 경기를 적절한 코트에서 진행해 최대한 좋은 경기가 되도록 만드려고 합니다.
좋은 경기라는 것은, 경기의 승자가 가장 잘하는 코트에서 진행되는 경기를 의미합니다. (예를 들어 A 코트에서 경기를 했을 때 승자는 A 코트에서의 순위가 2등이고, B 코트에서 경기를 했을 때 승자는 B 코트에서의 순위가 1등이라면 B 코트에서 경기를 진행하는 것이 더 좋은 경기입니다.)
어떤 두 코트에서 승자의 순위가 똑같다면, 패자의 순위가 더 높은 것이 좋은 경기입니다. 승자와 패자 모두 순위가 같다면 더 작은 번호의 코트에서 진행하면 됩니다.
N(N-1)/2번의 경기를 최대한 좋은 경기로 진행했을 때, 각 코트가 사용된 횟수와 각 선수가 이긴 횟수를 출력하면 됩니다.
N ≤ 3000인 경우 50점을 받고, N ≤ 1e5인 경우 60점을 받습니다.
풀이
각 경기를 (1) 어떤 선수의 최대 순위가 다른 선수의 최대 순위보다 높은 경우와 (2) 그렇지 않은 경우, 총 2가지로 분류할 수 있습니다. 또한 2번 케이스는 최대 3N번 밖에 일어나지 않습니다. (두 선수 모두 최대 순위가 i인 경우는 최대 3가지 밖에 없습니다.)
2번 케이스는 간단하게 처리할 수 있으니 1번 케이스만 설명하겠습니다. 2번 케이스에 대한 처리 방법이 궁금하면 아래 코드에서 go 함수를 참고하세요.
1번 케이스를 해결하는 방법을 알아봅시다.
기본적인 접근 방법은, 각 사람에 대해 그 사람보다 잘하는 사람의 수(그 사람이 지는 경우의 수)를 구하는 것입니다.
각 사람의 순위가 제일 높아지는 경기장을 비트로 표현합시다. 3비트로 표현되고, 공집합은 없기 때문에 7가지가 나옵니다. 순위가 제일 높은 경기장이 bit인 사람들의 최고 등수를 정렬해서 배열 vec[bit]에 저장합시다.
vec[bit]에서 i번째 사람보다 잘하는 사람의 수는 이진탐색(std::lower_bound)를 이용해 쉽게 구할 수 있습니다. 어떤 코트에서 경기를 진행해야할지 결정만 하면 됩니다.
승자는 비트가 켜져있는 코트에서 모두 동일한 퍼포먼스가 나오기 때문에 패자의 순위만 최소화하면 됩니다. 이것은 비트마스크를 이용해 간단하게 구현할 수 있습니다.
정답 코드
Task 1
1 |
|
Task 2
1 |
|
Task 3
1 |
|
Task 4
1 |
|
Task 5
1 |
|
부분 점수 코드
Task 4 - O(N log^3 N) with Merge Sort Tree
1 |
|
Task 4 - O(N log^2 N) with Persistent Segment Tree
1 |
|