|  그리디란?

- 그리디 알고리즘은, 매 선택에서 지금 이 순간 가장 최선이라 생각되는 답을 선택해 결과를 도출하는 것을 말한다.

- 여기서 Greedy을 직역하면 '탐욕'을 뜻한다.

 

- 왜 '탐욕'이라는 말을 쓸까?

  그건 지금 이 순간의 부분적인 최적해가 항상 종합적인 최적해는 아닐 수 있음에도,

  욕심쟁이와 같이 지금 보는 이 부분적인 선택이 전체의 최선이라 단정짓기 때문이다.

  rf. 여러 개의 도시가 연결된 도로가 있고, 그 도로 간에 이동 거리가 각각 다른 상황에서

      가장 이동거리가 짧은 거리를 찾아야한다고 할 때, 그리디를 쓰면 가장 짧은 도로들을 선택하는 게 될 수 있다.

- 정확함 보다는 적당히 맞는 답을 찾을 때 좋은 알고리즘이다. 따라서, 그리디 알고리즘을 근사 알고리즘으로도 부른다.

 

- 언제 쓰는 게 좋을까?

   탐욕 선택 속성, 최적 부분 구조 특정을 가진 문제를 해결하는 데에 강점이 있다.

탐욕 선택 속성 지금 선택이 다음 선택에 영향을 주지 않음
최적 부분 구조 전체 문제의 최적해는 부분 문제의 최적해로 이루어짐

   ex. 서울에서 대구를 거쳐 부산까지 가는 최단 경로 : 서울~대구 최단 경로 + 대구 ~ 부산 최단 경로

 

출처:나무위키

- 그럼 그리디로 풀지 못하는 문제들은 뭘로 풀어야 하지? 

  - 그리디로 풀지 못하는 문제들을 나무위키에서는 아래와 같이 두 가지를 소개했다.

  - 두 문제 모두 DP(동적 계획법)으로 풀 수 있는데, 두 가지 모두 차후 풀이 후 정리해보려고 한다.

외판원 문제 https://www.acmicpc.net/problem/2098
배낭 문제 https://www.acmicpc.net/problem/12865

 

- 그리디 알고리즘의 풀이 순서

  순서 예시 (ex. N구의 멀티탭 하나로 여러 제품을 사용할 때)
1 최적해 선택
: 지금 가장 최선이라 생각되는 해를 선택
ex. 이미 N개가 껴있는 상태에서 최소 교체 수 얻기
      - 지금 제품이 이미 껴 있으면 무시하고
      - 안 껴져 있으면 껴있는 것 중에
        내 다음으로 쓸 제품 중이 아닌 것과 교체        
2 적절성 검사
: 지금 고른 최적해가 전체 문제의 제약 조건을 벗어나는지 확인
ex. N개만 들어가야 하는 조건 성립하는가
3 해 검사
: 현재까지 선택한 해 집합이 전체 문제를 해결하는지 검사
ex. 지금까지 고른 최소값이 전체의 최소가 맞는가

 

|  대표적인 그리디 문제 

Activity Selection Problem 가장 많은 활동을 하려면? https://www.acmicpc.net/problem/12018
거스름돈 문제 가장 적은 동전의 수는? https://www.acmicpc.net/problem/11047

 

|  그리디 문제집

https://www.acmicpc.net/step/33

 

그리디 알고리즘 단계

동전의 조건이 특별해서 동적 프로그래밍보다 빠르게 답을 찾을 수 있는 문제

www.acmicpc.net

 

 

[ 참고 및 출처 ]

부트캠프 강의 후 정리

[Do it 알고리즘 코딩 테스트 - 자바 편]

나무위키

'Computer Science > Algorithm' 카테고리의 다른 글

모듈러 산술  (0) 2022.08.25
[알고리즘] 분할정복  (0) 2022.08.25
[알고리즘] 탐색 - DFS, BFS, 이진 탐색  (0) 2022.08.25
HashSet<int[]>와 Hashset<ArrayList<Integer>>  (0) 2022.08.17
[알고리즘] 정렬  (0) 2022.08.11

|  탐색이란?

주어진 데이터들 중에서 원하는 데이터를 찾아내는 알고리즘

 

|  탐색의 종류

  언제? 특징 시간복잡도
DFS(깊이 우선 탐색) 그래프의 모든 노드를
탐색하고자 할 때
- 재귀 또는 스택을 통해 구현
- 트리의 pre-order, in-order, post-order 순회 모두 DFS이다.
* 인접 행렬 : O(V+E)
* 인접 리스트 : O(V^2)
BFS(너비 우선 탐색) 그래프의 가까운 지점부터 탐색하고자 할 때 - 큐를 통해서 구현
- 트리의 level-order 가 BFS이다.
* 인접 행렬 : O(V+E)
* 인접 리스트 : O(V^2)
이진 탐색      

* DFS, BFS 모두 간선의 갯수가 적을 때에, 인접 리스트를 쓰는게 유리하다.

 

1. DFS (깊이 우선 탐색)

DFS란, 루트에서 시작하여 다음 분기로 넘어가기 전에 해당 분기를 완벽하게 탐색하는 방법이다.

[출처] https://gmlwjd9405.github.io/2018/08/14/algorithm-dfs.html

- 참조한 블로그에서는 미로를 탐색하는 것을 이야기했는데, 미로의 여러 갈랫길 중 첫번째 길만 쫒아가다가 다시 돌아와(backtracking) 방문하지 않은 곳을 탐색하여, 결국 모든 곳을 가는 것을 DFS라고 했다.

- 하나의 노드 A와 인접한 노드가 B, C가 있다고 할 때 먼저 B의 분기(인접한 노드들)를 모두 마쳐야만, C를 갈 수 있다.

- 트리의 level-order을 제외한 나머지 순회들과의 차이점은 어떤 노드를 방문했는지를 방문 배열을 통해 검사한다는 것.

 

출처:위키백과

(1) 구현 

스택 재귀 (순환 호출)
1. 시작 노드 정한 후, 사용할 자료구조 초기화
- 인접 리스트로 그래프 표현
- 방문 배열 초기화 후 시작지점 true 표시
- 시작 노드를 스택에 삽입

2. 스택에서 노드 꺼낸 후 인접 노드 다시 삽입하며 방문 체크

3. 스택에 값이 없을 때까지 2를 반복
1. 사용할 자료구조 초기화
- 인접리스트로 그래프 표현
- 방문 배열 초기화

2. 루트 노드(=시작 노드)를 방문하여 방문 체크

3. 인접한 노드들 중 방문하지 않은 곳을 방문

4. 방문할 노드들이 없을 때까지 3를 반복

- 백준의 아래 문제를 통해 한 번 구현해보았다.

[1] 먼저 자료구조를 초기화한다. : 정점/간선 갯수나, 인접리스트, 방문배열은 전역으로 빼주었다.

public class Ex11724 {

    static int N;
    static int M;
    static List<List<Integer>> graph;
    static boolean[] visited;
    static int cnt;

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

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

        // 인접 리스트 그래프 초기화
        graph = new ArrayList<>();
        for (int i = 0; i <= N; i++) {
            graph.add(new ArrayList<>());
        }

        for (int i = 0; i < M; i++) {
            st = new StringTokenizer(br.readLine());

            int start = Integer.parseInt(st.nextToken());
            int end = Integer.parseInt(st.nextToken());

            graph.get(start).add(end);
            graph.get(end).add(start);
        }

