자바 코딩테스트 강좌, DFS와 BFS 프로그램

알고리즘 문제 해결 능력을 기르는 것은 소프트웨어 개발자에게 매우 중요합니다. 특히 취업 준비를 하고 있는 경우, 다양한 알고리즘과 자료구조를 이해하고 활용하는 능력이 필요합니다. 본 강좌에서는 깊이 우선 탐색(DFS)과 너비 우선 탐색(BFS)의 개념을 이해하고, 실제 알고리즘 문제를 풀어보겠습니다. 이 과정에서는 문제 정의, 접근 방식, 자바 코드 구현 및 시간 복잡도를 함께 살펴보겠습니다.

문제 정의

주어진 문제는 다음과 같습니다:

문제: 0에서 N까지의 정점으로 구성된 무방향 그래프가 주어질 때, 특정한 시작 정점에서 시작하여 모든 정점에 도달할 수 있는지를 확인하는 프로그램을 작성하라. 두 가지 방법인 DFS와 BFS를 사용하여 이 문제를 해결해보자.

문제 접근 방식

그래프 탐색 알고리즘은 깊이 우선 탐색과 너비 우선 탐색의 두 가지 주요 방법이 있습니다. 이 두 가지 방식은 각각의 특징과 활용도가 다릅니다. 따라서, 문제를 풀기 위해 두 가지 방법의 구현을 준비하겠습니다.

1. DFS (Depth First Search)

DFS는 깊이를 우선으로 탐색하여 그래프의 최대 깊이까지 내려간 후, 더 이상 방문할 수 있는 정점이 없을 때 돌아오는 방식입니다. 이 방식은 스택을 활용하여 구현하거나 재귀 함수를 통해 쉽게 구현할 수 있습니다.

DFS 구현

다음은 DFS를 사용하여 그래프를 탐색하는 자바 코드입니다:

import java.util.*;

public class Graph {
    private int vertices;
    private LinkedList[] adjacencyList;

    public Graph(int vertices) {
        this.vertices = vertices;
        adjacencyList = new LinkedList[vertices];
        for (int i = 0; i < vertices; i++) {
            adjacencyList[i] = new LinkedList<>();
        }
    }

    public void addEdge(int source, int destination) {
        adjacencyList[source].add(destination);
        adjacencyList[destination].add(source); // 무방향 그래프
    }

    public void dfs(int start) {
        boolean[] visited = new boolean[vertices];
        dfsUtil(start, visited);
    }

    private void dfsUtil(int vertex, boolean[] visited) {
        visited[vertex] = true;
        System.out.print(vertex + " ");

        for (Integer neighbor : adjacencyList[vertex]) {
            if (!visited[neighbor]) {
                dfsUtil(neighbor, visited);
            }
        }
    }
}

2. BFS (Breadth First Search)

BFS는 너비를 우선으로 탐색하여 현재 정점의 이웃을 먼저 탐색한 후, 그 이웃의 이웃을 탐색하는 방식입니다. 주로 큐를 사용하여 구현합니다.

BFS 구현

다음은 BFS를 사용하여 그래프를 탐색하는 자바 코드입니다:

import java.util.*;

public class Graph {
    private int vertices;
    private LinkedList<Integer>[] adjacencyList;

    public Graph(int vertices) {
        this.vertices = vertices;
        adjacencyList = new LinkedList[vertices];
        for (int i = 0; i < vertices; i++) {
            adjacencyList[i] = new LinkedList<>();
        }
    }

    public void addEdge(int source, int destination) {
        adjacencyList[source].add(destination);
        adjacencyList[destination].add(source); // 무방향 그래프
    }

    public void bfs(int start) {
        boolean[] visited = new boolean[vertices];
        Queue<Integer> queue = new LinkedList<>();

        visited[start] = true;
        queue.add(start);

        while (!queue.isEmpty()) {
            int vertex = queue.poll();
            System.out.print(vertex + " ");

            for (Integer neighbor : adjacencyList[vertex]) {
                if (!visited[neighbor]) {
                    visited[neighbor] = true;
                    queue.add(neighbor);
                }
            }
        }
    }
}

코드 실행 예제

