https://www.acmicpc.net/problem/2493

 

 

1. 단순 탐색 풀이 - O(n^2)

시간복잡도 O(n^2)를 가지는 모든 요소를 탐색하는 방식으로 풀이했다. 당연히, 시간초과로 실패했다.

N = int(input().strip())
heights = list(map(int, input().strip().split()))

def get_answer():
    answer = [0 for _ in range(N)]
    for i in range(len(heights), 0, -1):
        if i == 1:
            break
        current_top = heights[i - 1]
        for j in range(i - 1, 0, -1):
            left_top = heights[j - 1]
            if left_top >= current_top:
                answer[i - 1] = j
                break
    print(" ".join(map(str, answer)))

get_answer()

 

 

2. 스택 이용한 풀이 - 최악 O(n^2), 최선 O(n)

스택을 사용하면 각 요소를 한 번만 처리(삽입/삭제)하게 되어, 이미 검사한 요소들을 재검사하지 않아도 된다.

일반적으로 스택 기반 알고리즘에서는 리스트를 한 번 순회하며, 현재 요소보다 작은(또는 큰) 후보들은 스택에서 제거하는 식으로 진행하여, 각 요소가 스택에 한 번 push되고 최대 한 번 pop되도록 한다.

 

최악의 경우:
예를 들어, 만약 리스트가 내림차순 정렬되어 있다면, 각 요소마다 내부 for 루프가 거의 전체 남은 요소를 검사하게 된다.
따라서 전체 연산 횟수는 약 n+(n−1)+(n−2)+…+1=O(n^2)가 된다.

N = int(input().strip())
heights = list(map(int, input().strip().split()))

def get_answer():
    answer = [0 for _ in range(N)]

    while heights:
        height = heights.pop()

        for i in range(len(heights) - 1, -1, -1):
            if height < heights[i]:
                answer[len(heights)] = i + 1
                break

    print(" ".join(map(str, answer)))

get_answer()

 

 

3. 스택 이용한 풀이 - 최적화 O(n)

N = int(input().strip())
heights = list(map(int, input().strip().split()))

def get_answer():
    answer = [0 for _ in range(N)]
    stack = [] # 인덱스 저장

    for i in range(len(heights)):
        height = heights[i]
        # 스택에 있는 인덱스의 높이가 현재보다 작으면, 이들은 현재 탑의 신호를 받을 수 없음
        while stack and heights[stack[-1]] < height:
            stack.pop()

        # 스택이 남아 있으면, 스택의 최상단 인덱스가 현재 탑의 신호를 수신하는 탑이다
        if stack:
            answer[i] = stack[-1] + 1
        else:
            answer[i] = 0

        stack.append(i) # 현재 탑 인덱스 추가

    print(" ".join(map(str, answer)))

get_answer()
반응형

DFS(Depth-First Search)와 BFS(Breadth-First Search)는 그래프나 트리 탐색에서 사용하는 대표적인 탐색 알고리즘이다. 두 알고리즘 모두 특정 노드를 방문하고, 그 노드와 연결된 다른 노드들을 탐색하는 방식으로 동작한다. DFS는 깊이를 우선으로 탐색하고, BFS는 너비를 우선으로 탐색한다.

 

1. DFS (Depth-First Search, 깊이 우선 탐색)

DFS는 탐색할 때 가능한 한 깊게 들어가는 방식으로 탐색한다. 즉, 한 경로를 따라 끝까지 가고, 더 이상 갈 곳이 없으면 다시 돌아와 다른 경로를 탐색한다. 이 방식은 재귀스택을 사용해 구현할 수 있다.

 

DFS 예시: 그래프 탐색

function dfs(graph, start, visited = new Set()) {
    // 현재 노드 방문 처리
    visited.add(start);
    console.log(start);

    // 현재 노드와 연결된 노드들을 재귀적으로 방문
    for (let neighbor of graph[start]) {
        if (!visited.has(neighbor)) {
            dfs(graph, neighbor, visited);
        }
    }
}
// 그래프 표현 (인접 리스트)
const graph = {
    1: [2, 3],
    2: [4],
    3: [5],
    4: [],
    5: []
}

dfs(graph, 1);  // 출력: 1 2 4 3 5

 

DFS의 시간 복잡도

  • 시간 복잡도: O(V + E) (V는 노드 수, E는 간선 수)
  • 그래프의 모든 노드를 방문하고 모든 간선을 처리해야 하기 때문에, 위와 같은 복잡도를 가진다.

 

