반응형

 

이번에는 백준 24060번 문제를 통해 병합 정렬 개념을 다시 다져보자.
이번 문제도 저번 개념 정리와 같이 재귀 방식으로 계속 나누고 
병합하며 정렬한다.

 

 

[ 알고리즘 ] 병합 정렬 - JAVA

백준 24060번 문제를 풀기 전에 병합 정렬 알고리즘의 로직에 대해아주 자세히 포스팅 해보려 한다. 사실 이미지로 본다면 아주 쉬운데 이걸 구현하고자 하면 머리가 아플 수도 있다. 때문에 이

p-coding.tistory.com

 

🔍 문제 핵심

  • 병합 정렬은 정렬 알고리즘 중 하나로, 분할 정복(Divide and Conquer) 방법을 사용한다.
  • 주어진 배열을 계속해서 절반으로 나누고, 더 이상 나눌 수 없을 때까지 나눈 뒤, 정렬된 상태로 병합한다.
  • 문제에서 예시로 주어진 병합 정렬 과정을 잘 이해하고, 이를 직접 구현해보는 것이 핵심이다.

📌 병합 정렬이란?

1. 분할 (Divide)

  • 배열을 절반으로 나눈다.
  • 이 과정을 배열의 크기가 1이 될 때까지 반복한다.
  • 각 하위 배열은 재귀적으로 처리된다.

2. 정복 (Conquer)

  • 크기가 1인 배열끼리 **두 개씩 병합(Merge)**하며 정렬해나간다.
  • 병합 과정에서 두 배열의 원소를 비교하여 작은 값을 먼저 넣는 방식으로 정렬이 이루어진다.

3. 병합 (Combine)

  • 정렬된 두 배열을 하나로 합치며 정렬을 유지한다.

 

 

 

재귀 방식으로 정렬하는 복잡한 구조로 공부하기 좋은 문제이다.
이해도를 높이기 위해 많은 관련 문제를 푸는 것을 추천한다.

전체 코드

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

public class Main {

    private static int count = 0;
    private static int number = -1;
    private static int K;

    private static void mergeSort(int[] arr, int left, int right) {
        if(left < right) {
            int mid = (left + right) / 2;

            mergeSort(arr, left, mid);
            mergeSort(arr, mid + 1, right);
            merge(arr, left, mid, right);
        }
    }

    private static void merge(int[] arr, int left, int mid, int right) {

        int[] temp = new int[right - left + 1];
        int i = left, j = mid + 1;
        int z = 0;

        while(i <= mid && j <= right) {
            if(arr[i] <= arr[j]) {
                temp[z++] = arr[i++];
            }
            else {
                temp[z++] = arr[j++];
            }

        }

        while(i <= mid) {
            temp[z++] = arr[i++];
        }

        while(j <= right) {
            temp[z++] = arr[j++];
        }


        for (int l = 0; l < temp.length; l++) {
            arr[left + l] = temp[l];
            count++;
            if(count == K) {
                number = temp[l];
            }
        }


    }

    public static void main(String[] args) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer stringTokenizer = new StringTokenizer(bufferedReader.readLine());

        int N = Integer.parseInt(stringTokenizer.nextToken());
        K = Integer.parseInt(stringTokenizer.nextToken());

        int[] arr = new int[N];

        stringTokenizer = new StringTokenizer(bufferedReader.readLine());
        for (int i = 0; i < N; i++) {
            arr[i] = Integer.parseInt(stringTokenizer.nextToken());
        }

        mergeSort(arr, 0, N - 1);

        System.out.println(number);

    }
}
반응형
반응형
이번에는 백준 4779번 문제를 통해 분할 정복의 개념을 다져보자.

칸토어 집합이란 간략히 설명하면
세 개로 분할하여 가운데를 제거해 나가는 작업을 반복하여 결과를 얻는 집합이다.

두 개로 나누었던 병합 정렬을 활용하면 풀어낼 수 있다.
 

[ 알고리즘 ] 병합 정렬 - JAVA

백준 24060번 문제를 풀기 전에 병합 정렬 알고리즘의 로직에 대해아주 자세히 포스팅 해보려 한다. 사실 이미지로 본다면 아주 쉬운데 이걸 구현하고자 하면 머리가 아플 수도 있다. 때문에 이

p-coding.tistory.com



문제 핵심

  • 3의 N제곱 길이 만큼의 '-'가 반복된 문자열
  • 3등분 하여 가운데를 공백으로 바꾼다.
    • 이 과정을 선의 길이가 1일 때 까지 반복

풀이 과정

합병 정렬을 이용하여 재귀를 통해 3등분 하여 처리한다.

반환 조건은 길이가 1일 때 반환한다.

핵심 코드

  1. 길이가 1일 때 반환한다.
  2. 크기를 3등분 한다.
  3. 중간지점 부분을 모두 공백으로 바꾼다.
    1. 시작점 + 크기 = 중간지점의 시작점
    2. 시작점 + ( 2 * 크기 ) = 세번째 지점 시작점
  4. 왼쪽 부분 재귀 실행
  5. 오른쪽 부분 재귀 실행
