Skip to content

[프로그래머스+LeetCode 75] [Lucy] 25년 14주차 3문제 풀이 #118

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Lucy/2025/4-W01/547_Number_of_Provinces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 547. Number of Provinces

## 문제 정보

- URL: [547. Number of Provinces 풀어보기](https://leetcode.com/problems/number-of-provinces/description/?envType=study-plan-v2&envId=leetcode-75)
- LEVEL: Medium
- TOPICS: Depth-First Search, Breadth-First Search, Union Find, Graph

## 문제 회고

도시들 간의 연결 정보가 인접 행렬 형태로 주어지고, 직접 또는 간접적으로 연결된 도시들을 하나의 **province(도시 집단)**으로 본다고 했을 때, 총 몇 개의 독립된 집단이 존재하는지를 구하는 문제이다.

이 문제는 결국 연결 요소의 개수를 구하는 문제이고, 대표적인 그래프 탐색 방식인 DFS 또는 Union-Find(Disjoint Set) 알고리즘으로 해결할 수 있다.

문제를 보고 가장 먼저 떠오른 것은 Union-Find 알고리즘이었다. 서로 연결된 도시들을 하나의 집합으로 묶고, 마지막에 각 도시의 최상위 부모 노드(unique root) 수를 세면 province의 수가 된다고 생각했다.

처음에 parent 배열을 그대로 new Set(parent)로 넘겨서 unique한 값을 세는 방식으로 풀었는데, 모든 노드의 부모가 find()를 통해 갱신되지 않은 상태라 잘못된 결과가 나왔다.

`해결 방법`: parent.map(find)로 모든 노드의 최상위 부모를 찾아서 배열을 새로 만든 뒤, 그 배열을 Set에 넘겨 중복을 제거했습니다. 이걸 통해 올바르게 정답을 구할 수 있었다.

문제를 다 푼 후, 다른 사람들의 풀이를 보니 DFS 방식으로 풀이를 한 것을 볼 수 있었다. DFS 방식에서는 각 도시를 순회하면서 아직 방문하지 않은 도시가 있으면, 그 도시를 시작점으로 DFS 탐색을 수행하고, 새로운 province가 하나 생겼다고 판단해 카운트하여 답을 구했다.

Union-Find는 집합을 효율적으로 병합하고 관리할 수 있지만, 최상위 부모 노드를 업데이트하지 않으면 오답이 나올 수 있음을 알게 되었다. DFS 방식은 더 간단하게 연결 요소의 개수를 구할 수 있음을 배울 수 있었다.
91 changes: 91 additions & 0 deletions Lucy/2025/4-W01/547_Number_of_Provinces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// union-find algoritm(disjoint set) case 2
function findCircleNum(isConnected: number[][]): number {
const parent: number[] = Array.from({ length: isConnected.length }, (_, i) => i);

const find = (i: number): number => {
if (parent[i] === i) {
return i;
}

return (parent[i] = find(parent[i]));
};

const union = (i: number, j: number): void => {
const iOfParent = find(i);
const jOfParent = find(j);

if (iOfParent < jOfParent) {
parent[jOfParent] = iOfParent;
} else {
parent[iOfParent] = jOfParent;
}
};

for (let i = 0; i < isConnected.length; i++) {
for (let j = 0; j < isConnected.length; j++) {
if (isConnected[i][j] === 1) {
union(i, j);
}
}
}

return new Set(parent.map(find)).size;
}

// union-find algorithm case 1
function findCircleNum(isConnected: number[][]): number {
const parents: number[] = Array.from({ length: isConnected.length }, (_, i) => i);

const find = (i: number): number => {
if (parents[i] !== i) {
parents[i] = find(parents[i]);
}

return parents[i];
};

const union = (i: number, j: number): void => {
const iRep: number = find(i);

const jRep: number = find(j);

if (iRep !== jRep) {
parents[jRep] = iRep;
}
};

for (let i = 0; i < isConnected.length; i++) {
for (let j = 0; j < isConnected[i].length; j++) {
if (isConnected[i][j] === 1) {
union(i, j);
}
}
}

return new Set(parents.map(find)).size;
}

// dfs algorithm (모범 답안)
function findCircleNum(isConnected: number[][]): number {
const visited = Array.from({ length: isConnected.length }, () => false);
let provinces = 0;

const dfs = (city: number): void => {
visited[city] = true;

for (let j = 0; j < isConnected[city].length; j++) {
if (isConnected[city][j] === 1 && !visited[j]) {
dfs(j);
}
}
};

for (let i = 0; i < isConnected.length; i++) {
if (!visited[i]) {
provinces++;
dfs(i);
}
}

return provinces;
}
17 changes: 17 additions & 0 deletions Lucy/2025/4-W01/841_Keys_and_Rooms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# 841. Keys and Rooms

## 문제 정보

- URL: [841. Keys and Rooms 풀어보기](https://leetcode.com/problems/keys-and-rooms/description/?envType=study-plan-v2&envId=leetcode-75)
- LEVEL: Medium
- TOPICS: Depth-First Search, Breadth-First Search, Graph

## 문제 회고

이 문제는 그래프 탐색 문제로 볼 수 있었고, 각 방을 노드로, 열쇠를 통해 접근할 수 있는 방을 간선으로 해석해 DFS로 접근했다.

처음엔 canVisitAllRooms 함수 내부에 dfs 함수를 정의해서 풀이했는데, 이 방식은 49ms로 다소 느렸다. 이후 다른 사람들의 풀이를 참고하니 함수를 외부에 정의하거나, 구조를 조금 단순하게 구성한 경우가 많았다.

함수 내부에 함수를 정의해서 성능이 저하된 이유는 명확히 모르겠지만, 재귀 함수가 매 호출 시 새로운 스코프를 생성하고, 클로저로 인해 메모리 사용량이 증가할 수도 있다는 생각이 들었다.

이 문제를 통해 그래프 DFS 방식을 복습할 수 있었다. DFS 방식이 익숙해졌지만, 여전히 성능 최적화 관점에서는 더 깊이 있는 공부가 필요하다고 느꼈다.
19 changes: 19 additions & 0 deletions Lucy/2025/4-W01/841_Keys_and_Rooms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
function canVisitAllRooms(rooms: number[][]): boolean {
const visited: boolean[] = new Array(rooms.length).fill(false);
visited[0] = true;

dfs(rooms, visited, 0);

return visited.every((el) => el);
}

function dfs(rooms: number[][], visited: boolean[], currentKey: number) {
const keys = rooms[currentKey];

for (const key of keys) {
if (!visited[key]) {
visited[key] = true;
dfs(rooms, visited, key);
}
}
}
11 changes: 11 additions & 0 deletions Lucy/2025/4-W01/사라지는_발판.Md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 사라지는 발판

## 문제 정보

- URL: [사라지는 발판 풀어보기](https://school.programmers.co.kr/learn/courses/30/lessons/92345)
- LEVEL: Lv3
- TOPICS: 완전 탐색

## 문제 회고

처음 문제를 읽고 “모든 경우의 수를 고려해야 하지 않을까?”라는 생각이 들어서 완전 탐색을 떠올렸다. 30분 이상을 고민했지만 실마리를 못 잡아서, 결국 카카오 테크 블로그의 문제 해설과 ChatGPT와의 대화를 통해 백트래킹 알고리즘을 사용하면 되겠구나 힌트를 얻었다. 매 턴마다 현재 플레이어가 이길 수 있다면 최소 이동 횟수, 질 수밖에 없다면 최대 이동 횟수를 저장해서 반환한다. 즉, 승리할 수 있는 상황이면 빠르게 이기고, 질 수밖에 없는 상황이면 최대한 오래 버틴다는 전략을 코드에 구현하는 것이 핵심이었다. 문제를 풀고 나니 복잡한 조건이나 추상적인 상황을 코드로 표현하는 훈련이 부족했던 것 같다. 다음에는 비슷한 상황을 작은 단위로 쪼개서 시뮬레이션하며 접근해봐야겠다.
49 changes: 49 additions & 0 deletions Lucy/2025/4-W01/사라지는_발판.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
function solution(board, aloc, bloc) {
const ROW = board.length;
const COL = board[0].length;
const directions = [
[0, 1],
[0, -1],
[1, 0],
[-1, 0],
];

const isMove = (r, c) => r >= 0 && r < ROW && c >= 0 && c < COL && board[r][c] === 1;

const play = (r, c, opponentR, opponentC, moveCount) => {
// 현재 위치가 사라졌다면 패배 (이전 턴에서 상대가 발판을 없앴기 때문)
if (board[r][c] === 0) return [false, moveCount];

let canMove = false;
let minMoves = Infinity;
let maxMoves = -Infinity;
let isWin = false;

for (const [dr, dc] of directions) {
const [nr, nc] = [r + dr, c + dc];

if (isMove(nr, nc)) {
canMove = true;

// 이동 후 현재 위치 발판 없애기
board[r][c] = 0;
const [opponentWin, opponentMoves] = play(opponentR, opponentC, nr, nc, moveCount + 1);
board[r][c] = 1; // 원래대로 되돌리기 (백트래킹)

if (opponentWin) {
maxMoves = Math.max(maxMoves, opponentMoves);
} else {
isWin = true;
minMoves = Math.min(minMoves, opponentMoves);
}
}
}

// 이동할 곳이 없는 경우 (현재 위치에서 움직일 수 없는 경우)
if (!canMove) return [false, moveCount];

return isWin ? [true, minMoves] : [false, maxMoves];
};

return play(aloc[0], aloc[1], bloc[0], bloc[1], 0)[1];
}