반응형

오늘의 학습 키워드

그리디 알고리즘


알고리즘 문제

LeetCode Medium - 1338. Reduce Array Size to The Half


공부한 내용

그리디 알고리즘

  • 최적해를 구하는 알고리즘
  • 전체 최적해를 구하기 위해 각 단계에서 최선의 선택을 한다.
  • 문제를 해결하는 과정에서 현재 상황에서의 최적의 해법을 찾는 과정을 반복하며 최종 해답을 도출한다.

오늘의 회고

처음 시도

  • 완전 탐색을 해야해서 DFS를 사용해야 하나 생각했다. 
    • 문제1. 배열 최대 크기가 10^5인데, 모두 탐색하면 2^10^5 = 1,125,899,906,842,624 번을 탐색해야 한다.
    • 문제2. DFS로 최적의 해를 찾더라도, 다시 돌아와서 다른 경로를 탐색하면 최적의 해를 놓칠 수 있다.

해결

  • 그리디 알고리즘으로 가장 빈도수가 많은 숫자부터 주어진 배열의 전체 길이의 반이 될때까지 빈도수를 더해서 답을 구할 수 있다.

문제 풀이

import java.util.*;

class Solution {
    public int minSetSize(int[] arr) {
        int len = arr.length;
        Map<Integer, Integer> map = new HashMap<>();
        for (int n : arr) {
            map.put(n, map.getOrDefault(n, 0)+1);
        }
        List<Integer> values = new ArrayList<>(map.values());
        Collections.sort(values);

        int res = 0, sum = 0;
        for (int i = values.size()-1; i >= 0; i--) {
            sum += values.get(i);
            res++;

            if (sum >= Math.round(len/2)) break;
        }

        return res;
    }
반응형
반응형

오늘의 학습 키워드

Stack
Queue


알고리즘 문제

LeetCode Medium. 341. Flatten Nested List Iterator


공부한 내용

Queue, 재귀 함수를 활용


오늘의 회고

  • 갈수록 문제 해결 시간이 빨라지고, 스스로 해결할 수 있는 문제도 많아져서 좋다.
  • 처음에 재귀 함수를 만들어서 푸는 문제가 이해가 되지도 않고 어려웠는데, 이제는 어떤 느낌인지 조금은 감이 오는 것 같다. => 어떤느낌? 배열/리스트, 큐/스택 자료구조를 사용하면서 같은 Format의 형태를 탐색해야 한다거나 등등.

처음 시도

  • 주어진 nestedList를 보면 숫자와 숫자로 된 리스트가 섞여있는데, 차례대로 숫자 배열로 반환하는 게 목표였다.
  • nestedList 형태를 봤을 때, NestedInteger 타입으로 되어 있고, NestedInteger는 숫자 또는 리스트 형태로 된 걸 확인했을 때, 아! 반복해서 nestedList를 순서대로 꺼내서 Integer로 된 큐에 담아야 겠다고 생각했다.
  • 같은 형태의 반복, Integer를 담을 큐, 순서대로 반복 => DFS 방식으로 풀면 좋겠다!

해결

  • 주어진 nestedList를 순서대로 꺼내서 Integer 타입이면 바로 Queue에 넣고, List 타입이면 다시 순서대로 꺼내서 Queue에 넣는 것을 반복하는 재귀 함수를 사용한 DFS 방식으로 풀었다.

문제 풀이

내가 푼 풀이 (Queue + DFS 방식)

/**
 * // This is the interface that allows for creating nested lists.
 * // You should not implement it, or speculate about its implementation
 * public interface NestedInteger {
 *
 *     // @return true if this NestedInteger holds a single integer, rather than a nested list.
 *     public boolean isInteger();
 *
 *     // @return the single integer that this NestedInteger holds, if it holds a single integer
 *     // Return null if this NestedInteger holds a nested list
 *     public Integer getInteger();
 *
 *     // @return the nested list that this NestedInteger holds, if it holds a nested list
 *     // Return empty list if this NestedInteger holds a single integer
 *     public List<NestedInteger> getList();
 * }
 */
import java.util.*;

public class NestedIterator implements Iterator<Integer> {
    private Queue<Integer> queue;
    public NestedIterator(List<NestedInteger> nestedList) {
        this.queue = new LinkedList<>();
        reculsive(nestedList);
    }

    public void reculsive(List<NestedInteger> list) {
        for (int i = 0; i < list.size(); i++) {
            if (!list.get(i).isInteger()) {
                reculsive(list.get(i).getList()); 
            } else {
                queue.offer(list.get(i).getInteger());
            }
        }
    }