private static void can(char[] arr, int start, int size) {
    if(size <= 1 ) {
        return;
    }

    int onePerThird = size / 3;

    for(int i = start + onePerThird; i < start + 2 * onePerThird; i++) {
        arr[i] = ' ';
    }

    can(arr, start, onePerThird);
    can(arr, start + 2 * onePerThird, onePerThird);
}

 

합병 정렬만 활용한다면 바로 풀 수 있는 쉬운 문제이다.
분할하여 재귀방식으로 풀어나가는 문제로 길이만 유의하여 코드를 작성하면 되겠다.


전체 코드

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

public class Main {

    private static void can(char[] arr, int start, int size) {
        if(size <= 1 ) {
            return;
        }

        int onePerThird = size / 3;

        for(int i = start + onePerThird; i < start + 2 * onePerThird; i++) {
            arr[i] = ' ';
        }

        can(arr, start, onePerThird);
        can(arr, start + 2 * onePerThird, onePerThird);
    }

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

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder stringBuilder = new StringBuilder();
        while (true) {
            String input = br.readLine();
            if (input == null || input.isBlank()) {
                break;
            }

            int n = Integer.parseInt(input);
            if(n == 0) {
                stringBuilder.append("-\n");
                continue;
            }

            int size = (int) Math.pow(3, n);

            char[] result = new char[size];

            for (int i = 0; i < size; i++) {
                result[i] = '-';
            }

            can(result,0, size);
            stringBuilder.append(result).append('\n');
        }

        System.out.println(stringBuilder);
    }
}
반응형
반응형
요즘 시험이니 일이니 핑계 거리가 생겨 포스팅이 많이 늦어졌습니다. 
다시 매일 노력하여 포스팅 하도록 노력하겠습니다. 감사합니다.

이번에는 백준 15649번 문제를 통해 백트래킹 알고리즘을 실습해 보자. 

백트래킹이란 간단히 설명하면
가능한 모든 경로 탐색 중 조건이 맞지 않으면 이전 단계로 돌아가 다시 탐색하는 알고리즘이다.

 


 

문제 핵심

  • 1부터 N까지의 자연수 중 중복 없이 M개를 고른 수열
  • 1 <= M <= N < 8

풀이 과정

반복과 재귀를 통해 깊이에 따라 값을 처리한다.

dfs를 참고할 수 있겠다.

핵심 코드

  1. 현재 깊이를 체크한다.
    1. 최대 깊이에 도달한다면 리스트 값을 저장하고 리턴한다.
  2. 반복문을 통해 방문되지 않은 리스트 요소를 특정하여 방문한다.
  3. 리스트 현재 깊이 인덱스에 값을 저장한다. 
  4. 재귀를 통해 다음 깊이로 이동한다.
  5. false를 통해 방문을 초기화 하고 이전으로 이동
private static void backTracking(int depth, int M, int N) {
    if(depth == M) {
        for(int i : arr) {
            stringBuilder.append(i).append(" ");
        }
        stringBuilder.append("\n");
        return;
    }

    for(int i = 0; i < N; i++) {
        if(!visited[i]) {
            visited[i] = true;
            arr[depth] = i + 1;
            backTracking(depth + 1, M, N);
            visited[i] = false;
        }
    }
}

EX) N : 4, M : 2

  1. N과 M이 위와 같을 때 1회전의 경우 다음과 같이 진행된다.
  2. 이후 depth + 1을 통해 M과 같아지게 되므로 저장 후 리턴된다.
  3. 마지막 방문했던 인덱스를 false를 통해 이전으로 돌아온다.
    1. 이 때 arr 배열에는 이후 값을 덮어쓰기 때문에 제거할 필요 없다.

 

 

다소 난해하지만, 재귀 방식을 이용한다면 풀 수 있는 문제이다.
이번 풀이에서는 DFS와 비슷하다고 볼 수 있다.
일정 깊이까지 도달한 후 다시 돌아오는 알고리즘으로
디버깅을 통해 로직이 진행되는 순서를 확인하면 좋겠다.

전체 코드

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

public class Main {
    private static int [] arr;
    private static boolean [] visited;
    private static StringBuilder stringBuilder = new StringBuilder();

    private static void backTracking(int depth, int M, int N) {
        if(depth == M) {
            for(int i : arr) {
                stringBuilder.append(i).append(" ");
            }
            stringBuilder.append("\n");
            return;
        }

        for(int i = 0; i < N; i++) {
            if(!visited[i]) {
                visited[i] = true;
                arr[depth] = i + 1;
                backTracking(depth + 1, M, N);
                visited[i] = false;
            }
        }
    }

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

        int N = Integer.parseInt(st.nextToken());
        int M = Integer.parseInt(st.nextToken());

        arr = new int[M];
        visited = new boolean[N];

        backTracking(0, M, N);

        System.out.println(stringBuilder);
    }
}
반응형
반응형
이번 글에서는 데이터 삭제 이후 복구에 관한 포스팅을 해보려 한다.
휴지통에 있던 파일을 삭제했을 때,
Shift+delete를 이용하여 영구 삭제 했을 때,
다음과 같은 영구 삭제된 파일에 대해 복구이다.



파일 복구에 대해 급하신 분들이 많아 먼저 복구하는 방법 이후 왜 가능한지 같이 설명하겠다.

( 뭔가 추가로 알려주실 내용이 있다면 알려주시면 감사드립니다. )

 

