Dynamic Progamming 이란?

  • 알고리즘에서의 '동적'은 별 의미가 없다. '기억하기'로 생각하면 편하다.
  • '프로그래밍'은 '테이블을 만든다'는 뜻이라고 생각하면 됨.

중복되는 문제

  • 피보나치 수열 코딩 시, F(n) = F(n-1) + F(n-2) 이기 때문에 단순 재귀 함수로 구현하면..
public class Simple {

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		int n = sc.nextInt();
		dp  = new int[n+1];
		System.out.println(fibo(n));
	}
	
	// 단순 재귀
	static int fibo(int x) {
		if( x ==1 || x==2) return 1;
		return fibo(x-1) + fibo(x-2);
	}
}
  • 위 코드와 같이 메모이제이션을 사용하지 않으면, 같은 함수를 중복 호출하기 때문에
    재귀 함수로 구현하면 시간 복잡도 O(2^N)을 갖는다.
  • 중복되는 호출로 인해 굉장히 좋지 않은 효율을 갖는다.

DP 문제가 성립할 조건

  1. 최적 부분 구조 (Optimal Substructure) : 상위 문제를 하위 문제로 나눌 수 있으며 하위 문제의 답을 모아서 상위 문제를 해결할 수 있다! ( 겨수님 : "작은 문제의 답들 사이의 관계.. ")
  2. 중복되는 부분 문제 : 동일한 작은 문제를 반복적으로 해결해야 한다.

피보나치 수열은 DP 사용 조건에 만족한다.

 

DP 알고리즘 기법은 무엇인가?

  • DP 알고리즘 기법은 이미 계산된 (하위 문제)는 별도의 메모리 영역에 저장하여 다시 계산하지 않도록 설계함으로써, 
    메모리를 적절히 사용하여 수행 시간 효율성을 비약적으로 향상시키는 방법이다.
  • DP 구현 방법은 일반적으로 두 가지 방식, Top-down(하향식)과 Bottom-up(상향식)으로 구성된다.

 

Top-down (하향식)

  • 상위 문제를 해결하기 위해서 하위 문제를 재귀적으로 호출하여 상위 문제를 해결하는 방식
    하위 문제를 저장해 놓기 위해 Memoization이 사용된다.
  • 하향식 방식은 재귀 호출을 사용하여 구현한다.
public class Topdown {
	static int[] dp;
        
        public static void main(String[] args) {
		    Scanner sc = new Scanner(System.in);
		    int n = sc.nextInt();
		    dp  = new int[n+1];
		    System.out.println(fibo(n));
        }
        static int fibo(int x) {
        	if(x == 1 || x == 2) {
                		return 1;
                }
                if(dp[x] != 0) {
                		return dp[x];
                }
                dp[x] = fibo(x-1) + fibo(x-2);
                return dp[x];
        }
}

Bottom-up (상향식)

  • 하위에서부터 문제를 해결해나가면서 먼저 계산했던 문제들의 값을 활용해서 상위 문제를 해결해나가는 방식
  • DP의 전형적인 형태인 상향식 방법이다.
  • 여기서 사용되는 문제 결과 값 저장용 리스트는 DP 테이블이라고 부른다.
  • 상향식은 반복문을 사용하여 구현한다.
public class BottodmUp {
	static int[] dp;
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		int n = sc.nextInt();
		dp  = new int[n+1];
		System.out.println(fibo(n));
		
	}
    
    
    static int fibo(int x) {
    	dp[1] = 1;
        dp[2] = 1;
        for(int i=3; i<x+1; i++) {
        	dp[i] = dp[i-1] + dp[i-2];
        }
        return dp[x];
    }
}

 

 

Memoization

  • 메모이제이션은 DP 구현 방법 중의 하나로, 한번 계산된 결과를 메모리 공간에 메모하는 기법이다.
  • 같은 문제를 다시 호출하면 메모했던 결과를 그대로 가져온다.
  • 값을 기록한다는 부분에서 캐싱이라고도 함.
  • DP에만 국한된 개념이 아니다. 한 번 계산된 결과를 담아 놓기만 하고 DP 가 아닌 다른 방식으로 사용될 수 있다.

 

 