이제 DFS와 BFS를 이용하여 그래프를 탐색하는 예제를 실행해보겠습니다. 아래 코드는 그래프를 생성하고 두 가지 방식으로 탐색을 수행합니다:

public class GraphTraversalExample {
    public static void main(String[] args) {
        Graph graph = new Graph(5);
        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(1, 3);
        graph.addEdge(1, 4);

        System.out.println("DFS Traversal:");
        graph.dfs(0);

        System.out.println("\nBFS Traversal:");
        graph.bfs(0);
    }
}

시간 복잡도 분석

DFS와 BFS의 시간 복잡도는 다음과 같습니다:

  • DFS: O(V + E) 여기서 V는 정점(Vertex)의 수, E는 간선(Edge)의 수입니다.
  • BFS: O(V + E) 그래프의 모든 정점과 간선을 한 번씩 방문합니다.

결론

DFS와 BFS는 그래프 탐색을 위한 기초적인 알고리즘입니다. 이 두 가지 알고리즘을 이해하고 활용함으로써, 다양한 문제를 해결할 수 있습니다. 본 강좌를 통해 그래프의 기본 개념과 구현 방법을 익혔다면, 더 복잡한 문제에 도전할 준비가 되었다고 할 수 있습니다. 이후에는 실제 코딩 테스트에서 자주 등장하는 다양한 문제를 풀어보며 이론을 실습으로 연결해보는 것이 좋습니다.

추후에는 더 다양한 자료구조와 알고리즘을 학습하여 여러분의 프로그래밍 실력을 한 단계 끌어올리길 바랍니다.

자바 코딩테스트 강좌, DDR을 해보자

최근 취업 시장에서 알고리즘 문제는 중요한 요소 중 하나로 자리잡고 있습니다. 특히 소프트웨어 엔지니어링 직무를 위해서는 알고리즘과 자료구조에 대한 이해가 필수적입니다. 이번 강좌에서는 “DDR(Dance Dance Revolution)”이라는 주제로 알고리즘 문제를 다루어 보겠습니다. DDR은 춤을 추는 게임으로, 주어진 시간 안에 제시된 발판을 정확히 밟아야 합니다. 이를 프로그래밍 문제로 변환하여 알고리즘을 공부해 보겠습니다.

문제 설명

당신은 DDR의 챌린지에 참가했습니다. 화면에 나타나는 발판의 순서가 주어지면, 이를 정확히 따라 밟아야 합니다. 발판은 1부터 9까지의 정수로 표시되며, 1은 왼쪽 하단, 2는 가운데 하단, 3은 오른쪽 하단, 4는 왼쪽 중간, 5는 가운데, 6은 오른쪽 중간, 7은 왼쪽 상단, 8은 가운데 상단, 9는 오른쪽 상단을 의미합니다.

당신은 현재 상태에서 어디에 발을 놓고 있느냐에 따라 발판을 밟는 시간과 거리가 달라질 수 있습니다. 그러므로 최적의 경로를 찾아야 합니다.

입력 형식

첫 번째 줄에는 발판의 개수 N (1 ≤ N ≤ 1000)이 주어집니다. 두 번째 줄에는 N개의 발판이 주어지며, 각 발판은 1부터 9까지의 정수를 가집니다.

출력 형식

모든 발판을 밟기 위해 필요한 최소 시간을 출력합니다.

문제 예시

    
    입력:
    5
    1 3 5 7 9

    출력:
    8
    
    

문제 풀이 과정

1단계: 발판의 좌표 정의

먼저 발판의 위치를 정의해야 합니다. 다음과 같이 2D 배열을 사용하여 발판 간의 거리를 계산할 수 있습니다.

    
    // 발판 위치를 (행, 열)로 정의
    int[][] position = {
        {3, 1}, // 1
        {2, 1}, // 2
        {1, 1}, // 3
        {3, 2}, // 4
        {2, 2}, // 5
        {1, 2}, // 6
        {3, 3}, // 7
        {2, 3}, // 8
        {1, 3}  // 9
    };
    
    

2단계: 거리 계산 함수 구현