        // 방문 배열 초기화
        visited = new boolean[N + 1];

        cnt = 0;
    }
}

[2] 문제에서 요구하는 연결망의 갯수 구하는 코드를 넣는다.

- 이 문제는 프로그래머스의 '네트워크' 문제와도 유사했는데, 분리된 연결망을 구하는 문제였다.

> 분기를 확인하기 위한 코드

// 2. dfs
for (int i = 1; i <= N; i++) {
    if (!visited[i]) {
        dfs_stack(i);
        cnt++;
    }
}

System.out.println(cnt);

> 예외 상황 : 노드가 하나밖에 없고 간선이 없는 경우 - 네트웍은 하나다.

// 노드가 하나밖에 없고 간선이 없어도 네트웍은 하나로 치부
if (N == 1 && M == 0) {
    System.out.println(1);
    return;
}

[3] 본격적인 dfs를 짜기

 

[3-1] 재귀 방식의 dfs

public static void dfs(int root) {

    // 루트 방문 체크
    visited[root] = true;

    // 인접한 노드들 중 방문하지 않은 곳을 방문
    for (int i = 0; i < graph.get(root).size(); i++) {

        int adjNode = graph.get(root).get(i);

        if (!visited[adjNode]) {
            dfs(adjNode);
        }
    }

}

 

[3-2] 스택 방식의 dfs

public static void dfs_stack(int start) {

    Stack<Integer> stack = new Stack<>();
    visited[start] = true;
    stack.push(start);

    while (!stack.isEmpty()) {
        int curNode = stack.pop();

        for (int i = 0; i < graph.get(curNode).size(); i++) {
            int adjNode = graph.get(curNode).get(i);

            if (!visited[adjNode]) {
                stack.push(adjNode);
                visited[adjNode] = true;
            }
        }
    }
}

[ 전체 코드 - 더보기 ]

더보기
package graph;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import java.util.StringTokenizer;


public class Ex11724 {

    static int N;
    static int M;
    static List<List<Integer>> graph;
    static boolean[] visited;

    static int cnt;

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

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

        if (N == 1 && M == 0) {
            System.out.println(1);
            return;
        }

        // 1. 시작 노드를 정한 후 사용할 자료구조 초기화
        // 인접 리스트 그래프 초기화
        graph = new ArrayList<>();
        for (int i = 0; i <= N; i++) {
            graph.add(new ArrayList<>());
        }

        for (int i = 0; i < M; i++) {
            st = new StringTokenizer(br.readLine());

            int start = Integer.parseInt(st.nextToken());
            int end = Integer.parseInt(st.nextToken());

            graph.get(start).add(end);
            graph.get(end).add(start);
        }

        // 방문 배열 초기화
        visited = new boolean[N + 1];

        cnt = 0;

        // 2. dfs
        for (int i = 1; i <= N; i++) {
            if (!visited[i]) {
                dfs_stack(i);
                cnt++;
            }
        }

        System.out.println(cnt);
    }

    public static void dfs(int root) {

        // 루트 방문 체크
        visited[root] = true;

        // 인접한 노드들 중 방문하지 않은 곳을 방문
        for (int i = 0; i < graph.get(root).size(); i++) {

            int adjNode = graph.get(root).get(i);

            if (!visited[adjNode]) {
                dfs(adjNode);
            }
        }

    }

    public static void dfs_stack(int start) {

        Stack<Integer> stack = new Stack<>();
        visited[start] = true;
        stack.push(start);

        while (!stack.isEmpty()) {
            int curNode = stack.pop();

            for (int i = 0; i < graph.get(curNode).size(); i++) {
                int adjNode = graph.get(curNode).get(i);

                if (!visited[adjNode]) {
                    stack.push(adjNode);
                    visited[adjNode] = true;
                }
            }
        }
    }

}

 

(2) 시간 복잡도

- DFS는 그래프의 정점의 수(V)와 간선의 수(E)를 통해 시간 복잡도를 표현하는데,

- 인접 행렬을 사용하는 경우, 행으로는 전체 정점(V)을, 열로는 인접한 정점과의 연결 여부(E)를 확인하므로, 

  -- O(V^2) 의 시간 복잡도를 가진다.

- 인접 리스트를 사용하는 경우, 각 정점(V)을 방문한 후, 인접 노드를 따라 하나씩 탐색하기 때문에,

  -- O(V + E)의 시간 복잡도를 가진다.

- 그래프를 완전 탐색할 때에는 따라서, 간선이 적을 때에 인접 리스트를 사용하는 것이 좋다.

 

 

2. BFS (너비 우선 탐색)

bfs란, 루트에서 시작해서 인접한 노드를 먼저 탐색하는 방법

[출처] https://gmlwjd9405.github.io/2018/08/15/algorithm-bfs.html

- DFS의 경우, 깊이 있게 탐색했다면, BFS는 가까운 지점을 먼저 탐색하는 "넓은" 탐색이다.

- 참고한 블로그에서는, 두 노드 사이의 최단 경로 혹은 임의의 경로를 찾고 싶을 때에 이 방법을 쓴다고 했다.

- BFS를 활용한 알고리즘이 바로 최단 경로를 구하는 다익스트라였다. 

  * BFS와 차이점이 있다면, 다익스트라는 DP를 사용한다는 점..

https://why-dev.tistory.com/247?category=958869 

 

[알고리즘] 최단 경로 구하기 - 1. 다익스트라

| 다익스트라 - 가중치가 있는 그래프에서, 최단 경로를 구하는 알고리즘 - 핵심 : 그래프의 인접리스트와 DP를 이용 - 기능 : 출발 노드와 모든 노드 간의 최단 거리 탐색 - 특징 : 간선은 모두 양

why-dev.tistory.com

- BFS는 구현 방법을 정리하기 보다, 문제를 보면서 얘기해보려고 한다.

- 아래는, 프로그래머스의 게임 맵 최단 거리를 구하는 문제이다.

https://school.programmers.co.kr/learn/courses/30/lessons/1844#

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

(1) 코드

: 아래를 보면 알 수 있듯이, 게임 최단 거리를 구하는 문제는 BFS로 풀어야 했는데,

: 그 이유는 사방위로 움직여서 갈 수 있는 경로 중 도착지에 가까운 경로를 선택하려면,

  깊이있는 탐색보다는 너비 탐색을 통해서 사방위중 도착지에 가까운 방향 (아래로, 우측으로)으로 먼저 탐색을 하고,

  탐색한 횟수에 대해 저장하며 나아가야 했기 때문이다.

: DFS로 풀게될 경우에는 이런 탐색 횟수를 저장하며 갈 때 너무 많은 시간이 소요된다. 

  (왜냐면 깊이 탐색 후, 다시 제자리로 돌아와서 또 다른 갈래로 깊이 탐색 후, 또 다른 갈래로 깊이 탐색을 해야해서...)

: 그렇기에 그래프 최단 경로를 구할 때에도 BFS를 응용한 다익스트라를 사용한다.

import java.util.LinkedList;
import java.util.Queue;

public class Solution {

    class Node {
        int r;
        int c;
        int cnt;

        public Node(int x, int y, int cnt) {
            this.r = x;
            this.c = y;
            this.cnt = cnt;
        }
    }

    int[][] maps;
    int[][] dir = {{0, 1},{1, 0},{0, -1},{-1, 0}};

    public int solution(int[][] maps) {
        this.maps = maps;

        return bfs(maps.length, maps[0].length);
    }

