728x90
반응형
SMALL

문제 설명

n명의 사람이 일렬로 줄을 서고 있습니다. n명의 사람들에게는 각각 1번부터 n번까지 번호가 매겨져 있습니다. n명이 사람을 줄을 서는 방법은 여러가지 방법이 있습니다. 예를 들어서 3명의 사람이 있다면 다음과 같이 6개의 방법이 있습니다.

  • [1, 2, 3]
  • [1, 3, 2]
  • [2, 1, 3]
  • [2, 3, 1]
  • [3, 1, 2]
  • [3, 2, 1]

사람의 수 n과, 자연수 k가 주어질 때, 사람을 나열 하는 방법을 사전 순으로 나열 했을 때, k번째 방법을 return하는 solution 함수를 완성해주세요.

제한사항

  • n은 20이하의 자연수 입니다.
  • k는 n! 이하의 자연수 입니다.

입출력 예

n k result

3 5 [3,1,2]

입출력 예시 설명

입출력 예 #1

문제의 예시와 같습니다.

팩토리얼을 사용하면 n명의 사람이 있을 때 줄을 서는 방법의 수를 구할 수 있다. 그리고 재귀를 사용하면 k번째 방법을 찾을 수 있다.

재귀를 사용하여 풀기 위해선, k가 n-1 팩토리얼보다 작거나 같은지 확인해야 한다. 만약 작거나 같다면, 첫 번째 사람은 항상 1번 사람이며, 나머지 사람들은 재귀적으로 다시 순열을 구하면 된다. 만약 k가 n-1 팩토리얼보다 크다면, 첫 번째 사람은 1번이 아니라 k/(n-1 팩토리얼) + 1 번째 사람이며, 나머지 사람들은 다시 순열을 구하면 된다.

package LV2;

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

public class H12936 {
    public int[] solution(int n, long k) {
        // 1부터 n까지의 숫자를 저장할 리스트를 생성
        List<Integer> numberList = new ArrayList<>();
        for(int i = 1; i <= n; i++) {
            numberList.add(i);
        }

        // 팩토리얼 값을 저장할 배열을 생성
        long[] factorial = new long[n+1];
        factorial[0] = 1;
        for(int i = 1; i <= n; i++) {
            factorial[i] = factorial[i-1] * i;
        }

        // 재귀 함수를 호출하여 결과를 반환
        return solve(numberList, factorial, n, k);
    }

    private int[] solve(List<Integer> numberList, long[] factorial, int n, long k) {
        // n이 0이면 빈 배열을 반환 (재귀 종료 조건)
        if(n == 0) {
            return new int[0];
        }
        // k 번째에 위치할 첫 번째 숫자를 계산
        long fact = factorial[n-1];
        int index = (int)((k-1) / fact);
        // 계산한 인덱스의 숫자를 리스트에서 제거
        int number = numberList.remove(index);
        // k를 갱신하고 나머지 숫자들에 대해 재귀 호출
        int[] subResult = solve(numberList, factorial, n-1, k - fact * index);
        // 결과를 저장할 배열을 생성하고 첫 번째 숫자를 넣음
        int[] result = new int[n];
        result[0] = number;
        // 나머지 숫자들을 결과 배열에 넣음
        for(int i = 1; i < n; i++) {
            result[i] = subResult[i-1];
        }
        // 결과 배열을 반환
        return result;
    }
}

팩토리얼을 이용해서 주어진 n명의 사람들을 나열하는 방법 중에서 k번째 방법을 찾는 알고리즘이다. 이 알고리즘은 재귀적으로 문제를 해결한다. 먼저 n명의 사람들을 팩토리얼을 이용해서 나열하는 방법의 수를 계산한다. 그리고 이를 이용해서 k번째에 위치할 첫 번째 숫자를 찾는다. 그 다음으로 이 숫자를 리스트에서 제거하고, k를 갱신해서 나머지 숫자들에 대해서 재귀적으로 같은 과정을 반복한다. 이렇게 해서 k번째 방법을 찾게 된다.

728x90
반응형
LIST

'알고리즘 > 프로그래머스 JAVA LV.2' 카테고리의 다른 글

무인도 여행  (0) 2023.06.20
테이블 해시 함수  (0) 2023.06.19
가장 큰 정사각형 찾기  (0) 2023.06.15
연속된 부분 수열의 합  (0) 2023.06.15
택배상자  (0) 2023.06.15
728x90
반응형
SMALL

