오늘이 마지막 날인데, 이전날까지 만점자가 100명이 넘게 있었다. 어떻게든 최종 만점자를 줄이기 위해서는 마지막 기회라서 좀 어려운 문제가 나오지 않을까 예상.
'조건을 만족하는 부분배열중 가장 길이가 큰 것을 찾는 문제' 에 해당하는데, 이것은 보통 투포인터를 써서 풀리는 유형이 많다.
엄밀하게는, 부분배열을 특정 상태값으로 표현할때, (1)어떤 상태가 조건을 만족하는지를 체크하는 연산, (2)부분배열에 원소를 추가할때 상태를 업데이트하는 연산, (3)부분배열에서 원소를 삭제할때 상태를 업데이트하는 연산들을 빠르게 처리할수 있다면, 투포인터가 효율적이다.
(1),(2),(3)을 처리하는 시간 O(T)라면, 총 O(NT)에 최대 부분배열을 찾을 수 있다.
이 문제에서, 주어진 부분배열이 조건을 만족하는지 여부는 체크하는 방법을 생각해보자. 예시를 들어 생각해보면, 1이 두장 이상 있으면 안된다. 1이나 2가 합쳐서 3장 이상 있으면 안된다, … 일반화하면, x이하 숫자가 x개보다 많이 있으면 안되고, 그런 경우가 없으면 조건을 만족시킨다. 이걸 체크하는 방법은 대충 두가지가 떠오른다
1) K개의 숫자들을 정렬시킨다. 모든 i에 대해서 a[i]>i 이면 만족한다 (i는 0-indexed)
2) K개의 숫자들에 대한 카운터를 만든다. 모든 x⇐K에 대해서, count[0]+count[1]+…+count[x] ⇐ x 이면 만족한다.
(1)번을 사용하려면, 우선 원소를 추가/삭제할때마다 정렬된 컬렉션을 유지해야 한다. 이거는 tree set 등으로 O(logn)에 가능하다 쳐도, 모든 i에 대해서 확인하려면 O(n)이 걸리는걸 피할수 없다.
(2)번을 사용하는 쪽으로 생각하자. 원소 추가/삭제시 카운터를 업뎃하는거는 O(1)이다. 하지만 sum(count[1:x]) 를 구하는 것이 어려울것 같다. 차라리, 카운터가 아니라 카운터의 누적합을 유지하는 것은? 펜윅트리를 쓰면 O(logn)에 업데이트가 가능하다.
실제 예선문제를 풀때에는 여기까지 생각이 미친 다음에, 실제로 m 이라는 원소가 업데이트 되었을 경우, m보다 작은 값들에 대한 카운터의 누적합은 변하지 않으니까, sum(count[1:m]) 만 확인하면 되고, 이것도 펜윅트리로 O(logn)에 쿼리가 되므로 이제는 풀수 있겠다고 생각했다. 그리고 이렇게 구현해서 제출했다가 바로 오답을 한번 먹었다;
그렇다, sum(count[1:m])⇐m 인지 뿐만 아니라 sum(count[1:m+1])⇐m+1, sum(count[1:m+2])⇐m+2, sum(count[1:K])⇐K 를 모두 다 확인해야지 제대로 처리되는데 이것을 간과했다. 반례로는 3 4 3 4 3 4 가 있다.
작전을 바꾼다. 이것을 모두 체크하는 것을 한번에 처리하기 위해, 식을 이런식으로 변형해보자
sum(count[1:m])-m ⇐ 0 && sum(count[1:m+1])-(m+1) ⇐ 0 && sum(count[1:m+2])-(m+2) ⇐ 0 ….
=⇒ max(sum(count[1:m])-m, sum(count[1:m+1])-(m+1), sum(count[1:m+2])-(m+2)) ⇐ 0
count의 누적합에 초기값을 주어서 초기화하고, range add 로 갱신, range max 로 쿼리를 하는 레이지 세그트리를 쓰면 O(logn)업뎃, O(logn)쿼리가 가능하다
이제 이대로 구현. 제출해서 통과. 시간복잡도는 O(nlogn)
설마 진짜 레이지 세그가 정해라고? 뭔가 더 쉬운 방법을 놓친것 같기도 했지만 일단 통과했으니 넘어갔다
나중에 후기로는, 더욱 나이브한 방법들도 시간 안에 돌아갔다는 이야기를 들었다 (하지만 이건 c언어에나 해당되는 이야기이지 파이썬은 안됐을 것이다)
투포인터를 쓰되, K개의 숫자들을 정렬시키고, 모든 i에 대해서 a[i]>i 이면 만족하는지를 체크하는 방법 → 심지어 그냥 정렬된 배열에서 원소 추가/삭제를 O(n)에 처리해서 총 O(n^2)에 한것도 아니고, 추가되는 원소를 매번 정렬을 다시 해서 O(n^2logn)에 돌렸지만 돌아갔다고 한다.
투포인터를 쓰지 않고, K를 고정한후, 슬라이딩 윈도우로 해당되는 배열이 있는지 O(n^2)에 찾고, 이분탐색으로 K값을 찾는 방법. 역시 총 O(n^2logn)