순열

순열은 "서로 다른 n개의 원소에서 r개를 중복 없이, 순서에 상관 있게 선택하는 경우의 수"다.

예를 들어 {1, 2, 3}라는 원소들이 있다고 한다면, 이 중 2개를 중복 없이 순서에 상관 있게 선택하는 경우의 수는 다음과 같다.

 

(1, 2) 
(1, 3) 
(2, 1)
(2, 3) 
(3, 1) 
(3, 2) 

 

각 경우의 수에서 각각의 원소들은 1개만 포함되어 있다.

이것이 "중복 없이" 뽑는다는 의미다.

 

또한 (a, b)와 (b, c)이 다른 경우의 수로 취급되고 있다.

이것이 "순서에 상관있게"의 의미다.

 

조합

조합은 "서로 다른 n개의 원소에서 r개를 중복 없이, 순서에 상관 없게 선택하는 경우의 수"다.

예를 들어 {1, 2, 3}라는 원소들이 있다고 한다면, 이 중 2개를 중복 없이 순서에 상관 없게 선택하는 경우의 수는 다음과 같다.

 

(1, 2)
(1, 3)
(2, 3)
(3, 2)

 

앞서 본 순열과 같이 각 경우의 수에서 각각의 원소들은 1개만 포함되어 있다.

이것이 "중복 없이" 뽑는다는 의미다.

 

그리고 순열과 다르게 (a, b)와 (b, c)이 같은 경우의 수로 취급되고 있다.

순열에서는 (1, 2)와 (2, 1)이 서로 다른 경우의 수로 취급되지만, 조합에서는 같게 취급된다.

즉, a와 b를 선택할 때 a가 앞에 오든, b가 앞에 오든 같은 경우의 수로 취급하고 있는 것이다.

이것이 "순서에 상관 없게"의 의미다.

 

순열과 조합 코드(List)

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.*;

class Solution {

    static LinkedList<Integer> list = new LinkedList<>();
    static int N;
    static int R;
    static int[] arr;

    public static void comb(int depth, int start) {
        if (list.size() == R) {
            for (Integer i : list) {
                System.out.print(i + " ");
            }
            System.out.println();
            return;
        }

        for (int i = start; i < arr.length; i++) {
            if (!list.contains(arr[i])) {
                list.addLast(arr[i]);
                comb(depth + 1, i + 1);
                list.removeLast();
            }
        }
    }


    public static void perm(int depth){
        if (list.size() == R) {
            for (Integer i : list) {
                System.out.print(i + " ");
            }
            System.out.println();
            return;
        }

        for (int i = 0; i < arr.length; i++) {
            if (!list.contains(arr[i])) {
                list.addLast(arr[i]);
                perm(depth + 1);
                list.removeLast();
            }
        }

    }

    public static void main(String[] args) throws Exception{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        N = Integer.parseInt(br.readLine());
        R = Integer.parseInt(br.readLine());
        arr = new int[N];

        for (int i = 0; i < N; i++) {
            arr[i] = i + 1;
        }

        System.out.println("Permutation");
        perm(0);
        list = new LinkedList<>();
        System.out.println("\nCombination");
        comb(0, 0);
    }
}

 

순열과 조합 구현(배열)

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.*;

class Solution {

    static LinkedList<Integer> list = new LinkedList<>();
    static int N;
    static int R;
    static int[] arr;
    static int[] num;
    static boolean[] isUsed;

    public static void comb(int depth, int start) {
        if (depth == R) {
            for (int i = 0; i < R; i++) {
                System.out.print(num[i] + " ");
            }
            System.out.println();
            return;
        }

        for (int i = start; i <= arr.length; i++) {
            if (!isUsed[i]) {
                num[depth] = i;
                isUsed[i] = true;
                comb(depth + 1, i + 1);
                isUsed[i] = false;
            }
        }
    }