거리 계산에는 맨해튼 거리를 사용할 것입니다. 맨해튼 거리란 두 점 (x1, y1)과 (x2, y2) 간의 거리는 |x1 – x2| + |y1 – y2|로 정의됩니다. 이를 Java 함수로 구현해보겠습니다.

    
    public int calculateDistance(int a, int b) {
        int x1 = position[a - 1][0];
        int y1 = position[a - 1][1];
        int x2 = position[b - 1][0];
        int y2 = position[b - 1][1];
        
        return Math.abs(x1 - x2) + Math.abs(y1 - y2);
    }
    
    

3단계: 메인 로직 구현

이제 발판을 한 번씩 밟기 위한 최적의 경로를 계산하는 메인 로직을 구현합니다. 현재 위치와 밟아야 할 발판 위치 간의 거리를 계산하고, 최소 거리를 누적합니다.

    
    public int minTime(int[] steps) {
        int totalTime = 0;
        int currentPosition = 5; // 처음 발판은 5의 위치(중앙)

        for (int step : steps) {
            totalTime += calculateDistance(currentPosition, step);
            currentPosition = step; // 현재 위치 업데이트
        }
        return totalTime;
    }
    
    

4단계: 전체 코드

최종적으로 발판 정보를 입력받아 최소 시간을 계산하는 전체 코드를 작성하겠습니다.

    
    import java.util.Scanner;

    public class Main {
        static int[][] position = {
            {3, 1}, // 1
            {2, 1}, // 2
            {1, 1}, // 3
            {3, 2}, // 4
            {2, 2}, // 5
            {1, 2}, // 6
            {3, 3}, // 7
            {2, 3}, // 8
            {1, 3}  // 9
        };

        public static void main(String[] args) {
            Scanner scanner = new Scanner(System.in);
            int N = scanner.nextInt();
            int[] steps = new int[N];

            for (int i = 0; i < N; i++) {
                steps[i] = scanner.nextInt();
            }

            System.out.println(minTime(steps));
        }

        public static int minTime(int[] steps) {
            int totalTime = 0;
            int currentPosition = 5; // 처음 발판은 5의 위치(중앙)

            for (int step : steps) {
                totalTime += calculateDistance(currentPosition, step);
                currentPosition = step; // 현재 위치 업데이트
            }
            return totalTime;
        }

        public static int calculateDistance(int a, int b) {
            int x1 = position[a - 1][0];
            int y1 = position[a - 1][1];
            int x2 = position[b - 1][0];
            int y2 = position[b - 1][1];
            
            return Math.abs(x1 - x2) + Math.abs(y1 - y2);
        }
    }
    
    

결론

이번 강좌에서는 간단한 알고리즘 문제를 통해 발판의 위치와 거리 계산을 학습했습니다. 이 문제는 실제 코딩 테스트에서 자주 등장할 수 있는 유형의 문제입니다. 다양한 알고리즘 문제를 풀어보면 자연스럽게 자료구조와 알고리즘에 대한 이해도를 높일 수 있습니다. 계속해서 많은 문제를 풀며 경험치를 쌓아 가길 바랍니다!

자바 코딩테스트 강좌, Ax + By = C

본 강좌에서는 A, B, C가 주어졌을 때 Ax + By = C의 정수해를 구하는 문제를 다룹니다.

문제 설명

주어진 정수 A, B, C에 대해 Ax + By = C를 만족하는 모든 정수 쌍 (x, y)를 구하는 프로그램을 작성하시오.
이때 A, B, C는 다음과 같은 조건을 만족합니다:

  • 1 ≤ |A|, |B|, |C| ≤ 10^3
  • x와 y는 정수입니다.

예를 들어, A = 2, B = 3, C = 6의 경우, 가능한 해는 (0, 2), (3, 0), (1, 1) 등입니다.
그러나, 해가 무한히 존재하는 경우도 있으며, 해가 존재하지 않을 때도 있습니다.

문제 풀이 과정

1. 문제 분석

문제를 해결하기 위해 먼저 문제를 분석해보면, 주어진 식 Ax + By = C는 x와 y의 조합을 찾는 것입니다.
A와 B의 부호에 따라 가능한 해의 개수와 범위가 달라집니다.
예를 들어, A와 B 모두 0인 경우는 예외로, 그 외의 경우는 두 변수 중 하나가 다른 변수를 기준으로 어떻게 변할 수 있는지를 파악하는 것이 중요합니다.

