17272번: 리그 오브 레전설 (Large)
규환이는 리그 오브 레전설이라는 게임을 좋아한다. 이 게임에서는 N초의 시간 동안 싸움을 하는데, 규환이가 플레이하는 캐릭터는 A, B 두 가지 스킬을 사용할 수 있다. A 스킬의 시전 시간은 1
www.acmicpc.net
이 문제와 비슷한 형태의 행렬 점화식이 문제에서 종종 나오는데, 난이도가 꽤 있는 유형이라고 생각합니다.
시간이 N일 때 가능한 스킬 조합의 수를 $A_{N}$ 이라고 해보겠습니다.
우선 간단한 경우를 먼저 탐색해보기 위해 $M=2$ 라고 해보겠습니다.
그 경우 $A_{1}=1,A_{2}=2,A_{3}=3,A_{4}=5,A_{5}=8,\cdots$ 나름대로 피보나치 수열과 비슷한 모습입니다.
$M=3$ 이라고 해보겠습니다.
그러면 $A_{1}=1,A_{2}=1,A_{3}=2,A_{4}=3,A_{5}=4,A_{6}=6,\cdots$ 규칙이 보이시나요?
저는 잘 안 보였습니다.
만약 보였다고 해도, 이 문제에선 $M$ 의 값에 의존하는 규칙은 그렇게 좋은 규칙이 아닐 것입니다.
ex) M=2일 때 피보나치와 겹치는 것, etc.
다시 원점으로 돌아와서 $A_{N}$의 값을 알 때 $A_{N+1}$의 값을 구할 방법이 있을까요?
일단, $N+1$초 동안 스킬을 쓰는 건 다음의 두 가지 경우가 있겠습니다.
$N$초 동안 스킬을 쓴 뒤 $N$초부터 $N+1$초까지 스킬 A를 쓰는 것
$N+1-M$초 동안 스킬을 쓴 뒤 $N+1-M$초부터 $N+1$초까지 스킬 B를 쓰는 것
따라서, $A_{N+1}=A_{N}+A_{N+1-M}$ 이었습니다.
이 점화식은 2 이상 100 이하의 모든 $M$ 과 모든 $N\geq M$에 대해 성립하므로 좋은 점화식이라고 생각할 수 있습니다.
보통 이런 모양의 점화식은 행렬로 해결합니다.
그런데 그 모양이 잘 보이지 않을 수도 있으니, 우선 $M$의 값을 작은 것부터 대입하여 규칙성을 도출해보겠습니다.
$M=2$ 일 때
$A_{N}=A_{N-1}+A_{N-M}=A_{N-1}+A_{N-2}$
$A_{4}=A_{3}+A_{2}$
$\therefore \begin{bmatrix}A_{2}\\ A_{3}\\ A_{4}\end{bmatrix}=\begin{bmatrix}0 & 1 & 0\\ 0 & 0 & 1\\ 0 & 1 & 1\end{bmatrix}\begin{bmatrix}A_{1}\\ A_{2}\\ A_{3}\end{bmatrix}$
$A_{5}=A_{4}+A_{3}$
$\therefore \begin{bmatrix}A_{3}\\ A_{4}\\ A_{5}\end{bmatrix}=\begin{bmatrix}0 & 1 & 0\\ 0 & 0 & 1\\ 0 & 1 & 1\end{bmatrix}\begin{bmatrix}A_{2}\\ A_{3}\\ A_{4}\end{bmatrix}$
$\therefore \begin{bmatrix}A_{N-2}\\ A_{N-1}\\ A_{N}\end{bmatrix}=\begin{bmatrix}0 & 1 & 0\\ 0 & 0 & 1\\ 0 & 1 & 1\end{bmatrix} \begin{bmatrix}A_{N-3}\\ A_{N-2}\\ A_{N-1}\end{bmatrix}$
같은 방법으로 $M=3$ 일 때
$A_{N}=A_{N-1}+A_{N-3}$
$\therefore \begin{bmatrix}A_{N-3}\\ A_{N-2}\\ A_{N-1}\\ A_{N}\end{bmatrix}=\begin{bmatrix}0 & 1 & 0 & 0\\ 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 1\\ 0 & 1 & 0 & 1\end{bmatrix} \begin{bmatrix}A_{N-4}\\ A_{N-3}\\ A_{N-2}\\ A_{N-1}\end{bmatrix}$
이제 규칙성이 보이나요?
자연수 $M$ 에 대해 필요한 행렬의 크기는 $\left ( M+1 \right )\times \left ( M+1 \right )$이 될 것입니다.
또한 이 식이 성립할 것입니다.
$\begin{bmatrix}A_{N-M}\\ A_{N-M+1}\\ A_{N-M+2}\\ \vdots\\ A_{N-2}\\ A_{N-1}\\ A_{N}\end{bmatrix}=\begin{bmatrix}0 & 1 & 0 & 0 & \cdots & 0 & 0\\ 0 & 0 & 1 & 0 & \cdots & 0 & 0\\ 0 & 0 & 0 & 1 & \cdots & 0 & 0\\ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \vdots\\ 0 & 0 & 0 & 0 & \cdots & 1 & 0\\ 0 & 0 & 0 & 0 & \cdots & 0 & 1\\ 0 & 1 & 0 & 0 & \cdots & 0 & 1\end{bmatrix}\begin{bmatrix}A_{N-1-M}\\ A_{N-1-M+1}\\ A_{N-1-M+2}\\ \vdots\\ A_{N-3}\\ A_{N-2}\\ A_{N-1}\end{bmatrix}$
이제 이를 이용하여 잘 구현하면 끝입니다.
이때 다음이 $M$ 에 관계없이 항상 성립함을 쉽게 알 수 있습니다.
$1\leq i \leq M-1$ 일 때 $A_{i}=1$
$A_{M}=2$
$A_{M+1}=3$
최종 시간복잡도는 $O\left ( M^{3}logN \right )$이 되겠습니다.
코드는 다음과 같습니다.
#include <stdio.h>
#define mod 1000000007
typedef struct {
long long array[101][101];
} Matrix;
Matrix matrix_multiply_modular (Matrix A, Matrix B, int size);
Matrix matrix_power_modular (Matrix Base, int size, long long power);
int main() {
long long N;
int M;
scanf("%lld %d", &N, &M);
if (N < M) {
printf("1");
}
else if (N == M) {
printf("2");
}
else if (N == M + 1) {
printf("3");
}
else {
int size = M + 1;
long long power = N - M - 1;
Matrix Base = { {0,} };
for (int i = 0; i < size - 1; i++) {
Base.array[i][i+1] = 1;
}
Base.array[size-1][1] = Base.array[size-1][size-1] = 1;
Matrix Answer = { {0,} };
for (int i = 0; i < size - 2; i++) {
Answer.array[i][0] = 1;
}
Answer.array[size-2][0] = 2, Answer.array[size-1][0] = 3;
Base = matrix_power_modular (Base, size, power);
Answer = matrix_multiply_modular (Base, Answer, size);
printf("%lld", Answer.array[size-1][0]);
}
return 0;
}
Matrix matrix_multiply_modular (Matrix A, Matrix B, int size) {
Matrix Result = { {0,} };
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
for (int k = 0; k < size; k++) {
long long temp = (A.array[i][k] * B.array[k][j]) % mod;
Result.array[i][j] = (Result.array[i][j] + temp) % mod;
}
}
}
return Result;
}
Matrix matrix_power_modular (Matrix Base, int size, long long power) {
Matrix Result = { {0,} };
for (int i = 0; i < size; i++) {
Result.array[i][i] = 1;
}
while(power) {
if (power % 2) {
Result = matrix_multiply_modular (Result, Base, size);
}
Base = matrix_multiply_modular (Base, Base, size);
power /= 2;
}
return Result;
}
(C11, 1712KB, 460ms, 제출번호 26473172)
코드 길이 좀 줄여보려고 구조체 초기화를 좀 했는데, 그냥 memset을 쓰는 게 나았을 수도 있겠다 싶습니다.
백준에서 경고가 뜨네요.
$M=3$일 때 규칙을 쉽게 찾았다면, $M$ 의 값을 늘려가면서 $A_{N}=A_{N-1}+A_{N-M}$을 쉽게 유도할 수 있었을 것입니다.
$1\leq i\leq M-1$ 일 때 $A_{i}=1$ 인 것은 가능한 스킬 조합이 A, AA, AAA, $\cdots$, 밖에 없기 때문입니다.
$A_{M}=2$ 인 것은 $AAA\cdots AAA$ 또는 $B$ 밖에 없기 때문입니다.
$A_{M+1}=3$ 는 $BA$ 나 $AB$ , 또는 $AAA\cdots AAA$ 밖에 없기 때문입니다.
구조체 초기화와 locality에 관하여 코드 수정을 했습니다.
수정된 코드는 다음과 같습니다.
#include <stdio.h>
#define mod 1000000007
typedef struct {
unsigned long long array[101][101];
} Matrix;
Matrix matrix_multiply_modular (Matrix A, Matrix B, int size);
Matrix matrix_power_modular (Matrix Base, int size, long long power);
int main() {
long long N;
int M;
scanf("%lld %d", &N, &M);
if (N < M) {
printf("1");
return 0;
}
if (N == M) {
printf("2");
return 0;
}
if (N == M + 1) {
printf("3");
return 0;
}
int size = M + 1;
long long power = N - M - 1;
Matrix Base = { {{0}} };
for (int i = 0; i < size - 1; i++) {
Base.array[i][i+1] = 1ULL;
}
Base.array[size-1][1] = Base.array[size-1][size-1] = 1;
Base = matrix_power_modular (Base, size, power);
unsigned long long Answer = 0;
for (int i = 0; i < size - 2; i++) {
Answer = (Answer + Base.array[size-1][i]) % mod;
}
Answer = (Answer + Base.array[size-1][size-2] * 2) % mod;
Answer = (Answer + Base.array[size-1][size-1] * 3) % mod;
printf("%llu", Answer);
return 0;
}
Matrix matrix_multiply_modular (Matrix A, Matrix B, int size) {
Matrix Result = { {{0}} };
for (int i = 0; i < size; i++) {
for (int k = 0; k < size; k++) {
for (int j = 0; j < size; j++) {
unsigned long long temp = (A.array[i][k] * B.array[k][j]) % mod;
Result.array[i][j] = (Result.array[i][j] + temp) % mod;
}
}
}
return Result;
}
Matrix matrix_power_modular (Matrix Base, int size, long long power) {
Matrix Result = { {{0}} };
for (int i = 0; i < size; i++) {
Result.array[i][i] = 1;
}
while(power) {
if (power % 2) {
Result = matrix_multiply_modular (Result, Base, size);
}
Base = matrix_multiply_modular (Base, Base, size);
power /= 2;
}
return Result;
}
(C11, 1632KB, 244ms, 제출번호 28022545)
'분할 정복을 이용한 거듭제곱 > 행렬 거듭제곱' 카테고리의 다른 글
백준 13430번: 합 구하기 (0) | 2021.02.19 |
---|---|
백준 16467번: 병아리의 변신은 무죄 (0) | 2021.02.19 |
백준 13246번: 행렬 제곱의 합 (0) | 2021.02.19 |
백준 1160번: Random Number Generator (0) | 2021.01.29 |
백준 15712번: 등비수열 (0) | 2021.01.29 |