완전 탐색은 가능한 모든 경로를 탐색하여 최적의 경로를 찾는 방식이다. 이 방식은 경로의 수가 적을 때는 유용하지만, 경로의 수가 많아질수록 연산 비용이 증가하는 단점이 있다. 그리디 알고리즘은 항상 현재 상황에서 가장 좋은 선택을 하는 방식으로 최적의 해를 찾는 방법이다. 이를 위해 각 도시 간의 거리 정보가 필요하며, 이를 이용하여 현재 위치에서 가장 가까운 도시를 선택해 나가면 된다.

1.알고리즘 '복잡도(complexity)'


복잡도란

  • 알고리즘의 성능을 나타내는 척도
  • 크게 시간 복잡도, 공간 복잡도로 나눌 수 있다.

1.시간 복잡도

  • 특정한 크기의 입력에 대해 알고리즘이 얼마나 오래 걸리는지를 의미한다.
  • 알고리즘을 위해 필요한 연산의 횟수
  • 복잡도를 표현하기 위해 빅오 표기법을 사용한다.
  • 최악의 경우에 대한 연산 횟수가 가장 중요하다.
  • N의 범위에 따라 시간 복잡도를 계산하고 사용할 수 있는 알고리즘을 선택하는 방법도 있다.

※ 빅오 표기법 (O, big-O)

빅오란 입력값이 무한대로 향할때 함수의 상한을 설명하는 수학적 표기 방법이다.

  • 빅오는 점근적 실행 시간을 표기할 때 가장 널리 쓰이는 수학적 표기 방법이여, 여기서 점근적 실행 시간이란 간단하게 N이라는 입력값이 무한대로 커질때의 실행 시간의 추이를 의미한다.
  • 따라서 충분히 큰 입력값에서의 알고리즘의 효율성에 따라 수행 시간이 크게 차이가 날 수 있다.

빅오 표기법 표현 설명

O(1) 상수 입력값에 상관없이 일정한 실행시간을 최고!의 알고리즘
O(logN) 로그 로그는 매우 큰 입력값에서도 크게 영향을 받지 않는 편이다.
O(N) 선형 알고리즘을 수행하는데 걸리는 시간은 입력값에 비례, 모든 입렵값을 적어도 한 번 이상은 살펴봐야 한다.
O(NlogN) 로그 선형 병합 정렬등의 대부분 효율이 좋은 알고리즘이 이에 해당, 아무리 좋은 알고리즘이라 할지라도 n log n 보다 빠를 수 없다.
O(N^2) 다항 버블 정렬 같은 비효율저긴 정렬 알고리즘이 이에 해당 한다.
O(2^N) 지수 피보나치의 수를 재귀로 계산하는 알고리즘이 이에 해당 한다. n^2와 혼동되는 경우가 있는데 2^n이 훨씬 더 크다.

2.공간 복잡도

  • 특정한 크기의 입력에 대해 알고리즘이 얼마나 많은 메모리를 차지하는지를 의미한다.
  • 알고리즘을 위해 필요한 메모리의 양

2. P vs NP


1.다항 시간 알고리즘(Polynomial-Time Algorithms)

  • 다항 시간(합리적인 시간)안에 풀 수 있는 문제.
  • 모든 문제가 다항 시간안에 해결되지 않는다.
    • Ex) 튜링의 "Halting Problem(정지 문제)" : 정지 문제란
    프로그램을 설명한 것과 처음 입력값이 주어졌을 때, 이 프로그램에 입력값을 넣고 실행한다면 이 프로그램이 계산을 끝내고 멈출지 아니면 영원히 계속 계산할지 판정하라.
    • 1936년 앨런 튜링이 모든 가능한 입력값에 대해 정지 문제를 풀 수 있는 일반적인 알고리즘은 존재하지 않는다는 것을 증명했다.

2.클래스 P

  • 다항 시간내에 해결되는 알고리즘을 가지고 있는 결정(decision) 문제의 클래스
    • 어떤 상수 k에 대해 O(N^K) 시간에 풀 수 있는 문제
  • O(P(N)) - 이때 P(N)은 N상의 다항시간
  • 항상 올바른 답을 계산하는 방법
  • 다항시간내에 해결되지 않으면 비효율적

3.클래스 NP

  • '비결정적인 다항시간' 상에 존재(stand)
  • 주로 "추측" or "평행선화(parallelize)"방식으로 계산
  • 다항 시간에 "확인할 수 있는" 문제로 구성
    • 해의 "후보"가 주어지면 문제 입력 크기에 대한 다항 시간에 그 후보가 올바른 해인지 확인할 수 있음을 의미
  • 이 방식의 문제점은 "속도(quickly)"

4.P ⊆ NP (아직 증명X)NP-complete(완비) 문제는 NP에서 가장 어려운 문제이다.대부분의 전문적인 문제는 P or NP-complete 문제이다.NP-complete 문제 : P로 해결 가능하지만 NP로 해결되지 않는 문제(미제문제)