    @Override
    public Integer next() {
        return queue.poll();
    }

    @Override
    public boolean hasNext() {
        return !queue.isEmpty();
    }
}

/**
 * Your NestedIterator object will be instantiated and called as such:
 * NestedIterator i = new NestedIterator(nestedList);
 * while (i.hasNext()) v[f()] = i.next();
 */

 

다른 풀이 (Stack + DFS 방식)

/**
 * // This is the interface that allows for creating nested lists.
 * // You should not implement it, or speculate about its implementation
 * public interface NestedInteger {
 *
 *     // @return true if this NestedInteger holds a single integer, rather than a nested list.
 *     public boolean isInteger();
 *
 *     // @return the single integer that this NestedInteger holds, if it holds a single integer
 *     // Return null if this NestedInteger holds a nested list
 *     public Integer getInteger();
 *
 *     // @return the nested list that this NestedInteger holds, if it holds a nested list
 *     // Return empty list if this NestedInteger holds a single integer
 *     public List<NestedInteger> getList();
 * }
 */
public class NestedIterator implements Iterator<Integer> {
    private LinkedList<NestedInteger> stack; // Stack to keep track of NestedInteger objects

    public NestedIterator(List<NestedInteger> nestedList) {
        stack = new LinkedList<>();
        // Push nestedList elements in reverse order onto the stack
        for (int i = nestedList.size() - 1; i >= 0; i--) {
            stack.push(nestedList.get(i));
        }
    }

    @Override
    public Integer next() {
        return stack.pop().getInteger(); // Return the integer from the top of the stack
    }

    @Override
    public boolean hasNext() {
        while (!stack.isEmpty()) {
            NestedInteger curr = stack.peek();
            if (curr.isInteger()) {
                return true; // Found an integer
            }

            // Flatten nested list
            stack.pop(); // Remove the list
            for (int i = curr.getList().size() - 1; i >= 0; i--) {
                stack.push(curr.getList().get(i)); // Push its elements in reverse order
            }
        }
        return false; // No more integers
    }
}

/**
 * Your NestedIterator object will be instantiated and called as such:
 * NestedIterator i = new NestedIterator(nestedList);
 * while (i.hasNext()) v[f()] = i.next();
 */
반응형
반응형

오늘의 학습 키워드

DFS
Combination


알고리즘 문제

LeetCode Medium 1286. Iterator for Combination


공부한 내용

오늘은 스스로 생각한 DFS를 활용해서 처음부터 끝까지 혼자힘으로 풀었다. 너무 기분이 좋다!


오늘의 회고

처음 시도

  • 주어진 문자열의 한 문자씩 모두 탐색해서 원하는 길이의 문자열을 만드는(조합하는) 문제였기 때문에 DFS로 문자열의 0번 인덱스부터 마지막 인덱스까지 순서대로 조합을 찾는 방법을 생각했다.
    • 문제에서 이미 오름차순으로 정렬된 문자열이 주어지기 때문에 앞에서부터 순서대로 자신의 이후 인덱스부터 차례로 조합하면서 모든 조합을 찾을 수 있다.
  • DFS로 생각했고, 큐(Queue) 자료구조를 사용해 조합한 문자열을 큐에 넣고, 오름차순으로 정렬되서 이미 큐에 들어가기 때문에 그대로 앞에서부터 꺼내면 된다.
  • 그 다음으로 재귀 종료 조건을 생각해야 하는데 반복하면서 문자열에 1씩 증가하는 인덱스의 값을 더할때마다 카운팅을 하면서 원하는 문자열 길이만큼 조합을 만들고, 원하는 문자열 길이만큼 카운팅이 되었을 때 return하도록 생각했다.

해결

  • DFS 알고리즘과 Queue 자료구조로 해결했다!

문제 풀이

import java.util.*;

class CombinationIterator {
    private String characters;
    private int len;
    private Queue<String> queue;

    public CombinationIterator(String characters, int combinationLength) {
        this.characters = characters; 
        this.len = combinationLength;
        this.queue = new LinkedList<>();
        for (int i = 0; i < characters.length(); i++) {
            dfs(characters.substring(i,i+1), i, 1);
        }
    }
    
    public void dfs(String str, int index, int count) {
        if (count == len) {
            queue.offer(str);
            return;
        }

        for (int i = index+1; i < characters.length(); i++) {
            dfs(str+characters.substring(i,i+1), i, count+1);
        }
    }

