DFS / BFS를 따지면 드라마에 비유 할 수 있음
한 드라마가 종영 될 때까지 기다렸다가 한번에 본다 -> DFS
여러 드라마를 한편씩 업로드 마다 챙겨본다 -> BFS
대표적으로 이러한 그래프 관련 문제들은
그렇기에 여행 경로, 단어 변환, 네트워크, 타겟넘버 등의 문제를 보면 아 이 알고리즘이구나를 떠올리면 좋음
-> 재귀 (한놈만 패니까)
(재귀로 여러 케이스를 다 준다음, 한 케이스를 빠져 나오면 (ex, 덧셈 뺄셈) 그 다음 케이스를 준비하여 모든 케이스를 파악, 모든 경로를 살피는데 적합하고 백트래킹과 함께 사용함)
-> Queue / LinkedList (여러놈 패니까)
(연결한다고 생각하면 됨, 가장 먼저 넣고 넣은 애와 연결된 걸 찾고 Queue가 빌 때까지 계속 반복, 큐를 사용해 넓게 탐색, 최단 거리 계산에 적합, 레벨별로 탐색하는데 유리)
Q. 어떤 걸 써야하나요?
그냥 자기에게 맞는 걸 하면 됨. 두 알고리즘 중 아무거나 사용하더라도 문제는 풀리기 때문
DFS는 하나의 조합을 완성해서 정답과 비교하고 또 다른 조합을 만들어서 정답과 비교하는 방식이라 내가 검증하기 쉬움
하지만 BFS도 필요할 때도 있음 DFS는 한 놈이 너무 깊으면 안 되기 때문에 모든 조합에서 시간을 끌어버리기 때문
BFS는 한 정답을 찾으면 그 정답을 적용하기 쉬워서 시간 복잡도가 낮음
경로탐색, 네트워크, 조합 만들기 => DFS, BFS
DFS의 대명사라 해도 되는 문제라고 생각되는 문제
예전에 풀었던 기록이 새록하지만 다시 꼼꼼히 해설해보았음
function solution(numbers, target) {
let count = 0;
const dfs = (index, currentSum) => {
console.log(`DFS 호출: index = ${index}, currentSum = ${currentSum}`); // 현재 상태 출력
// 모든 숫자를 탐색한 경우에 target값과 맞는지 비교
// ! 애초에 if문에 만족하지 않으면 return을 만나지 않고 다시 함수 재실행
// ! index가 5 (4+1)라는 것은 모든 인덱스를 다 탐색했다는 것
// ! 즉, 인덱스를 다 탐색했을 때 과연 target과 맞냐는 확인
// 함수에서 return을 만나면 그 함수는 종료
// 하지만 중요한 점은 해당 함수 호출이 종료될 뿐, 그 함수가 호출된 이전 상태로 돌아가서 다음 작업이 진행
if (index === numbers.length) {
if (currentSum === target) {
console.log(`🎯 조건 만족: currentSum(${currentSum}) === target(${target})`);
count++;
} else {
console.log(`❌ 조건 불만족: currentSum(${currentSum}) !== target(${target})`);
}
return; // 탐색 종료
}
// ! 더하기를 진행하다 return을 만나면 불렀던 더한 경우는 종료, 뺀 경우가 진행
// 숫자를 더한 경우
dfs(index + 1, currentSum + numbers[index]);
// ! 더하기를 다 진행 후 - 빼기도 진행하다가 끝난 경우에도 바로 더하기를 진행
// 숫자를 뺀 경우
dfs(index + 1, currentSum - numbers[index]);
}
dfs(0, 0); // 초기 상태에서 탐색 시작
return count;
}
DFS는 정말 재귀만을 생각하고 풀면 됨
유의해야할 것은 어떤 경우에 조건문 탈 것이고 건너 뛸 것인지에 대해 유념해야 한다는 것
조건문을 타지 않아도 다시 한번 더 더하기가 진행될 것이고
빼기가 진행되더라도 마치면 다시 더하기가 한번 더 진행된다는 걸 유념해야함
또한, 더하기, 뺴기의 경우임을 인덱스를 조절해서 더하고 뺴는 것을 원할하게 해야함
가장 깔끔하고 명확한 코드가 있어서 가져와봤음
function solution(numbers, target) {
let answer = 0;
getAnswer(0,0);
function getAnswer(x,value) {
if(x<numbers.length){
getAnswer(x+1,value + numbers[x]);
getAnswer(x+1,value - numbers[x]);
} else{
if(value === target){
answer++
}
}
}
return answer;
}
댓글 중에는 한 노드에서 두 노드로 뻗쳐나가는 것 같다고 했는데 그 말이 맞다고 생각될 정도로 깔끔하다!
BFS의 대명사 문제
let dx = [-1, 0, 1, 0]
let dy = [0, 1, 0, -1]
로 방향을 설정한 다음 visited를 채우던 문제, 오랜만에 봤지만 사알짝 떠오르던 문제였음
하지만 여기에서 유의해야할 것은 dx(row)이므로 세로방향으로 이동
ex) maps[0][1] -> maps[1][1]로 이동이라면 dx가 1증가 즉, 아래로 하나이동한 거기 때문에 그 점만 유의하면 괜찮음
function solution(maps) {
let ans = 1; // 이동 횟수 (출발지 포함)
let n = maps[0].length; // 지도 가로 길이
let m = maps.length; // 지도 세로 길이
let dx = [-1, 0, 1, 0]; // 상, 우, 하, 좌 방향
let dy = [0, 1, 0, -1];
let visited = []; // 방문 여부를 기록할 배열
for (let i = 0; i < m; i++) visited.push(new Array(n).fill(false));
visited[0][0] = true; // 시작점 방문 처리
let queue = []; // BFS를 위한 큐
queue.push([0, 0]); // 시작점을 큐에 추가
while (queue.length) {
let [x, y] = queue.shift(); // 큐에서 현재 위치를 가져옴
for (let i = 0; i < 4; i++) {
// 이동 후의 x 좌표
let nx = x + dx[i];
// 이동 후의 y 좌표
let ny = y + dy[i];
// ex. 여기에서 처음에 x가 0이고 dx[0] = -1이 들어가면 nx는 -1이므로 아래를 벗어남
// 조건 1. 지도 범위를 벗어나지 않고 (nx >= 0 && nx < m && ny >= 0 && ny < n)
// 조건 2. 방문하지 않았으며 (maps[nx][ny] === 1)
// 조건 3. 이동 가능한 경우 (!visited[nx][ny])
if (nx >= 0 && nx < m && ny >= 0 && ny < n && !visited[nx][ny] && maps[nx][ny] === 1) {
// 방문 처리
visited[nx][ny] = true;
// 이동후에 좌표에 현재까지의 거리 + 1
maps[nx][ny] = maps[x][y] + 1;
// 해당 방문지 큐에 추가 (다음타겟)
queue.push([nx, ny]);
}
}
}
// 도착지에 도달할 수 없으면 -1 반환
if (maps[m - 1][n - 1] === 1) return -1;
// 최종 목적지까지의 거리 반환
return maps[m - 1][n - 1];
}
코테가 국어문제인 걸 느낀 게.. 이해 하는데 한참 걸리고 로직을 짜도 많이 이해가 안 갔던 문제
하지만 해결 방법은 몇 가지만 따져보면 금방 해결할 수 있었던 것 같다
function solution(n, computers) {
let visited = new Array(n).fill(false);
let cnt = 0;
for (let i=0; i<n; i++) {
// 새로운 컴퓨터를 발견시마다 cnt를 증가시켜줌
// (= 아직 i번째 컴퓨터를 탐색하지 않았다면)
if(!visited[i]) {
cnt++;
let queue = []
queue.push(i)
while(queue.length) {
let currentComputer = queue.shift();
// 여기에서 queue에 담긴 값을 true로 바꿔주기 때문에
// 외부 반복문에서 삽입한 i는 후순위로 처리
visited[currentComputer] = true;
for (let j=0; j<n; j++) {
// ★ 첫번째 컴퓨터와 j번쨰 컴퓨터가 연결되어있고 (1로)
// j번째에 해당하는 컴퓨터를 아직 탐색하지 않으면
// ★ 단, 여기에서 if문에 해당하지 않더라도 for문은 마저 수행
// 만약 여러개가 해당시 여러개를 push
if(computers[currentComputer][j] === 1 && !visited[j]) {
queue.push(j)
}
// if문에 모두 해당하지 않아서 queue가 빈 배열로 나가더라도
// while문을 벗어나 외부 for문을 타므로 정상적 수행
}
}
}
}
return cnt
}
결론적으로는 위와 같은 코드가 나옴
여기에서 핵심은
생각보다 어려웠다...
function solution(n, computers) {
let visited = new Array(n).fill(false); // 방문 여부를 기록할 배열
let cnt = 0; // 네트워크 개수
// DFS 함수 정의
function dfs(currentComputer) {
visited[currentComputer] = true; // 현재 컴퓨터 방문 처리
// 현재 컴퓨터와 연결된 다른 컴퓨터 탐색
for (let j = 0; j < n; j++) {
if (computers[currentComputer][j] === 1 && !visited[j]) {
dfs(j); // 연결된 컴퓨터로 재귀 호출
}
}
}
// 모든 컴퓨터를 순회하며 DFS 탐색 시작
for (let i = 0; i < n; i++) {
if (!visited[i]) {
cnt++; // 새로운 네트워크 발견 시 증가
dfs(i); // 새로운 네트워크에 대한 DFS 탐색 시작
}
}
return cnt;
}
트리 탐색의 핵심처럼 재귀 같은 반복이 이루어 짐
여기에선 dfs를 통해 j값을 확인 한 후
한 번의 for문을 더 돌며 visited의 모든 요소를 탐색함
다 풀고 나서야 알았지만 이런 문제는 큐 관리의 복잡성을 피하기 위해 보통 dfs를 더 많이 활용한다고 함
게다가 "연결된 모든 노드를 탐색하는 문제"로, DFS의 깊이 우선 탐색이 자연스럽게 잘 맞는다고 함
-> 한 컴퓨터의 모든 연결된 점을 확인하고 연결 되어있으면 dfs를 통해 j번째를 실행하면 되기 떄문
-> 반복이 안 되면 아래의 반복문을 통해 다음 차례의 dfs를 수행하면 됨
내가 처음에 짰던 코드
function solution(begin, target, words) {
// 예외처리
// if (words.indexOf(target) === -1) return 0
// indexOf말고 includes 쓰기
if (words.indexOf(target) === -1) return 0
let visited = [];
let queue = [];
queue.push([begin, 0])
for(let i=0; i<words.length; i++) {
// 큐의 길이가 존재하고 visited가 words[i]번쨰가 없을 때
if (queue.length && visited.indexOf(words[i]) === -1) {
// 큐에서 꺼내와서
let [currentWord, cnt] = queue.shift();
console.log('currentWord: ', currentWord,' / cnt: ', cnt)
let diff = 0;
for(let j=0; j<begin.length; j++) {
// diff의 값을 따져본 후에
if (currentWord[j] !== words[i][j]) {
diff++
}
if (diff > 1) {
break
}
if (diff === 1) {
let newWord = words[i]
if (newWord === target) return cnt
cnt++
if (!visited.includes(newWord)) {
queue.push([newWord, cnt]);
visited.push(newWord);
}
}
}
}
console.log('visited', visited)
console.log('queue', queue)
}
// 이 문제는 한 인덱스를 집중적으로 보니까 dfs일 것 같음
// 각 단어의 길이만큼을 각 스펠링으로 나누어 배열에 삽입 (2차원 배열로 변경)
// 각 배열을 순회하며 해당하는 자신과 다른애가 가장 적은애를 찾아
// 앞 인덱스부터 자기와 다른 인덱스를 맞바꾸어줌
// 최소한의 단계를 return 해야함
// 만약 words안에 target단어가 없으면 return 0
}
이 문제는 dfs가 아닌 최단 경로를 구하는 것이기 때문에 bfs로 풀어야함
function solution(begin, target, words) {
// indexOf 쓰지 말고 inlcudes 쓰기
if (!words.includes(target)) return 0;
let visited = [];
let queue = [];
// 초기값 삽입
queue.push([begin, 0]);
// while문 세팅
while (queue.length > 0) {
let [currentWord, cnt] = queue.shift();
// words를 순회하며 변환 가능한 단어 탐색
for (let i = 0; i < words.length; i++) {
// ★ 이미 방문한 단어는 스킵
if (visited.includes(words[i])) continue;
let diff = 0;
for (let j = 0; j < currentWord.length; j++) {
if (currentWord[j] !== words[i][j]) diff++;
// 1이상이라 어차피 변환 불가능하면 for문 break를 통해 반복문 탈출
// *깨알 기초 다지기 : continue는 해당 조건만 스킵, break는 반복문 완전 스킵
if (diff > 1) break;
}
// 1인 애만
if (diff === 1) {
// 그떄의 words[i]를 새로운 단어에 넣어줌
let newWord = words[i];
// 만약 새로운 단어가 target값과 동일하다면 여태에 +1 해주고 바로 return
if (newWord === target) return cnt + 1;
// 아직 일치 하지 않으면, 바뀐 단어와 현재까지의 cnt+1을 통해 dfs재반복
queue.push([newWord, cnt + 1]);
// visited에 기록
visited.push(newWord);
}
}
}
}
생각해보면 단순한 문제지만 어렵게 생각해서 너무 어렵게 생각해서 어렵게 접근하게 된 문제인 것 같다.
dfs, bfs에 대한 명확한 구분 후에 어떤식으로 접근할지 설계를 더 하고 다가가면 쉬울 것 같음
전체 풀이코드, 처음엔 주어진 좌표를 2배할 생각도 없고 이유도 몰랐지만 풀다보면서 헤매다보니 어찌저찌 2배를 해서 푸는 게 더 낫다고 확인이 들었던 문제
function solution(rectangle, characterX, characterY, itemX, itemY) {
// 1. 좌표 확장: 모든 좌표를 2배로 확장하여 정수 좌표만 다룰 수 있게 처리
const expanded = rectangle.map(([x1, y1, x2, y2]) => [x1 * 2, y1 * 2, x2 * 2, y2 * 2]);
const startX = characterX * 2;
const startY = characterY * 2;
const endX = itemX * 2;
const endY = itemY * 2;
// 2. 문제에 주어진 크기의 2배를 하여 0으로 채워진 그리드 생성
const size = 101;
const grid = [];
for (let i=0; i<size; i++) {
grid.push(new Array(size).fill(0))
}
// 3. 사각형 테두리에 포함되는 그리드를 1으로 바꿈
expanded.forEach(([x1, y1, x2, y2]) => {
// 상단, 하단 테두리 설정
// x는 x1 ~ x2까지
for (let x = x1; x <= x2; x++) {
grid[x][y2] = 1; // 상단 테두리
grid[x][y1] = 1; // 하단 테두리
}
// 좌측, 우측 테두리 설정
// y는 y1 ~ y2까지
for (let y = y1; y <= y2; y++) {
grid[x1][y] = 1; // 좌측 테두리
grid[x2][y] = 1; // 우측 테두리
}
});
// 4. 테두리 제외 0으로 변경
expanded.forEach(([x1, y1, x2, y2]) => {
// x는 x1+1부터 x2-1까지
for (let x = x1 + 1; x < x2; x++) {
// y는 y1+1부터 y2-1까지
for (let y = y1 + 1; y < y2; y++) {
grid[x][y] = 0; // 내부 좌표 제거
}
}
});
// 5. 방문 여부를 기록할 배열 생성
let visited = [];
for (let i=0; i<size; i++) {
visited.push(new Array(size).fill(false))
}
visited[startX][startY] = true;
// 7. BFS 탐색을 위한 큐 초기화 및 시작점 방문 처리
const queue = [];
queue.push([startX, startY]);
// 8. 이동 방향 (dx: 열 이동, dy: 행 이동)
const dx = [-1, 0, 1, 0];
const dy = [0, 1, 0, -1];
// 9. BFS 탐색 시작
while (queue.length) {
const [x, y] = queue.shift(); // 현재 좌표를 가져옴
// 4방향 탐색
for (let i = 0; i < 4; i++) {
const nx = x + dx[i]; // 다음 열(가로 좌표)
const ny = y + dy[i]; // 다음 행(세로 좌표)
// 유효 범위 내에 있고, 방문하지 않았으며, 테두리인 경우
// ★ 유의점 : 항상 범위내를 먼저 탐색하고 visited와 grid를 탐색해야함
if (nx >= 0 && nx < size && ny >= 0 && ny < size && !visited[nx][ny] && grid[nx][ny] === 1) {
visited[nx][ny] = true; // 방문 처리
grid[nx][ny] = grid[x][y] + 1; // 이동 거리 기록
queue.push([nx, ny]); // 큐에 추가
}
}
}
// 10. 최종 결과 반환 (확장된 좌표계에서의 거리를 원래 거리로 복원)
const originResult = Math.floor(grid[endX][endY] / 2);
return originResult;
}
이번 문제를 풀면서 어느 정도 BFS에 감을 잡은 것 같다.
다시 정리를 해보자면
그나마 조금 더 bfs에 친해진 문제.. 최단 경로를 탐색할 땐 항상 주의하고 염두하며 풀어야겠다!
여행 '경로'이기 때문에 모든 곳을 탐색하는 dfs가 활용될 걸 알았다 다행히 문제에서 출발지점이 ICN인 걸 알려줘서 접근이 쉬웠다
기억해야할 점은
a[1] - b[1]을 쓰지 말고
알파벳순 정렬할 거면 a[1].localeCompare([b1]))을 사용해야한다는것
function solution(tickets) {
// 시작점 지정
const route = ["ICN"];
// 티켓을 도착지 기준으로 알파벳 순 정렬.
tickets.sort((a, b) => a[1].localeCompare(b[1]));
// 각 공항의 방문 여부를 기록하는 배열 초기화.
const visited = new Array(tickets.length).fill(false);
// DFS 함수 정의.
const dfs = (current) => {
// 경로가 완성되었으면 함수 종료
if (route.length === tickets.length + 1) {
return true;
}
// 모든 티켓을 확인.
for (let i = 0; i < tickets.length; i++) {
// 현재 티켓이 사용되지 않았고, 출발지가 현재 위치와 같다면 탐색.
if (!visited[i] && tickets[i][0] === current) {
// 현재 티켓 사용 처리.
visited[i] = true;
// 도착지를 경로에 추가.
route.push(tickets[i][1]);
// 도착지로 한번 더 dfs 탐색 진행
// 만약 한번 더 진행했다가 위에 종결 조건을 만족하면 그대로 종료 후 반환
if (dfs(tickets[i][1])) return true;
// 백트래킹 진행 (진행한 단계 무효화)
// 현재 경로가 유효하지 않거나,
// 모든 티켓을 사용하였지만 경로가 완성되지 않았을 때
visited[i] = false;
route.pop();
}
}
};
dfs("ICN");
return route;
}
function solution(tickets) {
let answer = [];
const result = [];
const visited = [];
tickets.sort();
const len = tickets.length;
const dfs = (str, count) => {
result.push(str);
if(count === len) {
answer = result;
return true;
}
for(let i = 0; i < len; i++) {
if(!visited[i] && tickets[i][0] === str) {
visited[i] = true;
if(dfs(tickets[i][1], count+1)) return true;
visited[i] = false;
}
}
result.pop();
return false;
}
dfs("ICN", 0);
return answer;
}
이거 아마 한번에 풀었을텐데 나름 잘했다는 생각이 듦