반응형

코테를 보고나서 DFS의 중요성을 뼈저리게 느끼고, 한 번 더 개념을 잡고, 흐름을 잡고자 풀어본 문제이다.

 

✅ 코테에서 머뭇거렸던 부분

  • DFS함수를 어느 시점에서 불러야 할지
  • DFS함수를 어떻게 구성해야 할지
    (함수 내에서 재귀적으로 호출하는 부분은 어떻게?)
    (해당 노드를 방문할지 말지를 어떻게?)
  • 방문하고 나서 다시 이전 단계로 돌아와 다른 경로를 찾게 하려면 코드를 어떻게?⭐⭐⭐(이게 관건임)

 

위와 같은 이유들은 내가 dfs를 스택으로 해결한 문제만 풀어봤기 때문이고, dfs함수를 써서 재귀적으로 함수를 호출하며 방문 여부를 체크하는 배열을 사용한 문제를 접해보지 못했기 때문에, 그 로직을 정확히 이해하지 못한 것 같다.

 

이 문제를 계기로 dfs의 전체적인 흐름을 바로잡고자 한다.

 

 

💡 문제
두 개의 단어 begin, target과 단어의 집합 words가 있습니다. 아래와 같은 규칙을 이용하여 begin에서 target으로 변환하는 가장 짧은 변환 과정을 찾으려고 합니다.

1. 한 번에 한 개의 알파벳만 바꿀 수 있습니다.
2. words에 있는 단어로만 변환할 수 있습니다.

예를 들어 begin이 "hit", target가 "cog", words가 ["hot","dot","dog","lot","log","cog"]라면 "hit" -> "hot" -> "dot" -> "dog" -> "cog"와 같이 4단계를 거쳐 변환할 수 있습니다.
두 개의 단어 begin, target과 단어의 집합 words가 매개변수로 주어질 때, 최소 몇 단계의 과정을 거쳐 begin을 target으로 변환할 수 있는지 return 하도록 solution 함수를 작성해주세요.


변환할 수 없는 경우에는 0를 return 합니다.

 

다음과 같은 것을 고려해 볼 수 있다.

  1. 현재 begin과 비교했을 때 words 중에 딱 한 문자만 다른 단어가 있는지?
    => 이를 위한 함수가 필요하다.
  2. 만약 한문자만 다른 단어가 있으면 그 단어에서 다음 변경될 단어를 찾을 때 역시 한 문자만 다른지를 확인해야하고, 그 문자를 방문한 이력이 있는지를 확인해야한다.
    =>해당 단어를 방문한 이력이 있는지를 기록하는 boolean타입의 배열이 필요하다.
  3. 조건에 맞는 단어를 방문하고 나서 다음 방문 여부를 확인하고 이어서 또 방문하기 위해선 dfs함수안에 dfs함수를 재귀적으로 호출하여 해당 루트에 대해 끝까지 방문하도록 해야한다.
  4. 재귀함수이더라도 끝은 있어야 한다. dfs함수 내에 if문으로, 현재 방문한 단어가 target과 같을 시에는 answer를 갱신하고 dfs를 빠져나오도록 한다.
  5. 항상 dfs를 재귀로 호출한 다음에는, 이전 단계로 돌아왔을 때 다른 루트를 고려하기 위해서 해당 노드의 방문여부를 다시 false로 바꾸어주는 단계가 필요하다.(=이것은 즉, 이 노드를 방문하지 않았다면?과 같은 의미이다=>이어지는 다음 루프에서는 해당 노드를 방문하지 않은채로 고려할 수 있기 때문이다⭐⭐⭐)
✏ [5에 대한 부가 설명]

1,2,3 노드가 있다고 하자.
현재 1번 노드를 방문한 상태이고, 앞으로 방문기록이 없는 2와 3에 대해 방문을 할것이다.
for문을 통해 번호순으로 방문할 것이기 때문에, 2를 먼저 방문한다.
2를 방문했으므로 visited배열에 2를 방문했다는 표시(true)를 한다.
그리고 다시 2를 시작으로 남은 노드를 순차적으로 방문한다. 
현재 예시에서는 방문기록이 없는것 중 남은게 3뿐이므로 3을 방문한다.
더이상 방문할 곳이 없기 때문에 다시 이전 단계로 돌아가 본다.
2를 다시 방문하지 않은 상태로 둔다.
하지만 이전 for루프에서는 순차적으로 2를 방문한 것이었고, 이번 for루프에서는 3을 방문할 차례이다.
따라서 3을 방문하고, visited배열에 3을 방문했다는 표시를 한다.
그리고 다시 3을 시작으로 남은 노드를 순차적으로 방문한다.
현재 방문기록이 없는게 2뿐이므로 2를 방문한다.