    public String next() {
        return queue.poll();
    }
    
    public boolean hasNext() {
        return !queue.isEmpty();
    }
}

/**
 * Your CombinationIterator object will be instantiated and called as such:
 * CombinationIterator obj = new CombinationIterator(characters, combinationLength);
 * String param_1 = obj.next();
 * boolean param_2 = obj.hasNext();
 */
반응형
반응형

오늘의 학습 키워드

그래프, BFS


알고리즘 문제

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 


공부한 내용

Tree 자료 구조

  • Parent - Child 관계를 나타낼 수 있는 자료구조
  • 노드 한 개에 1개의 Parent 노드가 있을 수 있다.
  • 노드 한 개에 N개의 Child 노드가 있을 수 있다. 
  • e.g.) 폴더, 조직도, 가족 관계도
  • Binary Tree: 노드 한 개에 최대 2개의 Child 노드를 가질 수 있는 자료구조. => 왼쪽 Child 노드, 오른쪽 Child 노드.
class Node {
    int value;
    Node next;
    
    Node(int val) {
        this.value = val;
        this.next = null;
    }
}

class BinaryNode {
    int value;
    BinaryNode leftChild;
    BinaryNode rightChild;
    
    BinaryNode(int val) {
    	this.value = val;
        this.leftChild = null;
        this.rightChilde = null;
    }
}

 

DFS

  • 한 방향을 먼저 쭉 탐색하고, 그 후에 다음 노드로 올라와서 쭉 탐색한다.
  • 재귀 (Reculsive)로 간단하게 구현 가능.

BFS

  • Level 별로 탐색한다. 루트노드로부터 1칸 떨어진 노드들 탐색 -> 2칸 떨어진 노드들 탐색 -> ... 
  • Queue 자료구조로 구현 가능.
    • 처음에 시작할 루트노드를 queue에 먼저 넣는다.
    • 이후에 큐가 비어있지 않으면 계속 반복하면서 탐색한다.
    • 일반적으로 탐색 종료 조건은 큐의 사이즈가 0. 즉, 큐가 비어있으면 탐색 종료를 의미한다.

오늘의 회고

처음 시도

  • 이전에 BFS, DFS 개념은 알고 있었지만 막상 알고리즘을 짜려고 보니 아직 개념이 부족하다고 느껴서 다시 Tree 자료구조에 대해 공부했다.
  • BFS, DFS 개념을 다시 보니 이번 문제는 BFS로 풀면서 정해진 루트노드 1부터 시작해서 depth 별로 탐색하면서 depth마다 노드가 몇 개인지 카운트를 해야 한다고 생각했다.

해결

  • BFS로 문제를 풀 때, 먼저 자료구조 Queue를 사용하면 depth 별로 queue에 담을 수 있어서 편리하다.
  • BFS 문제 풀 때, 방문한 노드 체크 배열과 인접 행렬(노드 간 연결 관계 2차원 배열)을 만들어서 탐색 관리를 한다.
    • 인접 행렬 => 노드 A와 노드 B 사이에 간선 존재 여부를 시간복잡도 O(1)로 조회할 수 있다. (1이면 존재)
    • 방문 체크 배열 => 총 노드 개수 +1 사이즈로 1차원 배열을 만들어서 방문했다면 true로 알 수 있다.
  • 일반적으로 BFS 문제는 queue가 빈 경우를 탐색 종료 조건(반복문 종료)으로 사용한다. 
    • 이번 문제에서는 존재하는 마지막 depth에 노드 개수를 알아야 하기 때문에, depth를 반복할 때마다 큐의 사이즈를 업데이트하도록 했다.
    • 그리고 큐가 비어있다면, depth(노드들)이 더이상 존재하지 않기 때문에 종료하고, 마지막에 업데이트된 큐 사이즈가 마지막 depth의 노드 개수를 의미하게 된다.

문제 풀이

import java.util.*;

class Solution {
    //return: 1번 노드와 가장 멀리 떨어진 노드는?
    boolean[] visit;		// 해당 노드(배열 인덱스)에 이미 방문했는지 여부
    boolean[][] relative;	// 인접 행렬 (그래프 관계)
    