    public static void perm(int depth){
        if (depth == R) {
            for (int i = 0; i < R; i++) {
                System.out.print(num[i] + " ");
            }
            System.out.println();
            return;
        }

        for (int i = 1; i <= arr.length; i++) {
            if (!isUsed[i]) {
                num[depth] = i;
                isUsed[i] = true;
                perm(depth + 1);
                isUsed[i] = false;
            }
        }

    }

    public static void main(String[] args) throws Exception{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        N = Integer.parseInt(br.readLine());
        R = Integer.parseInt(br.readLine());
        arr = new int[N];
        isUsed = new boolean[N + 1];
        num = new int[R];

        for (int i = 0; i < N; i++) {
            arr[i] = i + 1;
        }

        System.out.println("Permutation");
        perm(0);
        isUsed = new boolean[N + 1];
        num = new int[R];
        System.out.println("\nCombination");
        comb(0, 1);
    }
}

 

main에서는 n과 r에 해당하는 값을 입력받는다.

만약 n이 5라면, 원소는 {1, 2, 3, 4, 5}로 구성되며, 이 중 r개 만큼의 원소를 선택하는 경우의 수를 구하게 된다.

 

백트래킹 알고리즘을 이용해 순열과 조합을 구할 수 있다.

순열과 조합의 차이는 순서의 차이기 때문에 전체적인 로직은 비슷하다.

둘 다 재귀 호출로 구현하였으며, r의 길이에 도달하게 되면 리스트에 있는 모든 원소를 출력한다.

재귀 호출 시에는 다음 자리에 올 수를 선택하기 위해 depth + 1을 패러미터로 준다.

또한 모든 경우의 수를 구하기 위해 list의 인덱스(depth)를 하나 채우고(addLast) 재귀 호출 후에 다시 그 인덱스를 비우는 식(removeLast)으로 진행된다.

 

순열과 조합의 차이점은 바로 패러미터에 start라는 변수가 들어가는지의 여부다.

start 변수는 순서에 상관 없게 출력하기 위한 변수다.

 

예를 들어 1, 2와 2, 1은 조합에서 같은 경우의 수로 취급되는데, 이를 위해서 현재 선택된 값보다 더 작은 값이 선택되지 않도록 한다.

만약 전체 원소가 {1, 2, 3, 4}이고 현재 선택된 수가 2라면, 2보다 작은 값인 1이 선택되지 않게 하기 위해서 i + 1부터 다음 자리에 대한 for문을 돌리는 것이다.

즉, for 문은 2보다 큰 값인 3 -> 4 순으로 돌게 되어 순서만 다른 경우의 수를 제거한다.

 

배열로 구현하게 되면 isUsed와 같은 변수를 두어 중복을 제거해주어야 한다.

하지만 List와 같은 자료구조를 사용하면 contains()과 같은 메소드로 원소가 사용되었는지 간편하게 체크할 수 있다.

 

그러나 이에 따른 오버헤드가 발생할 것이다.

isUsed를 배열로 만들고, 이를 통해 체크하게 되면 O(1)의 시간에 체크할 수 있다.

하지만 contains() 메소드를 사용하면 리스트의 크기가 N이라고 할 때 O(N)의 시간 복잡도를 가지게 될 것이다.

또한 리스트 자료구조에서 노드를 추가하고 제거하는 것을 반복하는 것에서도 오버헤드가 있을 것이다.

 

그럼에도 불구하고 본인은 해당 방법이 가지는 구현의 편리함이 크다고 판단한다.

아무래도 숫자를 저장할 num 배열과 사용 여부를 저장할 isUsed 배열 두 개를 사용하는 것보단, 하나의 리스트를 쓰는 것이 간편하기 때문이다.

또한 List를 이용한 구현에서 perm과 comb 메소드에 패러미터 depth를 넣어주고 있지만, 해당 패러미터가 없어도 작동한다.

왜냐하면 list.size()를 이용하여 현재 숫자의 길이(depth)를 구할 수 있기 때문이다.

 

