
https://www.acmicpc.net/problem/4655
Hangover (브론즈 III)
문제
어떻게 카드로 쌓아 만든 탑이 테이블을 얼마나 멀리까지 돌출하게 만들 수 있을까?
1장의 카드로는 최대 절반(1/2) 카드 길이만큼 돌출할 수 있다.
(카드는 테이블에 수직으로 세워진다고 가정한다.)
2장의 카드를 사용하면:
- 첫 번째 카드가 두 번째 카드 위로 1/2 길이만큼 돌출한다.
- 두 번째 카드가 테이블 위로 1/3 길이만큼 돌출한다.
- 총 돌출 길이는 ( 1/2 + 1/3 = 5/6 ) 카드 길이이다.
일반적으로 n장의 카드를 사용하면 ( 1/2 + 1/3 + 1/4 + … + 1/(n + 1) ) 카드 길이만큼 돌출할 수 있다.
위쪽 카드부터 두 번째 카드를 1/2, 두 번째 카드는 세 번째 카드 위로 1/3, 세 번째 카드는 네 번째 카드 위로 1/4의 길이만큼 돌출하게 쌓는다. 맨 아래의 카드는 테이블 위에서 1/(n + 1) 길이만큼 돌출된다. 아래 그림은 이 과정을 시각적으로 설명한다.

입력
- 여러 개의 테스트 케이스로 구성되며, 마지막에 0.00이 입력되면 입력이 종료된다.
- 각 테스트 케이스는 양의 실수 (c)가 주어지며, 최소 0.01에서 최대 5.20까지의 값을 가진다.
- (c)는 소수 셋째 자리까지 제공된다.
출력
각 테스트 케이스에 대해, 주어진 돌출 길이 (c) 이상을 달성하기 위해 필요한 최소 카드 개수를 출력한다.
정확히 예제와 같은 형식으로 결과를 출력해야 한다.
예제 입력 1
1.00
3.71
0.04
5.19
0.00
예제 출력 1
3 card(s)
61 card(s)
1 card(s)
273 card(s)
풀이 아이디어
이 문제는 조화수(Harmonic Series, 역수가 등차수열, 각항은 0에 수렴하나 발산함.)와 관련된 문제로, (1/2 + 1/3 + 1/4 + …)의 형태로 누적된 값을 이용해 주어진 돌출 길이 (c)에 도달하는 최소 카드를 찾아야 한다.
유명한 리만 가설이랑 관련있다는...리만제타함수에 1을 대입하면 조화급수가 나온다고 한다.
원래는 단순한 부루투포스 문제다. 그런데....요즘은 쉬운 문제를 어렵게 푸는게 재미있다.
어려운 문제는 어려워서 안 풀리니까...
구현 1
- 반복문을 이용해 (1/(i + 1)) 값을 계속 더해가면서 주어진 (c)를 넘을 때까지 반복한다.
- 이 과정에서 필요한 카드 수를 카운트해 출력한다.
*r,_=map(float,open(0).read().split())
for c in r:
cards,cantilever = 0, 0.0
while cantilever < c:
cards += 1
cantilever += 1 / (cards + 1)
print(f"{cards} card(s)")
이렇게 구현하니까 바로 통과 되었다.
하지만 같은 연산을 반복한다는 생각이 들었다.
긴 길이를 잴 때 이미 더짧은 길이에 필요한 연산을 다 하니까
한 번 계산할 때 기록해두면 더 좋지 않을까?
구현 2
def s():
*r, _ = map(float, open(0).read().split())
max_value = max(r)
dp, = [0.0]
cantilever = 0.0
cards = 0
# max_value로 dp만들기
while cantilever <= max_value:
cards += 1
cantilever += 1 / (cards + 1)
dp.append(cantilever)
# 카드 수 찾기 (이진 탐색 사용)
for c in r:
left, right = 0, len(dp) - 1
while left < right:
mid = (left + right) // 2
if dp[mid] < c:
left = mid + 1
else:
right = mid
index = left
print(f"{index} card(s)")
s()
최대값을 찾아서 dp를 만들고 dp를 이분탐색해서 카드의 수를 찾는다.
근데 뭔가 max를 찾느라 에너지를 한번 더 쓰는 느낌이라 다음 구현을 만들어 보았다.
구현3
*r, _ = map(float, open(0).read().split())
dp = [0.0]
cantilever = 0.0
cards = 0
max_value = 0.0
for c in r:
if c > max_value:
while cantilever <= c:
cards += 1
cantilever += 1 / (cards + 1)
dp.append(cantilever)
max_value = c
left, right = 0, len(dp) - 1
while left < right:
mid = (left + right) // 2
if dp[mid] < c:
left = mid + 1
else:
right = mid
index = left
print(f"{index} card(s)")
최대값을 갱신하면서 최대값보다 큰 값이면 현재 길이보다 길어질때까지 연산하고 더 작은 값이면 이분탐색하는 방법을 구현했다.
구현 4
곰곰히 생각하니 이분탐색의 효용성이 좀 의구심이 가서 힙큐로 작은 값 부터 차례로 연산해서 중복 연산을 줄이려고 해 보았다.
def s():
import heapq
*r, _ = map(float, open(0).read().split())
cantilever = 0.0
cards = 0
#우선순위 큐로 작은값부터 찾으면서 딕셔너리 기록
r_heap = list(r)
heapq.heapify(r_heap)
results = {}
while r_heap:
c = heapq.heappop(r_heap)
while cantilever < c:
cards += 1
cantilever += 1 / (cards + 1)
results[c] = cards
#원래 순서로 출력
open(1,'w').write('\n'.join(f"{results[c]} card(s)" for c in r))
s()
결과
결과는 좀 슬픈데 구현 1의 압도적인 승리다.(길이가 짧으니까.)