2. 해의 개수 계산

문제의 해를 찾기 위해, 먼저 C가 A와 B의 최대공약수(GCD)가 되는지를 확인해야 합니다.
GCD가 C의 약수가 아니면 해는 존재하지 않으니, GCD를 이용해 간략하게 아래와 같이 설명할 수 있습니다.

                gcd(a, b) = ax + by
            

여기서 x, y는 정수입니다. 이 식을 변형하여 정수해를 찾을 수 있음을 알 수 있습니다.

3. 자바 구현

이제 위의 논리를 바탕으로 자바를 사용하여 프로그램을 구현해보겠습니다.
각 단계에 대해 설명하고, 코드도 포함하겠습니다.

3.1. 최대공약수 (GCD) 계산 함수


public static int gcd(int a, int b) {
    while (b != 0) {
        int temp = b;
        b = a % b;
        a = temp;
    }
    return a;
}
            

위 코드는 두 수 a, b의 최대공약수를 계산하는 Euclid 알고리즘을 이용한 함수입니다.

3.2. 해 찾기 및 출력 함수


public static void findSolutions(int A, int B, int C) {
    // GCD 계산
    int gcdAB = gcd(Math.abs(A), Math.abs(B));
    
    // C가 GCD의 배수인지 확인
    if (C % gcdAB != 0) {
        System.out.println("해가 없습니다.");
        return;
    }

    // x,y 계산 로직
    int x = C / A;
    int y = 0; // 초기 y 값
    // 해 출력
    System.out.println("가능한 해: x = " + x + ", y = " + y);
    // 또는 반복문을 사용하여 다른 해 출력 가능
}
            

4. 전체 코드


public class LinearEquationSolver {
    public static int gcd(int a, int b) {
        while (b != 0) {
            int temp = b;
            b = a % b;
            a = temp;
        }
        return a;
    }

    public static void findSolutions(int A, int B, int C) {
        int gcdAB = gcd(Math.abs(A), Math.abs(B));
        if (C % gcdAB != 0) {
            System.out.println("해가 없습니다.");
            return;
        }

        // Fixed-point iteration for solution finding
        for (int x = -1000; x <= 1000; x++) {
            if ((C - A * x) % B == 0) {
                int y = (C - A * x) / B;
                System.out.println("가능한 해: x = " + x + ", y = " + y);
            }
        }
    }

    public static void main(String[] args) {
        findSolutions(2, 3, 6);
        // 추가 테스트 케이스
        findSolutions(1, -1, 0);
    }
}
            

위 코드를 사용하여 다양한 조합의 A, B, C에 대해 해를 찾을 수 있습니다.
해를 출력하는 부분은 직접 반복문을 통해 찾는 방식이며,
보통 수가 적을 경우 더 빠른 결과를 보여줍니다.

마무리 및 요약

이번 강좌에서는 Ax + By = C의 정수해를 구하는 방법에 대해 살펴보았습니다.
GCD를 사용하여 해가 존재하는지를 판별하고, 반복문을 사용하여 가능한 해를 출력하는 기본적인 방식에 대해 설명했습니다.
이러한 기법은 다양한 문제에 응용될 수 있으며, 알고리즘의 심화 학습에 큰 도움이 될 것입니다.

이 글이 자바 코딩테스트 준비에 많은 도움이 되길 바랍니다.

자바 코딩테스트 강좌, ATM 인출 시간 계산하기

문제 설명

당신은 ATM 기계에 서 있습니다. ATM에서 돈을 인출하기 위해서는 몇 가지 과정을 거쳐야 합니다.
각 사용자마다 인출을 완료하기까지 걸리는 시간이 다르며, 이 시간은 사용자가 ATM에서 기능을 수행하는 데 걸리는 시간입니다.
사용자는 `n`명의 사용자가 있으며, 각 사용자의 인출 시간이 주어질 때, 모든 사용자가 인출을 완료하는 데 걸리는 총 시간을 계산하는 함수를 작성해야 합니다.

