[Baekjoon,1086] 박성원
이 문제는 주어진 숫자들로 조합을 하여 주어진 $K$로 나누어 질 수 있는 조합의 개수를 찾아 해결하는 문제이다. 이는 모든 조합을 탐색하며 해당 조합의 숫자가 $K$로 나누어 떨어지는지 찾으면 된다. 이때 주어지는 숫자의 개수가 15개이므로 비트 마스크를 이용하여 해당 숫자에 대해 방문 여부를 체크하면 메모이제이션이 가능하다.
하지만 문제가 되는 것은 아래의 두 가지이다.
- 숫자의 길이가 50이나 되는데 어떻게 나머지를 구하지?
- 숫자를 조합하여 탐색하다 보면 숫자의 길이가 엄청 길어지는데 이를 어떻게 처리하지?
큰 숫자의 나머지를 구하는 방법
1번을 해결하는 방법은 실제로 나머지 연산하는 과정을 따라 가 보면 알 수 있다. 예를 들어 $54321$을 $16$으로 나눈 나머지를 구하는 과정을 살펴보자.
$ \begin{array}{*3r @{\hskip5pt}c@{\hskip5pt} *5r}
& & 0 & 3 & 3 & 9 & 5 \\ \hline
16 & \big) & 5 & 4 & 3 & 2 & 1 \\
& & 0 & & & & \\ \hline
& & 5 & 4 & & & \\
& & 4 & 8 & & & \\ \hline
& & & 6 & 3 & & \\
& & & 4 & 8 & & \\ \hline
& & & 1 & 5 & 2 & \\
& & & 1 & 4 & 4 & \\ \hline
& & & & & 8 & 1 \\
& & & & & 8 & 0 \\ \hline
& & & & & & 1 \\
\end{array} $
- 처음에는 5의 자리 수 $5$가 $16$으로 나눠지는 지 확인할 것이다. 하지만 나눠지지 않는다. 이는 $5mod16=5$로 나머지가 $5$가 나온다.
- 주어진 나머지 $5$를 다음 4번째 자릿수인 $4$의 앞으로 두어 합친다. 이 다음에 합쳐진 $54$를 $16$으로 다시 나눈 나머지를 구한다. 이는 $6$이다.
- 다시 주어진 $6$을 3번째 자릿수인 $3$의 앞으로 두어 합친다. 합쳐진 $63$을 $16$으로 나눈다. 이는 $15$이다.
- 계속 반복한다. 최종적으로 $81$을 $16$으로 나눈 나머지 $1$을 얻을 수 있다.
이러한 방법으로 현재까지 만들어진 수열의 뒷 부분에 다음 수열을 뒤에 이어 나머지 연산을 진행할 수 있다.
즉, 매개 변수로 나머지만 넘기면 된다. 하지만 매번 다음 나머지를 찾기에는 너무 오랜 시간이 걸려 아래와 같은 구현은 시간초과
를 받는다.
int getNextMod(string num, int hereMod)
{
int mod=(hereMod*10+num[0])%K;
for(int i=1;i<num.size();++i)
mod=(mod*10+int(num[i]-'0'))%K;
return mod;
}
// 느려 통과할 수 없는 코드이다.
long long try1(int mod, int visited)
{
// 순열이 완성된 경우
if(visited==((1<<N)-1))
return mod==0;
long long& ret=cache[mod][visited];
if(ret!=-1)
return ret;
ret=0;
// 모든 조합을 만들어 본다.
for(int i=0;i<N;++i)
if(!(visited&(1<<i)))
{
int nextMod=getNextMod(perms[i],mod);
ret+=try1(nextMod,visited|(1<<i));
}
return ret;
}
미리 계산하자
그렇다면 해당 값들을 미리 계산해 두면 되지 않을까? 위 코드에서 알 수 있는 다음 나머지에 대한 식은 아래와 같다.
$ nextMod=(hereMod*10^{perms[i].size()}+perms[i])modK $
이는 기존 나머지 뒤에 새로운 수열을 이어서 나머지를 구하는 것이다. 이는 모듈러 연산의 분배법칙을 이용하여 아래와 같이 변형이 가능하다.
$ nextMod=(hereMod * 10^{perms[i].size( ) } modK + perms[i]modK) modK $
이때 $ pow10[i]=10^{perms[i].size( ) } modK, \ \ mods[i]=perms[i]modK $ 라고 정의하여 두 배열을 미리 계산하면 빠른 속도로 해결 가능하다.
int moduler(string num)
{
int mod=0;
for(int i=0;i<num.size();++i)
mod=(mod*10+int(num[i]-'0'))%K;
return mod;
}
void preCacl()
{
memset(cache,-1,sizeof(cache));
for(int i=0;i<N;++i)
mods[i]=moduler(perms[i]);
pow10[0]=1%K;
for(int i=1;i<51;++i)
pow10[i]=(pow10[i-1]*10)%K;
}
구현
그 후 유클리드 호제법을 이용하여 최대 공약수로 분모, 분자를 나눠 결과를 출력하면 된다.
#include <iostream>
#include <vector>
#include <string>
#include <cstring>
using namespace std;
const int MAX_N=15;
const int MAX_K=101;
int N,K;
vector<string> perms;
// mods[i] = perms[i] % K, pow10[i] = 10^i % K
int mods[MAX_N], pow10[51];
// [뒤의 3자리 수열][방문한 곳]
long long cache[MAX_K][1<<MAX_N+1];
int moduler(string num)
{
int mod=0;
for(int i=0;i<num.size();++i)
mod=(mod*10+int(num[i]-'0'))%K;
return mod;
}
void preCacl()
{
memset(cache,-1,sizeof(cache));
for(int i=0;i<N;++i)
mods[i]=moduler(perms[i]);
pow10[0]=1%K;
for(int i=1;i<51;++i)
pow10[i]=(pow10[i-1]*10)%K;
}
long long solve(int mod, int visited)
{
// 순열이 완성된 경우
if(visited==((1<<N)-1))
return mod==0;
long long& ret=cache[mod][visited];
if(ret!=-1)
return ret;
ret=0;
// 모든 조합을 만들어 본다.
for(int i=0;i<N;++i)
if(!(visited&(1<<i)))
{
int nextMod=(mod*pow10[perms[i].size()]+mods[i])%K;
ret+=solve(nextMod,visited|(1<<i));
}
return ret;
}
// n,m에 대한 최대 공배수를 구한다.
long long gcd(long long n, long long m)
{
if(m==0)
return n;
return gcd(m,n%m);
}
int main()
{
cin>>N;
perms=vector<string>(N);
for(int i=0;i<N;++i)
cin>>perms[i];
cin>>K;
preCacl();
long long p=solve(0,0);
long long q=1;
for(int i=2;i<=N;++i)
q*=i;
long long div=gcd(p,q);
if(p==0)
cout<<"0/1"<<endl;
else if(p==q)
cout<<"1/1"<<endl;
else
cout<<p/div<<'/'<<q/div<<endl;
return 0;
}
회고
초기에 $1234$를 $12$로 나눈 나머지와 앞에 숫자를 붙힌 $81234$를 $12$로 나눈 나머지를 같다고 생각했었다. 지금 보면 절대 같을 수 없는데 말이다.
댓글남기기