    public int bfs(int r, int c){

        Queue<Node> q = new LinkedList<>();
        q.add(new Node(0, 0, 1));
        maps[0][0] = 0;

        while (!q.isEmpty()) {
            Node cur = q.poll();

            if (cur.r == r - 1 && cur.c == c - 1) {
                return cur.cnt;
            }

            for (int[] d : dir) {
                int newR = cur.r + d[0];
                int newC = cur.c + d[1];

                if (newR >= 0 && newC >= 0 && newR < r && newC < c) {
                    if (maps[newR][newC] == 1) {
                        maps[newR][newC] = 0;
                        q.add(new Node(newR, newC, cur.cnt + 1));
                    }
                }
            }
        }

        return -1;
    }
}

 

 

[ 참고 및 출처 ]

부트캠프 수업 내용 정리

[Do it 알고리즘 코딩 테스트 - 자바 편]

https://gmlwjd9405.github.io/2018/08/14/algorithm-dfs.html

https://gmlwjd9405.github.io/2018/08/15/algorithm-bfs.html

프로그래머스 

'Computer Science > Algorithm' 카테고리의 다른 글

[알고리즘] 분할정복  (0) 2022.08.25
[알고리즘] 그리디  (0) 2022.08.25
HashSet<int[]>와 Hashset<ArrayList<Integer>>  (0) 2022.08.17
[알고리즘] 정렬  (0) 2022.08.11
[알고리즘] 알고리즘 개요  (0) 2022.08.09

|  HashSet<int[]>와 Hashset<ArrayList<Integer>>

문제를 풀다보면, 좌표를 집합에 넣어야 하는 경우가 있었다.

예를 들어, (0, 1), (2, 1) 

그래서 아무 생각 없이 아래와 HashSet<int[]>를 쓰게 되면 contains를 할 때 검색이 제대로 되지 않는 걸 볼 수 있다.

 

왜 그럴까?

 

이유는 배열의 경우 Hashcode를 비교하고, 

리스트의 경우 Wrapper된 Integer값을 비교하기 때문.

HashSet<int[]> int 배열의 hashcode를 비교
HashSet<ArrayList<Integer>> Integer List의 Wrapper객체(Integer)를 비교

 

따라서, 좌표와 같은 수열의 집합을 만든다고 하면 HashSet 안에 ArrayList로 넣는 것이 좋다.

 

[ 참고 ]

https://stackoverflow.com/questions/65454683/check-if-an-array-exists-in-a-hashsetint

'Computer Science > Algorithm' 카테고리의 다른 글

[알고리즘] 그리디  (0) 2022.08.25
[알고리즘] 탐색 - DFS, BFS, 이진 탐색  (0) 2022.08.25
[알고리즘] 정렬  (0) 2022.08.11
[알고리즘] 알고리즘 개요  (0) 2022.08.09
[백준 11660] 구간 합 구하기 5  (0) 2022.07.14

|  정렬이란

- 특정 값을 기준으로 데이터를 순서대로 배치하는 방법

- 기본 : 버블, 삽입, 선택

- 비교적 속도가 빠른 정렬 : 합병, 힙, 퀵, 트리

- 하이브리드 정렬 : 팀, 블록병합, 인트로

- 기타 : 기수, 계수(카운팅), 셸, 보고

 

|  정렬의 시간복잡도 요약

안정성이라는 것은, 어떤 데이터 { 1, 2, 3, 1, 1, 1 } 을 정렬한다고 할 때,

  같은 숫자 1에 대해서 { 1, 1, 1, 1 } 의 순서가 유지되도록 정렬되는 것을 '안정성이 있다'라고 한다.

- 아래의 보조 메모리는 추가적으로 드는 메모리를 이야기하는데,

  버블이나 삽입, 선택의 경우 swap시에 드는 변수 1개만 필요하기 때문에 보조메모리가 1개만 소요된다.

정렬 방법 시간 복잡도 보조 메모리 안정성
Ω(n) Θ(n) O(n)
버블 정렬 n n^2 n^2 1 o
삽입 정렬 n n^2 n^2 1 o
선택 정렬 n^2 n^2 n^2 1 x
합병 정렬 n log⁡n n log⁡n n log⁡n n o
정렬 n log⁡n n log⁡n n log⁡n 1 x
정렬 n log⁡n n log⁡n n^2 log⁡n x
트리 정렬 n log⁡n n log⁡n n^2 n x
기수 정렬 dn (*d:자릿수) dn dn n+k o
계수 정렬 n+k (*k:max) n+k n+k n+k o
셸 정렬 n log⁡n gap에 따라 다름 n^2 1 x

 

|  버블정렬 / 삽입 정렬 / 선택 정렬

// 버블 정렬
public static void bubbleSort(int[] arr) {
    for (int i = 1; i < arr.length; i++) {
        for (int j = 0; j < arr.length - i; j++) {
            if (arr[j] > arr[j + 1]) {
                swap(arr, j, j + 1);
            }
        }
    }
}

// 삽입 정렬
public static void insertionSort(int[] arr) {
    for (int i = 1; i < arr.length; i++) {
        for (int j = i - 1; j >= 0; j--) {
            if (arr[j] > arr[j + 1]) {
                swap(arr, j, j + 1);
            } else {
                break;
            }
        }
    }
}

// 선택 정렬
private static void selectionSort(int[] arr) {
    for (int i = 0; i < arr.length - 1; i++) {
        int min = i;
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[j] < arr[min]) {
                min = j;
            }
        }
        swap(arr, i, min);
    }
}

 

|  합병정렬 / 힙 정렬 / 퀵 정렬 / 트리 정렬

1. 합병정렬

public static void mergeSort(int[] arr, int[] tmp, int left, int right) {
    if (left < right) {
        int mid = (left + right) / 2;
        mergeSort(arr, tmp, left, mid);
        mergeSort(arr, tmp, mid + 1, right);
        merge(arr, tmp, left, right, mid);
    }
}

public static void merge(int[] arr, int[] tmp, int left, int right, int mid) {
    int lCursor = left;
    int rCursor = mid + 1;
    int idx = left;

    while (lCursor <= mid || rCursor <= right){
        if (lCursor <= mid && rCursor <= right) {
            if (arr[lCursor] <= arr[rCursor]) {
                tmp[idx++] = arr[lCursor++];
            }else {
                tmp[idx++] = arr[rCursor++];
            }
        } else if (lCursor <= mid && rCursor > right) {
            tmp[idx++] = arr[lCursor++];
        } else {
            tmp[idx++] = arr[rCursor++];
        }
    }

    for (int i = left; i <= right; i++) {
        arr[i] = tmp[i];
    }
}

2. 힙 정렬

public static void heapSort(int[] arr) {
    // 가장 끝의 자식이 있는 부모노드를 잡아서 최대힙구조로 정렬
    for (int i = arr.length / 2 - 1; i >= 0; i--) {
        heapify(arr, i, arr.length);
    }

    // 가장 앞의 노드가 최대값임을 이용하여 나머지 정렬
    for (int i = arr.length - 1; i > 0; i--) {
        swap(arr, 0, i);
        heapify(arr, 0, i);
    }        
}