파일 복구 프로그램은 많이 존재하는데 알다 시피 가격은 상당하다. 10만원 대의 프로그램.. 물론 소중한 데이터에 비하면 싼 가격이긴 하나 일 년에 한 번만 사용하면서 이 정도 가격을 주기는 고민이 많이 든다. 기능이나 UI 적으로는 편리하지만, 잠깐의 시간만, 몇 번의 클릭만 더해진다면 무료로 사용할 수 있다.

 

파일 복구 시에 가장 중요한 점은 무언가를 하지 않고 바로(빠르게) 복구를 하는 점이다.
여기서 무언가란 '조금이라도 다운로드 되는 것은 모두 다' 이다. 

 

 

간단하게 저장소인 디스크에 데이터가 삭제가 된다고 해도 사실 데이터의 정보는 남아 있다.

데이터의 위치 정보만 삭제하여 데이터를 찾지 못하게 하는 것이다. 그렇다면 그 데이터의 구조를 보고 추출하기만 한다면 데이터를 복구할 수 있다. 하지만 위치 정보가 없는 데이터인 상태로 다운로드 받는다면 예전의 데이터가 있던 곳에 덮어쓰게 된다. 이렇게 되면 점점 찾을 확률과 양이 줄어드게 된다.

 

SSD는 사실 어렵다.

SSD는 Trim이라는 옵션이 있다면 사실 복구하기는 힘들다. 이는 위 설명처럼 하드 디스크는 데이터 위치만 삭제하여 물리적으로 데이터가 남아있는 반면에 SSDTrim 옵션이 활성화 되어 있다면 삭제 즉시 데이터를 완전히 삭제해 버린다. 이는 복구 가능성이 매우 낮다. 다음 명령어를 통해 trim 옵션에 대해 확인 할 수 있다.

fsutil behavior query DisableDeleteNotify

 

  • DisableDeleteNotify = 0 → Trim 활성화됨 (복구 어렵거나 불가능)
  • DisableDeleteNotify = 1 → Trim 비활성화됨 (복구 가능성이 높음)

 

Windows 파일 복원 도구 사용 (Microsoft 공식 툴)

  • Windows file Recovery
  • Microsoft Store에서 Tool을 다운 받아 준다.
    • 이 정도의 다운은 거의 복구 가능하다.

 

 

복구 진행

  • 이후 관리자 권한으로 cmd를 실행하고 다음 명령어를 입력한다.

*****************명령어 입력전 주의사항*****************

저 또한 마찬가지로 실수로 D: 드라이브의 개인 내용을 잘못 삭제하여 복구하였지만,

항상 복구는 다른 디스크에 해야 합니다.

기존의 설명처럼 그 위에 덮어써버린다면 복구가 잘 되지 않는 현상(거의 불가)이 나타납니다.

 

D:에서 일어났다면? -> C:

C:에서 일어났다면? -> D:

나는 디스크가 하나다? -> usb나 추가 디스크 필요

 


ex) 모드 예시 - 1

winfr D: C:\Recovery /extensive
  • 복구할 디스크 D:
  • 복구내용 저장 공간 C:\Recovery
  • /extensive
    • 심층 검색 - 포맷된 데이터나 손상된 드라이브
    • 파일 시스템이 NTFS, FAT, exFAT, ReFS일 때 사용 가능
  • /regular
    • 일반 검색 - 최근 삭제된 파일 빠르게 복구
    • NTFS 드라이브에서 사용 가능
  • /segment
    • NTFS 드라이브 파일 조각화가 심한 경우 사용 
    • NTFS 드라이브에서 사용 가능

ex) 모드 예시 - 2

winfr D: C:\Recovery /extensive /n *.hwp /n *.hwpx /n *.docx /n *.txt

 

  • /n 해당 파일 확장자만 복구하도록 필터링
  • *.hwp → 한글(HWP) 문서
  • *.hwpx → 새로운 한글 문서 포맷
  • *.docx → MS Word 문서
  • *.txt → 일반 텍스트 문서

파일 여는 법

복구 이후 C:에서 아무리 찾아보더라도 보이지 않을 텐데 cmd에 다음과 같이 입력하면 파일을 열 수 있다.

explorer C:\Recovery

 

 

소중한 데이터 삭제하자마자 저도 식겁했습니다.
프로그램 가격은 너무 비싸고 원하는 데이터가 복구가 되는지도 모르겠고..
그러던 중 대학교 때 수업 중 디스크 삭제 방식의 이론이 기억나서
그 방식으로 살릴 수 있지 않을까?
데이터까지만 접근 가능하다면 분석해서 복구하려고 찾아보던 중
다행히 그런 툴을 발견해 복구에 성공했습니다.
다만 심층 검색모드로 아주 예전에 삭제했었던 데이터까지 대량의 데이터로 인해
다소 시간이 많이 걸렸으나 복구한 것에 대해 긍정적으로 생각하고 있습니다.

보편적으로 SSD + HDD 조합에 개인 데이터는 HDD에 두는 사람들이 많아
이 글이 모두에게 도움이 되었으면 합니다. 감사합니다.

 