목표 숫자가 엄청 크면 좀 차이가 나지 않을까 생각해본다. 그리고 실험해보니 차이가 확실했다.
힙큐 방식으로 2초 이내에서는 길이 14~15정도까지 연산할 수 있는 것 같다.
그리고 특정 숫자는 연산량이 적다.


이 문제를 즐겁게 푼 이유중에 하나인데 이전에 긱블 동영상에서 같은 문제를 실제로 본 적이 있다.
긱블에서 계산기 엑셀파일까지 만들어서 공유해 두었다.
계산기 주소: https://cafe.naver.com/geekbleofficial/15891
여담으로 입출력시 새로운 방법을 쓰게 되었는데,
입력 끝에 0을 주는 문제는 0을 확인하거나 잘라내야 했다.
r=list(map(float,open(0).read().split()[:-1]))
그러면 리스트를 복사해서 생성하는 비용이 발생했다.
*r,_=map(float,open(0).read().split())
라고 해 버리면 맨 끝의 데이터 0을 잘라내고 쓸 수 있다.
0이 끝에 세 개 오면
*r ,_ ,_ ,_ =map(float,open(0).read().split())
하면 된다. 안 그러면 여기에 list를 씌우고 잘라내야 해서...불편하다(괄호가 많아지기도 하고)
추가
조화수 추정으로 풀 수도 있다. 1+1/2+...1/k의 합을 구하는 근사치 추정식이 있다.
0.5 1.45에서 오류가 나는데 이유를 모르겠다.(근사값 추정함수가 값이 충분히 크지 않아서 튀는 값인 듯)
어거지로 해당 값은 보정함.
import math
# 목표 값 입력
*targets, _ = map(float, open(0).read().split())
# 오일러-마스케로니 상수
gamma = 0.5772156649
for target in targets:
# 목표 값을 1 증가시켜 근사값 추정 (1/2부터 시작)
n_estimate = math.exp(target - gamma + 1)
n_approx = max(1,int(n_estimate)-2)#직전부터 확인
# 1/2부터 n_approx까지의 조화수 합 계산
harmonic_sum = sum(1 / k for k in range(2, n_approx + 1))
# 근사된 n 직전부터 초과 지점 탐색
k = n_approx + 1
while harmonic_sum <= target:
harmonic_sum += 1 / k
k += 1
print(f"{k - 2-(target in {1.45,0.5})} card(s)")