3.여행하는 외판원 문제(Traveling-Salesman-Problem)


1.여행하는 외판원 문제(TSP)

외판원 문제 또는 순회 외판원 문제는 초합 최적화 문제의 일종이다. 이 문제는 NP-난해에 속하며, 흔히 계산 복잡도 이론에서 해를 구하기 어려운 문제의 대표적인 예로 많이 다룬다.

  • 외판원은 자신이 사는 도시에서 출발해서 어떤 순서로든 다른 도시를 모두 방문하고 나서 다시 출발점으로 돌아와야 한다.
    • 여기서 목표는 각 도시를 정확히 한 번씩(반복 없이) 방문하고,
    • 전체 여행한 거리를 최소로 만드는 것이다.⇒ 그래프 이론의 용어로 엄밀하게 정의한다면, "각 변에 가중치가 주어진 완전 그래프(weighted complete graph)에서 가장 작은 가중치를 가지는 해밀턴 순회를 구하라" 라고 표현할 수 있고, 반드시 시작점으로 돌아와야 한다는 제약 조건을 없애도 계산 복잡도는 변하지 않는다.
  • 외판원 순회 알고리즘은 무식하게 풀 수 있다. 무조건 0번 도시에서 출발한다고 가정해도 경로의 길이는 다르지 않다.
  • 그러면 남은 도시들을 어떤 순서로 방문할지를 정하기만 하면 된다.
  • 남은 n-1개의 도시를 나열하는 방법으로는 (n-1)!가지가 있다.
  • 그래서 재귀 호출을 통해 쉽게 외판원 순회 알고리즘을 설계할 수 있다.
  • 그러나 보통 n!의 풀이는 사용하지 않는다. 12!만 해도 479,001,600라는 엄청난 수를 지니기 때문이다.

2.외판원 순회 알고리즘을 더 빠르고 효율적으로 구하는 방법은?

  • 모든 정점을 한 번씩 방문하는(출발 정점 제외) 최단 순환 경로를 탐색하는데 최적의 효율로 탐색하기 위해서 사용한다.

DP 메모제이션

  • (n-1)!의 모든 경로를 조사하지 않고 중복된 경로를 제거하는 dp 메모제이션 기법을 사용하면 된다.
    • 동일한 하위 문제의 반복을 막아줌으로써 더 높은 효율로 연산이 이루어지게 해준다. 중복되는 경로의 조사를 제거해주는 것이다.
  • 예를 들어, 피보나치 함수는 dp 메모제이션 기법을 통해 O(2^n)의 복잡도를 O(N)까지 줄여준다. **그렇다고 무조건 dp 메모제이션을 적용하면 각 문제의 성질이 다르기 때문에 O(n)으로 줄어든다는 뜻은 아니다.

비트마스킹

  • 비트마스킹을 사용하면 bit연산이기 때문에 다른 자료구조(boolean배열)을 사용하는 것보다 훨씬 빠르게 동작되고 코드도 간결해진다.
  • 가장 큰 장점은 N이 16개인 경우 2^16가지의 경우를 16bit로 표현이 가능하다.

⇒ 정리하면 TSP(외판원 순회)는 최단 순환 경로를 탐색해야하는데 1) N! 의 중복 경로를 제거해주는 DP 메모제이션 기법을 사용한다. 그래도 2^N의 모든 경우의 수를 표현해야 하기 때문에 그만큼의 공간복잡도가 필요하다. 2) 메모리 사용량도 줄이고 성능 향상을 위해서 2^N의 경우의 수를 Nbit로 표현할 수 있는 비트마스킹으로 사용한다.

⇒ 올라가야 할 계단의 수를 100 개에서 1개로 줄여주는 설계 방법 이라고 보면 된다.

그렇다면 왜 한 정점만 탐색해도 될까?

  • DP 메모제이션 기법으로 엄청나게 시간을 단축 할 수 있는 것은 그만큼 중복되는 경로가 많기 때문이다.
  • 이 순회 경로는 싸이클로 n개의 정점 중 어느 정점에서 탐색을 시작해도 결과는 똑같다는 것을 알아야 한다.

  • 어차피 최적 경로 싸이클은 어디서 시작해도 똑같이 나오기 때문에 한 정점에서만 탐색해줘도 된다.
  • 1번에서 출발 : 1 → 2 → 5 → 3 → 4 → 12번에서 출발 : 2 → 5 → 3 → 4 → 1 → 23번에서 출발 : 3 → 4 → 1 → 2 → 5 → 3

알고리즘 참고 영상 :

[youtube] hungarian dance - sorting algorithms

참고 링크 :

[알고리즘] 복잡도란 무엇인가

빅오 표기법 (O, big-O)