반응형
반응형
백준 24060번 문제를 풀기 전에 병합 정렬 알고리즘의 로직에 대해
아주 자세히 포스팅 해보려 한다.
사실 이미지로 본다면 아주 쉬운데 이걸 구현하고자 하면 머리가 아플 수도 있다. 
때문에 이해하기 언제보더라도 이해하기 쉽게,
이번 포스팅은 한줄 한줄 뜯어보도록 하겠다.

합병 정렬 ( Merge Sort )

합병 정렬 or 병합 정렬이라고 하며, 비교 기반이고, 분할 정복 알고리즘 중 하나이다.

 존 폰 노이만이 1945년에 개발했다.

 

최악, 평균, 최선에서 일정한 시간 복잡도 O(n log n)를 가지며,

이는 배열을 나누는데 O(log n)

합병하는데 O(n)이 걸리기 때문이다.

 

순서는 다음과 같다.

 

1. 분할

정렬되지 않은 리스트를 절반으로 잘라 비슷한 크기의 두 부분으로 나눈다.

 

2. 정복

각 부분 리스트를 재귀과정으로 정렬한다.

 

3. 결합

두 부분을 하나의 정렬된 리스트로 합병한다.

 

4. 복사

임시 배열에 저장된 결과를 원래 배열에 복사

 


 

다음과 같은 예시로 알아보자

8개의 정수를 가진 배열 Array가 존재한다.

int[] arr = {38, 27,33, 1, 43, 3, 9, 10};

 

다음을 그림과 같이 표현할 수 있다.

 

사실 이 그림은 합병 정렬이라면 지겹게 보았을 거다.

근데 이 그림만 보고서는 재귀적 요소 때문에 코드가 잘 이해되지 않을 수 있다.

 

분할

private static void mergeSort(int[] arr, int left, int right) {
    if(left < right) {
        int mid = (left + right) / 2;

        mergeSort(arr, left, mid);
        mergeSort(arr, mid + 1, right);
        merge(arr, left, mid, right);
    }
}

 

결합

private static void merge(int[] arr, int left, int mid, int right) {

    int[] temp = new int[right - left + 1];
    int i = left, j = mid + 1;
    int k = 0;

    while(i <= mid && j <= right) {
        if(arr[i] <= arr[j]) {
            temp[k++] = arr[i++];
        }
        else {
            temp[k++] = arr[j++];
        }
    }

    while(i <= mid) {
        temp[k++] = arr[i++];
    }

    while(j <= right) {
        temp[k++] = arr[j++];
    }


    for (int l = 0; l < temp.length; l++) {
        arr[left + l] = temp[l];
    }


}

 

MergeSort 메서드 - 1 [ mergeSort(arr, 0, 7) ]

  •  다음과 같이 분할되어 재귀호출 한다.

MergeSort 메서드 - 2 [ mergeSort(arr, 0, 3) ]

  •  다음과 같이 분할되어 재귀호출 한다.

 

MergeSort 메서드 - 3 [ mergeSort(arr, 0, 1) ]

  • 이번에는 left == mid 와 mid + 1 == right 이기  때문에 if문에서 막혀 실행되지 않는다.
  • 바로 다음 merge() 메서드가 실행된다.

Merge 메서드 - 4 [ merge(arr, 0, 0, 1) ]

  • 먼저 병합할 개수만큼 temp 배열을 생성한다.
  • 이후 다음 코드와 같이 각각 시작할 값을 설정한다
    • 왼쪽 분할 리스트에서 시작값 --> left ex) 0
    • 오른쪽 분할 리스트에서 시작값 -> mid + 1 ex) 1

  • 이후 다음 첫 번째 코드로 인해 temp에 위 그림과 같이 저장된다.
  • 그 다음 두 번째 코드로 기존 배열에 정렬된 값이 저장된다.
    • 기존의 값들 만큼만 꺼내고 그 값들로만 정렬하여 다시 재배치 하기 때문에 그대로 넣어주기만 하면 된다.
while(i <= mid && j <= right) {
    if(arr[i] <= arr[j]) {
        temp[k++] = arr[i++];
    }
    else {
        temp[k++] = arr[j++];
    }
}
for (int l = 0; l < temp.length; l++) {
    arr[left + l] = temp[l];
}

MergeSort 메서드 - 5 [ mergeSort(arr, 2, 3) ] & Merge 메서드 - 6 [ merge(arr, 2, 2, 3) ]

  • 2번 째 스텝의 분할된 오른쪽 리스트를 처리할 차례이다.
  • 4번 째와 동일한 방법으로 실행된다. ( 간략하게 코드는 제외 후 그림만 )

Merge 메서드 - 7 [ merge(arr, 0, 1, 3) ]

  •  2번 째 스텝에서 재귀 호출을 모두 마치고 이 코드로 돌아왔다.
  • 그렇다면 이제 합병 정렬할 차례이다.

  • 먼저 초기 값들은 다음과 같이 구성된다.

  • 이후 다음과 같은 코드를 통해 진행된다.
while(i <= mid && j <= right) {
    if(arr[i] <= arr[j]) {
        temp[k++] = arr[i++];
    }
    else {
        temp[k++] = arr[j++];
    }
}

 

1회전 - arr[i] = 27 > arr[j] = 1

2회전 - arr[ i ] = 27 <= arr[ j ] = 33