물론 메모리나 속도 제한이 엄격한 조건에서는 배열을 사용하는 것이 좋다고 생각한다.

BOJ 10974(모든 순열) 문제에서 리스트와 배열을 사용한 메모리와 시간은 각각 아래와 같다.

리스트로 구현: 메모리 82604 KB, 시간 2692 ms

배열로 구현: 메모리 55752 KB, 시간 1956 ms

문제

N×M크기의 배열로 표현되는 미로가 있다.

1 0 1 1 1 1
1 0 1 0 1 0
1 0 1 0 1 1
1 1 1 0 1 1

미로에서 1은 이동할 수 있는 칸을 나타내고, 0은 이동할 수 없는 칸을 나타낸다. 이러한 미로가 주어졌을 때, (1, 1)에서 출발하여 (N, M)의 위치로 이동할 때 지나야 하는 최소의 칸 수를 구하는 프로그램을 작성하시오. 한 칸에서 다른 칸으로 이동할 때, 서로 인접한 칸으로만 이동할 수 있다.

위의 예에서는 15칸을 지나야 (N, M)의 위치로 이동할 수 있다. 칸을 셀 때에는 시작 위치와 도착 위치도 포함한다.

 

입력

첫째 줄에 두 정수 N, M(2 ≤ N, M ≤ 100)이 주어진다. 다음 N개의 줄에는 M개의 정수로 미로가 주어진다. 각각의 수들은 붙어서 입력으로 주어진다.

 

출력

첫째 줄에 지나야 하는 최소의 칸 수를 출력한다. 항상 도착위치로 이동할 수 있는 경우만 입력으로 주어진다.

 

전체 코드

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayDeque;
import java.util.Deque;

class Main {

    static int n;
    static int m;
    static int[][] map;
    static int[][] dist;
    final static int[] dx = {1, 0, -1, 0};
    final static int[] dy = {0, 1, 0, -1};

    public static void main(String args[]) throws Exception {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] input = br.readLine().split(" ");

        n = Integer.parseInt(input[0]);
        m = Integer.parseInt(input[1]);

        map = new int[n + 1][m + 1];

        for (int i = 1; i <= n; i++) {
            String row = br.readLine();

            for (int j = 1; j <= m; j++) {
                map[i][j] = row.charAt(j - 1) - '0';
            }
        }

        dist = new int[n + 1][m + 1];

        for (int i = 0; i <= n; i++) {
            for (int j = 0; j <= m; j++) {
                dist[i][j] = -1;
            }
        }

        Deque<Integer[]> queue = new ArrayDeque<>();
        queue.addLast(new Integer[] {1, 1});
        dist[1][1] = 1;

        while (!queue.isEmpty()) {
            Integer[] cur = queue.removeFirst();

            for (int dir = 0; dir < 4; dir++) {
                int nx = cur[0] + dx[dir];
                int ny = cur[1] + dy[dir];

                if (nx < 1 || nx > n || ny < 1 || ny > m) continue;
                if (dist[nx][ny] != -1 || map[nx][ny] != 1) continue;

                dist[nx][ny] = dist[cur[0]][cur[1]] + 1;
                queue.addLast(new Integer[]{nx, ny});
            }
        }

        System.out.println(dist[n][m]);
    }
}

 

소견

BFS의 거리 측정 알고리즘을 이용하여 해결하였다.

전체적인 알고리즘은 BFS의 기본 틀을 따르고 있으며, 방문 상태를 나타내는 visited 배열 대신 dist 배열을 사용하였다.

 

BFS에서 현재 위치(cur[0], cur[1])와 다음 이동할 위치(nx, ny)의 거리 차이가 항상 '1' 이라는 점을 이용하여 미로 탐색에서의 이동 거리를 구할 수 있다.

이것을 코드로 구현한 부분이 바로 dist[nx][ny] = dist[cur[0]][cur[1]] + 1이다.

 