    public int bfs(int[][] edge) {
        Queue<Integer> que = new LinkedList<>();
        que.offer(1);
        visit[1] = true;
        
        for (int i = 0; i < edge.length; i++) {
            int x = edge[i][0];
            int y = edge[i][1];
            
            relative[x][y] = true;
            relative[y][x] = true;
        }
		
        int size = 0;
        while (!que.isEmpty()) {
            size = que.size();
            
            for (int i = 0; i < size; i++) {
                int current = que.poll();
                
                for (int j = 1; j < visit.length; j++) {
                    if (!visit[j] && relative[current][j]) {
                        que.add(j);
                        visit[j] = true;
                    }
                }
            }
        }
        return size;
    }
    
    public int solution(int n, int[][] edge) {
        int answer = 0;
        
        this.visit = new boolean[n+1];
        this.relative = new boolean[n+1][n+1];
        
        answer = bfs(edge);
        
        return answer;
    }
}
반응형
반응형

오늘의 학습 키워드

DP


알고리즘 문제


공부한 내용

DFS

DP

  • Top-Down: 큰 문제에서 작은 문제로 쪼개가면서 해결. 초기 상태로부터 점차 확장 . 재귀와 메모이제이션을 사용
  • Bottom-Top: 작은 부분 문제를 해결하며 전체 문제를 해결

오늘의 회고

처음 시도 (DFS)

  • 처음에 순서를 고려해서 모든 조합을 만드는 문제라고 생각해서 DFS로 구현해보려고 했다.
    • 먼저 모음 배열이 필요하다. [ a, e, i, o. u]
    • 문자열 1개일 때 a, e, i, o, u / 2개일 때 aa, ae, ai, ao, au, ee, ei, eo, eu, ii, io, iu, oo, ou, uu / ... 이렇게 조합이 됨.
    • 주어지는 n은 조합할 문자열 길이이기 때문에 조합할 문자열 수를 재귀 호출할 때마다 +1해서 종료조건을 n과 +1씩한 조합할 문자열 수(strlen)가 같아질 때라고 생각했다.
    • 종료 조건 이후에 오는 로직에 idx와 같거나 보다 큰 인덱스로 문자열을 만드는 로직이 필요했다.
    • 또한 문제 출력 조건은 조합할 수 있는 문자열 총 개수여서 카운팅이 필요했는데 나는 이 때 List에 문자열들을 담아서 List의 size로 카운트를 하려고 했더니 로직이 복잡해지면서 구현이 어려워졌다.

해결 (DFS)

  • 카운팅 해결
    • count라는 클래스 전역 변수를 두고, "재귀 함수의 return;" 은 원하는 문자열을 만들어서 반환되는 의미이기 때문에 반환 바로 직전에 count를 +1 해주므로써 카운팅을 하면 된다.
  • 문자열 조합 해결 
    • n = 2라고 가정한다. 
    • 문자열을 출력하지 않아도 되기 때문에 문자열이 생성됐다고 치고 카운트를 하면 된다. 이렇게 하면 List, Map 같은 문자열을 저장할 자료구조가 필요없기 때문에 성능면으로도 로직 복잡성 측면에서도 좋다고 생각한다.
    • 내가 처음에 생각한 재귀함수와 매개변수: dfs("", 0, 0)?  or dfs("", 0, 1)? ... I don't know..
      • 왜 이렇게 생각했나 되짚어보면,
      • 처음에 만든 문자열이 없으니까 ""로 시작했다. 
      • 두번째 파라미터는 추가할 모음의 인덱스(in 모음 배열)여서 0이면 a, 1이면 e 이런식이다.
      • 세번째 파라미터는 만든 문자열의 길이이다. 0으로 할지 1로 할지 고민했다.
      • strlen이 0이면 현재 만든 문자열이 없는 상태("")이고, 1이면 현재 만든 문자열 길이가 1("a")인 상태이기 때문에 결론적으로 0이 맞다.
        => strlen이 0이면, ""인 상태 > 재귀 함수 호출할 때 현재 문자열("")에 "a"를 추가하면 현재 문자열("a")
        => strlen이 1이면, "a"인 상태 > 재귀 함수 호출할 때 현재 문자열에 "a"를 추가하면 현재 문자열("aa")
    • 처음 시작 dfs(0, 0)
      • dfs(0, 0) [""] -> dfs(0, 1) ["a"] -> dfs(0, 2) ["aa"] dfs() 호출 시 만든 문자열 "a", "aa", return(카운트+1 )
      • dfs(1, 0) [""] -> dfs(1, 1) ["e"] -> dfs(1, 2) 는 "e", "ee", return(카운트+1)
      • ...
      • dfs(4, 0) [""] -> dfs(4, 1) ["u"] -> dfs(4, 2) ["uu"] -> return;(카운트+1)