3회전 - arr[ i ] = 38 > arr[ j ] = 33

  • 이 이후는 j = 4로 right = 3 보다 크기 때문에 while문을 빠져나온다.
  • 그렇다면 잔여값은 어떻게 될까

 

 

while(i <= mid) {
    temp[k++] = arr[i++];
}

while(j <= right) {
    temp[k++] = arr[j++];
}

 

  • 위 코드를 통해 기존의while(i <= mid && j <= right) { 코드 형태를 유지한 채 i or j 중 최대에 도달하지 못했다면 남은 자리를 잔여 값으로 채워준다.
  • 다음 잔여값을 넣어야 할 위치는? k가 가지고 있다.

  • 이후 이전과 같이 temp값을 그대로 넣어준다.
for (int l = 0; l < temp.length; l++) {
    arr[left + l] = temp[l];
}

이후

이후에는 분할된 남은 부분 즉, MergeSort 메서드 - 1 [ mergeSort(arr, 0, 7) ] 부분에서 이루어진 오른쪽 리스트들을 분할 정복하면 된다. 이는 그림과 같이 한번 직접 해보시길 바란다.

 

정렬 전 : [38, 27, 33, 1, 43, 3, 9, 10]
정렬 후 : [1, 3, 9, 10, 27, 33, 38, 43]

 

 

합병 정렬은 임시 메모리를 생성해야 하기 때문에
큰 배열에서는 메모리 사용량이 크다. 하지만 안정적인 정렬로 고려해서 사용하는게 좋다.

이번 정렬은 간단하면서도 복잡한 알고리즘으로
기본적인 분할 정복 알고리즘의 대표적인 예라고 생각한다.
때문에 꼭 이 알고리즘을 정복하고 넘어가는 것을 추천한다.

전체 코드

import java.util.Arrays;

//TIP 코드를 <b>실행</b>하려면 <shortcut actionId="Run"/>을(를) 누르거나
// 에디터 여백에 있는 <icon src="AllIcons.Actions.Execute"/> 아이콘을 클릭하세요.
public class Main {

    private static void mergeSort(int[] arr, int left, int right) {
        if(left < right) {
            int mid = (left + right) / 2;

            mergeSort(arr, left, mid);
            mergeSort(arr, mid + 1, right);
            merge(arr, left, mid, right);
        }
    }

    private static void merge(int[] arr, int left, int mid, int right) {

        int[] temp = new int[right - left + 1];
        int i = left, j = mid + 1;
        int k = 0;

        while(i <= mid && j <= right) {
            if(arr[i] <= arr[j]) {
                temp[k++] = arr[i++];
            }
            else {
                temp[k++] = arr[j++];
            }
        }

        while(i <= mid) {
            temp[k++] = arr[i++];
        }

        while(j <= right) {
            temp[k++] = arr[j++];
        }


        for (int l = 0; l < temp.length; l++) {
            arr[left + l] = temp[l];
        }


    }

    public static void main(String[] args) {
        int[] arr = {38, 27, 33, 1, 43, 3, 9, 10};
        System.out.println("정렬 전 : " + Arrays.toString(arr));
        mergeSort(arr, 0, arr.length - 1);

        System.out.println("정렬 후 : " + Arrays.toString(arr));
    }
}
반응형
반응형

 

이번 글에서는 백준 26069번 문제를
hashSet를 활용해서 풀어보자

 

 

문제 핵심

  • N : 사람들이 만난 기록 수
  • 최대 길이 20의 문자열인 다른 문자열 A와 B가 공백과 함께 한줄에 주어진다.
  • ChongChong 기록에서 1회 이상 주어짐
  • ChongChong을 만난 사람은 무지개 댄스에 감염
    • ( ChongChong과 chongchong은 다른 이름 )

풀이 과정

ChongChong을 만난 순간 무지개 댄스를 추게 된다.

즉, 무지개 댄스를 한 명이라도 추고 있다면 모두 HashSet에 넣는다.

(Hash)Set에서는 중복값을 제외시켜 저장하기 때문에 모든 입력이 끝난 후  HashSet의 크기를 측정 한다. 

 

핵심 코드

1. 한 줄씩 읽어온다.

2. 두 사람 모두 댄스를 무지개 댄스를 추는지 확인

3. 한 명이라도 춘다면 HashSet에 삽입

        rainbowDanceHumans.add("ChongChong");
        for (int i = 0; i < N; i++) {
           stringTokenizer = new StringTokenizer(bufferedReader.readLine());
           String firstPerson = stringTokenizer.nextToken();
           String secondPerson = stringTokenizer.nextToken();

           if(rainbowDanceHumans.contains(firstPerson) || rainbowDanceHumans.contains(secondPerson)) {
               rainbowDanceHumans.add(firstPerson);
               rainbowDanceHumans.add(secondPerson);
           }
        }

 

Set에 특성만 활용한다면 아주 쉽게 풀 수 있는 문제이다.

(추가) 다들 환절기에 몸 조심 하시길 바랍니다.
두통에 2일 간 작성하지 못했네요..
백준 문제뿐만 아니라 Java class나 다양한 예제로 찾아 뵙겠습니다.

 

전체 코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashSet;
import java.util.StringTokenizer;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer stringTokenizer;
        int N = Integer.parseInt(bufferedReader.readLine());

        HashSet<String> rainbowDanceHumans = new HashSet<>();

        rainbowDanceHumans.add("ChongChong");
        for (int i = 0; i < N; i++) {
           stringTokenizer = new StringTokenizer(bufferedReader.readLine());
           String firstPerson = stringTokenizer.nextToken();
           String secondPerson = stringTokenizer.nextToken();

           if(rainbowDanceHumans.contains(firstPerson) || rainbowDanceHumans.contains(secondPerson)) {
               rainbowDanceHumans.add(firstPerson);
               rainbowDanceHumans.add(secondPerson);
           }
        }

        System.out.println(rainbowDanceHumans.size());

    }
}
반응형
반응형
이번글에서는 백준 25192번 문제를 통해 집합과 맵에 대해 알아보고자 한다.
원래는 집합과 맵에 대해 설명하고 문제를 포스팅하지만,
해시, 트리, 맵, 집합 자바에서는 함께 사용되기 때문에
추후에 포스팅 하려고 한다.