2. BFS (Breadth-First Search, 너비 우선 탐색)

BFS는 탐색할 때 가까운 노드부터 차례대로 방문한다. 즉, 특정 노드에서 연결된 모든 노드를 먼저 탐색한 후, 그 다음 깊이의 노드들을 탐색한다. 이 방식은 큐(Queue) 자료 구조를 사용해 구현한다.

 

BFS 예시: 최단 경로 탐색

function bfs(graph, start) {
    const queue = [start]; // 탐색할 노드를 저장할 큐
    const visited = new Set(); // 방문한 노드를 저장할 집합
    visited.add(start);

    while (queue.length > 0) {
        const node = queue.shift(); // 탐색할 노드를 큐에서 꺼냄
        console.log(node);

        for (let neighbor of graph[node]) {
            if (!visited.has(neighbor)) {
                visited.add(neighbor);
                queue.push(neighbor);
            }
        }
    }
}

// 그래프 표현 (인접 리스트)
const graph = {
    1: [2, 3],
    2: [4],
    3: [5],
    4: [],
    5: []
};

bfs(graph, 1);  // 출력: 1 2 3 4 5

 

BFS의 시간 복잡도

  • 시간 복잡도: O(V + E) (V는 노드 수, E는 간선 수)
  • 모든 노드와 모든 간선을 한 번씩 방문하기 때문에 DFS와 마찬가지로 같은 복잡도를 가진다.

 

DFS와 BFS의 차이

  • 탐색 방식: DFS는 깊이 우선으로 탐색하며, BFS는 너비 우선으로 탐색한다.
  • 구현 방식: DFS는 재귀나 스택을 사용하고, BFS는 큐를 사용해 구현한다.
  • 사용 사례:
    • DFS는 경로 탐색(예: 미로 찾기)나 백트래킹이 필요한 문제에 적합하다.
    • BFS는 그래프에서 최단 경로를 찾는 문제에 적합하다.

 

예시 문제

 

1. 미로 찾기

문제: N x M 크기의 미로가 주어질 때, 출발점에서 도착점까지 최단 경로를 찾으세요. 벽은 통과할 수 없으며, 상하좌우로만 이동할 수 있습니다.

 

// BFS
function shortestPath(maze, start, end) {
    const directions = [[0, 1], [1, 0], [0, -1], [1, 0]];
    const queue = [[start, 0]]; // [현재위치, 이동거리]
    const visited = new Set();
    visited.add(start.toString());

    while (queue.length > 0) {
        const [[x, y], distance] = queue.shift();

        if (x === end[0] && y === end[1]) return distance;

        for (let [dx, dy] of directions) {
            const newX = x + dx;
            const newY = y + dy;

            if (newX >= 0 && newY >= 0 &&
                newX < maze.length && newY < maze[0].length &&
                maze[newX][newY] === 0 && !visited.has([newX, newY].toString())
            ) {
                visited.add([newX, newY].toString());
                queue.push([[newX, newY], distance + 1]);
            }
        }
    }

    return -1; // 도착할 수 없는 경우
}

const maze = [
    [0, 1, 0, 0, 0],
    [0, 1, 0, 1, 0],
    [0, 0, 0, 1, 0],
    [0, 1, 1, 1, 0],
    [0, 0, 0, 0, 0]
];

console.log(shortestPath(maze, [0, 0], [4, 4]));  // 출력: 8 (최단 경로 거리)

 


2. 섬 개수 세기

문제: 2차원 배열에서 육지를 1, 바다를 0으로 나타내는 섬의 지도에서, 섬(연결된 1의 집합)의 개수를 구하세요.

 

// DFS
function numIslands(grid) {
    let count = 0;

    function dfs(grid, x, y) {
        if (x < 0 || y < 0 || x >= grid.length || y >= grid[0].length || grid[x][y] === '0') {
            return;
        }

        grid[x][y] = '0'; // 방문한 곳은 0 처리
        dfs(grid, x + 1, y);
        dfs(grid, x - 1, y);
        dfs(grid, x, y + 1);
        dfs(grid, x, y - 1);
    }

    for (let i = 0; i < grid.length; i++) {
        for (let j = 0; j < grid[0].length; j++) {
            if (grid[i][j] === '1') {
                count++;
                dfs(grid, i, j);
            }
        }
    }

    return count;
}

const grid = [
    ['1', '1', '0', '0', '0'],
    ['1', '1', '0', '0', '0'],
    ['0', '0', '1', '0', '0'],
    ['0', '0', '0', '1', '1']
];