처음에 dist 배열을 -1로 초기화하는데, -1라는 값이 visited 배열에서의 false와 같은 역할을 수행한다.

해당 위치에 대한 방문이 완료되었다면 dist[nx][ny] = dist[cur[0]][cur[1]] + 1에 의해서 해당 위치에 대한 dist 인덱스는 -1 이상의 값을 가지게 된다.

 

문제에서는 시작 위치와 끝 위치가 주어져 있고, 좌표가 1, 1로 시작하기 때문에 이에 따라 배열의 크기와 인덱스를 설정하였다.

다만, 인덱스가 1부터 시작하도록 처리하는 것이 번거로운 점이 있었기 때문에 다음에 구현할 때는 시작 인덱스를 0, 0으로 구현하는 것이 나아 보인다.

시작 위치를 0, 0으로 한다고 하더라도 끝 위치에 대한 거리를 출력할 때 dist[n-1][m-1]로 출력하는 것 말고는 번거로움이 없기 때문이다.

문제

어떤 큰 도화지에 그림이 그려져 있을 때, 그 그림의 개수와, 그 그림 중 넓이가 가장 넓은 것의 넓이를 출력하여라. 단, 그림이라는 것은 1로 연결된 것을 한 그림이라고 정의하자. 가로나 세로로 연결된 것은 연결이 된 것이고 대각선으로 연결이 된 것은 떨어진 그림이다. 그림의 넓이란 그림에 포함된 1의 개수이다.

 

입력

첫째 줄에 도화지의 세로 크기 n(1 ≤ n ≤ 500)과 가로 크기 m(1 ≤ m ≤ 500)이 차례로 주어진다. 두 번째 줄부터 n+1 줄 까지 그림의 정보가 주어진다. (단 그림의 정보는 0과 1이 공백을 두고 주어지며, 0은 색칠이 안된 부분, 1은 색칠이 된 부분을 의미한다)

 

출력

첫째 줄에는 그림의 개수, 둘째 줄에는 그 중 가장 넓은 그림의 넓이를 출력하여라. 단, 그림이 하나도 없는 경우에는 가장 넓은 그림의 넓이는 0이다.

 

전체 코드

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.LinkedList;

class Main {

    static int[] dx = {1, 0, -1, 0};
    static int[] dy = {0, 1, 0, -1};
    static boolean[][] v;
    static int[][] arr;
    static LinkedList<Integer[]> queue = new LinkedList<>();
    static int n;
    static int m;


    public static void main(String args[]) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        String[] input = br.readLine().split(" ");
        n = Integer.parseInt(input[0]);
        m = Integer.parseInt(input[1]);

        v = new boolean[n][m];

        arr = new int[n][m];

        for (int i = 0; i < n; i++) {
            String[] row = br.readLine().split(" ");

            for (int j = 0; j < m; j++) {
                arr[i][j] = Integer.parseInt(row[j]);
            }
        }

        int cnt = 0;
        int max = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                int k = bfs(i, j);

                if (k > 0) {
                    cnt++;
                    if (max < k) {
                        max = k;
                    }
                }
            }
        }

        System.out.println(cnt);
        System.out.println(max);

    }

    static int bfs(int x, int y) {
        int size = 0;

        if (arr[x][y] != 0 && v[x][y] == false) {
            size++;
            queue.addLast(new Integer[]{x, y});
            v[x][y] = true;
        }

        while (!queue.isEmpty()) {
            Integer[] xy = queue.removeFirst();
            for (int i = 0; i < 4; i++) {
                int nx = xy[0] + dx[i];
                int ny = xy[1] + dy[i];

                if (nx < 0 || nx >= n || ny < 0 || ny >= m)
                    continue;
                if (arr[nx][ny] == 0 || v[nx][ny] == true)
                    continue;

                queue.addLast(new Integer[]{nx, ny});
                v[nx][ny] = true;
                size++;
            }
        }
        return size;
    }
}

 