정지 문제

P와 NP

여행하는 외판원 문제(TSP)

728x90
반응형
LIST
728x90
반응형
SMALL

선택 정렬

  1. 배열에서 최솟값을 찾는다.
  2. 최솟값과 정렬되지 않은 맨 앞의 수와 위치를 바꾼다.
  3. 최솟값이 이동한 부분은 고정된다.
  4. 고정된 부분 이 후부터 배열을 돌면서 1번부터 반복한다.

선택 정렬 구현(Java) 오름차순

void selectionSort(int[] arr) {
    int indexMin, temp;
    for (int i = 0; i < arr.length-1; i++) {        
        indexMin = i;
        for (int j = i + 1; j < arr.length; j++) {  
            if (arr[j] < arr[indexMin]) {           
                indexMin = j;  // 최솟 값 찾기
            }
        }
        // 최솟값과 제일 앞의 수를 바꿔 주기
        temp = arr[indexMin];
        arr[indexMin] = arr[i];
        arr[i] = temp;
  }
  System.out.println(Arrays.toString(arr));
}

선택 정렬 시간 복잡도

이중 for문에서 알 수 있듯이 O(N^2) 이다.

첫 번째 값을 찾기 위해 N번 확인 해야 하고, 두 번째 값을 찾기 위해 N+1번 확인하고.. 반복하면

N+ (N-1) + … + 2 + 1 = N(N+1) 이므로 O(N^2)이 된다.

퀵 정렬

배열을 두 부분으로 나누고, 나뉜 부분을 다시 퀵 정렬 하는 대표적인 *분할 정복 방법을 사용한다.

*분할 정복 방법 : 문제를 작은 2개의 문제로 분리하고 각각을 해결한 다음, 결과를 모아서 원래의 문제를 해결하는 전략

  1. 배열에서 하나의 원소를 골라 이 원소를 피벗(pivot)이라고 한다.
  2. 피벗 앞에는 피벗보다 작은 원소가, 피벗 뒤에는 피벗보다 큰 원소가 오도록 이동 시킨다.(divide)
  3. 피벗 기준으로 좌우 부분에서 재귀적(recursion)으로 1번부터 반복한다.

(책에서는 정렬해야 할 원소들이 알파벳으로 이루어진 이름이라는 것이 명확하여 알파벳 순서대로 반을 나누어 정렬했지만, 숫자를 정렬한다고 생각하면 범위를 알 수 없으니 고정된 기준으로 나눌 순 없고 배열의 원소를 기준으로 잡고 나눠야 한다)

퀵 정렬 구현(Java) 오름차순

public static void quickSort(int[] arr) {
    quickSort(arr, 0, arr.length - 1);
  }

  private static void quickSort(int[] arr, int start, int end) {
    // start가 end보다 크거나 같다면 정렬할 원소가 1개 이하이므로 정렬하지 않고 return
    if (start >= end)
      return;
    
    // 가장 왼쪽의 값을 pivot으로 지정, 실제 비교 검사는 start+1 부터 시작
    int pivot = start;
    int lo = start + 1;
    int hi = end;
    
    // lo는 현재 부분배열의 왼쪽, hi는 오른쪽을 의미
    // 서로 엇갈리게 될 경우 while문 종료
    while (lo <= hi) {
      while (lo <= end && arr[lo] <= arr[pivot]) // 피벗보다 큰 값을 만날 때까지
        lo++;
      while (hi > start && arr[hi] >= arr[pivot]) // 피벗보다 작은 값을 만날 때까지
        hi--;
      if (lo > hi)				 // 엇갈리면 피벗과 교체
        swap(arr, hi, pivot);
      else
        swap(arr, lo, hi);			 // 엇갈리지 않으면 lo, hi 값 교체 
      }
	
    // 엇갈렸을 경우, 
    // 피벗값과 hi값을 교체한 후 해당 피벗을 기준으로 앞 뒤로 배열을 분할하여 정렬 진행
    quickSort(arr, start, hi - 1);
    quickSort(arr, hi + 1, end);

  }

  private static void swap(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
  }

퀵 정렬 시간 복잡도

O(NlogN) - N이 1이 될 때까지 2로 나누는 횟수.

최악의 경우는 O(N^2)이 된다.

1 2 3 4 5 6

(리스트가 불균형하게 나누어 지는 경우 - 정렬하고자 하는 배열이 정렬이 되어 있는 경우)

정렬이 되어 있으면 가운데 수를 선택하면 좋다! 그니까 무조건 가운데를 피벗으로 선택하는게 이득!


선택 정렬(Selection Sort) | 👨🏻‍💻 Tech Interview (gyoogle.dev)

 

728x90
반응형
LIST

+ Recent posts