문제 핵심

  • N : 채팅방 총 기록 수
  • ENTER : 사람의 입장
  • 나머지 : 유저의 닉네임
  • 구하려는 값 : 곰곰티콘 사용횟수

풀이 과정

채팅 봇으로 ENTER이후 처음으로 나타난 닉네임은 인사하는 곰곰티콘이다.

즉, 동일한 닉네임이 두번 나타났다면 일반채팅으로 곰곰티콘이 아니다.

우리는 곰곰티콘을 사용하는 순간으로 ENTER 이후 나타나는 닉네임을 한 번씩 체크하면 된다.

즉, Set 클래스를 이용해서 중복없이 저장하여 그 크기를 통해 알 수 있다.

 

핵심 코드

1. 한 줄씩 읽어온다.

2. ENTER인 HashSet을 초기화

3. 존재하지 않는 경우에만 값을 넣고 카운트 증가

HashSet<String> nickNames = new HashSet<>();

for (int i = 0; i < recordCount; i++) {

    String line = bufferedReader.readLine();

    if(line.equals("ENTER")) {
        nickNames.clear();
        continue;
    }

    if(!nickNames.contains(line)){
        nickNames.add(line);
        bearGreetCount++;
    }
}

 

다른 풀이

HashSet 클래스를 사용하며 Set이라는 클래스 의미를 생각하면 사실 위 코드는 Set 보다는 Hash를 이용하여 값을 빨리 찾아내는 방식이다. Set이라는 클래스도 모두 이용하려면 다음과 같다.

 

1. ENTER가 나온 경우에만 지금까지 넣었던 값 개수를 카운트 해주고 초기화 한다.

2. 이후 반복문이 종료된 이후에는 카운트 되지 않은 값들이 남아있기 때문에 추가로 카운트 한다.

HashSet<String> nickNames = new HashSet<>();

for (int i = 0; i < recordCount; i++) {

    String line = bufferedReader.readLine();

    if(line.equals("ENTER")) {
    	bearGreetCount += nickNames.size();
        nickNames.clear();
        continue;
    }

    nickNames.add(line);

}

bearGreetCount += nickNames.size();

 

이번 문제에서 사용한 HashSet 또한 다른 문제 풀이에 자주 사용되는 클래스이다.
Hash, Set, Map, Tree 등 다양한 구조를 이해하는 것이 좋다.
기본 구조로써 기본 개념을 숙지하고 넘어가자.

 

전체 코드

package org.example;

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

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));

        int recordCount = Integer.parseInt(bufferedReader.readLine());

        int bearGreetCount = 0;

        HashSet<String> nickNames = new HashSet<>();

        for (int i = 0; i < recordCount; i++) {

            String line = bufferedReader.readLine();

            if(line.equals("ENTER")) {
                nickNames.clear();
                continue;
            }


            if(!nickNames.contains(line)){
                nickNames.add(line);
                bearGreetCount++;
            }
        }

        System.out.println(bearGreetCount);
    }
}

반응형
반응형
이번 글에서는 백준 1037번 문제를 통해,
알고리즘 기본 문제 단골인 소수, 약수, 공배수 들 중 하나인 약수를 활용하여 문제를 풀어보자

문제 핵심

  • 첫째 줄 : 약수의 개수
  • 둘째 줄 : 약수들
  • 구하려는 정수 : N
    • ( 풀이 과정에서는 구하려는 정수 N을 x라고 설명 )

풀이 과정

자연수 x을 구하려면 1과 x을 제외한 주어진 약수 중에서 선택해야 한다.

만약 이 때 조건과 같이 30의 약수를 차례대로 나열하면 [ 2, 3, 5, 6, 10, 15 ] 이다.

그렇다면 양쪽에서 순서대로 모두 짝지어서 곱한다면 x인 30을 구할 수 있다.

즉, 제일 큰 값과 작은 값을 구한 후 곱해주면 된다.

 

핵심 코드

for (int i = 0; i < N; i++) {
    int num = Integer.parseInt(stringTokenizer.nextToken());
    if(num > max) {
        max = num;
    }
    if(num < min) {
        min = num;
    }
}

 

예외

9와 같은 약수는 1과 9는 제공되지 않기 때문에 3 숫자 하나만 주어진다.

이때 3의 제곱이 x가 되므로 첫째 줄이 1이라면 제곱해준다.