public static void heapify(int[] arr, int parentIdx, int size) {
    int leftIdx = parentIdx * 2 + 1;
    int rightIdx = parentIdx * 2 + 2;
    int maxIdx = parentIdx;

    if (leftIdx < size && arr[maxIdx] < arr[leftIdx]) {
        maxIdx = leftIdx;
    }

    if (rightIdx < size && arr[maxIdx] < arr[rightIdx]) {
        maxIdx = rightIdx;
    }

    if (maxIdx != parentIdx) {
        swap(arr, parentIdx, maxIdx);
        heapify(arr, maxIdx, size);
    }
}

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

3. 퀵 정렬

- pivot 값은 처음값이거나 중앙값이 될 수 있다. (상황에 맞게 선택하면 된다)

- 퀵 정렬은 pivot을 기준으로 좌우 정렬을 하고, pivot의 양 옆을 다시 분할하여 정렬하는 것을 반복한다.

public static void quickSort(int[] arr, int left, int right) {
    // 탈출문
    if (left >= right) {
        return;
    }

    // 피봇값 지정
    int pivot = partition(arr, left, right);

    // 피봇값의 좌우를 다시 퀵소트
    quickSort(arr, left, pivot - 1);
    quickSort(arr, pivot + 1, right);
}

public static int partition(int[] arr, int left, int right) {
    // 피봇값과 양 옆의 커서를 지정
    int pivot = arr[left];
    int lCursor = left;
    int rCursor = right;

    // 1. 양 끝의 커서가 서로 같기 전까지
    // 우측은 피봇값보다 더 작은 값을 찾고
    // 좌측은 피봇값보다 같거나 더 큰 값을 찾아 swap
    // 2. 양 끝의 커서가 서로 같아지면
    // 한쪽 커서와 피봇을 swap
    while (lCursor < rCursor) {
    	while (arr[rCursor] > pivot && lCursor < rCursor) {
            rCursor--;
        }
    
        while (arr[lCursor] <= pivot && lCursor < rCursor) {
            lCursor++;
        }

        swap(arr, lCursor, rCursor);
    }
    swap(arr, left, lCursor);

    return lCursor;
}

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

4. 트리 정렬

- 이진탐색트리를 만들어 정렬하는 방식으로, 알고리즘 복잡도는 NlogN이다.

- 이 부분은 비선형구조 > 이진탐색트리 포스팅에 적은 AVL, Red-Black을 확인해주세요.

 

|  기수 정렬 / 계수 정렬 / 셸 정렬

1. 기수 정렬

- 기수 : 자릿수를 의미한다.

- 기수정렬 : 일의 자리, 십의 자리, 백의 자리.. 순으로 각 자릿수에 해당되는 수를 큐에 담은 후 기존 배열에 넣는 방식

public static void radixSort(int[] arr) {
    // 각 자리에 큐를 담은 배열
    ArrayList<Queue<Integer>> list = new ArrayList<>();
    for (int i = 0; i < 10; i++) { // 10진수 기준으로 자리 수 0 ~ 9
        list.add(new LinkedList<>());
    }

    // 최대 자릿수
    int maxLen = getMaxLen(arr);
    // 나누는 수
    int div = 1;
    // 배열에 넣어줄 인덱스
    int idx = 0;

    // 기수 정렬에 대한 반복문
    for (int i = 0; i < maxLen; i++) {
        // 해당 자릿수의 큐에 데이터를 추가
        for (int j = 0; j < arr.length; j++) {
            list.get((arr[j] / div) % 10).offer(arr[j]);
        }

        // 해당 자릿수의 큐를 가져와서, 기존 배열(arr)에 넣는다.
        for (int j = 0; j < 10; j++) {
            Queue<Integer> queue = list.get(j);

            while (!queue.isEmpty()) {
                arr[idx++] = queue.poll();
            }
        }

        // 그 다음 자릿수를 위한 준비작업
        idx = 0;
        div *= 10;
    }

}

// 데이터의 최대 자릿수 반환
public static int getMaxLen(int[] arr) {
    int maxLen = 0;
    for (int i = 0; i < arr.length; i++) {
        int len = (int)Math.log10(arr[i]); // 자릿수
        if (maxLen < len) {
            maxLen = len;
        }
    }
    return maxLen;
}

2. 계수 정렬

- 계수 : 관계가 있는 수, 상수를 의미한다.

- 계수정렬 : 상수 data 수열에서 최대값 + 1을 size로 배열에 생성해, 카운트한 후 다시 기존 배열에 넣는 방식

public static void countingSort(int[] arr) {
    // 최댓값의 + 1 만큼 배열을 잡아둔다
    int max = Arrays.stream(arr).max().getAsInt();
    int[] cntArr = new int[max + 1];

    // 숫자데이터에 해당되는 인덱스의 cnt를 하나 늘린다.
    for (int i = 0; i < arr.length; i++) {
        cntArr[arr[i]]++;
    }

    // cntArr을 한바퀴씩 돌면서 기존 배열(arr)에 해당 인덱스의 갯수만큼 삽입
    int idx = 0;
    for (int i = 0; i < cntArr.length; i++) {
        while (cntArr[i] > 0) {
            arr[idx++] = i;
            cntArr[i] -= 1;
        }
    }
}

//최댓값 하나만 너무 크면 비효율적 -> HashMap을 통해 보완할 수 있다
public static void countingSort2(int[] arr) {
    // key : data, value : 갯수
    HashMap<Integer, Integer> map = new HashMap<>();
    for (int item : arr) {
        map.put(item, map.getOrDefault(item, 0) + 1);
    }

    // key를 배열에 담아서 정렬한 후, 기존 배열(arr)에 해당인덱스의 갯수만큼 다시 삽입한다.
    ArrayList<Integer> keyList = new ArrayList<>(map.keySet());
    Collections.sort(keyList);
    int idx = 0;
    for (int key : keyList) {
        int cnt = map.get(key);
        while (cnt > 0) {
            arr[idx++] = key;
            cnt--;
        }
    }
}

3. 셸 정렬

- 삽입 정렬을 보완한 형태로 

- 삽입 정렬은 idx 1부터 arr.length-1까지 처음부터 좌측의 데이터를 확인했다면

- 셸 정렬은 간격을 두고 간격의 거리만큼 떨어진 데이터를 확인하는 것을 반복한다.

- 이렇게 하게 되면 기존의 삽입 정렬 보다는 교환 횟수가 감소할 수 있다.

public static void shellSort(int[] arr) {
    // 초기 gap을 설정
    int gap = arr.length / 2;

    // 더 이상 간격이 없을 때까지 간격을 /2한다.
    for (int g = gap; g > 0; g /= 2) {
        // 초기 간격부터 간격의 길이만큼 증가하면서
        for (int i = g; i < arr.length; i++) {
            // 지정된 위치의 좌측 간격들의 데이터를 비교 & 교환한다.
            int tmp = arr[i]; // 짚은 데이터를 tmp에 넣고
            int j = 0; // 짚은 데이터의 좌측 데이터들을 확인
            for (j = i - g; j >= 0; j -= g) {
                // 큰 값이 나오면 좌측 데이터들을 하나씩 당긴다
                if (arr[j] > arr[j + g]) {
                    arr[j + g] = arr[j];
                } else {
                    break;
                }
            }
            arr[j + g] = tmp;
        }
    }
}

 

 

[ 출처 ]

- 부트캠프 강의를 듣고 정리한 내용입니다.

|  알고리즘이란?

알고리즘이란, 어떤 문제를 해결하기 위한 절차나 방법을 말한다.

- 알고리즘의 조건

