정말정말정말 힘들었던 문제ㅎㅎㅎ사실 아직 완전히 이해를 완료하지 못했고 위 참고 사이트들의 도움 없이는 다시 풀지도 못 할 것 같지만 컨닝으로라도 문제를 푼 과정을 정리하는 것은 의미가 있을 것 같아 여기에 정리한다.
괜히 주저리주저리...문제로 들어가자.
dp
배열문제 해결을 위한 알고리즘이 DP와 비트마스킹이라는 것만 알고 시작했는데, dp
배열에 무엇을 저장해야 할지 전혀 감을 잡지 못했다.
찾아보니 아래와 같이 저장하면 된다고 한다.
dp[i][j] : i(비트)번 숫자들을 사용했을 때 나머지가 j(정수)인 경우의 수 (j ≤ K)
ex) 순열(arr
)이 이고 가 일 때, dp[1010][3]
를 계산해보자.
dp[1010][3]=1
이다. 위에 따르면 결국 구해야 하는 값은 의 값과 이다.
i
비트들로 만든 숫자 뒤에 l
번째 숫자를 뒤에 더한다고 해보자.
즉 위의 예시에서 2번째와 4번째(1010
) 숫자를 가지고 만든 의 뒤에 3번째 숫자인 를 더하는 것이다. 그 결과는 이다.
새로 만들어진 수()로 업데이트할 수 있는 것을 찾는 것이 이 문제의 핵심이다.
다시 말해 dp[i][j]
의 값을 통해 dp[i | 1 << l][next]
의 값을 업데이트할 것인데 이때 next
의 값이 무엇인지, 어떻게 업데이트되는 것인지를 파악해야 한다.
우선 next
의 값이 무엇인지 알아보자. 다음 식을 통해 알 수 있다.
의 값, l번째 수
는 문제에서 주어지므로 값만 안다면 next
의 값을 알 수 있다.
이제 next
의 값은 알았는데, 대체 dp[i|1<<l][next]
의 값은 어떻게 안단 말인가? 정답은 더해주는 것이다. (이 부분이 내게 매우 어려운 부분이었다!)
dp[i|1<<l][next]
의 의미를 다시 생각해보자.
i
번 숫자들을 사용해서 만든 숫자들에 l
번 숫자를 뒤에 붙였을 때 만들어지는 새로운 숫자들이 로 나눈 나머지가 next
인 경우의 수를 의미한다. next
는 i
와 j
와 l
에 의해 결정된다. [i, j, l]
의 조합에 대해 나오는 경우의 수를 다 더하는 것이 dp[i|1<<l][next]
의 값이 된다. 미리 코드로 표현하자면 다음과 같다.
for i in range(1 << N):
for l in range(N):
if i & (1 << l):
continue
for j in range(K):
# next_val = (j * (10^(len(arr[l])) % K) + (arr[l] % K)) % K
next_val = next_vals[l][j] # next_vals 배열은 뒤의 코드에서 확인 가능
dp[i | (1 << l)][next_val] += dp[i][j]
점화식도 세웠으니 이제 dp
배열의 초기값만 정해주면 된다.
dp[0][0]
의 값의 의미를 생각해보면 아무 숫자도 고르지 않은 수를 로 나눈 나머지가 인 경우의 수이다.
'아무 숫자도 고르지 않은 수'를 이라고 보면 dp[0][0]=1
임을 생각해낼 수 있다.
dp
배열의 의미, 점화식, 초기값 모두 정해졌으니 이제 로직만 만들면 된다.
dp
배열을 만들어주자. 행은 비트를 표현하므로 개 필요하고 열은 로 나눈 나머지를 의미하므로 개 필요하다.
그리고 초기값을 로 설정한다.
dp = [[0] * K for _ in range(1 << N)]
dp[0][0] = 1
next
배열앞서 본 것처럼 next
의 값은 다음 수식으로 표현할 수 있다.
변수로 따지만 j
과 l
밖에 없으므로 사실상 i
를 고려하지 않고 값을 계산할 수 있다.
프로그래밍에서 이는 비트의 반복문인 i
의 반복문과 관계가 없다는 의미이므로 next
값을 미리 계산해놓은 배열을 만들어놓고 i
의 반복문에서 해당 값을 참조하는 형태로 만드는 것이 바람직하다.
next_vals = []
for l in range(N):
temp = []
for j in range(K):
temp.append((j * (10 ** (len(str(arr[l]))) % K) + (arr[l] % K)) % K)
next_vals.append(temp)
모든 가능한 [i, j, l]
의 조합을 따져야 하므로 삼중 for문
을 사용한다. 이때 비트를 의미하는 i
의 반복문 뒤에 순열의 숫자를 의미하는 l
의 반복문을 넣어 경우의 수를 최소화 한다.
for i in range(1 << N):
for l in range(N):
if i & (1 << l):
continue
for j in range(K):
next_val = next_vals[l][j]
dp[i | (1 << l)][next_val] += dp[i][j]
dp
배열의 업데이트가 끝났으면 기약분수로 나타내면 되는데, 여기에는 최대공약수를 구하는 로직이 필요하다. 파이썬이라면 math
모듈로 쉽게 구할 수 있지만 백준의 또다른 문제에서 풀어본 적 있는 방법을 사용했다.
def gcd(a: int, b: int):
if b == 0:
return a
return gcd(b, a % b)
분모는 dp
배열의 마지막 행의 값의 합, 분자는 마지막 행 번째 열의 값이다. 다음과 같이 작성하면 된다.
p = dp[(1 << N) - 1][0]
q = sum(dp[(1 << N) - 1])
g = gcd(p, q)
print(f"{p // g}/{q // g}")
import sys
def gcd(a: int, b: int):
if b == 0:
return a
return gcd(b, a % b)
def main():
input = sys.stdin.readline
N = int(input())
arr = [int(input()) for _ in range(N)]
K = int(input())
dp = [[0] * K for _ in range(1 << N)]
dp[0][0] = 1
next_vals = []
for l in range(N):
temp = []
for j in range(K):
temp.append((j * (10 ** (len(str(arr[l]))) % K) + (arr[l] % K)) % K)
next_vals.append(temp)
for i in range(1 << N):
for l in range(N):
if i & (1 << l):
continue
for j in range(K):
next_val = next_vals[l][j]
dp[i | (1 << l)][next_val] += dp[i][j]
p = dp[(1 << N) - 1][0]
q = sum(dp[(1 << N) - 1])
g = gcd(p, q)
print(f"{p // g}/{q // g}")
if __name__ == '__main__':
main()
너무 빡세다...........비트 마스킹은 아직도 어떤 걸 배열의 인덱스로 사용해야 하는지 헷갈린다........공부가 많이 필요함....^^