문제
수학을 좋아하는 하늘이는 수학을 싫어하는 재우에게 문제를 냈다. i∈N과 α,β∈N에 대하여 0<ai<ai+1와 αai<ai+2, 그리고 ai+2≡αai(mod βai+1), ai∈N을 만족하는 수열 {ai}가 있다.
이때 자연수 N이 주어질 때, 위의 조건을 만족하는 수열 중 aN을 최소로 만드는 수열을 f(N)={a1,a2,⋯,aN}라고하자. 또한, 이러한 f(N)에 대하여 원소의 합을 S(N)=∑Ni=1ai라고 하자.
수학이 어려운 재우를 도와 자연수 N이 주어졌을 때, S(N)을 구하는 프로그램을 작성하시오.
입력
첫 번째 줄에 자연수 N, α, β가 공백으로 구분되어 주어진다. (1≤N≤1018, 1≤α≤1018, 1≤β≤1018)
출력
첫 번째 줄에 S(N)을 출력한다. 단, 수가 너무 커질 수 있으므로 109+7로 나눈 나머지를 출력한다.
Subtask 1 (10 points)
N,α,β≤10
Subtask 2 (15 points)
N,α,β≤1000
Subtask 3 (25 points)
N,α,β≤106
Subtask 4 (50 points)
추가적인 제약조건 없음
23/03/15
동아리 활동 중 해결했던 문제 중 하나이다. 서브태스크 문제이기 때문에 각 서브테스크 별로 해결할 수 있는 풀이를 적어보고자 한다.
문제 접근 방식:
먼저, 모든 Subtask를 해결하기에 앞서서, 문제의 조건이 2가지가 있는데, 1번 조건을 먼저 해석해야한다.
1번 조건은 f(N)을 어떻게 구성할 것인가?이다.
이 조건을 해석하여 구현하는 것에 따라 Subtask를 3까지 얻어낼 수 있다.
2번 조건은 S(N)을 어떻게 효율적으로 구할 것인가?이다.
1번 조건과 2번 조건이 동시에 수반되어야만 만점을 받을 수 있다.
내가 제시하는 코드는 만점을 받는 코드로, 각 Subtask별로 점수를 받는 코드는 따로 적지 않고 해설만 할 것이다.
먼저 1번 조건을 해결을 해야한다.
사실 이 문제에서 가장 어려운 부분으로, 실질적인 점화식을 구해야 되는 부분이기 때문이다.
먼저, ai+2≡αai(mod βai+1)라는 조건에 주목해 볼 필요가 있다.
이 말에 따르면, ai+2=αai+kβai+1,k∈Z이라는 조건을 얻어낼 수 있다.
그 이유는 ai+2≡αai(mod βai+1)의 의미는, ai+2를 βai+1로 나누었을 때 나머지가 αai라는 뜻이기 때문에, ai+2는 βai+1의 배수에 αai를 더한 값으로 나타낼 수 있기 때문이다.
여기서 k를 결정짓는데에는 αai<ai+2라는 조건과 ai∈N라는 조건, f(N)이 aN을 최소로 만드는 수열이라는 세가지 조건을 활용해야 한다.
먼저, αai<ai+2이기 때문에, 위의 점화식에 따르면, αai<ai+2=αai+kβai+1이므로, kβai+1은 양수가 될 수 밖에 없다.
여기서 ai∈N라는 조건을 이용하면, k는 양수인 정수, 즉, 자연수가 될 수 밖에 없다.
이때 f(N)이 aN을 최소로 만드는 수열이라고 했으므로, aN이 최소가 되려면 k가 1이 되야만 aN이 최소가 될 수 있다.
따라서, 최종적인 점화식은 ai+2=αai+βai+1가 된다.
초기항을 결정을 지으려면, 0<ai<ai+1과 f(N)이 aN을 최소로 만드는 수열이라는 조건을 사용하면, a1=1,a2=2라는 결론을 얻어낼 수 있다.
따라서 초기항 2개와 최종적인 점화식을 활용하여 각각의 ai를 구하여 N까지의 합을 구해 109+7로 나누어주면 해결할 수 있다.
Subtask 1은 이 점화식을 재귀적으로 구현하면 해결할 수 있다. 재귀적으로 해결하면 10까지의 작은 경우는 가능하나, Subtask 2는 해결할 수 없을 것이다.
Subtask 2와 3은 이 점화식과 메모이제이션 기법을 활용, 즉 DP를 적용하면 해결할 수 있을 것이다.
하지만 Subtask 4는 1018이라는 아주 큰 제한을 가지고 있기 때문에 DP로 풀 수 없다.
이때 2번 조건을 해결하면 된다.
S(N)을 어떻게 효율적으로 구할 것인가?
결국 우리가 원하는 것은 아래 식과 같다.
S=N∑i=1ai
또한, 우리는 아래의 점화식을 알고 있다.
ai+2=αai+βai+1,a1=1,a2=2
이 점화식의 모든 변에 ∑ 기호를 씌워주면 아래와 같이 변할 수 있다.
N−2∑i=1ai+2=αN−2∑i=1ai+βN−2∑i=1ai+1
이를 S를 활용하여 정리하면 아래와 같이 정리할 수 있다.
S−a1−a2=α(S−aN−aN−1)+β(S−aN−a1)
S−3=αS−αaN−αaN−1+βS−βaN−β
(α+β−1)S=(α+β)aN+αaN−1+β−3
S=(α+β)aN+αaN−1+β−3α+β−1
결국, 우리는 aN과 aN−1의 값만 빠르게 찾아 낼 수 있다면, 이 식을 토대로 S의 값을 빠르게 구할 수 있는 것이다.
선형 점화식이 주어져 있을 때, N번째 항을 빠르게 찾아낼 수 있는 방법은 행렬을 이용한 선형 점화식 단순화 + 분할정복을 이용한 거듭제곱을 섞으면 N번째 항을 O(log N)의 시간 복잡도로 구할 수 있다.
우리는 109+7, 즉, 소수로 나눈 나머지를 구하고 싶어하므로, α+β−1의 109+7에 대한 역원을 페르마 소정리를 통해 구할 수 있다.
만약 파이썬이라면 pow(α+β−1, −1, MOD)로 역원을 구할 수 있다.
이 역원을 곱해주고 MOD를 취해주면 문제를 해결할 수 있다.
아래는 내가 위의 접근 방식과 같이 작성한 파이썬 코드이다. 더보기를 누르면 확인할 수 있다.
def matrixmult(A, B, mod, k):
K = [[0 for _ in range(k)] for _ in range(k)]
for i in range(k):
for j in range(k):
ele = 0
for m in range(k):
ele += (A[i][m]*B[m][j])%mod
K[i][j] = ele%mod
return K
def multiply(A, n, mod, k): # 행렬, 몇제곱할건지, 나머지, 행렬크기
if n == 1:
return A
temp = multiply(A, n//2, mod, k)
if n%2 == 0:
return matrixmult(temp, temp, mod, k)
else:
return matrixmult(A, matrixmult(temp, temp, mod, k), mod, k)
def linear_recurrence(alpha, beta, n, mod):
matrix = [[beta, alpha], [1, 0]]
if n < 2:
if n == 0:
return 1
else:
return 2
else:
K = multiply(matrix, n-1, mod, 2)
val = (2*K[0][0] + K[0][1]) % mod
return val
MOD = 1000000007
N, alpha, beta = map(int, input().split())
an = linear_recurrence(alpha, beta, N-1, MOD)
an1 = linear_recurrence(alpha, beta, N-2, MOD)
k = (alpha+beta)%MOD
a = alpha%MOD
b = (beta-3)%MOD
l = pow((alpha+beta-1), -1, MOD)
if N == 1:
print(1)
elif N == 2:
print(3)
else:
print((((k*an)%MOD + (a*an1)%MOD + b)%MOD)*l%MOD)
'알고리즘 > 그 외 문제들' 카테고리의 다른 글
[구름 알고리즘 먼데이 챌린지] 3주차 4번 순환하는 수로 (2) | 2022.11.07 |
---|