소견

BFS를 이용한 기본 문제였다. 전체적으로 BFS의 기본 코드를 따라서 작성하였다.

 

문제를 풀면서 예상치 못하게 시간을 많이 소모한 것은 nx, ny의 인덱스 경계값 조건을 설정하는 부분이었다. 처음에는 하나의 조건문 안에 큐에 원소를 넣게 되는 nx, ny의 조건과 arr[nx][ny], v[nx][ny]의 조건을 모두 넣으려고 하였다.

 

하지만 그렇게 하는 것보다, 큐에 원소를 넣지 않는 continue 조건을 설정하는 것이 가시성이 좋다고 보았다. 또한 인덱스(nx, ny)의 조건을 설정한 후에 arr[nx][ny], v[nx][ny]를 조건을 설정하는 것이 exception 발생을 줄일 수 있는 방법이다. nx, ny의 값을 체크하지 않은 채로 arr과 v의 조건을 체크하게 되면 잘못된 인덱스에 접근할 수 있기 때문이다. 

 

그리고 백트래킹의 코드를 보다가 BFS를 구현하려고 하니 패러미터로 오는 x, y의 활용을 헷갈려 시간을 소모했다. nx와 ny를 초기화 하는 부분에서 큐에서 꺼낸 값으로 nx, ny를 초기화해야 하는데, 그냥 x와 y를 이용해 초기화하여 잘못된 계산이 이루어졌다. 백트래킹은 재귀호출을 이용하기 때문에 패러미터로 온 값을 이용해 계산하는 것과 착오한 것이다.

 

BFS, DFS, 백트래킹은 얼추 비슷한 형태의 코드 구성을 가지고 있고, 제대로 이해하지 못하면 풀기 어려우면서도 코딩 테스트에서도 자주 다루는 주제인 만큼 이에 대한 이해를 확실히 해야겠다.

 

 

문제

자연수 N과 M이 주어졌을 때, 아래 조건을 만족하는 길이가 M인 수열을 모두 구하는 프로그램을 작성하시오.

  • 1부터 N까지 자연수 중에서 중복 없이 M개를 고른 수열

 

입력

첫째 줄에 자연수 N과 M이 주어진다. (1 ≤ M ≤ N ≤ 8)

 

출력

한 줄에 하나씩 문제의 조건을 만족하는 수열을 출력한다. 중복되는 수열을 여러 번 출력하면 안되며, 각 수열은 공백으로 구분해서 출력해야 한다.

 

수열은 사전 순으로 증가하는 순서로 출력해야 한다.

 

전체 코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;

public class Main {

    static int N;
    static int M;
    static LinkedList<Integer> list = new LinkedList<>();

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        String[] input = br.readLine().split(" ");
        N = Integer.parseInt(input[0]);
        M = Integer.parseInt(input[1]);

        func(0);
    }

    public static void func(int k) {
        if (k == M) {

            for (Integer i : list) {
                System.out.print(i + " ");
            }
            System.out.println();

            return;
        }

        for (int i = 1; i <= N; i++) {
            if (!list.contains(i)) {
                list.add(i);
                func(k+1);
                list.removeLast();
            }
        }
    }
}

 

풀이

백트래킹을 이용한 기본적인 문제다.

 

코드의 아이디어는 유튜브 알고리즘 강의를 참고하였다.

 

재귀 방식으로 백트래킹을 통해 가능한 모든 수열을 출력하는 방식으로 구현하였다.

핵심 알고리즘 부분은 func 메소드인데, 먼저 패러미터인 k는 list의 size이자, 수열의 현재 길이를 의미한다.

(배열로 구현하게 되면 배열의 인덱스를 의미하게 된다.)

 

k가 수열의 최종 길이인 M에 도달하게 되면 list의 원소들을 출력하여 수열을 출력한다.

 

