백설 공주와 난쟁이 N명과 함께 숲 속에 살고 있다. 난쟁이는 매일 광산에 일하러가고, 백설 공주는 그동안 페이스북을 하고 있다.
매일 아침 난쟁이는 한 줄로 휘파람을 불면서 광산으로 출근을 한다. 백설 공주는 그 주변을 돌아다니면서 난쟁이들 사진을 찍는다.
난쟁이가 광산에 들어가면, 백설 공주는 다시 집으로 돌아간다. 집으로 돌아가면서 찍은 사진 중에 페이스북에 올릴 예쁜 사진을 고른다. 각 난쟁이는 모두 모자를 쓰고 있다. 모자의 색상은 총 C가지가 있다. 사진에 찍힌 난쟁이가 쓰고 있는 모자의 색상 중 절반보다 많은 색이 같은 색이라면 예쁜 사진이다. 즉, 사진에 난쟁이가 K명 찍혀있고, K/2보다 많은 난쟁이의 모자 색이 같다면 예쁜 사진이다.
백설공주가 찍은 사진 M개와 각 사진에 찍힌 난쟁이가 주어졌을 때, 예쁜 사진인지 아닌지를 알아내는 프로그램을 작성하시오.
구간에 대한 처리를 오프라인으로 진행할 수 있다는 점에 착안하여 오프라인 쿼리 + Mo's로 먼저 도전해 보았다. 이 경우, 쿼리 처리에 대한 시간복잡도는 O((M+N)sqrt(N)) 이 되리라는 가설에서였다. 문제는 절반이 넘는 숫자를 판단해야하므로 C개 값에 대한 체크가 필요했고, 이 체크에 소요되는 시간은 최악의 경우 C가 된다. 즉 실제로는 O((MC+N)sqrt(N))의 시간복잡도로 수행하게 되어 python3에서는 시간 초과, pypy3으로 간신히 통과할 수 있었다.
풀이 코드 1 (pypy3)
from collections import defaultdict
import sys
input = sys.stdin.readline
N, C = map(int, input().split())
hats = list(map(int, input().split()))
M = int(input())
hat_dict = defaultdict(int)
ans = [0]*M
queries = [[i] + list(map(lambda x : int(x)-1, input().split())) for i in range(M)]
queries.sort(key = lambda x : (x[1] // (N ** 0.5), x[2]))
def check(K) :
for key, val in hat_dict.items() :
if val > K / 2 :
return key
return 0
l, r = 0, -1
for idx, i, j in queries :
while r < j :
r += 1
hat_dict[hats[r]] += 1
while r > j :
hat_dict[hats[r]] -= 1
if not hat_dict[hats[r]] :
del hat_dict[hats[r]]
r -= 1
while l < i :
hat_dict[hats[l]] -= 1
if not hat_dict[hats[l]] :
del hat_dict[hats[l]]
l += 1
while l > i :
l -= 1
hat_dict[hats[l]] += 1
ans[idx] = check(j - i + 1)
for v in ans :
if not v :
print("no")
else :
print("yes", v)
(어쨌든 맞았으니) 풀이에 사용할 수 있는 알고리즘 분류를 보자, 무작위화 알고리즘을 발견할 수 있었다. 진짜 충격적인 발상의 전환이기는 했다...
우선, 구간의 난쟁이가 과반수 이상인 경우를 생각해 보자. 이 구간에서 임의의 난쟁이를 뽑았을 때, 과반에 속하는 모자일 경우는 1/2 이상이다. 즉 과반에 속하지 않은 모자를 뽑았을 경우는 1/2 이하가 되며, K번 이를 반복할 경우 그 확률은 (1/2)^K이하로 줄어든다.
즉 K번 동안
유니폼 랜덤하게 모자를 뽑아서
그 모자가 구간 내에 과반이라면 무조건 yes.
만약 그렇지 않다면 continue
로 K번을 수행함으로써 효과적으로 계산해 볼 수 있는 것이다! 충분한 K값이면 틀릴 확률이 매우 적어진다는 사실을 알 수 있으므로(이를테면 K = 20일 때 2^20 ~= 10^6이므로 1e-6의 오차확률 내), 충분히 적용 가능해보인다(물론 거진 로또맞을 확률로 틀릴 수 있다는 점은 부정할 수 없다).
이 경우 과반을 빠르게 계산하려면 전처리를 통하여 O(logN)의 시간 내에 계산 가능하다. 오른쪽 구간 경계의 upper bound, 왼쪽 경계의 lower bound를 구하는 방식으로 적용해볼 수 있다. 즉 총 시간복잡도는 O(MKlogN)이 된다. 오늘도 새롭게 한 수 배워간다...!
from random import randint
import sys
input = sys.stdin.readline
MAX = 20
N, C = map(int, input().split())
hats = list(map(int, input().split()))
M = int(input())
hatidx = [list() for _ in range(C+1)]
for i in range(N) :
hatidx[hats[i]].append(i)
def cal(l, r, key) :
lstart = rstart = 0
lend = rend = len(hatidx[key])
while lstart < lend :
mid = (lstart + lend) // 2
if hatidx[key][mid] < l :
lstart = mid + 1
else :
lend = mid
while rstart < rend :
mid = (rstart + rend) // 2
if hatidx[key][mid] <= r :
rstart = mid + 1
else :
rend = mid
return rend - lend
for _ in range(M) :
l, r = map(lambda x : int(x)-1, input().split())
K = r - l + 1
flg = False
for _ in range(MAX) :
key = hats[randint(l, r)]
if cal(l, r, key) > K / 2 :
flg = True
break
if flg :
print("yes", key)
else :
print("no")