문제 입력

  • 첫 번째 줄: 사용자 수를 나타내는 정수 `n` (1 ≤ n ≤ 1000)
  • 두 번째 줄: 각 사용자의 인출 시간을 나타내는 `n`개의 정수 (1 ≤ 시간 ≤ 10000)

문제 출력

모든 사용자들이 인출을 완료하기까지 걸리는 총 시간을 출력한다.

예제 입력

    5
    3 1 4 3 2
    

예제 출력

    32
    

문제 분석

모든 사용자가 ATM에서 인출을 마치는데 걸리는 총 시간은, 각 사용자가 인출을 완료할 때까지 대기해야하는 다른 사용자들의 시간을 포함해야 합니다.
즉, 특정 사용자가 인출을 마치는 데 걸리는 시간은 그 사람의 인출 시간과 그 이전까지 인출을 마친 모든 사람의 시간이 더해진 값이 됩니다.
이를 통해 모든 사용자의 인출 완료 시간을 계산할 수 있습니다.

문제 해결 방법

  1. 사용자의 인출 시간을 입력받아 배열에 저장합니다.
  2. 입력받은 인출 시간을 오름차순으로 정렬합니다.
  3. 총 인출 시간을 계산하기 위해, 각 사용자의 인출 완료 시간을 누적합 형태로 계산합니다.
  4. 모든 사용자의 인출 완료 시간을 합산하여 최종 시간을 구합니다.

자바 코드 구현

        import java.util.Arrays;
        import java.util.Scanner;

        public class ATMWithdrawal {
            public static void main(String[] args) {
                Scanner sc = new Scanner(System.in);
                
                // 사용자 수 입력
                int n = sc.nextInt();
                int[] times = new int[n];
                
                // 인출 시간 입력
                for (int i = 0; i < n; i++) {
                    times[i] = sc.nextInt();
                }
                
                // 시간 배열 정렬
                Arrays.sort(times);
                
                // 누적 시간 변수
                int totalTime = 0;
                int accumulatedTime = 0;
                
                // 각 사용자에 대해 누적 시간 계산
                for (int time : times) {
                    accumulatedTime += time; // 현재 사용자의 인출 시간 추가
                    totalTime += accumulatedTime; // 총 시간에 누적 시간 추가
                }
                
                // 결과 출력
                System.out.println(totalTime);
                
                sc.close();
            }
        }
    

코드 설명

위의 자바 코드는 ATM 인출 시간 계산 문제를 해결하기 위해 다음과 같은 과정으로 구성되어 있습니다:

  1. 먼저 입력을 받기 위해 Scanner 객체를 생성하고 사용자의 수 n을 입력받습니다.
  2. 그 다음, 각 사용자의 인출 시간을 입력받아 배열 times에 저장합니다.
  3. Arrays.sort(times)를 통해 인출 시간을 오름차순으로 정렬합니다. 이는 각 사용자의 대기 시간을 최소화하기 위해 중요합니다.
  4. 총 인출 시간을 계산하기 위해 두 개의 변수를 사용합니다: totalTime는 모든 사용자가 인출을 완료하는 데 걸린 총 시간을 저장하고, accumulatedTime는 현재까지의 누적 시간을 저장합니다.
  5. 각 사용자의 인출 시간을 순회하면서, 해당 사용자의 인출 시간이 누적된 시간에 추가되고, 이를 총 시간에 더합니다.
  6. 마지막으로 총 인출 시간을 출력합니다.

시간 복잡도 분석

이 알고리즘의 시간 복잡도는 주로 인출 시간 배열을 정렬하는 데 의존합니다.
배열을 정렬하는 가장 일반적인 정렬 알고리즘인 퀵 정렬의 평균 시간 복잡도가 O(n log n)이므로, 전체 알고리즘의 시간 복잡도는 O(n log n)입니다.
이후 인출 시간을 누적하는 작업은 한 번의 반복으로 해결되므로 O(n)입니다.
따라서 전체 시간 복잡도는 O(n log n)입니다.

종합 정리

