[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$로 나눈 나머지를 같다고 생각했었다. 지금 보면 절대 같을 수 없는데 말이다.
      
댓글남기기