if(N == 1) {
    int num = Integer.parseInt(bufferedReader.readLine());
    stringBuilder.append(num * num);
}

 

단계적 풀이에서 브론즈 문제 수준으로
사실 비교적 아주 쉬운 문제이다.

이제는 마지막 브론즈 문제로 앞으로는 실버 상위부터 골드 문제가 다량으로 나올 예정이다.
지금까지 기초와 개념을 활용한다면 문제없이 풀 수 있을 것이다.


전체 코드

package org.example;

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

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer stringTokenizer;
        StringBuilder stringBuilder = new StringBuilder();
        int N = Integer.parseInt(bufferedReader.readLine());

        if(N == 1) {
            int num = Integer.parseInt(bufferedReader.readLine());
            stringBuilder.append(num * num);
        } else {
            int max = 0;
            int min = Integer.MAX_VALUE;

            stringTokenizer = new StringTokenizer(bufferedReader.readLine());
            for (int i = 0; i < N; i++) {
                int num = Integer.parseInt(stringTokenizer.nextToken());
                if(num > max) {
                    max = num;
                }
                if(num < min) {
                    min = num;
                }
            }

            stringBuilder.append(max * min);
        }

        System.out.println(stringBuilder);

    }
}
반응형
반응형
이번 글에서는 백준 110번 문제를 통해 이항계수를 활용하고
최대 값을 넘어서는 순간을 다루는 방법을 알아보자.

문제 핵심

  • 서쪽 ( N )
  • 동쪽 ( M )
  • N <= M
  • 최대 한 개의 다리만 연결

풀이 과정

이 문제는 조합론으로 mCn이라고 할 수 있다.

다만 M의 최대값이 30일 때 factorial의 값은 다음과 같다.

 

  • 1! = 1
  • 2! = 2
  • 3! = 6
  • 4! = 24
  • 5! = 120
  • 6! = 720
  • 7! = 5040
  • 8! = 40,320
  • 9! = 362,880
  • 10! = 3,628,800
  • 11! = 39,916,800
  • 12! = 479,001,600
  • 13! = 6,227,020,800
  • 14! = 87,178,291,200
  • 15! = 1,307,674,368,000
  • 16! = 20,922,789,888,000
  • 17! = 355,687,428,096,000
  • 18! = 6,402,373,705,728,000
  • 19! = 121,645,100,408,832,000
  • 20! = 2,432,902,008,176,640,000
  • 21! = 51,090,942,171,709,440,000
  • 22! = 1,124,000,727,777,607,680,000
  • 23! = 25,852,016,738,884,976,640,000
  • 24! = 620,448,401,733,239,439,360,000
  • 25! = 15,511,210,043,330,985,984,000,000
  • 26! = 403,291,461,126,605,635,584,000,000
  • 27! = 10,888,869,450,418,352,160,768,000,000
  • 28! = 304,888,344,611,713,860,501,504,000,000
  • 29! = 8,841,761,993,739,701,954,543,616,000,000
  • 30! = 265,252,859,812,191,058,636,308,480,000,000

한계는 이미 초과했다.

이 때 우리는 각 프로그래밍언어에서 제공하는 클래스를 사용하면 된다.

Java에서는 BigInteger가 있다.

 

핵심 코드

이항 계수1 문제 처럼 나왔지만 이번에는 동쪽이 숫자가 항상 크다.

( 값을 받을 때 반대로 받았다. 헷갈리지 않게 N M으로 선언하는게 좋다. )

int T = Integer.parseInt(bufferedReader.readLine());
int N, K;
BigInteger result;

for (int i = 0; i < T; i++) {
    stringTokenizer = new StringTokenizer(bufferedReader.readLine());
    K = Integer.parseInt(stringTokenizer.nextToken());
    N = Integer.parseInt(stringTokenizer.nextToken());


    result = factorial(N).divide(factorial(K).multiply(factorial(N - K)));
    stringBuilder.append(result).append("\n");
}

 

 

이번 문제는 자료형의 최대값을 한참 넘어서기 때문에,
그 부분만 해결할 수 있다면 이전 이항계수 문제를 활용해 아주 쉽게 풀 수 있는 문제이다.

꼭 BigInteger같은 클래스를 사용하지 않더라도 비슷한 방식으로
직접 문자로 만들어서 처리하는 방법도 존재한다.

 

전체코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.util.StringTokenizer;

public class Main {

    public static BigInteger factorial(int n) {
        BigInteger result = BigInteger.ONE;
        for (int i = 2; i <= n; i++) {
            result = result.multiply(BigInteger.valueOf(i));
        }
        return result;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer stringTokenizer;
        StringBuilder stringBuilder = new StringBuilder();

        int T = Integer.parseInt(bufferedReader.readLine());
        int N, K;
        BigInteger result;

        for (int i = 0; i < T; i++) {
            stringTokenizer = new StringTokenizer(bufferedReader.readLine());
            K = Integer.parseInt(stringTokenizer.nextToken());
            N = Integer.parseInt(stringTokenizer.nextToken());


            result = factorial(N).divide(factorial(K).multiply(factorial(N - K)));
            stringBuilder.append(result).append("\n");
        }

        System.out.println(stringBuilder);
    }
}
반응형
반응형

 