console.log(numIslands(grid));  // 출력: 3 (3개의 섬)

 

반응형

cherry-pick

git cherry-pick <commit_hash>

git cherry-pick이란 다른 브랜치에 있는 커밋을 선택적으로 내 브랜치(현재 브랜치)에 적용시킬 때 사용하는 명령어다.

git cherry-pick <commit_hash_1> <commit_hash_2>

명령어 뒤에 commit hash를 나열해 주면 여러 개를 한 번에 적용시킬 수 있다.

git cherry-pick <commit_hash_1>...<commit_hash_4>

<commit_hash_1>...<commit_hash_4> 처럼 명령을 하면, 1번~4번 사이의 커밋들을 cherry-pick 하게 된다. 이때, hash1이 시간상 가장 빠른 커밋이고, hash1은 cherry-pick에 포함되지 않는다.

이 외에 대표적인 옵션들은 다음이 있다.

  • git cherry-pick <commit>: 해당 커밋을 현재 브랜치에 적용합니다.
  • git cherry-pick -n <commit>: 해당 커밋을 적용하지만 커밋을 생성하지 않습니다. (드라이 런 모드)
  • git cherry-pick -x <commit>: 적용된 커밋에 원본 커밋의 정보를 추가합니다.

 

 

conflict가 발생하는 경우

cherry-pick 하려는 커밋과 현재 브랜치 사이에 conflict가 발생할 수 있다. 이때 사용가능한 옵션이 두 가지가 있다고 한다.

  1. conflict를 수정하고 cherry-pick 진행하기
  • 이 경우는 merge를 할 때처럼, conflict난 파일들을 수정한다.
  • git add 로 수정된 코드를 git에 올린다.
  • git cherry-pick -continue 명령어를 통해 다시 진행시킨다.
  1. cherry-pick 중단하기
  • git cherry-pick -abort 명령어를 사용해 중단시켜, cherry-pick 이전 상태로 돌아간다.

 

주의점

cherry-pick을 하는 경우 같은 내용을 갖고 있는 커밋이 여러 개 생기기 때문에, 나중에 누가 누굴 cherry-pick 했는지 모르는 상황이 생길 수 있다. 가능하다면 git rebase를 사용하는 방법이 좋다고 한다.

반응형

싱글톤 패턴은 디자인 패턴 중 하나로, 특정 클래스의 인스턴스가 전체 시스템에서 오직 '한 개'만 생성되도록 보장하는 것을 목적으로 한다. 이 패턴을 사용하면 전역 변수를 사용하지 않고도 특정 클래스의 인스턴스에 대한 전역 접근이 가능하며, 이를 통해 코드의 유연성과 유지 보수성을 높일 수 있다. 예를 들어 로깅 라이브러리를 구현할 때, 여러 곳에서 로그를 출력하게 된다고 해보자. 이때 각각의 로그 출력 메서드마다 로깅 객체를 생성된다면 시스템 리소스의 낭비가 발생할 수 있다. 따라서, singleton 패턴을 사용하여 로깅 객체를 전체 시스템에서 한 번만 생성하도록 보장할 수 있다.

 

다음은 Java로 나타낸 싱글톤 패턴의 예제 코드다.

public class Logger {
    // 인스턴스 변수
    private static Logger instance = null;
    
    // 생성자를 private로 만들어 다른 클래스에서 인스턴스 생성을 막는다.
    private Logger() {}
    
    // getInstance() 메소드를 통해 전역적으로 유일한 인스턴스를 반환한다.
    public staitc Logger getInstance() {
    	if(instance == null) {
        	instance = new Logger();
        }
        return instance;
    }
    
    // 로그를 출력하는 메소드
    public void log(String message){
    	System.out.println(message);
    }
}

// Logger 인스턴스를 가져와서 로그를 출력한다.
Logger logger = Logger.getInstance();
logger.log("Hello, world!");

 

위 예시에서, Logger 클래스는 생성자를 private으로 만들어 다른 클래스에서 인스턴스를 생성할 수 없도록 했다. 대신, getInstance() 메서드를 통해 전역적으로 유일한 인스턴스를 반환하도록 했다. 이렇게 함으로써, Logger 클래스의 인스턴스는 오직 한 개만 생성될 수 있도록 보장할 수 있다. 위의 코드에서 Logger 클래스의 인스턴스를 가져와서  로그를 출력하게 되는데, 이때 getInstance() 메서드를 사용해 전역적으로 유일한 인스턴스를 가져오도록 했다. 따라서, Logger 클래스의 인스턴스는 오직 한 개만 생성되며, 로그를 출력하는 코드에서는 이를 공유하여 사용할 수 있다.

 

 