DP (Dynamic Programming)
  • Top-Down
  • Bottom-Top

 


문제 풀이

1번째 방법 DFS (문자열은 생성했다 치고, 카운팅)

class Solution {
    int count;
    int n;
    char[] vowels = {'a', 'e', 'i', 'o', 'u'};

    public void dfs(int idx, int strlen) {
    	//종료 조건 => 주어진 문자열 길이와 조합한 문자열 길이가 같은 경우
        if (strlen == n) {
            count++;
            return;
        }
		
        //idx => 현재 사용할 모음의 vowels의 인덱스
        //재귀 호출 시 vowels[idx] 으로 strlen+1 길이의 문자열을 만든다.
        //ex) n=2; count=0; idx=> 이번에 사용할 모음을 가리키는 인덱스 / strlen=> 현재 만들어진 문자열 길이
        //dfs(idx, strlen) [이번에 만들 문자열]
        //dfs(0,0) [""] -> dfs(0,1) ["a"] -> dfs(0,2) ["aa"] -> return; (count = 1)
        //		   		-> dfs(1,1) ["a"] -> dfs(1,2) ["ae"] -> return;	(count = 2)
        //				-> dfs(2,1) ["a"] -> dfs(2,2) ["ai"] -> return; (count = 3)
        //				-> dfs(3,1) ["a"] -> dfs(3,2) ["ao"] -> return; (count = 4)
        //				-> dfs(4,1) ["a"] -> dfs(4,2) ["au"] -> return; (count = 5)
        //				-> return; (count = 5)
        //dfs(1,0) [""] -> dfs(1,1) ["e"] -> dfs(1,2) ["ee"] -> return;	(count = 6)
        //				->
        //				-> return; (count = 9)
       	//...
        //
        //dfs(4,0) [""] -> dfs(4,1) ["u"] -> dfs(4,2) ["uu"] return;
        //				 -> return; (count = 15)
        
        for (int i = idx; i < vowels.length; i++) {
            dfs(i, strlen+1);
        }
    }

    public int countVowelStrings(int n) {
        this.count = 0;
        this.n = n;

        dfs(0, 0);

        return count;
    }
}

 

2번째 방법 DFS (문자열 + 카운팅)

class Solution {
    int count = 0;
    int n;
    char[] vowels = {'a', 'e', 'i', 'o', 'u'};

    public void dfs(String current, int idx, int depth) {
    	//카운팅 + 재귀 종료 조건
        if (depth == n) {
            count++;
            return;
        }

        for (int i = idx; i < vowels.length; i++) {
            // 현재 문자열에 모음을 추가해 길이+1 된 새로 조합한 문자열 생성
            dfs(current+vowels[i], i, depth+1);
        }
    }

    public int countVowelStrings(int n) {
        this.n = n;
        dfs("", 0, 0);  // 빈 문자열로 시작
        return count;
    }
}

 

3번째 방법 (DP : Bottom-Top)

class Solution {
    private Map<Integer, int[]> integerMap;
    public int countVowelStrings(int n) {
        if (Objects.isNull(integerMap)) {
            integerMap = new HashMap<>();
            integerMap.put(1, new int[]{1, 1, 1, 1, 1});
        }

        if (integerMap.containsKey(n)) {
            return Arrays.stream(integerMap.get(n)).sum();
        }

        for (int i = 2; i <= n; i++) {
            int[] prev = integerMap.get(i-1);
            int[] cur = new int[5];
            for (int j = 0; j < 5; j++) {
                for (int k = j; k < 5; k++) {
                    cur[j] += prev[k];
                }
            }
            integerMap.put(i, cur);
        }
        return Arrays.stream(integerMap.get(n)).sum();
    }
}

 

4번째 방법 (DP : Top-Down)

class Solution {
    private int[][] memo;

    public int countVowelStrings(int n) {
        memo = new int[n + 1][6]; // n+1 for length, 6 for vowel count from 0 to 5
        return count(n, 5);
    }

    private int count(int n, int a) {
        if (n == 1) return a;
        if (a == 1) return 1;
        if (memo[n][a] != 0) return memo[n][a];

        int result = 0;
        for (int i = a; i >= 1; i--) {
            result += count(n - 1, i);
        }

        memo[n][a] = result;
        return result;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.countVowelStrings(2)); // Expected output: 15
    }
}
반응형
반응형

