완전 탐색은 가능한 모든 경우의 수를 전부 탐색하여 정답을 찾는 기법이다.
이름 그대로 하나도 빠짐없이 모든 경우를 확인한다는 뜻이다.
다른 최적화 기법을 사용하지 않더라도 반드시 정답을 찾을 수 있다는 장점이 있지만,
경우의 수가 많아질수록 시간이 급격히 증가하는 단점이 있다.
완전 탐색은 다음 조건을 만족할 때 사용한다.
완전 탐색은 모든 경우를 전부 확인하므로 시간이 오래 걸릴 수 있다.
따라서 시간 복잡도 계산이 매우 중요하다.
일반적으로 약 10⁸번 이하의 연산은 시간 제한 내에 가능하다.
입력 크기와 경우의 수를 미리 계산하여 완전 탐색이 가능한지 판단한 뒤 사용해야 한다.
완전 탐색은 구현 방식에 따라 여러 가지 알고리즘으로 나뉜다.
가장 단순한 방식으로 반복문을 사용해 모든 경우의 수를 무식하게 탐색한다.
재귀 함수를 사용해 모든 선택을 단계적으로 탐색한다.
깊이(depth)를 기준으로 경우를 쌓아간다.
각 상태를 이진수(bit) 로 표현하여 완전 탐색하는 방법이다.
부분집합 구하기
const arr = ['A','B','C'];
const n = arr.length
// 1 << 3 = 1000 = 2^3 = 8
for(let mask=0;mask<(1 << n);mask++){
const subset = [];
for(let i=0;i<n;i++){
// 각각의 마스크에 대해 자리별로 선택 여부 체크
// 1 << 0 = 001, 1 << 1 = 010, 1 << 2 = 100
if(mask & (1 << i)){
subset.push(arr[i]);
}
}
console.log(subset)
}
완전 탐색 중에 더 이상 탐색할 필요가 없는 경우를 미리 차단(가지치기) 하는 기법이다.
예시)
대부분의 실전 문제에서 완전 탐색을 그대로 쓰기보다는 백트래킹과 함께 사용한다.
그래프 구조에서 모든 정점이나 모든 경로를 탐색하는 완전 탐색 방식이다.
자주 사용되는 문제 유형:
특히 BFS는 최단 거리 문제에서 강력하다.
분석
모든 재료에 있어 선택/미선택으로 나뉘므로 부분집합을 활용해 풀어준다.
각각의 부분집합에 대해 신 맛은 곱, 쓴 맛은 합하여 가장 작은 경우를 갱신해준다.
const fs = require('fs');
const input = fs.readFileSync(process.platform === 'linux' ? '/dev/stdin' : './test.txt').toString().trim().split('\n');
const N = +input.shift();
const ingredients = input.map(i => i.split(' ').map(Number));
let answer = Infinity;
for(let mask=1;mask<(1<<N);mask++){
const subset = [];
for(let i=0;i<N;i++){
if(mask & (1 << i)){
subset.push(ingredients[i])
}
}
let sour = 1;
let bitter = 0;
for(let j=0;j<subset.length;j++){
const [s,b] = subset[j];
sour *= s
bitter += b
}
answer = Math.min(answer, Math.abs(sour - bitter))
}
console.log(answer)
const fs = require('fs');
const input = fs.readFileSync(process.platform === 'linux' ? '/dev/stdin' : './test.txt').toString().trim().split('\n');
const [N, M] = input[0].split(' ').map(Number);
const board = input.slice(1).map(line => line.split(' ').map(Number));
const dx = [-1, 0, 1, 0];
const dy = [0, 1, 0, -1];
const directions = [
[],
[[0], [1], [2], [3]],
[[0, 2], [1, 3]],
[[0, 1], [1, 2], [2, 3], [3, 0]],
[[0, 1, 2], [1, 2, 3], [2, 3, 0], [3, 0, 1]],
[[0, 1, 2, 3]]
];
const cctvs = [];
let answer = Infinity;
// CCTV 위치 저장
for (let i = 0; i < N; i++) {
for (let j = 0; j < M; j++) {
if (board[i][j] >= 1 && board[i][j] <= 5) {
cctvs.push([i, j, board[i][j]]);
}
}
}
// 맵 복사
function copyMap(map) {
return map.map(row => [...row]);
}
// 감시
function watch(x, y, dir, map) {
let nx = x;
let ny = y;
while (true) {
nx += dx[dir];
ny += dy[dir];
if (nx < 0 || nx >= N || ny < 0 || ny >= M) break;
if (map[nx][ny] === 6) break;
if (map[nx][ny] === 0) {
map[nx][ny] = -1;
}
}
}
function dfs(idx, map) {
if (idx === cctvs.length) {
let count = 0;
for (let i = 0; i < N; i++) {
for (let j = 0; j < M; j++) {
if (map[i][j] === 0) count++;
}
}
answer = Math.min(answer, count);
return;
}
const [x, y, type] = cctvs[idx];
for (const dirSet of directions[type]) {
const copied = copyMap(map);
for (const dir of dirSet) {
watch(x, y, dir, copied);
}
dfs(idx + 1, copied);
}
}
dfs(0, board);
console.log(answer);
상하좌우로 N번만큼 움직일 때 같은 곳을 다시 방문하지 않을 확률을 구해야 한다.
방문 여부 체크를 위해 2*n+1 x 2*n+1 크기의 배열을 만들었다. (시작 지점을 (n,n)으로 하고 최대 +n, -n 만큼 움직이기 때문)
확률 단위는 %이기 때문에 미리 100으로 나누어 두었다.
이후 상하좌우로 이동하며 맵을 벗어나거나 이미 방문한 경우는 건너 뛴다.
확률은 계속 곱하고 방문 횟수도 depth로 카운트하며 N번 만큼 이동을 완료했을 때 확률을 더해준다.
백트래킹으로 방문 여부를 조절하며 효율화했다.
const fs = require('fs');
const input = fs.readFileSync(process.platform === 'linux' ? '/dev/stdin' : './test.txt').toString().trim().split('\n');
const [n, E, W, S, N] = input[0].split(' ').map(Number);
const visited = Array.from({length: n*2+1}, () => Array(n*2+1).fill(false));
const dx = [-1, 0, 1, 0];
const dy = [0, 1, 0, -1];
const percent = [N,E,S,W].map(p => p / 100);
let answer = 0;
visited[n][n] = true;
function dfs(x, y, depth, p) {
if(depth === n){
answer += p;
return;
}
for(let i=0;i<4;i++){
const nx = x+dx[i];
const ny = y+dy[i];
if(nx < 0 || nx >= 2*n+1 || ny < 0 || ny >= 2*n+1) continue;
if(visited[nx][ny]) continue;
visited[nx][ny] = true;
dfs(nx, ny, depth+1, p*percent[i]);
visited[nx][ny] = false;
}
}
dfs(n, n, 0, 1);
console.log(answer);