입력 데이터의 입력
출력 처리 후 출력
명확성 동작의 흐름(flow)에 대한 명확성
유한성 정해진 시간 및 공간 내에서의 처리
효율성 같은 동작을 하더라도 시간 및 공간 면에서 보다 효율적이어야 한다.

 

|  알고리즘은 결국 정확성과 시간 복잡도, 공간 복잡도를 말한다.

- 어떻게 보다 적은 자원을 효과적으로, 정확하게 만들어 내느냐가 알고리즘의 핵심

 

|  알고리즘 정리 개요 

- 앞으로 정리할 알고리즘 목록입니다.

- (글 작성 후 링크 업로드 예정)

정렬  
이진탐색 / 투 포인터  
그리디 알고리즘  
분할 정복 / 다이나믹 프로그래밍  
백 트래킹  
최단 경로  
최소 신장 트리  

 

 

[ 참고 및 출처 ]

부트 캠프 강의를 들은 후 정리한 내용입니다.

|  시작하기 전에

지난번에는 일차원 배열에 있는 데이터에 대한 구간합을 구하는 법을 다루었다면,

이번에는 이차원 배열에 있는 데이터에 대한 구간합을 구하는 법을 다루어보았다.

🔖 일차원 구간합에 대한 포스팅은 아래에

 

[백준 11659] 구간합

🛎 11659 구간합 | 문제 수 N개가 주어졌을 때, i번째 수부터 j번째 수까지 합을 구하는 프로그램을 작성하시오. | 입력 첫째 줄에 수의 개수 N과 합을 구해야 하는 횟수 M이 주어진다. 둘째 줄에는 N

why-dev.tistory.com

 

🛎 11660 구간 합 구하기 5

|  문제 

N×N개의 수가 N×N 크기의 표에 채워져 있다. (x1, y1)부터 (x2, y2)까지 합을 구하는 프로그램을 작성하시오. (x, y)는 x행 y열을 의미한다.

예를 들어, N = 4이고, 표가 아래와 같이 채워져 있는 경우를 살펴보자.

1 2 3 4
2 3 4 5
3 4 5 6
4 5 6 7

여기서 (2, 2)부터 (3, 4)까지 합을 구하면 3+4+5+4+5+6 = 27이고, (4, 4)부터 (4, 4)까지 합을 구하면 7이다.

표에 채워져 있는 수와 합을 구하는 연산이 주어졌을 때, 이를 처리하는 프로그램을 작성하시오.

|  입력

첫째 줄에 표의 크기 N과 합을 구해야 하는 횟수 M이 주어진다. (1 ≤ N ≤ 1024, 1 ≤ M ≤ 100,000) 둘째 줄부터 N개의 줄에는 표에 채워져 있는 수가 1행부터 차례대로 주어진다. 다음 M개의 줄에는 네 개의 정수 x1, y1, x2, y2 가 주어지며, (x1, y1)부터 (x2, y2)의 합을 구해 출력해야 한다. 표에 채워져 있는 수는 1,000보다 작거나 같은 자연수이다. (x1 ≤ x2, y1 ≤ y2)

|  출력

총 M줄에 걸쳐 (x1, y1)부터 (x2, y2)까지 합을 구해 출력한다.

|  예제 입력

4 3
1 2 3 4
2 3 4 5
3 4 5 6
4 5 6 7
2 2 3 4
3 4 3 4
1 1 4 4

|  예제 출력

27
6
64

 

🔎 문제분석

이 문제도 지난번 일차원 구간합 구하기 문제처럼, 반복문을 사용하면 시간 초과가 난다.

[1] 자료형을 뭘 써야할까? 만약, 여기서 단순 반복을 사용하면 얼마나 오랜 시간이 걸릴까?

아래는 입력값들의 범위인데,

변수명 입력값 범위
N 표의 크기 (행의 크기) 1 ~ 1,024
데이터 배열의 데이터 1 ~ 1,000
M 합을 구해야하는 횟수 1 ~ 100,000

- 표의 크기최대 1024 x 1024 이며, 그렇게 되면 최대 1,048,576개의 데이터 수를 받게 된다.

- 데이터의 크기는 최대 1000인데, 최대 표 크기에서 모든 데이터가 1000이라고 할 때,

  최대 범위인 (1, 1)부터 (N, N)까지 최대 구간합은 1,048,576,000가 된다. 

- int타입 데이터의 표현 범위는 -2,147,483,648 ~ 2,147,483,647(약 -21억 ~ 21억, -2^31 ~ 2^31 - 1)이기 때문에 

  구간합의 자료형은 int로 두어도 괜찮다.

 

이제 단순 반복을 했을 때에 얼마나 시간이 걸릴 것인가를 구해 보려고 한다.

[행 x 열]에 대한 이중 반복 루프에서 각 (x1, y1) ~ (x2, y2)까지의 합을 구한다고 하면 

- 시간 복잡도는 O(n^2)가 나온다. (만약 M만큼 범위를 입력받으면서 동시에 구간합을 처리하게 되면 O(n^3) )

 

만약에 최대 크기 배열M이 최댓값이 나온다하고 범위가 (1, 1) ~ (N, N)이면- 100,000 x 1024 x 1024(104,857,600,000)만큼 데이터를 확인하고 합을 구한다.

 

[2] 2차원 배열의 구간합을 구하는 방법을 구해보자..

(1) 먼저, 입력 배열은 D[N + 1][N + 1]로, 구간합 배열은 S[N + 1][N + 1]로 만들려 한다.

- 배열 크기는 입력을 (1, 1)부터 (N, N) 사이값으로 받기 때문에 N+1 x N+1으로 하는게 더 쉬웠다..

- (D는 dataArr의 약자고, S는 sumArr의 약자로 임의로 잡았다.)

 

(2) D --> S 로 구간합을 구하는 방법이다.

 

가. 먼저 행이 1일 때와, 열이 1일 때를 보면

  1 2 3 4 구간합
-------->
  1 2 3 4
1 1 2 3 4 1 1 3 6 10
2 2 3 4 5 2 3      
3 3 4 5 6 3 6      
4 4 5 6 7 4 10      

 

- 행이 1인 경우에는, S배열에서 바로 전 열(j - 1)의 값과 D[ i ][ j ] 를 더해주면 된다.

  S [ i ][ j ] = S [ i ][ j - 1 ] + D[ i ][ j ]

- 열이 1인 경우에도, S배열에서 바로 전 행(i - 1)의 값과 D[ i ][ j ] 를 더해준다. 

  S [ i ][ j ] = S [ i - 1 ][ j ] + D[ i ][ j ]

 

나. 행과 열이 1이 아닌 경우를 보면

  1 2 3 4 구간합
-------->
  1 2 3 4
1 1 2 3 4 1 1 3 6 10
2 2 3 4 5 2 3 8    
3 3 4 5 6 3 6      
4 4 5 6 7 4 10      

 

S[2][2]를 구하는 방식을 풀어보면 아래와 같다.

- S[1][2] = D[1][1] + D[1][2]

- S[2][1] = D[1][1] + D[2][1]

- S[2][2] = D[1][1] + D[1][2] + D[2][1] + D[2][2]

              = S[1][2] + S[2][1] - D[1][1] + D[2][2]

 

이를 토대로 공식을 만드면 아래와 같이 만들 수 있다.

- S [ i ][ j ] = S [ i ][ j - 1 ] + S [ i - 1 ][ j ] - S[ i - 1 ][ j - 1 ] + D[ i ][ j ]

 