오늘의 학습 키워드

DFS, 이진 트리


알고리즘 문제


공부한 내용

이진 트리

  • 트리 자료 구조
  • 각 노드가 최대 2개의 자식 노드(왼쪽, 오른쪽)을 가진다.
  • 노드의 Level(레벨)은 루트 노드에서 자신까지 가는 경로의 길이 + 1. 루트 노드 레벨은 1이다.
  • 노드의 Depth(깊이)는 루트 노드에서 자신까지 가는 경로의 길이. 루트 노드 깊이는 0이다. 
  • 리프 노드는 자식이 없는 노드를 의미한다.

이진 트리의 대칭 구조와 DFS

  • 서로 대칭 구조로 데칼코마니처럼 생각할 수 있다.
  • DFS로 재귀 함수 호출 시 같은 레벨의 가장 왼쪽 노드와 가장 오른쪽 노드를 swap하면 대칭적으로 reverse 가능하다.


오늘의 회고

처음 시도

  • DFS로 처음에 시도를 했는데 이진 트리의 대칭 구조를 활용하지 못하고 해당 노드의 왼쪽, 오른쪽 노드만 swap해서 case 3에서 실패했다. 
  • 두번째 시도에서 List<TreeNode> 자료구조를 사용해서 해보려 했는데 DFS와 BFS가 섞인 것 같았다. 

해결

  • 이진 트리의 특성을 잘 활용했어야 했고, 다른 사람의 풀이를 듣고 대칭 구조를 활용할 수 있다는 것을 알게 되었다.

문제 풀이

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */

class Solution {
    int tmp;

    public void dfs(TreeNode left, TreeNode right, int depth) {
    	//재귀 종료 조건 => 리프 노드인 경우 (자식 노드가 없는 경우)
        if (left == null && right == null) return;
		
        //depth가 홀수인 경우 left 노드와 right 노드를 swap
        if (depth % 2 == 1) {
            tmp = right.val;
            right.val = left.val;
            left.val = tmp;
        }

		//왼쪽 노드의 왼쪽 자식노드, 오른쪽 노드의 오른쪽 자식 노드 => 중앙 기준으로 바깥쪽의 노드를 swap하도록 세팅
        dfs(left.left, right.right, depth+1);
        //왼쪽 노드의 오른쪽 자식노드, 오른쪽 노드의 왼쪽 자식 노드 => 중앙 기준으로 안쪽의 노드들을 swap하도록 세팅
        dfs(left.right, right.left, depth+1);
    }

    public TreeNode reverseOddLevels(TreeNode root) {
        dfs(root.left, root.right, 1);

        return root;
    }
}
반응형
반응형

오늘의 학습 키워드

DFS


알고리즘 문제

 


공부한 내용

DFS, 재귀


오늘의 회고

처음 시도

  • 경로 탐색 문제가 나왔고, root 노드 부터 전체 길이(n) - 1 노드까지 순서대로 경로 탐색이어서 DFS가 적합하다고 생각했다.
  • DFS로 구현하다가 막혔다.. 종료조건을 잊고 있었다. 마지막 노드까지 탐색하는 조건을 중간에 잊고 있었다...
  • 스스로 생각해낸 부분
    • DFS로 구현하자.
    • 백트래킹 => path.remove() 로 경로 탐색의 마지막 요소 제거해서 다른 탐색에 노드를 탐색할 수 있도록 한다.
    •  

해결

  • 검색해서 해결했다.. 다른 코드 참고
  • node 자체와 경로만을 사용해 DFS로 구현
  • 재귀함수 호출 조건은 현재 노드를 경로에 추가한 후 그래프에서 현재 노드에서 갈 수 있는 노드들을 반복해서 호출한다.
  • 종료 조건은 현재 노드가 마지막 노드일 때
  • try-catch-finally 구문으로 재귀 호출 반환 시 finally에서 백트래킹 해줬다.

문제 풀이

import java.util.*;

//0 -> (1, 3, 4)
    //1 -> (2, 3, 4)
        //2 -> (3)
            //3 -> (4)
                // 4 -> []
    //1 -> (3, 4)
        //3 -> (4)
            //4 -> []
    //1 -> (4)
        //4 -> []
//0 -> (3, 4)
    //3 -> (4)
        //4 -> []
//0 -> (4)
    //4 -> []
class Solution {
    int[][] graph;
    List<List<Integer>> result;
    