이렇게 되면 1->2->3으로 한번 방문했고, 1->3->2로도 한번 방문했다.
이런식으로 첫 시작이 잡히면 그 뒤로 방문처리, 재귀적으로 호출, 다시 미방문처리, 다음루프 로 이어지면서 모든 경로를 탐색해 볼 수 있게 되는 것이다.

 

위와 같은 사항들을 고려하면서 코드를 작성하면 다음과 같은 단계로 작성할 수 있다.

 

✅ 코드 작성 단계

1. 간단한 예외 처리해주기.
   - 문제에서 주어진 예시를 보면 words배열에 target이 없는 경우 변환될 수 없는 것이기 때문에, 0을 answer로 리턴하도록 되어 있다.

   - 배열에 해당 요소가 있는지를 비교하면되는데, 이는 ArrayList의 contains() 메서드를 활용하면 매우 쉽다.

list = new ArrayList<>(Arrays.asList(words));//리스트에 Arrays.asList메서드를 통해
//words배열을 리스트로 변경하고 있다
        
if(!list.contains(target)) return 0;//만약 list에서 target이 없으면 0을 리턴

   -list의 경우 List<String> list 로 static 전역으로 선언하여 사용하였다.

 

2. solution함수 내에서 첫 시작 방문을 for문으로 돌기

   - 어떤 노드가 시작이 될지 모르기 때문에, 시작을 정해주기 위한 모든 경우의 수를 고려해야한다.

   - 따라서 for문을 words의 크기만큼 돌면서 현재 단어가 begin과 한글자만 차이나는지 확인해야 한다.

   - 위를 확인하기 위한 boolean타입의 함수를 생성해주어야 한다.