=> 이 상태에서 다시 위의 가.로 넘어가면 N + 1 x N + 1 크기를 만들어줄 것이므로,

    S [ i ][ j ] = S [ i ][ j - 1 ] + S [ i - 1 ][ j ] - S[ i - 1 ][ j - 1 ] + D[ i ][ j ]

    만약 행이 1이라면, 그 위인 S[0][N] 값은 모두 0이 될 것이므로

    S [ i ][ j ] = S [ i ][ j - 1 ] + 0 - 0 + D[ i ][ j ]

                  = S [ i ][ j - 1 ] + D[ i ][ j ]  // 나. 의 공식을 그대로 사용해도 무방하다는 걸 볼 수 있다.

    마찬가지로 열이 1일때도 동일하다. 

 

[3] 구간합 배열에서 범위값을 구하는 방법

만약에 (1, 1)에서 (3, 3)까지의 구간합을 구하라 하면 범위값을 구하는 건 쉽다.

- 그냥 S[3][3]를 반환하면 된다.

 

그런데 (2, 2)에서 (3, 3)까지의 구간합을 구하라 한다면?

 

  1 2 3 4 구간합
-------->
  1 2 3 4
1 1 2 3 4 1 1 3 6 10
2 2 3 4 5 2 3 8 15 24
3 3 4 5 6 3 6 15 27 42
4 4 5 6 7 4 10 24 42 64

 

S[3][3]에서 (1, 2) ~ (1, 3)과 (2, 1) ~ (2, 3) 합을 빼주고 (1, 1)값을 더해주어야 한다.

- 만약 범위를 (x1, y1) ~ (x2, y2)라고 한다면

  구간합 범위를 구하는 식 : S[x2][y2] - S[x1-1][y2] - S[x2][y1-1] + S[x1 - 1][y1 -1]

 

⌨️ 코드

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

public class Main {

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

        // 입력
        int N = Integer.parseInt(st.nextToken()); // 행의 길이
        int M = Integer.parseInt(st.nextToken()); // 케이스 수

        // 기존 배열 D 
        int[][] D = new int[N + 1][N + 1];
        for (int i = 1; i <= N; i++) {
            st = new StringTokenizer(br.readLine());

            for (int j = 1; j <= N; j++) {
                D[i][j] = Integer.parseInt(st.nextToken());
            }
        }

        // 구간합 배열 S  
        int[][] S = new int[N + 1][N + 1];
        for (int i = 1; i <= N; i++) {
            for (int j = 1; j <= N; j++) {
                S[i][j] = S[i][j-1] + S[i-1][j] - S[i-1][j-1] + D[i][j];
            }
        }

        // 케이스당 하나씩 처리
        // 범위 (x1, y1) ~ (x2, y2)의 구간합 구하기
        for (int i = 1; i <= M; i++) {
            st = new StringTokenizer(br.readLine());

            int x1 = Integer.parseInt(st.nextToken());
            int y1 = Integer.parseInt(st.nextToken());
            int x2 = Integer.parseInt(st.nextToken());
            int y2 = Integer.parseInt(st.nextToken());

            int sum = S[x2][y2] - S[x1-1][y2] - S[x2][y1-1] + S[x1 - 1][y1 -1];

            sb.append(sum);
            sb.append("\n");

        }

        System.out.println(sb.toString());

    }
}

 

팰린드롬이란, 앞 뒤가 똑같은 전화번호 문자열을 말한다고 합니다.

단순히 "abba" 문자열이 팰린드롬인가를 확인한다면, 

[1] 문자열을 거꾸로 뒤집어서 확인하거나

[2] 양끝에 left, right로 커서를 두고 left < right 일때까지 양 끝을 비교해도 되는데요.

 

물론 우리의 알고리즘은 그렇게로만 끝나지 않기 때문에 더 향상된 버전의 팰린드롬을 만날 수 있었습니다.

제 수준에서 풀만한 것들을 먼저 뽑아보았어요..

아래 중에서 하나씩 풀어보려 합니다.

|  백준 팰린드롬 문제 모음

브론즈 1259 팰린드롬수 바로가기
실버 1213 팰린드롬 만들기
1254 팰린드롬 만들기
17609 회문
바로가기
바로가기
바로가기
골드 1053 팰린드롬 공장 바로가기

 

🛎 17609 회문

|  문제 

회문(回文) 또는 팰린드롬(palindrome)은 앞 뒤 방향으로 볼 때 같은 순서의 문자로 구성된 문자열을 말한다. 예를 들어 ‘abba’ ‘kayak’, ‘reviver’, ‘madam’은 모두 회문이다. 만일 그 자체는 회문이 아니지만 한 문자를 삭제하여 회문으로 만들 수 있는 문자열이라면 우리는 이런 문자열을 “유사회문”(pseudo palindrome)이라고 부른다. 예를 들어 ‘summuus’는 5번째나 혹은 6번째 문자 ‘u’를 제거하여 ‘summus’인 회문이 되므로 유사회문이다.

여러분은 제시된 문자열을 분석하여 그것이 그 자체로 회문인지, 또는 한 문자를 삭제하면 회문이 되는 “유사회문”인지, 아니면 회문이나 유사회문도 아닌 일반 문자열인지를 판단해야 한다. 만일 문자열 그 자체로 회문이면 0, 유사회문이면 1, 그 외는 2를 출력해야 한다. 

|  입력

입력의 첫 줄에는 주어지는 문자열의 개수를 나타내는 정수 T(1 ≤ T ≤ 30)가 주어진다. 다음 줄부터 T개의 줄에 걸쳐 한 줄에 하나의 문자열이 입력으로 주어진다. 주어지는 문자열의 길이는 3 이상 100,000 이하이고, 영문 알파벳 소문자로만 이루어져 있다.

|  출력

각 문자열이 회문인지, 유사 회문인지, 둘 모두 해당되지 않는지를 판단하여 회문이면 0, 유사 회문이면 1, 둘 모두 아니면 2를 순서대로 한 줄에 하나씩 출력한다.

|  예제 입력

7
abba
summuus
xabba
xabbay
comcom
comwwmoc
comwwtmoc

|  예제 출력

0
1
1
2
2
0
1

 

⌨️ 코드1 - 반복문

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

public class Main {

    public static int isPanlindrome(char[] arr, int left, int right, int delCnt){

        while (left < right) {
            if (arr[left] == arr[right]) {
                // 회문인 경우
                left++;
                right--;
            } else {
                // 회문이 아닌 경우
                int l2 = left;
                int r2 = right;

                // 좌측 문자를 하나 삭제 후 확인
                l2 += 1;
                while (l2 < r2) {
                    if (arr[l2] == arr[r2]){
                        l2++;
                        r2--;
                    } else {
                        delCnt++;
                        break;
                    }
                }

                if (delCnt == 0) { // abca와 같이 좌측 제거 후 유사회문인 경우 고려
                    return 1;
                }

                // 우측 문자를 하나 삭제 후 확인
                l2 = left;
                r2 = right - 1;
                while (l2 < r2) {
                    if (arr[l2] == arr[r2]){
                        l2++;
                        r2--;
                    } else {
                        delCnt++;
                        break;
                    }
                }

                // 좌우축 모두 확인 후에 delCnt를 반환
                return delCnt; // 반환하지 x면 무한 루프

            }
        }

        return delCnt;
    }

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

        int T = Integer.parseInt(br.readLine());

        while (T --> 0) {
            String s = br.readLine();
            sb.append(isPanlindrome(s.toCharArray(), 0, s.length()-1,0));
            sb.append("\n");
        }

