https://www.acmicpc.net/problem/13172
문제 요약
n면체 주사위에 적힌 숫자의 합이 s라면, 기댓값은 s/n이다.
m개의 주사위의 n값과 s값이 주어졌을때, 기댓값의 합을 구하면 된다.
단, 기댓값이 분수라면 정확한 답을 구하기가 힘들기 때문에 모듈러 곱셈에 대한 역원(b ^ -1)을 활용한다. 모듈러 곱셈에 대한 역원은 다음을 만족한다.
따라서 기댓값이 기약분수 a/b라면, (a × b^-1) mod 1,000,000,007으로 대신한다.
단순한 풀이
이 문제의 풀이 과정은 간단하다. (다만 수학공식이 나와서 진입장벽이 높을 뿐이다!)
1) 주사위의 면수 n, 적힌 숫자의 합 s의 기댓값은 s/n이다.
2) s/n을 기약분수로 만든다.
3) s/n 대신 (s * n^-1) % MOD로 나타낸다.
로직을 체크하기 위해, 우선적으로 수학 공식을 배제하고 코드를 짜봤다.
// 기약분수를 만드는 함수
private static long[] getRedycedFraction(int n, int s) {
int end = Math.min(n, s);
long a = s;
long b = n;
long operand = 2;
while (operand <= end) {
if (a % operand == 0 && b % operand == 0) {
a /= operand;
b /= operand;
} else {
operand++;
}
}
return new long[] {a, b};
}
기약분수를 만드는 방법은 간단하다. 더이상 나누어지지 않을때까지 나누면 된다.
b의 모듈러 역원을 구하는 것은 어렵지만, 단순한 방법으로 접근해본다. 위의 식을 조금만 변형하면, (b * x) % MOD = 1이면 x가 b의 모듈러 곱셈의 역원이다. 모듈러 곱셈의 역원이 존재하지 않는 경우는 이 문제에서 존재하지 않는다.
// 모듈러 곱셈의 역원 구하기
private static int getReverseModuler(long n) {
for (int i = 1; i < MOD; i++) {
if ((i * n) % MOD == 1) {
return i;
}
}
// 모듈러 곱셈의 역원이 존재하지 않는 경우 (문제에서는 입력으로 주어지지 않음)
return -1;
}
x를 1부터 MOD까지 하나씩 다 테스트해보면서, 1일 경우 리턴해준다. 당연히 !! 시간 초과가 난다. 하지만 테스트케이스를 통해 해당 로직에는 문제가 없다는 것을 알 수 있다. 따라서, 여기에다가 공식을 적용해서 시간을 줄인다.
모듈러 곱셈의 역원 - 페르마의 소정리 적용
문제에서 친절하게 모듈러 곱셈의 역원을 간단하게 구할 수 있는 페르마의 소정리를 제공해준다. b의 모듈러 곱셈의 역원은, b의 MOD -2승과 같다. (문제에서 X의 값이 10_0000_0007로 소수이기때문에 적용 가능하다.)
private static long getReverseModuler(int num) {
return pow(num, MOD-2);
}
다만 MOD가 10억쯤되는 매우 큰 값이므로, 분할 정복을 통해 거듭제곱을 구해준다.
private static long pow(int num, int cnt) {
if (cnt <= 1) {
return num;
}
long temp = pow(num, cnt/2);
return ((temp * temp) % MOD * (cnt%2==1 ? num : 1)) % MOD;
}
기약분수 구하기 - 유클리드 호제법 적용
* 유클리드 호제법에 대한 내용은 아래 사이트를 참고했다.
The Euclidean Algorithm (article) | Khan Academy
Learn for free about math, art, computer programming, economics, physics, chemistry, biology, medicine, finance, history, and more. Khan Academy is a nonprofit with the mission of providing a free, world-class education for anyone, anywhere.
www.khanacademy.org
그다음은 기약분수를 구하는 과정을 단순화한다. s/n을 기약분수로 바꾸려면, 그들의 최대공약수(GCD)로 나누어주면 된다.
// 기약분수를 구하는 법 => 최대 공약수로 나눔
private static int[] getRedycedFraction(int s, int n) {
int GCD = getGCD(s, n);
return new int[] {s / GCD, n / GCD};
}
최대공약수를 구하기 위해서는 유클리드 호제법을 적용할 수 있다.
유클리드 호제법은 (A = B⋅Q + R)라면 GCD(A,B) = GCD(B,R)이라는 것이다.
우선, 유클리드 호제법을 적용하기 위해선 A != 0 && B != 0이라는 조건을 만족해야한다. 조건을 만족하지 못할 경우 A가 0이라면 B를, B가 0이라면 A를 리턴한다.
조건을 만족할 경우, GCD(A, B)를 A에 대한 식으로 바꿔본다.
A가 270, B가 192일 경우 270 = 192 * 1 + 78로 나타낸다. 따라서, 유클리드 호제법에 의해 GCD(270, 192) == GCD(192, 78) 과 동일하다. 즉, GCD(A, B) = GCD(B, A%B)로 나타낼 수 있다.
이런식으로 계속해서 타고들면서, A나 B가 0이라서 특정 값을 리턴할때까지 재귀함수를 호출한다.
// 유클리드 호제법을 이용한 최대공약수 구하기
public static int getGCD(int a, int b) {
if ( a== 0 || b == 0) {
// x ^ 0 == x 이므로 a^b를 하면 0이 아닌 값을 리턴함
return a^b;
}
return getGCD(b, a%b);
}
전체 코드
문제 자체가 이해하기 어렵지만, 문제에서 주는 공식들을 잘 적용하면 생각보다 간단하게 해결할 수 있는 문제였다. 다만 유클리드 호제법은 주어지지 않으므로 숙지하고 있는 것이 필요하다.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
public class Main {
final static int MOD = 10_0000_0007;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int m = Integer.parseInt(br.readLine());
StringTokenizer st = null;
long ans = 0;
for (int i = 0; i < m; i++) {
st = new StringTokenizer(br.readLine());
int n = Integer.parseInt(st.nextToken());
int s = Integer.parseInt(st.nextToken());
int[] num = getRedycedFraction(s, n);
if(num[1] == 1) {
ans += num[0] % MOD;
ans %= MOD;
continue;
}
long reverseModuler = getReverseModuler(num[1]);
ans += (num[0] * reverseModuler) % MOD;
ans %= MOD;
}
System.out.println(ans);
}
// 유클리드 호제법을 이용한 최대공약수 구하기
public static int getGCD(int a, int b) {
if ( a== 0 || b == 0) {
// x ^ 0 == x
return a^b;
}
return getGCD(b, a%b);
}
// 기약분수를 구하는 법 => 최대 공약수로 나눔
private static int[] getRedycedFraction(int s, int n) {
int GCD = getGCD(s, n);
return new int[] {s / GCD, n / GCD};
}
// 페르마 소정리 num ^ MOD-2 == num ^ -1
private static long getReverseModuler(int num) {
return pow(num, MOD-2);
}
private static long pow(int num, int cnt) {
if (cnt <= 1) {
return num;
}
long temp = pow(num, cnt/2);
return ((temp * temp) % MOD * (cnt%2==1 ? num : 1)) % MOD;
}
}
'Backend > Java-Spring' 카테고리의 다른 글
[Redis] LocalDateTime 직렬화/역직렬화 오류 발생 (0) | 2024.11.24 |
---|---|
[eclipse] xml파일의 "Downloading external resources is disabled." 오류 해결 (0) | 2024.04.28 |
[Java] 백준 11723번 - 집합 (0) | 2024.02.14 |
[Java] 백준 1149번 - RGB거리 (1) | 2024.02.10 |
[Java] 클래스.toString()을 public으로 선언해야 하는 이유 (1) | 2024.01.18 |