기본적인 접근: 모든 경우를 탐색한다.
우선 문제 별로 '경우'를 정의한다. 좀 더 풀어서 말하면 '어떻게 해야 한 가지 경우가 완성되는지'를 정의한다.
그 다음 "어떻게 하면 가능한 모든 경우를 만들어낼 수 있을 것인가"를 고민한다.
모든 경우를 만들어 내는 방법에는 크게 세 가지가 있었다.
dfs의 깊이가 깊어지고, 한 dfs 당 시간복잡도가 증가하면 가지치기를 해줘야 시간 내에 통과할 수 있다.
이때부턴 대개 백트래킹 문제로 분류되는 것 같다.
경우: 9명의 난쟁이 중 7명의 난쟁이를 고르면 한 가지 경우가 완성.
반대로 생각했을 때, "제외할 2명의 난쟁이"를 골라도 한 가지 경우가 완성된다.
N=9
itertools 의 combinations을 사용하면 7명의 난쟁이를 쉽게 고를 수 있다.
from itertools import combinations
import sys
heights = []
for i in range(9):
heights.append(int(sys.stdin.readline()))
for cbn in combinations(heights,7):
if sum(cbn)==100:
for val in sorted(cbn):
print(val)
exit(0)
itertools없이 2중 for문으로 뺄 난쟁이 두 명을 고를 수 있다.
import sys
arr = [int(sys.stdin.readline()) for _ in range(9)]
for i in range(9):
for j in range(i+1,9): # 조합이므로 i+1부터 시작
if sum(arr)-(arr[i]+arr[j])==100:
a = arr[i] # 배열에서 연속 두 번 원소 제거 시 인덱스가 바뀌므로 값을 저장해뒀다가 remove 사용해 삭제
b = arr[j]
arr.remove(a) # remove: 값으로 삭제, pop: 인덱스로 삭제
arr.remove(b)
print('\n'.join(map(str,sorted(arr))))
exit(0)
크기가 1인 부분수열~크기가 N인 부분수열 중 원소 합이 S가 되는 경우의 수를 구하면 된다.
경우: N개의 정수 중 k개의 정수를 고르면 한 가지 경우가 완성된다.
이때 k:부분수열의 크기, 1<=k<=N
이렇게 생각해서 k=1인 경우부터 k=N인 경우를 각각 생각하려고 했는데, 찾아보니 좀 더 쉬운 접근이 있었다.
N개의 원소가 있다고 하면, 각 원소 별로 두 가지의 선택지가 있다.
0: 고르지 않는다. 1: 고른다.
N개의 원소가 모두 선택을 하면 한 경우가 완성된 거다!라고 생각하면 쉽다.
N개 원소가 모두 1을 선택했으면 N개짜리 부분수열이 되는 거고, 모두 0을 선택했으면 0개짜리 부분수열이 된다.
1<=N<=20 이므로 O(2^N)이어도 1억번을 넘지 않는다.
import sys
N, S = map(int,sys.stdin.readline().split())
arr = list(map(int,sys.stdin.readline().split()))
cnt=0
def dfs(i,csum): # i:idx, csum: current sum
global cnt
if i==N:
if csum==S:
cnt+=1
else: # for문 대신 고른다/안고른다 두 가지 경우에 대해 dfs호출
dfs(i+1,csum)
dfs(i+1,csum+arr[i])
dfs(0,0)
if S==0: cnt-=1
print(cnt)
모든 경우를 탐색해야 하는 경우, 혹은 탐색하더라도 시간이 넉넉할 만큼 입력이 작은 경우에는 백트래킹이 아닌 일반 dfs를 사용해도 된다. 넣어주고, 되돌려주는 작업에서 코드가 복잡해지고 풀이 시간이 길어진다.
특히 이 문제의 경우 전부 탐색하지 않으면 답을 구할 수가 없다.
완전 탐색=백트래킹이 아니라는 점을 기억할 것!
완전 탐색은 그저 모든 경우를 탐색하는 문제 유형일 뿐이다. 그건 dfs로 충분하다.
경우: 모든 빈 칸을 채워야 한 가지 경우가 완성된다.
현재 i번째 빈칸까지 채운 상황.
i+1번째 빈칸에 올 수 있는 수는 1~9가 있다. => for문으로 탐색
가능한 수에 대해 그 다음 dfs를 호출한다.
스도쿠 빈칸 개수 최대 64개. 빈 칸마다 1~9를 탐색.
import sys
board = [list(map(int, sys.stdin.readline().split())) for _ in range(9)]
blanks = []
for i in range(9):
for j in range(9):
if board[i][j]==0:
blanks.append((i,j))
def check_row(x,r): # 가로 방향 확인
for i in range(9):
if board[r][i]==x:
return False
return True
def check_col(x,c): # 세로 방향 확인
for i in range(9):
if board[i][c]==x:
return False
return True
def check_sqr(x,r,c): # 3*3 정사각형 확인
nr = r//3 * 3
nc = c//3 *3
for i in range(nr,nr+3):
for j in range(nc,nc+3):
if board[i][j]==x:
return False
return True
def dfs(idx):
# idx번째 빈칸까지 채워진 상태, 해당 함수에서 idx+1번째 빈칸에 들어갈 수를 찾을 것
if idx==len(blanks):
for i in range(9):
print(' '.join(map(str,board[i])))
exit(0)
r,c = blanks[idx]
for k in range(1,10):
if check_row(k,r) and check_col(k,c) and check_sqr(k,r,c):
board[r][c]=k
dfs(idx+1)
board[r][c]=0
dfs(0)
이렇게 적어놓으니 간단한데 무척 헤맸던 문제.. 거의 세 시간 가량을 붙잡고 있었지만 못 풀었음 😭
여지껏 빈칸이 어떤 수로 채워졌었는지를 기록하기 위해 별도의 딕셔너리를 쓰려고 했다.
딕셔너리를 쓰려고 했던 이유는 1~9 중 들어갈 수 있는 수를 탐색하기 위해 가로, 세로, 사각형을 탐색할 때 좌표를 사용해서 O(1)로 값을 얻어내고 싶어서... 가능은 했을 것 같은데 끝까지 디버깅이 안됐다.
4<=N<=20 이다.
경우: 세 명을 뽑으면 한 가지 경우가 완성된다. 나머지 세 명이 자동으로 한 팀이 된다.
import sys
N = int(sys.stdin.readline())
board = [list(map(int,sys.stdin.readline().split())) for _ in range(N)]
selected=[False]*N
ans = 10000000
def dfs(idx,k):
# idx번째 선수까지 뽑은 상태, 해당 함수에서 idx+1번째 선수를 정할 예정
# idx번째 선수의 인덱스는 k
global ans,selected
if idx==N//2:
a,b=0,0
for i in range(N):
for j in range(i+1,N):
if selected[i] and selected[j]:
a+=(board[i][j]+board[j][i])
elif not selected[i] and not selected[j]:
b+=(board[i][j]+board[j][i])
ans = min(ans, abs(a-b))
return
for i in range(k+1,N): # 그 다음 선수는 k+1에서부터 찾아야 함
if not selected[i]:
selected[i]=True
dfs(idx+1,i)
selected[i]=False
dfs(0,0)
print(ans)
for i in range(N)
이 아닌 for i in range(k+1,N)
. 이걸 위해 dfs함수의 인자 k를 두었다.def dfs(idx,k): # idx번째 선수로 k번 선수를 고르기
if idx==N/2:
cal_sub()
return
selected[k]=1
for i in range(k+1,N):
if not selected[i]:
dfs(idx+1,i)
if idx+1==N/2:
break
selected[k]=0
원래 이렇게 for문 밖에서 표시해준 후 for문 밖에서 표시를 해제하는 방식과 혼용해서 썼었는데, 코드가 지저분하고 자꾸 오류가 난다.
위 방식은 dfs 호출 시 "idx번째 선수로 k번 선수를 고를게~" 하고 넘겨주면
dfs 내에서 표시하고, for문 돌린 다음 for문이 모두 종료되면 해제해주는 식이다.
바람직한 방식은 dfs 호출 시에 이미 표시를 하고, "idx번째 선수로 k번 선수를 골랐어!" 하는 식이다. 백트래킹 관련된 모든 처리를 하고, 그 다음에 다음 dfs를 호출한다.
백트래킹 이해하는데 매우 도움 된 문제.
4<=N,M<=500
N이 커졌다!
(0,0)~(N-1,M-1)을 차례대로 테트로미노의 첫 좌표로 설정한다고 하면 대략 O(N^2). 각 칸에서 dfs를 호출해야 하므로... 정확한 계산은 모르긴 몰라도 가지치기가 필요해 보인다!
기괴하게 생겼지만 dfs로 풀 수 있는 문제였다.
경우: 변끼리 이어져 있는 블록 4개를 고르면 한 가지 경우 완성
변끼리 이어져 있어야 하기 때문에, 현재위치에서 상,하,좌,우로 이동할 수 있다.
가지치기 방법: 아직 고르지 않은 블록들이 모두 보드판의 최댓값이라고 가정해서 계산을 해봐도 여태까지 나온 최댓값보다 작다면 탐색을 멈춘다.
예외 케이스: 'ㅜ'모양의 경우 일반적인 dfs로 구현할 수 없다. s==2일 때, 세번째 블록으로 (nr,nc)를 골랐지만 dfs 호출은 dfs(s+1,sum+board[nr][nc],nr,nc)
가 아닌 dfs(s+1,sum+board[nr][nc],r,c)
로 한다. 이렇게 되면 (r,c)를 기준으로 네 번째 블록을 탐색하게 되므로 'ㅗ'와 같이 양쪽으로 뻗어나간 모양을 만들 수 있다.
이것 또한 넣어주기
,돌려주기
를 for문 안에서 해야지만 가능했던 구현이라고 생각한다. 암튼 앞으로 백트래킹 할 때는 이 패턴을 유지하는 걸로.
import sys
N,M = map(int,sys.stdin.readline().split())
board = [list(map(int, sys.stdin.readline().split())) for _ in range(N)]
vst = [[0]*M for _ in range(N)]
max_val = max(map(max,board))
ans = -1
# s번째 블록의 위치가 (r,c). 여태 총합이 sum
# s+1번째 블록의 위치를 탐색하는 함수
def dfs(s,sum,r,c):
global ans
dr = [-1,1,0,0]
dc = [0,0,-1,1]
# 종료 조건 1: 남은 개수의 블록이 모두 보드판의 최댓값이라고 가정해도 현재 ans 보다 작다면 탐색 중지
if sum+(4-s)*max_val<=ans:
return
# 종료 조건 2: 4번째 블록 위치까지 결정된 경우 탐색 중지
if s==4:
ans = max(ans,sum)
return
# (r,c)에서 상,하,좌,우 네 방향 탐색
for dr,dc in zip(dr,dc):
nr = r+dr
nc = c+dc
if nr>=0 and nr<N and nc>=0 and nc<M and not vst[nr][nc]:
if s==2:
vst[nr][nc]=True
dfs(s+1,sum+board[nr][nc],r,c) # (nr,nc)가 아닌 (r,c)에서 다음 단계 탐색
vst[nr][nc]=False
vst[nr][nc]=True
dfs(s+1,sum+board[nr][nc],nr,nc) # (nr,nc)서 다음 단계 탐색
vst[nr][nc]=False # (nr,nc)서의 탐색이 모두 끝났다면 표시 해제, for문 다음 단계 진행
for i in range(N):
for j in range(M):
vst[i][j]=True
dfs(1,board[i][j],i,j)
vst[i][j]=False
print(ans)
dfs함수의 call signiture를 잘 정의&이해&활용 하는 게 중요한 문제였다.
dfs(s,sum,r,c)
= "s번째 블록의 위치가 (r,c). (r,c)를 기준으로 s+1번째 블록의 위치를 결정해라!". 라는 뜻이다.
예전엔 풀이 보고 풀었는데 이번에는 혼자 풀어서 뿌듯했다 ~.~
import sys
N = int(sys.stdin.readline())
vc = [0]*N # visited column
vdd = [0]*(2*N-1) # visited diagonal down \
vdu = [0]*(2*N-1) # visited diagonal up /
cnt = 0
def dfs(r,c):
global cnt
for j in range(N):
if vc[j] or vdd[(N-1)-(r+1-j)] or vdu[(r+1+j)]: continue
if r+1==N-1:
cnt+=1
return
vc[j]=1
vdd[(N-1)-(r+1-j)]=1
vdu[(r+1+j)]=1
dfs(r+1,j)
vc[j]=0
vdd[(N-1)-(r+1-j)]=0
vdu[(r+1+j)]=0
dfs(-1,-1)
print(cnt)
다음 퀸의 위치를 탐색할 때 처음엔 2중 for문을 돌렸다.
그러나 N*N짜리 보드에 N개의 퀸을 놓아야 하므로, 모든 행에 퀸이 하나씩 있어야 한다. 따라서 다음 행에 퀸을 놓을 수 있는지 없는지만 판단하면 된다. 만약 다음 행에 놓을 수 있는 자리가 없다면 그 경로는 가망이 없으므로 버린다.
스도쿠 문제에서 그랬던 것처럼, 퀸을 놓으려고 할 때 / \ | 세 가지 방향을 탐색해야 했기 때문에 각 방향을 따로 관리해 주었다. (vc,vdd,vdu)
열, 대각선에 퀸이 하나라도 있을 경우 그 열과 대각선에 해당하는 좌표에는 더이상 퀸을 놓을 수 없다. 우상향으로 2N-1개, 좌하향으로 2N-1개의 대각선이 존재한다. 좌표 -> 대각선 번호
하는 식을 계산했다. 이 식을 활용해 (i,j)가 속한 대각선 번호를 알아내고, 그 번호에 퀸이 있는지 확인 했다. (vdd, vdu을 확인)
여기서도 마찬가지로 for문 안에서 넣어주기, 되돌려주기를 해줬다.
첫 유형이라 비교 유형이 없어서 감이 잘 안오는데, 몇 가지 유형을 더 풀어보고 Brute force만의 특징을 좀 더 적어보겠다.