Singleton 패턴의 장점은 다음과 같다,.

 

1. 전역적인 인스턴스

singleton 패턴은 전역적으로 하나의 인스턴스만을 유지하므로, 모든 코드에서 해당 인스턴스에 대해 접근할 수 있다.

 

2. 리소스 절약

객체를 여러 번 생성하지 않으므로, 시스템 자원을 절약할 수 있다.

 

3. 데이터 공유

하나의 인스턴스를 공유하기 때문에, 데이터를 공유하거나 데이터를 수정하기 용이하다.

 

 

Singleton 패턴의 단점은 다음과 같다.

 

1. 테스트가 어려움

전역적인 인스턴스를 사용하므로, 테스트할때 다른 인스턴스를 사용할 수 없어 테스트가 어려울 수 있다.

 

2. 결합도 증가

다른 객체와의 결합도가 높아지므로, 유지보수가 어려워질 수 있다.

 

3. 멀티스레딩 이슈

싱글톤 패턴을 사용할 때, 멀티스레드 환경에서 동시에 인스턴스를 생성하려는 이슈가 발생할 수 있다.

 

 

그럼 Singleton 패턴을 언제 사용하는 것이 좋을지에 대해 알아본다면 다음이 있다.

 

1. 전역적으로 사용되는 객체

어떤 객체가 전체 시스템에서 단 하나만 존재해야할 때 사용한다.

 

2. 자원의 낭비를 방지해야 할 때

객체를 여러 개 생성할 필요가 없으므로, 시스템 자원의 낭비를 방지하기 위해 싱글톤 패턴을 사용한다.

 

3. 공유 자원

싱글톤 패턴을 사용하여 전역적으로 인스턴스를 공유할 수 있다. 예를 들어, 로깅이나 설정 정보를 공유하는 경우에 사용하기 적합하다.

 

4. 동일한 객체의 상태를 유지할 때

싱글톤 패턴을 사용해 하나의 인스턴스를 사용하면, 해당 객체의 상태를 유지할 수 있다.

 

 

 

Singleton 패턴은 다음과 같은 특징을 가지고 있다.

  1. 클래스 내부에서 인스턴스를 생성하고, 외부에서 직접 생성할 수 없도록 생성자를 private으로 선언한다.
  2. 유일한 인스턴스에 접근하기 위한 public static 메서드를 제공한다.
  3. 인스턴스가 이미 생성되어 있으면 해당 인스턴스를 반환하고, 생성되어 있지 않으면 인스턴스를 생성한 후 반환한다.

 

다음은 TypeScript를 사용한 싱글톤 패턴 예제 코드다.

class Singleton {
    private static instance: Singleton;
    
    // 생성자를 private으로 선언해 외부에서 인스턴스 생성을 막는다.
    private constructor() {}
    
    // 인스턴스를 반환하는 메서드를 static으로 선언하여 전역적으로 접근할 수 있게한다.
    public static getInstance(): Singleton {
        if(!Singleton.instance) {
            Singleton.instance = new Singleton();
        }
        return Singleton.instance;
    }
    
    public doSomething(): void {
    	console.log("Singleton instance is doing something...");
    }
}

// Singleton 클래스의 인스턴스를 생성할 수 없다.
// const instance = new Singleton(); // Error: 'constructor' is private.

// Singleton 클래스의 인스턴스는 getInstance 메서드를 통해서만 생성한다.
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

console.log(instance1 === instance2); // true

instance1.doSomething(); // "Singleton instance is doing something..."
instance2.doSOmething(); // "Singleton instance is doing something..."

 

위 코드에서 Singleton 클래스를 정의하고, private 생성자를 선언해 외부에서 인스턴스 생성을 막는다. 인스턴스를 반환하는 getInstance 메서드를 static으로 선언해 전역적으로 접근할 수 있도록 한다. 이때, 인스턴스가 생성되어 있지 않은 경우에만 인스턴스를 생성하고 반환하게 한다.

실제로 인스턴스를 사용할 때에는 getInstance 메서드를 호출해 인스턴스를 생성하고, 반환된 인스턴스에서 메서드를 호출한다. 인스턴스는 한 번 생성되면 전역적으로 유지되므로, 여러 곳에서 같은 인스턴스를 사용할 수 있다.

반응형

+ Recent posts