    public void dfs(int node, List<Integer> path) {
        try {
            path.add(node);
        
            if (node == graph.length-1) {
                result.add(new ArrayList<>(path));
                return;
            } 

            for (int nextNode : graph[node]) {
                dfs(nextNode, path);
            }
        } catch(Exception e) {
        } finally {
            path.remove(path.size()-1);
        }
    }
    public List<List<Integer>> allPathsSourceTarget(int[][] graph) {
        this.graph = graph;
        this.result = new ArrayList<>();
        List<Integer> path = new LinkedList<>();
        
        dfs(0, path);

        return result;
    }
반응형
반응형

오늘의 학습 키워드

BFS, 최단거리


알고리즘 문제

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr


공부한 내용

  • BFS
  • 최단 거리 알고리즘
  • DFS vs BFS

오늘의 회고

처음 시도

  •  경로 그려보면서 탐색 중단 케이스 확인해보기
    • 목표: (5,5)
      (1,1)->(1,2)=>벽(0)
      (1,1)->(2,1)->(2,2)=>벽(0)
      (1,1)->(2,1)->(3,1)->(3,2)=>벽(0)
      (1,1)->(2,1)->(3,1)->(4,1)->(4,2)->(4,3)->(4,4)=>벽(0)
      (1,1)->(2,1)->(3,1)->(4,1)->(4,2)->(4,3)->(5,3)=>벽(0)
      (1,1)->(2,1)->(3,1)->(4,1)->(4,2)->(4,3)->(3,3)->(3,4)->(3,5)->끝
      (1,1)->(2,1)->(3,1)->(4,1)->(4,2)->(4,3)->(3,3)->(3,4)->(3,5)->(4,5)->끝
      (1,1)->(2,1)->(3,1)->(4,1)->(4,2)->(4,3)->(3,3)->(3,4)->(3,5)->(4,5)->(5,5) [골인] 
      (1,1)->(2,1)->(3,1)->(4,1)->(4,2)->(4,3)->(3,3)->(3,4)->(4,4)=>벽(0)
      (1,1)->(2,1)->(3,1)->(4,1)->(4,2)->(4,3)->(3,3)->(3,4)->(2,4)=>벽(0)
      (1,1)->(2,1)->(3,1)->(4,1)->(4,2)->(4,3)->(3,3)->(4,3)=> 왔던길
      (1,1)->(2,1)->(3,1)->(4,1)->(4,2)->(4,3)->(3,3)->(2,3)->(2,4)=>벽(0)
      (1,1)->(2,1)->(3,1)->(4,1)->(4,2)->(4,3)->(3,3)->(2,3)->(3,3)=>왔던길
      (1,1)->(2,1)->(3,1)->(4,1)->(4,2)->(4,3)->(3,3)->(2,3)->(1,3)->(1,4)->(1,5)=>끝
      (1,1)->(2,1)->(3,1)->(4,1)->(4,2)->(4,3)->(3,3)->(2,3)->(1,3)->(1,4)->(1,5)->(2,5)-> (3,5)->(4,5)->(5,5) [골인]
      (1,1)->(2,1)->(3,1)->(4,1)->(4,2)->(4,3)->(3,3)->(2,3)->(1,3)->(1,4)
      (1,1)->(2,1)->(3,1)->(4,1)->(4,2)->(4,3)->(3,3)->(2,3)->(1,3)->(2,3)=>왔던길
      (1,1)->(2,1)->(3,1)->(4,1)->(4,2)->(5,2)=>벽(0)
      (1,1)->(2,1)->(3,1)->(4,1)->(5,1)=>벽(0)
  • 처음에 경로를 체크 해보면서, 경로 탐색 종료 조건인 경우를 확인해서 해봤던 탐색 방식인 DFS로 문제를 풀었다.
  • 결과는 정확성 테스트는 통과, 효율성 테스트는 모두 실패했다.
class Solution {
	int[][] maps;
    int xMax;
    int yMax;
    int resultCount;
    