[알고리즘] 동적계획법 DP (Dynamic Programming) 정리 (Java)

동적계획법 Dynamic Programming 동적 계획법은 프로그래밍 대회 문제에 가장 자주 출현하는 디자인 패러다임 중 하나로, 이름만 가지고는 무엇을 의미하는지 영 알 수 없기 때문에 가장 많은 오해를

loosie.tistory.com

 

'Algorithm' 카테고리의 다른 글

투포인터 (Two Pointers)  (2) 2024.01.18
Char 형의 모든 것  (2) 2024.01.18
DFS (Depth-First Search)  (1) 2024.01.18
위상정렬(Topological Sort)  (1) 2024.01.10

투포인터 알고리즘 (Two Pointers)

  • 리스트에 순차적으로 접근해야 할 때, 두 개의 점의 위치를 기록하면서 처리하는 알고리즘
  • 정렬되어 있는 두 리스트의 합집합에도 사용된다. 
  • 병합 정렬에 쓰임

투포인터 알고리즘의 대표 유형 - 특정한 합을 가지는 부분 연속 수열 찾기

  • 어떤 숫자들의 리스트가 주어질 때, 해당 리스트의 연속 수열의 합이 특정 값을 가지는 것을 확인하는 문제
  1. 시작점과 끝점이 첫번째 원소의 인덱스를 가리키도록 함.
  2. 현재 부분 합이 M과 같다면 카운트
  3. 현재 부분 합이 M보다 작다면 end를 1 증가
  4. 현재 부분 합이 M보다 크거나 같다면 start를 1 증가
  5. 모든 경우를 확인할 때까지 2~4 과정을 반복한다.

투 포인터 알고리즘 구현하기

int n = 5 //데이터 개수 N
int m = 5 //찾고자 하는 부분합 M

int count = 0;
int interval_sum = 0;
int start = 0;
int end = 0;

//start를 차례대로 증가시키며 반복
while(start < n) {
    //end만큼 이동시키기
    while(interval_sum < m && end < n) {
    	interval_sum += data[end];
        end++;
    }
    //부분합이 M일때 카운트 증가
    if(interval_sum == m) {
    	count++;
    }
    interval_sum -= data[start];   
    start++;
}

System.out.println(count);

투 포인터의 시간복잡도는 O(N)

  • 매 루프마다 항상 두 포인터 중 하나는 1씩 증가하고, 각 포인터가 n번 누적 증가해야 알고리즘이 끝난다.
    • 각각 배열 끝에 다다르는데 O(N)이라 둘을 합해도 여전히 O(N)이다.

 

 

[Algorithm] 투포인터(Two Pointer) 알고리즘

알고리즘 문제를 풀다보면 종종 나오는 투포인터 알고리즘! 막 꼬여가지고 ㅋㅋㅋ 저도 중간에 제대로 못짜고 그러는 경우가 많은데요, 많은 코딩테스트 문제에 등장하는 것은 아니지만 잊을만

butter-shower.tistory.com

 

'Algorithm' 카테고리의 다른 글

Dynamic Programming (동적 계획법)  (3) 2024.01.24
Char 형의 모든 것  (2) 2024.01.18
DFS (Depth-First Search)  (1) 2024.01.18
위상정렬(Topological Sort)  (1) 2024.01.10

소문자 'a'는 97이다

대문자 'A'는 32이다

 

charAt

String str = "aabbbaa";

for(int i=0; i<str.length; i++) {
	//str[i] x
    //str.charAt(i) o
}

 

char형 문자끼리 비교할 때 형변환 시 괄호 꼭 붙여야 오류가 나지 않는다?!

temp.charAt(temp.length() - 1) == (char)('a' + i))

'Algorithm' 카테고리의 다른 글

Dynamic Programming (동적 계획법)  (3) 2024.01.24
투포인터 (Two Pointers)  (2) 2024.01.18
DFS (Depth-First Search)  (1) 2024.01.18
위상정렬(Topological Sort)  (1) 2024.01.10

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