for(int i=0; i<words.length; i++){//모든 워드에 대해 시작을 탐색
//???

 

2-1. canChange함수 정의하기

   - 현재 begin 단어가 words의 해당 단어와 한글자만 차이나면 true를, 아니면 false를 반환하도록 하는 함수이다.

   - boolean을 반환한다.

   - for문을 돌면서 begin과 target의 한글자 한글자가 같은지 비교하고, 다르면 count한다.

   - count가 1이면 true를 아니면 false를 반환한다.

public boolean canChange(String target, String begin){//begin과 target을 인자로 받는다
        int count=0;//count는 0으로 초기화한다
        for(int i=0; i<begin.length(); i++){//begin글자수만큼 비교
            if(begin.charAt(i)!=target.charAt(i)) count++;//다르면 카운트
        }
        
        return count==1;//카운트가 1이면 한글자만 차이나는 것이므로 후보가 된다
    }

 

2-2. 한글자만 차이나는 모든 단어에 대해 해당 단어를 시작으로 하는 루트를 모두 탐색하기

   - 이제 2에서 설명한 for문에서 words 중에 현재 단어가 canChange를 만족하면, visited[] 배열을 새로 할당한다.

   - 해당 단어의 인덱스를 통해 visited에 방문했음을 표시한다.

   - DFS함수를 호출해서 현재 단어를 시작으로 아직 방문하지 않은 나머지 단어에 대해 위 과정을 거치도록 한다.

   - DFS함수를 구현해야 한다.

for(int i=0; i<words.length; i++){
            if(canChange(words[i], begin)){
                visited = new boolean[words.length];
                visited[i]=true;
                dfs(????);
            }
}

 

3. DFS함수 구현하기

   - dfs함수에서는 사실상 solution함수에 있는 것을 반복하는 것이라고 보면 되는데, 재귀적으로 호출할 것이기 때문에, 마지막에는 멈출 수 있는 조건이 항상 있어야 한다.

   - dfs함수에 현재 루트에서 몇번 노드를 탐색했는지(=answer와 직결됨), 현재 방문한 단어는 무엇인지, 최종 찾고자 하는 (=도달하고자 하는)단어는 무엇인지에 대해 인자로써 알려주어야 한다.

   - 따라서 몇번 노드를 탐색했는지는 int타입의 index, 현재 방문한 단어는 String의 begin, 찾고자 하는 단어는 String의 target이다. 

   - dfs에서는 일단 현재 방문한 노드 begin이 target과 동일한지 비교하고, 동일하면 answer를 갱신한다.

   - 이때 answer는 몇번 노드를 탐색했는지를 나타내는 index와 현재 answer중에 더 작은 것으로 선택하여 갱신하도록 한다. 그리고 return을 하여 dfs가 끝나도록 한다

   - 만약 같지 않다면, for문을 통해 다음 방문할 노드를 찾기 위해 words를 또한번 탐색하는데, visited와 canChange를 통해, 방문한 기록이 없고, 한글자 차이인 경우에는 그 단어를 방문한 상태로 바꿔준다.

   - 그리고 dfs를 호출해서 현재 막 방문한 상태로 바꿔준 단어를 기점으로 위 단계를 반복하도록 한다.

   - 만약 다 돌아서 이전 단계로 돌아왔을 때에는 다시 방문하지 않은 상태로 바꾸어주고, 다음 루프에서 다른 단어를 먼저 방문하더라도 이 노드가 방문될 수 있도록 (즉 여러 루트를 고려하도록 하기 위해, 위에 5번 설명 참고)한다.

   - 위 단계가 매우 중요하다. 이것이 없으면 다시 돌아가서 다른 루트를 찾는 과정을 거칠 수가 없기 때문이다.

public void dfs(int index, String[] words, String begin, String target){
        if(begin.equals(target)){
            answer=Math.min(index, answer);
            return;//이에 해당하면 아래를 실행하지 않기 위해
        }
        
        for(int i=0; i<words.length; i++){
            if(!visited[i]&&canChange(words[i],begin)){
                visited[i]=true;
                dfs(index+1, words, words[i], target);//재귀호출하여 현재 루트를 이어가도록한다
                visited[i]=false;//다른 루트를 탐색하도록 하기 위함이다.매우 중요!!!
            }
        }
    }

 

4. DFS함수 호출하고, 최종 answer 리턴하기.

   - 앞에서 정의한 dfs에 따라서 인자를 다음과 같이 설정해준다.

   - 첫 방문 노드의 경우 한번 방문했으므로 index는 1이된다. 그리고 방문했을 경우 begin은 words[i]가 되는 것이고, target은 최종 target이다.

for(int i=0; i<words.length; i++){
            if(canChange(words[i], begin)){
                visited = new boolean[words.length];
                visited[i]=true;
                dfs(1,words,words[i],target);
            }
        }
        
        return answer;

   - 이렇게 다 하고 나면 answer에는 최종 답이 들어간다.

 

5. 세부사항

   - min 연산을 위해 answer는 integer의 max_value로 초기화 하였다.

   - answer, visited배열, list는 static 전역으로 선언하였다.

   - answer는 그래서 모든 연산에서 실시간으로 제일 작은 값을 얻도록 하였다.

 

 

최종코드이다.

import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;

class Solution {
    static List<String> list;
    static int answer;
    static boolean[] visited;
    
    public int solution(String begin, String target, String[] words) {
        answer = Integer.MAX_VALUE;
        list = new ArrayList<>(Arrays.asList(words));
        
        if(!list.contains(target)) return 0;
        
        for(int i=0; i<words.length; i++){
            if(canChange(words[i], begin)){
                visited = new boolean[words.length];
                visited[i]=true;
                dfs(1,words,words[i],target);
            }
        }
        
        return answer;
    }
    public void dfs(int index, String[] words, String begin, String target){
        if(begin.equals(target)){
            answer=Math.min(index, answer);
            return;
        }
        
        for(int i=0; i<words.length; i++){
            if(!visited[i]&&canChange(words[i],begin)){
                visited[i]=true;
                dfs(index+1, words, words[i], target);
                visited[i]=false;
            }
        }
    }
    
    public boolean canChange(String target, String begin){
        int count=0;
        for(int i=0; i<begin.length(); i++){
            if(begin.charAt(i)!=target.charAt(i)) count++;
        }
        
        return count==1;
    }
}
반응형

+ Recent posts