func 메소드에서 재귀적 호출을 실행하는 부분은 바로 if 문 다음에 나오는 for 문이다.

여기서 for 문 안의 i가 의미하는 바는 가능한 숫자의 범위이다.

 

처음 func 메소드를 호출할 때 인자로 0을 넣고 있는데, 이는 리스트의 첫번째(수열의 첫번째) 자리에 들어갈 수를 의미한다.

여기서 1부터 N까지의 수가 가능하기 때문에, 첫번째 자리에 올 수 있는 수를 for 문을 통해 돌려주는 것이다.

 

그리고 !list.contains(i)라는 조건문을 이용한 다음 리스트에 삽입 -> k+1 재귀 호출 -> 리스트에서 삭제와 같은 로직으로 진행한다.

여기서 list.contains는 이미 사용한 원소를 제거하기 위한 조건문이다. 만약 list.contains(i)가 true라면 해당 수는 이미 사용했던 값이기 때문에 중복 없이 고른다는 문제 조건에 위배된다. 따라서 !list.contains(i)를 이용해 중복을 제거하는 것이다.

(이를 배열로 구현한다면, isUsed와 같은 배열을 이용 가능하다)

 

그 이후 k+1을 패러미터로 func를 재귀 호출하게 되는데, 이는 현재 "상태"에서 다음 자리(k+1)에 올 수를 결정하는 것이다.

여기서 현재 상태라는 의미는 k번째 자리의 값이 결정된 상태를 의미한다. 만약 func(0)로 처음 호출된 상태면, 여기서의 현재 상태는 리스트의 첫번째 자리로 1, 2, 3, 4가 선택된 4가지 상태가 존재할 수 있을 것이다. 그리고 예를 들어 첫번째 상태로 "2"가 선택된 상태에서는 func(1)이 호출되고 두번째 상태로 1, 3, 4가 선택되는 3가지 상태가 존재할 수 있다.

 

이런식으로 각 상태에서 가능한 경우의 수를 모두 탐색하여 출력하면 문제의 조건에 맞게 수열을 출력할 수 있게 된다.

 

이번에 처음 백트래킹 알고리즘을 공부하였는데, 재귀 호출을 사용하기 때문에 알고리즘을 정확히 이해하지 않고서는 코드를 구현하기가 어려웠다. 특히 재귀 호출 부분은 직접 숫자를 대입해가며 머리 속으로 또는 필기를 통해 시뮬레이션 하는 과정이 많이 도움이 되었다. 

 

또한 배열을 이용한 풀이와, 리스트를 이용한 풀이를 각각 해보면서 재귀 호출 과정을 어느 정도 이해하게 되었다.

 

N과 M 문제 시리즈는 BOJ에서 백트래킹을 연습하는데 대표적인 문제로, 앞으로도 더 많은 문제들을 학습하면서 백트래킹을 익혀보도록 해야겠다.

문제

지민이는 N개의 원소를 포함하고 있는 양방향 순환 큐를 가지고 있다. 지민이는 이 큐에서 몇 개의 원소를 뽑아내려고 한다.

 

지민이는 이 큐에서 다음과 같은 3가지 연산을 수행할 수 있다.

  1. 첫 번째 원소를 뽑아낸다. 이 연산을 수행하면, 원래 큐의 원소가 a1, ..., ak이었던 것이 a2, ..., ak와 같이 된다.
  2. 왼쪽으로 한 칸 이동시킨다. 이 연산을 수행하면, a1, ..., ak가 a2, ..., ak, a1이 된다.
  3. 오른쪽으로 한 칸 이동시킨다. 이 연산을 수행하면, a1, ..., ak가 ak, a1, ..., ak-1이 된다.

큐에 처음에 포함되어 있던 수 N이 주어진다. 그리고 지민이가 뽑아내려고 하는 원소의 위치가 주어진다. (이 위치는 가장 처음 큐에서의 위치이다.) 이때, 그 원소를 주어진 순서대로 뽑아내는데 드는 2번, 3번 연산의 최솟값을 출력하는 프로그램을 작성하시오.

 