        System.out.println(sb.toString());
    }
}

 

⌨️ 코드2 - 재귀

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

public class Main {

    // 재귀로 풀 경우
    public static int isPanlindrome2(char[] arr, int left, int right, int delCnt){

        while (left < right) {
            if (arr[left] == arr[right]) {
                // 회문인 경우
                left++;
                right--;
            } else {
                // 처음 불일치한 경우
                if (delCnt == 0) {
                    // 좌우측을 하나씩 삭제한 문자열이 회문인지 확인
                    if (isPanlindrome2(arr, left + 1, right, 1) == 0 ||
                            isPanlindrome2(arr, left, right - 1, 1) == 0) {
                        return 1;
                    } else {
                        return 2;
                    }
                } else {
                    return 2;
                }
            }
        }

        return 0;
    }

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

        int T = Integer.parseInt(br.readLine());

        while (T --> 0) {
            String s = br.readLine();
            sb.append(isPanlindrome2(s.toCharArray(), 0, s.length()-1,0));
            sb.append("\n");
        }

        System.out.println(sb.toString());
    }
}

행복한 수에 대한 연습문제를 풀다가, 백준에 관련된 문제가 있나 싶어 찾아보았습니다.

행복한 수만 구하는 문제는 아니고, 행복한 소수를 구하는 문제가 있었어요.

그래서 한 번 개념 정리를 먼저 하고 문제를 풀어보고 정리를 하려 합니다.

 

|  행복한 수란?

십진수의 수가 있을 때, 예를 들어 14가 있다고 할 때, 

각 자릿수를 제곱한 합은 1^2 + 4^2 = 18 입니다.

다시 29의 각 자릿수를 제곱한 합을 구하면 1^2 + 8^2 = 65

이 과정을 계속하다보면, 64,37,58,89,145,42,20,4,16,37,58,89,145,42.... 이 나오게 되는데요.

 

- 어떤 수는 14처럼 '1이 아닌 중복되는 수'를 지점으로 무한히 동일한 수열이 반복되는 반면,

- 어떤 수는 제곱합의 끝이 1이 되면서 이후에는 1이 반복되어 나옵니다.

 

'행복한 수'는 이처럼 제곱합의 끝이 1이 나오는 수를 이야기합니다.

 

[ 행복한 수를 찾는 코드 ]

public static boolean isHappy(int n) {

        // 중복된 지점부터는 저장X
        HashSet<Integer> set = new HashSet<>();

        while (set.add(n)) { //set이 이미 해당 값을 가지고 있을 때 false --> 루프탈출
            // 각 자릿수의 제곱합
            int sum = 0;
            while (n > 0) {
                sum += (int) Math.pow(n % 10, 2);
                n /= 10;
            }

			// 합이 1이 나오면 행복한 수, 아니면 set에 add하고 중복수 있는지 확인
            if (sum == 1) {
                return true;
            } else {
                n = sum;
            }
        }

        return false;
    }

 

|  소수 구하기

소수는 자연수 N을 1부터 N까지 나누었을 때 나누어떨어지는 수가 오직 1과 자기자신 뿐인 수를 말합니다.

단순 구현을 하게 되면 아래와 같이 풀 수 있는데요

// 방법1. 2부터 N - 1까지 루프를 돌려 n을 나누어 본다.
public static boolean isPrimeNumber(int n) {

    for (int i = 2; i <= n - 1; i++) {
        if (n % i == 0) {
            return false;
        }
    }

    return true;
}

아래의 두가지 원리를 이용해서 위를 개선하면 아래와 같았어요.

(1) 2가 아닌 짝수는 절대 소수일 수가 없다는 점 - ex. 4, 6, 8, 10.. 모두 2로 나누어떨어지죠
(2) 홀수 N을 짝수인 수로 나누면 나누어떨어지지 않는다는 점 - ex. N이 125일 때, 나누어떨어지는 수는 1, 5, 125
      > 이는 짝수인 수의 구구단을 생각하면 쉬웠어요. 
      > 예를 들어, 4의 구구단은 4x1 = 4, 4 x 2= 8, 4 x 3 = 12.... 모두 짝수가 나와요.
      > 결국 짝수인 수를 다른 자연수로 곱해봤자 절대 홀수가 나올 수 없다는 의미이죠.
// 방법2. 수학적 원리 사용 - 2가 아닌 짝수는 소수가 아님
public static boolean isPrimeNumber2(int n) {

    // 2가 아닌 짝수 여부
    if (n != 2 && n % 2 == 0) { 
        return false;
    }

    // 3부터 홀수로만 나누어본다.
    for (int i = 3; i <= n - 1; i+=2) {
        if (n % i == 0) {
            return false;
        }
    }

    return true;
}

 

🛎 10434 행복한 소수

|  문제 

Q. : 아래의 수열에서 다음에 올 수를 찾으시오.

313 331 367 ...

경복 : ??

강산 : 379.

경복 : 뭐?

강산 : 행복한 소수잖아.

경복 : 행복한 뭐?

강산 : 그러니까, 자리수의 제곱의 합을 구하는 연산을 계속 반복했을 때 1이 되는 수를 행복한 수라고 하잖아. 행복한 소수는 그 중 소수인 수이고.

7은 분명 소수이다. 과연 행복할까?

  • 7 → 72 = 49
  • 49 → 42 + 92 = 97
  • 97 → 92 + 72 = 130
  • 130 → 12 + 32 + 02 = 10
  • 10 → 12 + 02 = 1

7은 행복한 수이다 ☺.

사실 7은 행복한 소수 중 가장 작은 수이다. (이 문제에서는 1을 소수가 아닌 것으로 생각한다)

어떤 수가 주어지면 이 수가 행복한 소수인지 판정해보자.

|  입력

첫 줄에 테스트 케이스의 수 P가 주어진다. (1 ≤ P ≤ 1000)

각 테스트 케이스는 테스트 케이스 번호와 행복한 소수인지 판정해야 할 정수인 M으로 이루어져 있다. (1 ≤ m ≤ 10000).

|  출력

각 테스트 케이스마다, 테스트 케이스의 번호, 입력받은 수, 만일 M이 행복한 소수라면 YES 아니라면 NO를 공백으로 각각 구분하여 출력한다.

|  예제 입력

4
1 1
2 7
3 383
4 1000

|  예제 출력

1 1 NO
2 7 YES
3 383 YES
4 1000 NO

 

⌨️ 코드

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

public class Ex10434 {

	// 1단계 메서드 : 소수 확인 (단, 1은 소수가 아니다)
    public static boolean isPrimeNumber(int n) {

        // 탈출문1 : 1은 소수X
        if (n == 1) {
            return false;
        }

        // 탈출문2 : 2가 아닌 짝수는 소수x
        if (n != 2 && n % 2 == 0) {
            return false;
        }

        // 3 ~ N 까지 홀수로 나누어떨어지지 않는지 확인 
        for (int i = 3; i <= n - 1; i+=2) {
            if (n % i == 0) {
                return false;
            }
        }

        return true;
    }

	// 2단계 메서드 : 행복한 수 확인
    public static boolean isHappy(int n) {

        // 중복수 확인용 set
        HashSet<Integer> set = new HashSet<>();

        while (set.add(n)) { 
            // 각 자릿수의 제곱합이
            int sum = 0;
            while (n > 0) {
                sum += (int) Math.pow(n % 10, 2);
                n /= 10;
            }

            if (sum == 1) {
            	// 1이면 행복한 수
                return true;
            } else {
            	// 아니면
                n = sum;
            }
        }

		// 중복수가 나온 경우
        return false;
    }

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