깊이 우선 탐색이란?

  • 루트 노드에서 시작해서 다음 분기로 넘어가기 전에 해당 분기를 완벽하게 탐색하는 방법
    • 미로를 탐색할 때 한 방향으로 갈 수 있을 때까지 계속 가다가 더 이상 갈 수 없게 되면 다시 가장 가까운 갈림길로 돌아와서 이곳으로부터 다른 방향으로 다시 탐색을 진행하는 방법과 유사하다.
    • 즉, 넓게 탐색하기 전에 깊에 탐색하는 것이다.
    • When? : 모든 노드를 방문하고자 하는 경우에 이 방법을 선택한다!!!!!!!!
    • BFS(너비우선탐색)에 비해서 검색 속도가 느리다.

깊이 우선 탐색 특징

  • 자기 자신을 호출하는 순환 알고리즘의 형태를 가지고 있다.
  • 이 알고리즘을 구현할 때, 그래프 탐색의 경우 어떤 노드를 방문했었는지 여부를 반드시 검사해야 한다!!!!!!!!

깊이 우선 탐색의 과정

  1.  a노드(시작 노드)를 방문한다.
    • 방문한 노드는 방문했다고 표시
  2. a와 인접한 노드들을 차례로 순회한다.
    • a와 인접한 노드가 없다면 종료한다.
  3.  a와 이웃한 노드 b를 방문했다면, a와 인접한 또 다른 노드를 방문하기 전에 b의 이웃 노드들을 전부 방문해야 한다.
    • b를 시작 정점으로 DFS를 다시 시작하여 b의 이웃 노드들을 방문한다.
  4. b의 분기를 전부 완벽하게 탐색했다면 다시 a에 인접한 정점들 중에서 아직 방문이 안 된 정점을 찾는다.
    • 즉, b의 분기를 전부 완벽하게 탐색한 뒤에야 a의 다른 이웃 노드를 방문할 수 있다는 뜻이다.
    • 아직 방문이 안 된 정점이 없으면 종료한다.
    • 있으면 다시 그 정점을 시작으로 DFS를 시작한다.

깊이 우선 탐색의 구현

  • 구현 방법 2가지
    1. 순환 호출 이용
    2. 명시적인 스택 사용 : 명시적인 스택을 사용하여 방문한 정점들을 스택에 저장하였다가 다시 꺼내어 작업한다. 
  • 순환 호출을 이용한 DFS 의사 코드
void search(Node root) {
	if(root == null) return;
    
    //1. root 노드 방문
    visit(root);
    
    root.visited = true; //1-1. 방문한 노드를 표시
    
    //2. root 노드와 인접한 정점을 모두 방문
    
    for each (Node n in root.adjacent) {
    	if(n.visited == false) { //3-1. 인접하면서 (방문X) 정점!
        	search(n); //3. root 노드와 인접한 정점. 정점을 시작으로 DFS를 시작
        }
    }

}

 

 

 

 

[알고리즘] 깊이 우선 탐색(DFS)이란 - Heee's Development Blog

Step by step goes a long way.

gmlwjd9405.github.io

 

'Algorithm' 카테고리의 다른 글

Dynamic Programming (동적 계획법)  (3) 2024.01.24
투포인터 (Two Pointers)  (2) 2024.01.18
Char 형의 모든 것  (2) 2024.01.18
위상정렬(Topological Sort)  (1) 2024.01.10

개념

  • 그래프와 관련된 알고리즘 중 하나
  • 선후관계가 정의된 그래프 구조에서 정렬을 하기 위해 사용할 수 있다.
  • 예를 들어, 그래프가 어떠한 물건의 생산 흐름이라고 하면, 각각의 노드들이 특정한 작업이 되고,
    이때 작업의 순서를 구해야 한다면 위상 정렬을 사용할 수 있다.
  • 위상정렬을 사용하기 위해서는 그래프가 순환하지 않아야 한다. (DAG : 사이클이 없는 방향 그래프)
  • 위상정렬에서는 여러 가지 답이 존재할 수 있다 
    • 한 단계에서 큐에 새롭게 들어가는 원소가 2개 이상인 경우가 있다면 여러가지 답이 존재한다.