    // x,y 는 0,0부터 시작한다고 생각. 목표는 xMax, yMax
    public void dfs(int x, int y, int[][] visit, int count) {
    	// 동: x+1 / 서: x-1 / 남: y+1 / 북: y-1
        
        //종료 조건
        if (x > xMax || y > yMax || x < 0 || y < 0) return;	// 맵 벗어남
        if (visit[y][x] == 1) return;		// 왔던 길
        if (maps[y][x] == 0) return; 		// 벽
        if (x == xMax && y == yMax) {		// 목표 골인
            if (resultCount == -1 || count < resultCount) {	// 최단 거리 체크
                resultCount = count;
            }
            return;
        }
        
        count += 1;					//카운트
        visit[y][x] = 1;			//왔던길 체크
        
        dfs(x+1, y, visit, count);	//동쪽으로 이동
       	dfs(x, y+1, visit, count);	//남쪽으로 이동
        dfs(x-1, y, visit, count);	//서쪽으로 이동
        dfs(x, y-1, visit, count);	//북쪽으로 이동
        
        visit[y][x] = 0;			//백트래킹
    }
    
    public int solution(int[][] maps) {
        this.maps = maps;
   		this.xMax = maps[0].length-1;
   		this.yMax = maps.length-1;
		this.resultCount = -1;
        int[][] visit = new int[yMax+1][xMax+1];
        
        dfs(0, 0, visit, 1);
        
        return resultCount;
    }
}

해결

  • 해결이 안되서 찾아보니 최단 거리 문제는 BFS가 더 효율성이 좋을 수 있다는 것을 알았다
  • DFS의 경우 운이 좋으면 한 번에 경로를 찾을 수도 있지만, 운이 좋지 않는다면 효율성에서 최악의 시간복잡도가 나올 수 있기 때문

문제 풀이

  • BFS
  • DFS의 재귀 종료조건이 BFS에서 4방향 탐색에서 건너뛰는 조건으로 사용되었다.
  • DFS와 최대 차이점: 한 길로 탐색 후 다른 길 탐색(DFS) vs 모든 방향 탐색 후 다시 모든 방향 탐색(BFS)
    • DFS는 재귀를 통해 한 방향으로 쭉 탐색하고 현재 경로가 유효하지 않거나 더 나은 경로를 찾기 위해 탐색을 종료하고 이전 상태로 돌아가야 하는 경우 백트래킹을 해줘야 한다. (깊이 우선)
    • BFS는 큐를 통해 모든 방향에 대한 좌표값을 큐에 담아서 백트래킹 필요없이 탐색 종료 조건에 걸리면 패스하고, 탐색할 수 있는 방향이면 방문 체크 & 큐에 담아서 탐색할 수 있도록 해준다. (너비 우선)
import java.util.*;

class Solution {
    public int solution(int[][] maps) {
        int[] dx = {1, -1, 0, 0};       // 동서남북 방향 벡터
        int[] dy = {0, 0, 1, -1};       // 동서남북 방향 벡터
        int xMax = maps[0].length - 1;  // x 좌표의 최대값
        int yMax = maps.length - 1;     // y 좌표의 최대값

        boolean[][] visit = new boolean[yMax + 1][xMax + 1]; // 방문 체크 배열
        Queue<int[]> queue = new LinkedList<>(); // BFS를 위한 큐
        queue.add(new int[] {0, 0, 1}); // 시작점 (0, 0)과 시작 거리 1을 큐에 추가
        visit[0][0] = true; // 시작점 방문 체크

        while (!queue.isEmpty()) {          // 큐가 비어있지 않으면 반복
            int[] current = queue.poll();   // 큐에서 현재 노드를 꺼냄
            int x = current[0];             // 현재 x 좌표
            int y = current[1];             // 현재 y 좌표
            int dist = current[2];          // 현재까지의 거리

            if (x == xMax && y == yMax) {   // 목표 지점에 도달하면
                return dist;                // 현재까지의 거리를 반환
            }

            for (int i = 0; i < 4; i++) {   // 동서남북 네 방향으로 탐색
                int nx = x + dx[i];         // 새로운 x 좌표
                int ny = y + dy[i];         // 새로운 y 좌표

                // 탐색 종료 조건(맵 벗어남, 벽, 재방문)
                if (nx < 0 || ny < 0 || nx > xMax || ny > yMax) continue;
                if (maps[ny][nx] == 0) continue;
                if (visit[ny][nx]) continue;

                visit[ny][nx] = true;                   // 방문 체크
                queue.add(new int[] {nx, ny, dist+1});  // 현재 좌표 큐에 추가
            }
        }

        return -1; // 큐를 다 돌고 목표 지점에 도달할 수 없는 경우
    }
}

 

Github

 

Algorithm-Practice/프로그래머스/2/1844. 게임 맵 최단거리/게임 맵 최단거리.java at main · dev-jini

Contribute to dev-jinius/Algorithm-Practice development by creating an account on GitHub.

github.com

 

 

반응형

+ Recent posts