        int P = Integer.parseInt(br.readLine()); // 케이스 수
        int[] allCase = new int[P];

        // 모든 케이스 입력
        while (P > 0) {
            st = new StringTokenizer(br.readLine());
            int idx = Integer.parseInt(st.nextToken()) - 1;
            int num = Integer.parseInt(st.nextToken());

            allCase[idx] = num;

            P--;
        }

        // 각 케이스별 행복한 소수여부 확인
        for (int i = 0; i < allCase.length; i++) {

            System.out.printf("%d %d ", i + 1, allCase[i]);

            if (!isPrimeNumber(allCase[i])){
                System.out.print("NO\n");
                continue;
            }

            if (!isHappy(allCase[i])) {
                System.out.print("NO\n");
                continue;
            }

            System.out.print("YES\n");

        }

    }
}

🛎 11659 구간합

|  문제 

수 N개가 주어졌을 때, i번째 수부터 j번째 수까지 합을 구하는 프로그램을 작성하시오.

|  입력

첫째 줄에 수의 개수 N과 합을 구해야 하는 횟수 M이 주어진다. 둘째 줄에는 N개의 수가 주어진다. 수는 1,000보다 작거나 같은 자연수이다. 셋째 줄부터 M개의 줄에는 합을 구해야 하는 구간 i와 j가 주어진다.

|  출력

총 M개의 줄에 입력으로 주어진 i번째 수부터 j번째 수까지 합을 출력한다.

|  예제 입력

5 3
5 4 3 2 1
1 3
2 4
5 5

|  예제 출력

12
9
1

 

🔎 문제분석

이 문제를 반복문으로 풀려고 했을 때, 계속적으로 시간초과가 나타났다.

입출력을 내가 아는 모든 방법을 총동원해서 변경해보았지만 역부족.

'뭐지, 풀 수 없는 문제인가' 했었는데, 그게 아니라 이 문제를 푸는 다른 방법이 있어서였다.

 

※ 구간합을 구하는 방법

[1] 먼저, 둘째줄에서 받은 N개의 수들을 arr배열에 넣어준다.

[2] arr배열을 토대로 합배열을 별도로 만든다.

인덱스 0 1 2 3 4
arr 5 4 3 2 1
hap 5 9 12 14 15

[3] 구간 (x1, x2)가 있다고 할 때, 구간의 합을 구하는 방법은 다음과 같다.

1) 만약 x1이 1이라면 : 인덱스 0 부터 x2-1까지의 누적합을 구하는 것이므로 그냥 hap[x2-1]를 반환하면 된다.

2) 만약 x1이 1보다 크면 : 예를들어 (2, 5)라고 할때

    >> 실제 인덱스 범위는 1~4일 것이다.

    hap[4] = arr[0] + arr[1] + arr[2] + arr[3] + arr[4]

    hap[1] = arr[0] + arr[1]

    -----------------------------------------------------------------------------------

    hap[1~4] = arr[1] + arr[2] + arr[3] + arr[4]    <------- 구하려는 값

    >> 구하려는 값이 나오기 위해서는 실제 인덱스 범위는 결국 (x1-2, x2-1)일 것이다.

 

이제 위 원리는 바탕으로 코드를 만들면 다음과 같다.

 

⌨️ 코드

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.StringTokenizer;

public class Ex11659 {
    public static void main(String[] args) throws Exception{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        int N = Integer.parseInt(st.nextToken()); //수의 개수
        int M = Integer.parseInt(st.nextToken()); //합을 구해야 하는 횟수

        // 입력받은 수 배열
        int[] arr = new int[N];
        st = new StringTokenizer(br.readLine());
        for (int i = 0; i < N; i++) {
            int num = Integer.parseInt(st.nextToken());
            arr[i] = num;
        }

        // 합 배열을 구한다
        int[] hap = new int[N];
        for (int i = 0; i < N; i++) {
            if (i == 0) {
                hap[i] = arr[i];
                continue;
            }
            hap[i] = hap[i - 1] + arr[i];
        }

        // 구간합을 구한다.
        for (int i = 0; i < M; i++) {

            st = new StringTokenizer(br.readLine());

            int x1 = Integer.parseInt(st.nextToken());
            int x2 = Integer.parseInt(st.nextToken());

            int sum = 0;
            if (x1 == 1) {
                sum = hap[x2 - 1];
            }else {
                sum = hap[x2 - 1] - hap[x1 - 2];
            }

            bw.write(String.valueOf(sum));
            bw.write("\n");

        }
        
        bw.flush();
        bw.close();
        
    }
}

 

🛎 2231 분해합

|  문제 

어떤 자연수 N이 있을 때, 그 자연수 N의 분해합은 N과 N을 이루는 각 자리수의 합을 의미한다. 어떤 자연수 M의 분해합이 N인 경우, M을 N의 생성자라 한다. 예를 들어, 245의 분해합은 256(=245+2+4+5)이 된다. 따라서 245는 256의 생성자가 된다. 물론, 어떤 자연수의 경우에는 생성자가 없을 수도 있다. 반대로, 생성자가 여러 개인 자연수도 있을 수 있다.
자연수 N이 주어졌을 때, N의 가장 작은 생성자를 구해내는 프로그램을 작성하시오.

|  입력

첫째 줄에 자연수 N(1 ≤ N ≤ 1,000,000)이 주어진다.

|  출력

첫째 줄에 답을 출력한다. 생성자가 없는 경우에는 0을 출력한다.

|  예제 입력

216

|  예제 출력

198

 

🔎 문제분석

어떤 수 N의 분해합을 구하는 거라면 쉬웠을 텐데, 분해합의 가장 작은 생성자(=곧, 어떤 수 N)를 찾는 문제라 난해했다.

- 이 문제는 분해합과 생성자의 관계를 파악하려 하면 더 힘들었다. 
  * 왜냐하면 생성자->분해합은 값이 하나로 떨어지지만
  * 분해합->생성자를 구하는 건 결과값이 여러 개일 수 있기 때문이다.
- 여기저기 알아보다보니 어차피 O(N) 속도라면, 1부터 1000000까지 돌리면서 분해합이 N인 경우를 찾기도 한 걸 봤는데,
  아무리 O(N)이라지만, 백만개를 다 돌리는 건 비효율적일 것 같아 다른 방법을 찾아보았다

 

(1) N의 생성자의 최대값은 N - 1이다.

주어진 분해합 값 N의 범위는 (1 ≤ N ≤ 1,000,000) 

==> 그렇다면 N의 생성자의 자릿수는 최대 6개 곧, 999,999가 된다. (생성자는 분해합 보다 적어도 1만큼 적을 것)

 

(2) N의 생성자의 최소치는 N - 54이다.

* M = N의 생성자

N = M + (M의 각 자리수에 대한 합) 

==> M의 각 자릿수의 최대치는 999,999이다.

==> N = 999,999 + (9 * 6)

==> (M의 각 자리수에 대한 합)의 최대치는 (9 * 6)이다.

==> M의 최소치는 곧, 54

N - (9 * 6) = M

 

(3) (1)~(2)를 종합해보았을 때, N의 생성자의 범위는 

N - 54 <= M <= N - 1

 

⌨️ 코드

 

+ Recent posts