인공지능 모델 선정 중 무겁지 않고, 빠른 학습이 가능한 모델을 찾아보다
MobileNet을 보게 되었고, 여러 블로그와 논문을 보고 정리하는 글이다.

 

 

MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications

We present a class of efficient models called MobileNets for mobile and embedded vision applications. MobileNets are based on a streamlined architecture that uses depth-wise separable convolutions to build light weight deep neural networks. We introduce tw

arxiv.org

MobileNet ( 경량 모델 )

  • 모바일 및 임베 디드 비전 애플리케이션 용
  • 깊이별로 분리 가능한 컨볼류션 사용 - 가벼운 깊이를 구축
  • 소규모 네트워크에 관한 많은 논문들은 크기에만 초점을 맞추고 속도는 고려하지 않는데, MobileNet은 속도를 최적화 하는데 중점을 둔다. ( 물론 소규모 네트워크도 제공 하면서 )

Depthwise Separable Convolution

  • MobileNet Model은 Depthwise Separable Convolution을 기반으로 한다.
  • 표준 컨볼루션 ( standard convolution)을 두 단계로 분리하는 방법입니다.
  • 두 단계는 Depthwise Convolution과 1 x 1 PointWise Convolution 이다.
    • Depthwise Convolution: 각 입력 채널에 단일 필터를 적용합니다. 즉, 각 채널은 독립적으로 필터링됩니다.
      • 필터 적용시 Input채널의 개수와 필터의 수는 동일하게 작동합니다.
    • Pointwise Convolution: 이 후, 1×1 크기의 필터를 사용하여 Depthwise Convolution의 출력들을 결합합니다.

기존 표준 컨볼루션( Standard Convolution )의 총 연산량

  • Dk : 입력 값의 크기
  • M : 채널 입력 수
  • N : 출력 채널 수
  • Df : filter 크기

Depthwise Separable Convolution

  • Depthwise Convolution은 각 채널에 대해 개별적인 필터를 적용

Depthwise Separable Convolution

  • Depthwise Convolution은 각 채널에 대해 개별적인 필터를 적용

Depthwise Separable Convolution + Pointwise Convolution 연산량


Mobile Net Body Architecture

  • 초반부: 3×3 Conv + Depthwise Separable Convolution로 채널 확장
  • 중반부: Depthwise Conv를 반복해 특징 추출
  • 후반부: 1×1 Pointwise Conv로 연산량 줄이고 Global Average Pooling 적용
  • 마지막: Fully Connected Layer + Softmax로 최종 분류

 

  • 전체 연산량 중 **1×1 컨볼루션 (Pointwise Conv)**이 94.86% 차지 → 모델 최적화의 핵심!

Width Multiplier ( α )

  • 경량 네트워크이긴 하나, 특정 사용 사례에 맞춰 더 작은 모델이 필요할 수 있다.
  • Width Multiplier (α) 라는 매개변수를 도입하여 네트워크의 전체적인 채널 수를 줄이는 방식을 사용.
  • 특정 레이어에서 **입력 채널 수(M)과 출력 채널 수(N)**를 α 배로 줄임.
    • 입력 채널: αM
    • 출력 채널: αN

ex) 기본 MobileNet에서 어떤 레이어의 필터 수가 64라면,

  • α = 1.0 (기본 모델) → 64개
  • α = 0.75 → 48개
  • α = 0.5 → 32개
  • α = 0.25 → 16개

MobileNet의 연산량과 파라미터 수는 α² 비율로 감소.

  • 예를 들어 α = 0.5로 설정하면, 기본 모델 대비 연산량과 파라미터 수가 약 1/4로 줄어듦.

Resolution Multiplier ( ρ )

  • MobileNet에서 입력 이미지와 내부 feature map 크기를 줄이는 하이퍼파라미터입니다.
  • 연산량이 ρ² 비율로 감소 (해상도가 줄어들면 픽셀 개수가 제곱 비율로 감소하기 때문)

ex ) 이미지 해상도가 224, 224 라면,

  • ρ = 1.0 → 해상도 유지 (224×224)
  • ρ = 0.75 → 해상도가 75%로 감소 (192×192)
  • ρ = 0.5 → 해상도가 50%로 감소 (112×112)
  • ρ = 0.25 → 해상도가 25%로 감소 (56×56)

트레이드 오프 ( Trade-off )

  • α를 줄이면 모델 크기가 감소하여 속도가 빨라지지만, 정확도도 떨어질 수 있음.
  • 적절한 α 값을 선택하여 성능과 속도의 균형을 맞추는 것이 중요.
  • 연산량이 ρ² 비율로 감소하여 속도가 빨라지지만, 정확도도 떨어질 수 있음.
  • 하지만 해상도가 줄어들면 모델의 정확도가 낮아질 가능성이 있음

 


무거운 모델에 비해 다소 정확도가 낮을 수 있지만 ,
소규모 모델은 빠른 속도와 더 적은 리소스를 요구하는 장점이 있다.

이러한 점들을 고려하여 현재 모델을 배포할 환경에 맞는 선택을 하는 것이 중요하다.
또한, 데이터 가공 역시 중요한 요소이므로 이를 병행하여 고려하는 것이 좋다.
반응형

+ Recent posts