입력

첫째 줄에 큐의 크기 N과 뽑아내려고 하는 수의 개수 M이 주어진다. N은 50보다 작거나 같은 자연수이고, M은 N보다 작거나 같은 자연수이다. 둘째 줄에는 지민이가 뽑아내려고 하는 수의 위치가 순서대로 주어진다. 위치는 1보다 크거나 같고, N보다 작거나 같은 자연수이다.

 

출력

첫째 줄에 문제의 정답을 출력한다.

 

전체 코드

import java.io.*;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;

class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] s = br.readLine().split(" ");
        int N = Integer.parseInt(s[0]);
        int M = Integer.parseInt(s[1]);

        String[] input = br.readLine().split(" ");

        LinkedList<Integer> list = new LinkedList<>();

        for (int i = 1; i <= N; i++) {
            list.add(i);
        }

        int ans = 0;
        for (int i = 0; i < M; i++) {
            int target = Integer.parseInt(input[i]);

            if (list.peekFirst() == target) {
                one(list);
            } else {
                int idx = list.indexOf(target);

                if (idx <= Math.ceil(list.size() / 2)) {
                    while (true) {
                        two(list);
                        ans++;
                        if (list.peekFirst() == target) {
                            one(list);
                            break;
                        }
                    }

                } else {
                    while (true) {
                        three(list);
                        ans++;
                        if (list.peekFirst() == target) {
                            one(list);
                            break;
                        }
                    }
                }
            }
        }
        System.out.println(ans);
    }

    public static <T> void one(LinkedList<T> list) {
        list.removeFirst();
    }

    public static <T> void two(LinkedList<T> list) {
        list.addLast(list.removeFirst());
    }

    public static <T> void three(LinkedList<T> list) {
        list.addFirst(list.removeLast());
    }
}

 

풀이

간단해보이는 문제였으나, 예상보다 생각할 거리가 있는 문제였다.

 

우선 문제의 요구사항에 따라 3개의 연산을 메소드로 만들었다.

그리고 입력에 대한 처리를 한 후 알고리즘을 고민하였다.

 

문제 조건에서의 큐는 원형 큐의 형태로 볼 수 있으며, 2번째 연산 또는 3번째 연산 중 어느 것을 해야 하는지는 현재 리스트의 크기를 2로 나눈 값을 소수점 첫째 자리에서 올림한 값( idx <= Math.ceil(list.size() / 2) )에 따라 다르다.

 

만약 리스트의 크기가 5라면, 이를 2로 나눈 결과를 올림한 값은 3이 된다. 따라서 꺼내려고 하는 값의 인덱스 범위가 1 ~ 3이라면 two 연산을, 그 외의 범위라면 three 연산을 원하는 값이 첫번째 인덱스로 올 때까지 하는 것이 최소 횟수로 해결 가능한 방법이다. 이는 while (true) 문에서 peek 메소드를 이용해 조건을 확인하고, 해당될 때까지 반복한 후 break 하는 식으로 구현하였다.

 

느낀점

이러한 알고리즘을 유추하기 위해서 머리 속으로 문제의 테스트 입출력을 직접 돌려보면서, 어떤 인덱스 범위에서 어느 연산을 수행해야 하는지 고민해보는 것이 문제 난이도에 비해 많은 시간을 투자하게 한 것 같다.

 

또한 올림 함수 Math.ceil() 메소드를 사용해본지가 오래 되었는데, 반올림 함수인 round, 버림 함수인 floor 등과 같이 학습해보는 기회가 되었다. 특히 특정 자릿수에서 올림/반올림/버림 하기 위해서 Math.ceil( 값 * 10^(표현할 소수점 자릿수) / 10^(표현할 소수점 자릿수) )와 같은 테크닉도 다시 인식하게 되었다.

 

+ Recent posts