이 강좌에서는 ATM 인출 시간 계산 문제를 통해 기본적인 배열 정렬과 누적 합산을 연습할 수 있었습니다.
대부분의 알고리즘 문제는 기본적인 데이터 구조와 알고리즘을 통해 해결될 수 있습니다.
인출 시간이 적절히 최적화되면 대기 시간이 줄어들고, 결과적으로 모든 사용자들이 ATM을 사용하는 데 있어 더 효율적일 것입니다.
이와 같은 문제를 통해 다양한 알고리즘과 자료구조를 활용하여 취업 준비에 도움이 되는 연습을 할 수 있습니다.

자바 코딩테스트 강좌, 2 N 타일 채우기

문제 설명

N x 2의 공간에 2 x 1 크기의 타일을 채우는 방법의 수를 구하는 문제입니다.
주어진 공간의 크기가 N일 때, 여러 가지 방법으로 타일을 이어 붙여서 공간을 가득 채우는
경우의 수를 계산하는 것이 목표입니다.

이 문제는 동적 프로그래밍(Dynamic Programming) 기법을 활용해 해결할 수 있습니다.
각 타일의 배치 방식에 따라 이전의 상태를 이용해 현재 상태를 계산하는 방식으로 접근하게 됩니다.

문제 예제

예를 들어, N이 3일 경우 다음과 같은 배치 방법이 존재합니다:

  • 타일을 세로로 3개 배치
  • 타일을 가로로 2개, 세로로 1개 배치
  • 타일을 세로로 1개, 가로로 2개, 세로로 1개 배치
  • 타일을 가로로 1개, 세로로 2개, 가로로 1개 배치

위와 같은 배치를 통해 출력 결과는 5가 됩니다.

문제 풀이 과정

1단계: 상태 정의

문제를 해결하기 위해 우선적으로 상태를 정의해야 합니다.
f(N)이라는 함수가 N x 2 공간을 채우는 경우의 수라고 할 때,
다음과 같은 점화식을 세울 수 있습니다:

f(N) = f(N-1) + f(N-2)

여기서 f(N-1)은 N-1 x 2 공간에 타일을 세로로 놓은 경우이며,
f(N-2)는 N-2 x 2 공간에 타일을 두 개 가로로 놓은 경우를 의미합니다.

2단계: 초기 조건 정의

초기 조건을 정해야 합니다. N이 1일 때는 타일을 세로로 하나 배치할 수 있으므로 f(1) = 1입니다.
N이 2일 때는 타일을 두 개 세로 또는 가로로 놓을 수 있으므로 f(2) = 2입니다.

3단계: 점화식을 이용한 해결 방법

점화식을 이용해 N까지 차례로 계산해 나갈 수 있습니다.
배열을 사용할 수도 있지만, 두 개의 변수만으로도 충분히 가능합니다.
이전 두 값만 알면 다음 값을 계산할 수 있기 때문입니다.

자바 코드 예시

            
                public class Tiling {
                    public static void main(String[] args) {
                        int N = 5; // 예를 들어 N을 5로 설정
                        System.out.println("2 x " + N + " 공간을 채우는 경우의 수: " + countWays(N));
                    }

                    static int countWays(int N) {
                        if (N == 1) return 1; // 기본 조건 1
                        if (N == 2) return 2; // 기본 조건 2

                        int[] dp = new int[N + 1];
                        dp[1] = 1;
                        dp[2] = 2;

                        for (int i = 3; i <= N; i++) {
                            dp[i] = dp[i - 1] + dp[i - 2]; // 점화식
                        }

                        return dp[N];
                    }
                }
            
        

시간 복잡도와 공간 복잡도 분석

위 예제에서 시간 복잡도는 O(N)으로, N에 비례하여 증가합니다. 이는 모든 상태를 한 번씩 계산하기 때문입니다.
공간 복잡도는 O(N)이며, dp 배열을 사용하여 저장하기 위해 필요합니다.
그러나 변수를 두 개만 사용해도 가능하므로 O(1)로 최적화할 수 있습니다.

결론

2*N 타일 채우기 문제는 동적 프로그래밍의 대표적인 예제로,
메모리 사용을 최적화하면서 효율적인 알고리즘을 구현하는데 큰 도움이 됩니다.
이러한 문제를 통해 기본적인 알고리즘과 데이터 구조에 대한 이해를 심화할 수 있으며,
다양한 변형 문제로 연습할 수 있습니다. 시작해보세요!