프로그래머스 숫자 게임 풀이: 정렬과 그리디 매칭
작성·코드 검증: solve together · 작성 언어: Python
0. 문제 링크와 출처
- 문제: 숫자 게임
- 출처: 프로그래머스, Summer/Winter Coding(~2018)
- 자체 판단 난이도: Level 3
B팀은 A팀의 출전 순서를 미리 알고 있으므로, 자신의 숫자들을 원하는 순서로 배치할 수 있다. 매 경기마다 이길 수 있는 가장 작은 숫자를 선택하는 그리디 기준을 세우고, 정렬된 두 배열에서 사용한 숫자를 관리해야 하므로 Level 3으로 판단한다.
1. 문제 요약
A팀과 B팀에 각각 N개의 자연수가 주어진다. 모든 사원은 한 번씩 경기를 치르고,
각 경기에서 더 큰 숫자를 낸 팀이 1점을 얻는다. 같은 숫자라면 어느 팀도 점수를 얻지
못한다.
A팀의 출전 순서가 배열 A로 주어질 때, B팀은 배열 B의 원소를 원하는 순서로 배치해
얻을 수 있는 최대 승점을 구해야 한다. B팀 숫자는 반드시 A팀 숫자보다 커야 승리할
수 있다.
A,B의 길이는 같고 최대100,000이다.- 각 숫자는
1이상1,000,000,000이하다. - B팀의 숫자는 한 경기에서 한 번만 사용할 수 있다.
예를 들어 A = [5, 1, 3, 7], B = [2, 2, 6, 8]이면 B팀은 8, 2, 6, 2 순서로
출전해 세 경기를 이길 수 있다.
2. 문제 해결에 사용되는 알고리즘이나 자료구조 설명
그리디 선택 기준
A팀과 B팀의 숫자를 각각 오름차순으로 정렬한다. 가장 작은 A팀 숫자부터 확인하면서, 현재 숫자를 이길 수 있는 B팀의 숫자 중 가장 작은 것을 선택한다.
- B팀의 현재 숫자가
A의 숫자보다 작거나 같으면 이 경기에서 이길 수 없으므로 버린다. - B팀의 현재 숫자가 더 크면 이 숫자로 승리하고 다음 B팀 숫자로 이동한다.
현재 A팀 숫자를 이길 수 있는 B팀 숫자 중 가장 작은 숫자를 사용하면, 더 큰 B팀 숫자를 아껴 둘 수 있다. 남은 A팀 숫자는 현재보다 크거나 같으므로, 작은 숫자로 이길 수 없는 상대는 이후에도 이길 수 없다.
이길 수 있는 가장 작은 수를 먼저 배정하기
B팀이 현재 A팀 숫자를 이길 수 있다면, 더 큰 B팀 숫자를 사용해도 얻는 점수는 동일하게 1점이다. 따라서 가장 작은 승리 숫자를 선택해야 이후 더 큰 A팀 숫자에 대응할 수 있는 큰 숫자를 보존할 수 있다.
반대로 B팀의 가장 작은 남은 숫자가 현재 A팀 숫자보다 작거나 같다면, 이 숫자는 현재 상대도 이기지 못하고 앞으로 확인할 더 큰 A팀 숫자도 이길 수 없다. 이 숫자를 버리는 것은 어떤 승리 기회도 잃지 않는다.
그리디 선택이 안전한 이유
현재 가장 작은 A팀 숫자를 a, 이를 이길 수 있는 가장 작은 B팀 숫자를 b라고 하자.
어떤 최적 배치가 a를 더 큰 숫자 c로 이기고 있었다고 해도 b와 c를 바꿀 수 있다.
b가 사용되지 않았다면a에b를 배치해 같은 승점을 유지할 수 있다.b가 다른 A팀 숫자a'를 이기고 있었다면,c는b보다 크므로a'도 계속 이긴다. 따라서a에는b,a'에는c를 배치해 승점 수를 유지할 수 있다.
이 교환이 가능하므로 현재 A팀 숫자에 가장 작은 승리 숫자를 선택해도 최적해를 잃지 않는다.
오답 접근과 반례
각 A팀 숫자에 대해 항상 B팀의 최댓값을 배치하면 당장 이길 수는 있지만 큰 숫자를 낭비하게 된다.
A = [1, 2]
B = [2, 3]
첫 번째 A팀 숫자 1에 B팀의 최댓값 3을 사용하면 남은 2로는 A팀의 2를 이길 수
없어 1점만 얻는다. 반면 1에는 2, 2에는 3을 배치하면 2점이다. 이 문제에서는
이길 수 있는 가장 작은 B팀 숫자를 선택해야 한다.
숫자가 같을 때는 승리하지 못한다. A = [2], B = [2]에서는 B[0] > A[0]가
아니므로 정답은 0이다.
시간·공간 복잡도
두 배열을 정렬하는 데 O(n log n) 시간이 필요하고, 정렬된 배열을 한 번 순회하는 데
O(n) 시간이 필요하다. 따라서 전체 시간 복잡도는 O(n log n)이다.
코드에서는 두 배열을 제자리 정렬하고 인덱스 하나만 추가로 사용하므로 알고리즘 관점의
추가 공간은 O(1)이다. 다만 Python의 내부 정렬 구현은 정렬 과정에서 입력 크기에
비례하는 보조 메모리를 사용할 수 있다.
3. 문제 해결 코드 (Python)
def solution(A, B):
A.sort()
B.sort()
score = 0
b_index = 0
for a_value in A:
while b_index < len(B) and B[b_index] <= a_value:
b_index += 1
if b_index == len(B):
break
score += 1
b_index += 1
return score
A를 작은 값부터 확인하고 B도 작은 값부터 순서대로 확인한다. B[b_index]가
현재 A팀 숫자보다 작거나 같으면 승리할 수 없으므로 다음 B팀 숫자를 확인한다.
승리 가능한 숫자를 찾으면 점수를 1 증가시키고 해당 B팀 숫자를 사용 처리한다. 한 번
사용한 B팀 숫자는 b_index를 증가시켜 다시 사용하지 않는다.
복사해 실행할 테스트
아래 검사를 풀이 코드 아래에 붙여 python 파일명.py로 실행하면 이길 수 있는 상대를 순서대로 배정하는지 확인할 수 있다.
assert solution([5, 1, 3, 7], [2, 2, 6, 8]) == 3
assert solution([1, 1, 1], [1, 1, 1]) == 0
4. 공식 예제와 추가 검증
공식 예제와 경계 조건, 큰 입력을 Python 코드로 실행해 확인했다.
공식 예제
| 입력 | 기대 출력 | 확인 결과 |
|---|---|---|
A = [5, 1, 3, 7], B = [2, 2, 6, 8] |
3 | 통과 |
A = [2, 2, 2, 2], B = [1, 1, 1, 1] |
0 | 통과 |
추가 검증 1: 한 번 이길 수 있는 경우
입력: A = [1], B = [2]
기대 출력: 1
확인 결과: 통과
검증 목적: 최소 입력에서 B팀의 승리를 세는지 확인
추가 검증 2: 숫자가 같은 경우
입력: A = [1], B = [1]
기대 출력: 0
확인 결과: 통과
검증 목적: 같은 숫자를 승리로 계산하지 않는지 확인
추가 검증 3: 작은 승리 숫자를 먼저 사용하는 경우
입력: A = [1, 2], B = [2, 3]
기대 출력: 2
확인 결과: 통과
검증 목적: 큰 B팀 숫자를 아껴 두는 그리디 선택 확인
추가 검증 4: 제한에 가까운 입력
입력: A = [1, 2, 3, ..., 10000]
B = [2, 3, 4, ..., 10001]
기대 출력: 10000
확인 결과: 통과
검증 목적: 10,000개 원소를 정렬하고 모든 경기에서 승리하는지 확인
5. 다른 풀이 방식 검색 및 비교
- 프로그래머스 공식 문제는 A팀의 출전 순서를 보고 B팀의 순서를 정해 최대 승점을 구하는 조건을 정의한다.
- 프로그래머스 Python 풀이 그룹은 같은 문제에 대한 공개 풀이와 접근 방식을 확인할 수 있는 공식 참고 페이지다.
B팀 숫자를 정렬한 뒤 현재 A팀 숫자를 이길 수 있는 가장 작은 값을 선택하는 방식은 사용 가능한 숫자를 효율적으로 보존한다. 다른 구현에서는 최대 힙이나 두 배열의 양끝을 사용할 수도 있지만, 숫자 범위와 승리 조건을 고려하면 오름차순 정렬과 단일 포인터만으로 충분히 구현할 수 있다.
마무리
숫자 게임은 모든 경기를 직접 배치해 보지 않고, 정렬을 통해 현재 상대를 이길 수 있는
가장 작은 B팀 숫자를 선택하는 그리디 문제다. 이 선택은 큰 숫자를 불필요하게
소모하지 않으며, 이길 수 없는 작은 숫자는 과감히 버려도 최적 승점에 영향을 주지 않는다.
정렬과 두 포인터를 이용하면 O(n log n) 시간에 최대 승점을 구할 수 있다.