치킨너겟정리


Thumbnail generated by AI

숫자랑 실제 상자 안 갯수가 안 맞아서 불편

문제

치킨너겟을 각각 6개, 9개, 20개 담은 소/중/대 세 가지 크기의 박스로 판매한다. 그렇다면 박스 단위로만 구매할 때, 조합할 수 없는 가장 큰 치킨너겟 개수는 몇 개일까? (세 박스 용량의 최대공약수는 1이다. 두 개씩 서로소일 필요는 없다.)

예를 들어 치킨너겟 1개만 사는 것은 불가능하다. 최소 6개짜리 박스를 사야 하기 때문이다. 반면 15개는 가능하다. 6개짜리 하나와 9개짜리 하나를 사면 된다.

아래에서는 조합 가능성에 대한 수학적 설명과, 답을 구하는 알고리즘을 다룬다.

존재성

각 박스 용량을 xix_i로 표기하자. 일반성을 잃지 않고 xix_i는 엄격히 증가하는 수열이라고 해도 좋다. 여기서는 x1=6x_1=6, x2=9x_2=9, x3=20x_3=20이다. 또한 gcd(xi)=1\gcd(x_i) = 1이라고 가정한다.

존재성을 증명한다는 것은, 어떤 값 NN을 찾아 NN 이상의 모든 자연수가 조합 가능함을 보이는 것이다. 따라서 조합할 수 없는 수는 1 이상 NN 이하에만 존재한다.

최대공약수가 1이므로 icixi=1\sum_i{c_i x_i} = 1을 만족하는 정수 cic_i를 찾을 수 있다. ci+=max(ci,0)c_i^+=\max(c_i, 0), ci=min(ci,0)c_i^-=-\min(c_i, 0)으로 정의하자. 전자는 양수 부분만, 후자는 음수 부분의 절댓값만 취한 것이다. 그러면 ci=ci+cic_i=c_i^+-c_i^-이고, ci+c_i^+cic_i^-는 모두 0 또는 양수이다.

N=x1icixiN=x_1 \sum_i c_i^-x_i라고 하자. 여기서 x1=min(xi)x_1=\min(x_i)이다. 먼저 k0k \ge 0인 모든 N+kx1N + kx_1에 대해 조합이 가능하다. icixi=1\sum_i c_i x_i = 1이므로, x1x_1 박스를 kk개 더 추가하면 N+kx1N + kx_1을 만들 수 있다.

0j<x10 \le j \lt x_1jj에 대해서,

N+j=x1icixi+jicixi=(x1j)icixi+jici+xi\begin{aligned} N+j&=x_1 \sum_i c_i^-x_i+j\sum_i{c_i x_i} \\ &=(x_1 - j) \sum_i c_i^-x_i+j\sum_i c_i^+x_i \end{aligned}

각 항의 계수가 모두 0 이상이 된다. 이 변형은 0j<x10 \le j < x_1일 때만 성립한다. 앞서 N+kx1N + kx_1 형태의 수는 모두 표현 가능하므로, N+kx1+jN + kx_1 + j도 같은 논증으로 표현할 수 있다.

결과적으로 NN 이상의 모든 자연수는 표현 가능하므로, 답은 NN 미만에서 찾을 수 있다.

풀이

사실 박스가 2종류일 때는 닫힌 형태(closed form)의 답이 있지만, 3종류 이상에서는 알려진 일반 공식이 없다. 이 점은 아래 영상에서 확인할 수 있다.

이 문제는 알고리즘으로 풀어 보자.

우선순위 큐 q가 있고, 처음에는 0만 들어 있다고 하자. 이미 큐에 넣었던 값은 다시 넣지 않는다.

큐에서 꺼낸 원소 ee마다 e+xie + x_i를 큐에 넣는다. 언제 반복을 멈출지가 핵심인데, 꺼낸 값이 연속된 자연수로 x1x_1개 이상 이어지면 더 이상 확인할 필요가 없다. 정답은 그 연속 구간의 시작값에서 1을 뺀 값이다.

의사 코드는 다음과 같다.

def find_min_nugget(num_list: list[int]) -> int:
    """
    :param num_list: 정렬된 자연수 리스트
    :return: 조합할 수 없는 가장 큰 자연수
    """

    # Queue Initialize
    queue = HeapQueue()
    queue.put(0)

    # 연속인지 확인 용
    start_num = 0
    consecutive_count = 0
    
    while True:
        current = queue.get()
        for num in num_list:
            queue.put(current + num)

        # 연속인지 확인 및 종료 여부 체크
        if current == start_num + consecutive_count:
            consecutive_count += 1
            if consecutive_count >= num_list[0]:
                return start_num - 1
        else:
            start_num = current
            consecutive_count = 1

def main():
    print(find_min_nugget([6, 9, 20]))  # 결과는 43

마무리

xix_i가 작을 때는 큰 문제가 되지 않지만, 결국 답에 이르기까지의 수를 하나씩 확인해야 한다. 더 빠른 방법이 있는지는 아직 나에게는 열린 문제로 남아 있다.

이 글은 아래 영상에서 영감을 받아 작성했습니다.

why you can't order 43 nuggets. @MichaelPennMath