위상정렬 진행 순서

  • 또는 재귀 함수를 사용해야 한다.
    1.  그래프의 각 노드들의 진입 차수 테이블 생성 및 진입 차수 계산
    2.  진입 차수가 0인 노드 큐에 넣기 (어떤 노드가 먼저 시작하던지 관계없음)
    3.  큐에서 노드를 하나 꺼낸 후, 꺼낸 노드와 간선으로 연결된 노드들의 진입 차수 감소 (진입 차수 테이블 갱신)
    4.  진입 차수 테이블을 갱신 후, 진입 차수의 값이 0인 노드가 있다면 큐에 넣기 (없으면 아무것도 안 함)
    5.  3~4번의 순서를 큐에 더 이상 아무것도 없을 때까지 반복
  • 다음과 같은 순서로 위상 정렬은 수행되며, 모든 순서가 끝났는데도 진입 차수가 0이 아닌 노드가 있다면, 
    그래프 내에서 순환이 존재한다고 볼 수 있다.
  • 스택을 활용한 DFS를 이용해 위상 정렬을 수행할 수 있다.

 

위상정렬 Java 코드로 구현

import java.util.*;

public class Main {

    // 노드의 개수(V)와 간선의 개수(E)
    // 노드의 개수는 최대 100,000개라고 가정
    public static int v, e;
    // 모든 노드에 대한 진입차수는 0으로 초기화
    public static int[] indegree = new int[100001];
    // 각 노드에 연결된 간선 정보를 담기 위한 연결 리스트 초기화
    public static List<List<Integer>> graph = new ArrayList<ArrayList<Integer>>();

    // 위상 정렬 함수
    public static void topologySort() {
        ArrayList<Integer> result = new ArrayList<>(); // 알고리즘 수행 결과를 담을 리스트
        Queue<Integer> q = new LinkedList<>(); // 큐 사용

        // 처음 시작할 때는 진입차수가 0인 노드를 큐에 삽입
        for (int i = 1; i <= v; i++) {
            if (indegree[i] == 0) {
                q.offer(i);
            }
        }

        // 큐가 빌 때까지 반복
        while (!q.isEmpty()) {
            // 큐에서 원소 꺼내기
            int now = q.poll();
            result.add(now);
            // 해당 원소와 연결된 노드들의 진입차수에서 1 빼기
            for (int i = 0; i < graph.get(now).size(); i++) {
                indegree[graph.get(now).get(i)] -= 1;
                // 새롭게 진입차수가 0이 되는 노드를 큐에 삽입
                if (indegree[graph.get(now).get(i)] == 0) {
                    q.offer(graph.get(now).get(i));
                }
            }
        }

        // 위상 정렬을 수행한 결과 출력
        for (int i = 0; i < result.size(); i++) {
            System.out.print(result.get(i) + " ");
        }
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        v = sc.nextInt();
        e = sc.nextInt();

        // 그래프 초기화
        for (int i = 0; i <= v; i++) {
            graph.add(new ArrayList<Integer>());
        }

        // 방향 그래프의 모든 간선 정보를 입력 받기
        for (int i = 0; i < e; i++) {
            int a = sc.nextInt();
            int b = sc.nextInt();
            graph.get(a).add(b); // 정점 A에서 B로 이동 가능
            // 진입 차수를 1 증가
            indegree[b] += 1;
        }

        topologySort();
    }
}

[출처] https://loosie.tistory.com/161

 

'Algorithm' 카테고리의 다른 글

Dynamic Programming (동적 계획법)  (3) 2024.01.24
투포인터 (Two Pointers)  (2) 2024.01.18
Char 형의 모든 것  (2) 2024.01.18
DFS (Depth-First Search)  (1) 2024.01.18

+ Recent posts