문제 92
리더 찾기
$N$명의 사람이 일렬로 서 있다. 왼쪽에서 $i$번째 사람의 능력치는 $A_i$이다.
$i$번째 사람이 다음 조건 중 하나 이상을 만족하면 리더라고 한다.
- 자신의 왼쪽에 있는 모든 사람의 능력치가 $A_i$ 이상이다.
- 자신의 오른쪽에 있는 모든 사람의 능력치가 $A_i$ 이하이다.
단, 왼쪽 또는 오른쪽에 사람이 없는 경우, 해당 조건은 만족한 것으로 본다.
세종이는 리더가 많을수록 좋다고 생각한다. 이를 위해 한 사람을 줄에서 빼낸 뒤 원하는 위치에 다시 삽입하는 행동을 최대 한 번 할 수 있다.
세종이가 만들 수 있는 리더 수의 최댓값을 구하라.
입력
첫 번째 줄에 정수 $N$이 주어진다. $(1 \le N \le 200\,000)$
두 번째 줄에 $N$개의 정수 $A_1, A_2, \cdots, A_N$이 공백으로 구분되어 주어진다. $(1 \le A_i \le 10^9)$
출력
첫 번째 줄에 세종이가 만들 수 있는 리더 수의 최댓값을 출력한다.
예제 입력 1
1 1
예제 출력 1
1
예제 입력 2
5 3 5 4 2 3
예제 출력 2
5
공식 해설
사람을 고르는 경우와 삽입 위치를 모두 열거하면 $O(N^2)$개 이동을 확인해야 한다. $N\le200\,000$이므로 한 사람을 빼는 영향과 다시 넣는 위치를 분리해서 생각한다.
한 사람을 빼면 새 리더가 되는 경우
리더가 아니라는 것은 왼쪽에 자신보다 작은 사람이 있고, 오른쪽에 자신보다 큰 사람도 있다는 뜻이다. 한 사람을 빼는 것으로 기존의 다른 리더가 자격을 잃지는 않는다.
현재 리더가 아닌 $i$가 $j$를 뺀 뒤 리더가 되려면, $j$가 $i$의 왼쪽에서 자신보다 작은 유일한 사람이거나 오른쪽에서 자신보다 큰 유일한 사람이어야 한다. 둘 중 하나의 방해가 완전히 사라져야 하기 때문이다.
각 사람 $j$에 대해 이 조건으로 새 리더가 되는 사람 수를 $g_j$에 센다. 같은 $i$의 두 방해자는 서로 다른 쪽에 있으므로 하나의 $g_j$에 중복해서 더해지지 않는다.
새 리더를 유지하며 다시 넣을 수 있다
빼낸 능력치 $x$를 남은 줄에서 $x$보다 작은 첫 사람 바로 앞에 넣는다. 그런 사람이 없으면 맨 뒤에 넣는다. $x$의 앞에는 $x$ 이상인 사람만 있으므로 움직인 사람은 리더가 된다.
삽입 위치 앞의 사람은 능력치가 $x$ 이상이므로 오른쪽에 $x$가 생겨도 오른쪽 조건을 잃지 않는다. 삽입 위치 뒤의 사람은 능력치가 $x$ 이하라면 왼쪽 조건을 잃지 않는다. $x$보다 큰 사람은 이미 앞에 $x$보다 작은 사람이 있었으므로 원래도 왼쪽 조건으로 리더가 될 수 없었다. 이 사람들의 오른쪽 조건은 바뀌지 않는다.
따라서 빼낸 직후의 모든 리더를 유지하면서 움직인 사람도 리더로 만들 수 있다.
유일한 방해자를 선형 시간에 찾기
접두사 최솟값과 접미사 최댓값으로 기존 리더 여부를 구한다. 이어서 각 위치의 왼쪽 부분에 있는 가장 작은 두 원소와 최솟값의 위치, 오른쪽 부분에 있는 가장 큰 두 원소와 최댓값의 위치를 전처리한다. 두 원소는 서로 다른 값이 아니라 중복을 포함한 두 위치를 뜻한다.
리더가 아닌 $i$에 대해 왼쪽의 두 최솟값을 $m_1\le m_2$라 하면 $m_1<A_i\le m_2$일 때만 최솟값의 위치가 유일한 작은 방해자이다. 오른쪽의 두 최댓값을 $M_1\ge M_2$라 하면 $M_1>A_i\ge M_2$일 때만 최댓값의 위치가 유일한 큰 방해자이다. 해당 위치의 $g$를 $1$ 늘린다. 두 번째 원소가 없다면 각각 양의 무한대와 음의 무한대를 쓴다.
원래 리더 수를 $C$, $j$의 기존 리더 여부를 $I_j\in\{0,1\}$라 하면 $j$를 움직여 얻는 최댓값은
\[
C+g_j+1-I_j
\]
이다. 제거 직후의 리더 수가 상한이고 위 삽입법이 이를 달성하므로, 모든 $j$에 대해 이 식의 최댓값을 출력한다.
시간복잡도와 공간복잡도는 $O(N)$이다. $N=1$에서도 같은 식으로 $1$을 얻는다.
구현 · 언어별 풀이
C++ 구현
왼쪽의 두 최솟값과 오른쪽의 두 최댓값을 위치 정보와 함께 배열에 저장한다. 값이 같은 두 원소도 서로 다른 위치로 유지해야 유일한 방해자인지 판정할 수 있다. 새 리더 수를 원래 방해자의 인덱스에 누적한 뒤 모든 이동 대상의 답을 비교한다.
C 구현
두 최솟값과 두 최댓값은 서로 다른 위치의 값으로 저장하고 중복도 포함한다. 유일한 방해자의 원래 인덱스에 g를 증가시킨다. 후보 답 C+g[j]+1-I[j]를 int로 계산하며 N=1도 처리한다.
Python 구현
접두사·접미사의 두 극값과 첫 극값 인덱스를 배열로 유지한다. set으로 중복을 제거하면 유일한 방해자 판정이 틀린다. 본인을 포함하기 전의 극값으로 검사하고 g를 해당 방해자 위치에 더한다.
Java 구현
int 배열에 극값·인덱스·g·리더 표식을 둔다. 없는 두 번째 값에는 입력 범위 밖 sentinel을 사용한다. 동일 값의 두 위치를 모두 유지하고 본인 제외 접두사·접미사로 유일성을 판단한다.
Rust 구현
극값은 i64로 두고 입력 범위 밖의 양·음 sentinel을 사용하면 경계가 명확하다. 두 위치의 같은 값을 dedup하지 않는다. g는 원래 위치에 누적하고 후보 답은 부호 있는 정수로 계산한다.
JavaScript 구현
원소 값은 $10^9$ 이하, 추가 리더 수는 $N$ 이하라 Number와 정수 계수 배열로 충분하다. 왼쪽의 작은 두 값과 첫 값의 위치, 오른쪽의 큰 두 값과 첫 값의 위치를 전처리한다.
두 극값은 서로 다른 값 두 개가 아니라 서로 다른 위치 두 개이다. 같은 값이 나왔을 때도 두 번째 칸에 반영해야 유일한 방해자를 정확히 판정한다. Set으로 중복을 제거하지 않는다.
한 방향의 현재 값을 포함하기 전에 해당 위치의 조건을 검사한다. 두 번째 값이 아직 없으면 Infinity·-Infinity를 쓰며 이 값을 보관할 배열은 Float64Array 또는 일반 숫자 배열로 둔다. 정수 배열에 무한대를 넣으면 $0$으로 변환된다.
리더 여부는 Uint8Array, 방해자를 제거해 얻는 증가 수는 Int32Array에 두고 공통 풀이의 C + g[j] + 1 - isLeader[j] 최댓값을 구한다. 이동된 줄 전체를 만들지 않는다.