문제
- 링크: https://www.acmicpc.net/problem/16998
- 요약: 정수
p, q, n
에 대해S = \sum_{i=1}^{n} (p·i mod q)
를 구한다. 여러 테스트 케이스가 주어진다. 합 전체에는 모듈러를 취하지 않는다. - 제한:
1 ≤ W ≤ 1e5
,1 ≤ p, q, n ≤ 1e6
, 시간 5초, 메모리 512MB
입력/출력
|
|
예시
|
|
접근 개요
- 핵심 관찰:
(p·i mod q) = p·i - q·floor((p·i)/q)
. 따라서 \[ S = p\cdot\frac{n(n+1)}{2} - q \cdot \sum_{i=1}^{n} \left\lfloor \frac{p i}{q} \right\rfloor \] floor_sum(n, m, a, b) = \sum_{i=0}^{n-1} floor((a·i + b)/m)
를 유클리드 호제법으로 O(log m)에 계산 가능.- 또한
g = gcd(p, q)
로 나누어p = g·a
,q = g·m
이면(p·i mod q) = g·((a·i) mod m)
이고,gcd(a, m) = 1
일 때 길이m
의 완전한 주기에서 잔여 합은m(m-1)/2
. - 두 방법 모두 가능: (1) 직접 floor_sum로 계산하거나, (2)
g
로 분해 후 “완전 주기 + 접두부” 합을 구한 뒤g
를 곱한다.
시각화 (개략 흐름)
flowchart TD A[입력 p,q,n] --> B{g = gcd(p,q)} B --> C[a = p/g, m = q/g] C --> D{전부 floor_sum로 계산?} D -- 예 --> E[p*n*(n+1)/2 - q*floor_sum(n+1,q,p,0)] D -- 아니오 --> F[full = n div m, rem = n mod m] F --> G[주기합 = m*(m-1)/2] G --> H[partial = sum_{i=1..rem} (a*i mod m)] H --> I[답 = g*(full*주기합 + partial)] E --> J[출력] I --> J[출력]
알고리즘 설계
- 방법 A:
S = p*n(n+1)/2 - q * floor_sum(n+1, q, p, 0)
floor_sum
은 표준(AtCoder Library 스타일) 재귀/반복 구현 사용.
- 방법 B:
g = gcd(p, q)
로 분해하여 길이m = q/g
주기마다의 합(서로소 조건에서m(m-1)/2
)을 이용해O(1)
로 처리하고, 남은rem
개는floor_sum
으로 접두부만 계산. - 위 두 방법의 결과는 동일하며, 구현 난이도 및 상수로 인해 A가 간결, B가 수학적 직관이 명확.
정당성 근거
- 분해식
x mod q = x - q*floor(x/q)
에 의해 전체 합을 선형/바닥합으로 분리할 수 있음. gcd(a, m) = 1
이면 곱셈에 의한 잔여류 순열이 성립하여 한 주기의 합이0..m-1
의 합과 동일(=m(m-1)/2
).floor_sum
은 유클리드 호제법으로 분자로/분모를 교환하며 문제 크기를 감소시키므로 각 단계가m
을 줄여서O(log m)
에 종료.
복잡도
- 시간: 케이스당
O(log q)
- 공간:
O(1)
구현 (C++)
|
|
구현 (Python)
|
|
코너 케이스 체크리스트
q = 1
이면 모든 항이 0 → 합 0n < q
인 작은 접두부만 있는 경우gcd(p, q) > 1
로 주기 길이가 짧아지는 경우- 최대 경계:
p = q = n = 10^6
,W = 10^5
(케이스당O(log q)
보장 필요) - 오버플로: C++에서는 중간 계산에 128-bit 사용, Python은 빅인트 안전
제출 전 점검
- 표준 입출력, 개행 형식 확인
- C++: 64-bit 범위 점검, 곱셈 중간값 i128 캐스팅 적용
floor_sum
인덱스 범위 정의(0..n-1)와 사용처 일치 확인
참고자료
- 잔여류 주기성과
sum of floors
고전 테크닉 - AtCoder Library
floor_sum
아이디어