안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

 

이번 포스팅은 프로그래머스 표 병합을 자바로 푼 과정을 정리하도록 하겠습니다.

 

문제 출처:  https://school.programmers.co.kr/learn/courses/30/lessons/150366 

 

프로그래머스

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

programmers.co.kr

 

 

1. 풀이 소스

 

import java.util.*;
import static java.lang.Integer.parseInt;

class Solution {
    public String[] solution(String[] commands) {
        Matrix matrix = new Matrix();
        return matrix.apply(commands);
    }
}

class Matrix {
    static final int RANGE = 2501;
    
    int[] parent = new int[RANGE]; // 인덱스에 부모 인덱스를 넣기위한 일차원 배열
    String[] values = new String[RANGE]; // 셀의 값을 넣기 위한 배열
    
    Matrix() { // 초기화
        for (int i = 1; i < RANGE; i++) {
            parent[i] = i;
            values[i] = "";
        }
    };
    
    int find(int a) { // 재귀로 루트 인덱스를 찾기 위한 과정
        if (parent[a] == a) return a; // 만약 루트 노드가 없다면 자기 자신 인덱스 이므로 리턴
        else return parent[a] = find(parent[a]); // 재귀 시작 후 ex a[1] = 3 -> a[3] = 4 -> a[4] = 4 라면 종료 후 4 반환
    }
    
    void union(int a, int b) {
       if (a != b) parent[b] = a; // parent[b] = a 로 설정하면서 union 실행
    }
    // parent[7] = 4 --> parent[13] 즉 리프 노드부터 parent[4] = 4 까지 연결됌
    
    int convertIndex(int row, int col) {
        return 50 * (row - 1) + col; // row = 1, y = 1 -> 1 // row = 2, y = 1 -> 51
    }
    
    void update(int r, int c, String value) {
        int idx = convertIndex(r, c);
        values[find(idx)] = value; // 루트 노드를 찾아서(find) 반환 후 루트 노드의 values를 value 변경
    }
    
    void update(String before, String after) {
        for (int i = 1; i < RANGE; i++) {
            if (before.equals(values[i])) values[i] = after; // 루트 노드가 기록되므로 루트 노드의 value를 변경
        }
    }
    
    void merge(int r1, int c1, int r2, int c2) {
        
        if (r1 == r2 && c1 == c2) return; // 병합할 위치가 동일하면 종료 
        
        int idx1 = convertIndex(r1, c1); // 병합할 대상
        int idx2 = convertIndex(r2, c2); // 병합될(할) 대상
        
        int root1 = find(idx1); // 병합할 대상의 루트 노드 번호
        int root2 = find(idx2); // 병합될(할) 대상의 루트 노드 번호
        
        if (root1 == root2) return; // 두 루트 노드가 같다면 이미 병합되어 있는 노드임 
        
        String root = values[root1].isBlank() ? values[root2] : values[root1]; // root1의 값이 "" 라면, root2를 기준으로 병합
        values[root2] = ""; // root2는 값이 있더라도, root1을 기준으로 병합될 예정이므로 ""
        
        union(root1, root2); // root1 으로 병합
        values[root1] = root; // root1의 value를 root1 혹은 root2가 values에 가지고 있던 값으로 업데이트
    }
    
    void unmerge(int r, int c) {
        int idx = convertIndex(r, c);
        int root = find(idx); // 인덱스가 연결된 루트 노드
        
        String rootValue = values[root];
        values[root] = ""; // 루트 노드에 값을 ""로 바꿈으로써 병합되어 있던 셀들이 값을 잃음
        values[idx] = rootValue; // 선택된 노드에 rootValue를 참조 (만약 값이 있다면 병합을 해제하면 해당 포인트가 값을 얻음)
        List<Integer> dels = new ArrayList<>();
        for (int i = 1; i < RANGE; i++) {
            if (find(i) == root) { // find(i)로 루트 노드로 연결된 노드를 전부 탐색
                dels.add(i); 
            }
        }
        for (Integer i : dels) {
            parent[i] = i; // 해당 인덱스를 자기 자신으로 바꿈으로써 연결 노드 전부 제거
        }
    }
    
    String print(int r, int c) {
        int idx = convertIndex(r, c); 
        int root = find(idx);
        
        if (values[root].isBlank()) return "EMPTY";
        return values[root]; // 루트 노드의 값을 출력 = 병합된 노드의 루트 노드의 값
    }
    
    String[] apply(String[] commands) {
        List<String> result = new ArrayList<>();
        
        for (String command : commands) {
            String[] cmds = command.split(" ");
            
            String cmd = cmds[0];
            String v1 = cmds[1];
            String v2 = cmds[2];
            
            if (cmds.length >= 4) {
                String v3 = cmds[3];
                if (cmds.length == 5) merge(parseInt(v1), parseInt(v2), parseInt(v3), parseInt(cmds[4])); // merge
                else update(parseInt(v1), parseInt(v2), v3); // update 오버로딩
            } else {
                if (cmd.equals("UPDATE")) update(v1, v2); // update 오버로딩
                else if (cmd.equals("UNMERGE")) unmerge(parseInt(v1), parseInt(v2)); // unmerge
                else result.add(print(parseInt(v1), parseInt(v2))); // print
            }
        }
        
        return result.toArray(new String[0]);
    }
    
}

 

 

2. 풀이 중점

 

해당 문제는 Union - Find를 활용하여 문제를 해결하는 방법이 있습니다. Union - Find 방법은 재귀적으로 루트 노드를 찾은 후 루트 노드끼리 병합하여 하나의 연결 관계를 만드는 방법입니다.

 

가령 다음의 배열이 있습니다.

 

a[0] = 0,  a[1] = 1,  a[2] = 2,  a[3] = 3

여기서 a[0] 과 a[1] 를 연결된 하나의 집합으로 만들고 싶다면 연결관계를 설정할 수 있는 배열을 하나 생성한 후 (p 배열)

p[0] = 1,  p[1] = 1 을 처리하여 0 번째 인덱스에 1을 넣음으로써 1이 루트 노드가 되고 두 노드는 재귀식으로 연결관계를 파악할 수 있습니다.

 

이 문제도 다음과 같이 find(), union() 메서드를 선언하여 먼저 재귀 방법으로 root 노드를 찾은 후, 두 root 노드를 병합하는 과정을 수행할 수 있습니다. 

 

이후, 병합 해제는 루트 노드를 찾은 후, 루트 노드의 value를 ""으로 처리하여 값을 지웁니다. 이후, 재귀로 루트 노드를 가리키는 노드를 찾기 위해 find(i)를 하여 루트 노드라면 해당 값을 자신의 인덱스로 업데이트 합니다.

 

자세한 설명은 다른 풀이와 마찬가지로 주석으로 처리하였습니다.!

 

 

 

이상으로 표 병합 문제를 마치도록 하겠습니다. 감사합니다.!!!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 프로그래머스 미로 탈출 명령어 자바 풀이에 대해 작성하고자 합니다.

해당 문제는 카카오 채용 Lv3 문제로 많은 생각을 고려하는 문제였습니다.

 

최초 풀이는 BFS를 활용하였지만 메모리 초과가 발생하였습니다.

따라서, 보다 효과적으로 문제를 어떻게 풀이하면 좋을까 고민하다 문제를 자세히 읽어본 후 힌트를 얻을 수 있었습니다.

이전처럼 소스부터 정리한 후, 풀이 과정을 상세하게 정리하겠습니다.

 

문제 출처: 

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

 

프로그래머스

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

programmers.co.kr

 

 

1. 풀이 소스:

 

class Solution {
    
    Node node;
    int row; // 행 범위
    int col; // 열 범위
    int targetX; // 목표 x좌표
    int targetY; // 목표 y좌표
    int targetStep; // 만족해야하는 스텝 수
    
    public String solution(int n, int m, int x, int y, int r, int c, int k) {
        String answer = "";
        
        row = n + 1; 
        col = m + 1;
        targetX = r; 
        targetY = c;
        targetStep = k;
        node = new Node(x, y); // 출발 노드 설정
    
        while(node.step <= targetStep) {
            if (node.step == targetStep) {
                if (node.x == targetX && node.y == targetY) {
                    return node.answer;
                }
            }
            
            if (isPossibleD()) { // 아래로 갈 수 있는 조건을 만족한다면 x++ (행을 내려야 하므로 +)
                node.x++;
                node.answer += "d";
            } else if (isPossibleL()) { // 다음 우선 순위로 L 탐색 가능 확인
                node.y--;
                node.answer += "l";
            } else if (isPossibleR()) { // d, l로 갈 수 없다면 r 가능 여부 확인
                node.y++;
                node.answer += "r";
            } else if (isPossibleU()) { // 마지막 우선순위 u 확인
                node.x--;
                node.answer += "u";
            } else { // 위의 분기문을 먼저 고려한 결과 해당하지 않는다면 탈출
                answer = "impossible"; 
                break;
            }
            
            node.step++; // break되지 않았다면 어느 한 방향으로 움직였다는 의미이므로 step++
        }
        
        
        return answer;
    }
    
    
    class Node {
        int x;
        int y;
        int step;
        String answer = "";
        
        Node (int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
    
    boolean isPossibleD() { // down은 행을 내려야하므로 +
        return node.x > 0 && node.x < row - 1 && isPossibleMove(node.x + 1, node.y);
    }
    
    boolean isPossibleU() { // up은 행을 올려야 하므로 -
        return node.x > 1 && (node.x < row) && isPossibleMove(node.x - 1, node.y);
    }
    
    boolean isPossibleL() { // left는 열을 왼쪽으로 가야하므로 -
        return node.y > 1 && node.y < col && isPossibleMove(node.x, node.y - 1);
    }
    
    boolean isPossibleR() { // right는 열을 오른쪽으로 가야하므로 +
        return node.y > 0 && (node.y < col - 1) && isPossibleMove(node.x, node.y + 1);
    }
    
    // 하단에 자세하게 설명하였습니다.
    boolean isPossibleMove(int x, int y) {
        return Math.abs(targetX - x) + Math.abs(targetY - y) + node.step + 1 <= targetStep;
    }
    
}

 

 

2. 풀이 방법

 

문제는 k개의 문자열의 길이를 반드시 유지하면서 사전 순으로 탐색하라고 되어있습니다. 이는 곧 갈 수 있는 거리가 여러 개 있더라도 탐색은 사전순으로 하라는 것을 의미합니다.

 

기존 BFS 혹은 DFS 탐색은 상하좌우 형태로 무작위로 실행하되 완전 탐색 혹은 백트래킹 형태로 진행됩니다.

하지만, 사전순으로 탐색을 하게 된다면 무조건 가깝게 갈 수 있다면 가깝게 이동하도록 구현할 수 있습니다.

 

가령 A - B까지 (10, 0) 만큼 차이가 있고 14번의 움직임이 필요하다면 

d * 10 + 이후 d - l - r - u로 움직이면 됩니다.

 

즉, 최대한 사전으로 움직일 수 있는 만큼 while문으로 움직이되 isPossibleMove라는 메서드
다음에도 움직일 수 있는 범위가 유효한지 판단하기 위해  현재 step + 1과 목표 targetX, targetY (좌표)에서 현재 위치한 좌표를 빼서 타깃보다 같거나 작은 경우를 체크합니다.

 

이 제약 조건은 다음을 의미합니다.

 

만약, 해당 조건을 만족하지 않는 경우에는 예를 들어 최대 앞으로 움직일 수 있는 step이 10인데, 총거리가 10을 초과해 버리면 그 경로는 절대 목표점으로 갈 수가 없습니다.

 

따라서, while문으로 아래, 좌, 우, 위 방향으로 탐색을 진행하도록 하되, 해당 방향으로 이동했을 때 목표 step안에 도착할 수 없는 경로라면 if - else if - else 문으로 가능한 우선순위로 다시 움직이게 되고 전부 갈 수 없다면 else로 while문을 빠져나옵니다.

 

기타 자세한 풀이는 주석으로 설정하였습니다.

 

 

 

BFS 풀이 후 메모리 초과가 발생하여, 혹시 이 방법은 어떨까 불현듯 생각난 방법으로 풀었는데 한 번에 성공하여, 계속 꾸준히 알고리즘 문제를 푸니 정말 많이 성장했다는 점을 느끼게 되었습니다.

 

혹시 이 문제로 메모리 초과 혹은 시간초과가 발생하신다면, 저와 비슷한 방법으로 풀어보시면 좋을 것 같습니다.

이상으로 글을 마치도록 하겠습니다. 감사합니다.!!!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

 

이번 포스팅은 프로그래머스 - 두 큐 합 같게 만들기 자바 풀이를 정리하도록 하겠습니다.

 

문제 출처: https://school.programmers.co.kr/learn/courses/30/lessons/118667#

 

프로그래머스

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

programmers.co.kr

 

 

 

1. 풀이 소스:

 

import java.util.*;

class Solution {
    
    public int solution(int[] arr1, int[] arr2) {
        
        long arr1Sum = 0L; // 합을 위한 long 타입 초기화
        long arr2Sum = 0L;
        
        Queue<Integer> arr1Queue = new ArrayDeque<>();
        Queue<Integer> arr2Queue = new ArrayDeque<>();
        
        for (int i : arr1) {
            arr1Sum += i;
            arr1Queue.add(i);
        }
        for (int i : arr2) {
            arr2Sum += i;
            arr2Queue.add(i);
        }
        
        if (arr1Sum == arr2Sum) return 0; // 이미 합이 한 번에 같다면 종료
        
        EqualSum es = new EqualSum(arr1Sum, arr2Sum, arr1Queue, arr2Queue);
        
        return es.find();
    }

}

class EqualSum {
    
    int totalJob; // 최소 수행 과정
    int limit; // 무한 루프 제한 조건
    QueueSize big; // 합이 큰 Queue를 저장하려는 참조 QueueSize 인스턴스
    QueueSize small; // 합이 작은 Queue를 저장하려는 참조 인스턴스
    
    class QueueSize {
        long sum; // 합 저장
        Queue<Integer> queue; // 큐 참조
    
        QueueSize(long sum, Queue<Integer> queue) {
            this.sum = sum; 
            this.queue = queue;
        }  
    }
    
    EqualSum(long arr1Sum, long arr2Sum, Queue<Integer> arr1Queue, Queue<Integer> arr2Queue) {            
        if (arr1Sum >= arr2Sum) { // arr1Sum이 더 크면 big에 arr1Queue, small = arr2Queue
            big = new QueueSize(arr1Sum, arr1Queue);
            small = new QueueSize(arr2Sum, arr2Queue);         
            
        } else { // arr2Sum이 더 크다면 big에 arr2Queue 설정, small = arr1Queue
            big = new QueueSize(arr2Sum, arr2Queue);
            small = new QueueSize(arr1Sum, arr1Queue);
        }
        
        // 참조 #1
        limit = big.queue.size() * 3; // 한계치 설정을 위해 limit에 큐 크기 * 3 
    }
    
    void swap() { // 만약 queue에 있는 sum 크기가 달라진다면 swap으로 queue 참조 바꾸기
        QueueSize tmp = null; // swap을 위한 QueueSize tmp 생성
        if (big.sum < small.sum) {
            tmp = small;
            small = big;
            big = tmp;
        }
    }
    
    int find() {
        
        // 참조 #2
        while(!big.queue.isEmpty() && !small.queue.isEmpty()) { // 큐에서 더 이상 뺄 수 없는 경우 종료

            int pop = big.queue.poll(); // 합이 더 큰 queue에서 빼기
            small.queue.add(pop); // 작은 큐에 더하기
            
            big.sum -= pop; // 빅 큐 합 업데이트
            small.sum += pop; // 스몰 큐 합 업데이트
            totalJob++; // totalJob 업데이트
            
            if (big.sum == small.sum) return totalJob; // 두 큐의 합이 같다면 종료    
            if (big.sum < small.sum) swap(); // 만약 스몰 큐의 합이 더 크면 스왑
            
            if (totalJob > limit) break; // 큐 교체의 한계보다 더 크다면 무한 루프 해제
        }
        
        return -1; // 두 큐의 합을 같게 만들 수 없음
    }
}

 

 

 

 

2. 풀이 과정

 

해당 문제는 Queue1, Queue2에 대한 참조를 바꿔가며 합이 더 큰 큐에서 작은 큐로 이동하여 합을 비교하는 방식으로 문제를 풀이해야 합니다.

 

이러한 과정이 가능한 이유는 Big 큐는 Small 큐보다 무조건 크기 때문에 Big에서 Small 큐로 원소를 옮기다 보면
어느 순간 같게 되는 상황이 생길 수 있기 때문입니다.

 

예를 들면 아래의 테스트 케이스가 존재합니다.

 

Big : 1 100

Small: 1 98

 

Big : 100

Small: 1 98 1

 

하지만 Big Queue에서 Small Queue로 옮기다 보면 Small Queue가 더 커질 수 있습니다. 이 경우 Swap을 통해
참조를 바꿔주어야 합니다.

 

Big : 1 100 2

Small: 1 1 97

 

Big: 100 2

Small : 1 1 97 1

 

Big : 2

Small: 1 1 97 1 100

 

<Swap>

 

Big: 1 1 97 1 100

Small: 2

---- 진행 ----

 

Big: 1 100

Small: 2 1 1 97 이 완성됩니다.

 

 

3. 종료 조건 

 

주석 참조 #1, 참조#2는 무한 루프에 빠지지 않도록 종료하는 것입니다.

 

참조#2

 

Big: 1 5

Small: 1 1 

 

Big : 5

Small : 1 1 1

 

여기서 Big에서 하나 더 뺀다면 Big의 큐가 비워져 버립니다.

이를 고려하면 결국 다시 Small에서 Big을 스왑하고 Big에 Queue 참조를 옮겨야 합니다. 

 

Big : X (X는 Big의 원소의 합)

Small: Y(Y는 Small의 원소의 합)

 

Big: 0 

Small : Y + X  

X > Y 었으므로  원소의 합인 Y보다 Big에 옮겨지는 큐 원소는 모든 원소의 합 Y > (부분 원소 합)이 만족합니다. 따라서,

 

Big : Y 

Small : X 가 될 때까지 반복하게 됩니다. 

이 경우 스왑하면 다시 원점으로 돌아가게 되므로 종료 조건에 포함됩니다.

 

 

참조#1

 

Queue의 길이에 * 3인 이유는 아래의 케이스를 확인하면 볼 수 있습니다.

 

두 큐가 마지막 스왑하기 전까지 특정 원소는 한 번씩만 이동한다면 바뀔 수 있는 최대 경우는 하나씩 교체하는 것입니다.

(Q1.size = Q2.size = n일 때, Q1 -> Q2로 이동후 Q2 -> Q1에서 반절의 개수만큼 이동시 2n, 하나씩 교체를 하는 경우 3n)

 

Q1: 1 2 3

Q2: 4 5 6

 

Q1: 2 3

Q2: 4 5 6 1

 

Q1: 2 3 4

Q2: 5 6 1

 

Q1: 3 4

Q2: 5 6 1 2

 

Q1: 3 4 5

Q2: 6 1 2

 

Q1: 4 5 

Q2: 6 1 2 3

 

Q1: 4 5 6

Q2 : 1 2 3

 

추가로 루프에 빠지고 있지 않는지 확인하는 테스트 케이스를 제공하겠습니다.!

테스트 추가 : [1000, 1000], [500, 400], [-1] 

 

자세한 설명은 주석으로 처리하였습니다.!

이상으로 프로그래머스 두 큐 합 같게 만들기 풀이를 마치도록 하겠습니다. 감사합니다.!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

 

이번 포스팅은 프로그래머스 - 양궁대회 자바 풀이를 작성하고자 합니다.

 

문제 출처:  https://school.programmers.co.kr/learn/courses/30/lessons/92342

 

프로그래머스

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

programmers.co.kr

 

 

 

1. 풀이 소스

 

class Solution {

    int[] lion; // 라이언 점수
    int[] answer; // 정답을 복사할 배열
    int max = -1; // 최대값

    public int[] solution(int n, int[] info) {
        lion = new int[11];
        answer = new int[11];

        dfs(0, n, info);

        if (max == -1) return new int[]{-1}; // 경우의 수가 없는 경우 리턴

        return answer;
    }

    private void dfs(int depth, int n, int[] apeach) {
        if (depth == n) {
            int score = calculate(apeach); 
            if (max <= score) { // 참고 #1
                max = score; // 최대값 업데이트
                answer = lion.clone(); // lion의 배열의 값을 복사, 참조로 할 경우 lion이 바뀌면서 값이 바뀜
            }
            return;
        }

        // lion[i] <= apeach[i] 조건 (apeach의 10점을 3번 맞췄다면, lion은 10점을 얻으려면 4번을 맞춰야함)
        for (int i = 0; i < apeach.length && lion[i] <= apeach[i]; i++) {
            lion[i] += 1; // 해당 인덱스에 1 증가 -> 다음 dfs에서도 1 증가할 수 있음 따라서 위의 조건까지 반복
            dfs(depth + 1, n, apeach); 
            lion[i] -= 1; // 위에서 추가한 lion 배열에서 1을 감소하는 작업을 모든 dfs에서 수행하므로 원점 복구 가능
        }
    }

    int calculate(int[] apeach) {
        int apeachScore = 0;
        int lionScore = 0;

        for (int i = 0; i < lion.length; i++) {
            if (apeach[i] == 0 && lion[i] == 0) continue;
            if (apeach[i] >= lion[i]) apeachScore += (10 - i); 
            else lionScore += (10 - i);
        }

        if (apeachScore <= lionScore) return lionScore - apeachScore;
        return -1;
    }
    
    /*
    # 참고1
    해당 로직에서 스코어가 같으면 가장 작은 점수를 더 많이 맞춘 배열을 체크하지 않고 업데이트 하는 이유
    하단에 for문을 보면, i++로 상향식 반복문이 진행
    따라서, 가장 최근에 업데이트 되는 score는 가장 적은 점수의 과녁을 더 많이 맞춘 배열
    */
}

 

 

 

2. 문제 풀이 과정

 

해당 문제는 dfs로 해결해야 하는 문제입니다. 처음 풀이하는 과정에, 더 큰 점수 차이를 기록한 배열들이 여러 개인 경우 어떻게 차이를 구분해야 하는가에 대한 고민을 하였습니다.

 

이 과정에서, 주석 처리한 참고1을 확인하면 해당 문제를 해결할 수 있습니다.

상향식으로 반복문을 돌다보면, i++로 인해 배열의 끝에 있는 i를 업데이트하게 됩니다.

만약 더 큰 점수차이가 여러 개이더라도 더 적은 과녁을 하나라도 더 맞힌 배열을 리턴하게 됩니다. 

 

lion[i] += 1;

dfs()

lion[i] -= 1;

 

이 로직은, i 번째 과녁을 선택하지 않을 수 있습니다. 이 경우, 다음 인덱스로 넘겨주기 위해 재귀 성질을 이용하여 더한 만큼 다시 빼주면 원점으로 돌릴 수 있습니다.

 

자세한 풀이 과정은 마찬가지로 소스에 주석 처리하였습니다.

이상으로 프로그래머스 양궁대회 풀이를 마치겠습니다. 감사합니다.

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

 

이번 포스팅은 프로그래머스 파괴되지 않은 건물 자바 풀이를 작성하고자 합니다.

 

문제 출처:  https://school.programmers.co.kr/learn/courses/30/lessons/92344#

 

프로그래머스

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

programmers.co.kr

 

 

 

1. 풀이 소스

 

class Solution {
    public int solution(int[][] board, int[][] skills) {
        int n = board.length; 
        int m = board[0].length;

        int[][] sum = new int[n + 1][m + 1];
        for (int[] skill : skills) {
            int r1 = skill[1], c1 = skill[2];
            int r2 = skill[3], c2 = skill[4];

            int degree = skill[5] * (skill[0] == 1 ? -1 : 1); // type == 1 이라면 degree 음수, type == 2라면 degree 양수

            sum[r1][c1] += degree;
            sum[r1][c2 + 1] += (degree * -1); // 주어진 인덱스보다 열 인덱스가 1 크면 부호 반대
            sum[r2 + 1][c1] += (degree * -1); // 주어진 인덱스보다 행 인덱스가 1 크면 부호 반대
            sum[r2 + 1][c2 + 1] += degree; // 주어진 인덱스보다 행, 열 인덱스가 각 1씩 크면, 부호 그대로
        }

        for (int r = 1; r < n; r++) {
            for (int c = 0; c < m; c++) { // 상하로 계산 주석 #50 (Ry = Ry-1 + Ry)
                sum[r][c] += sum[r - 1][c];
            }
        }

        for (int c = 1; c < m; c++) {
            for (int r = 0; r < n; r++) { // 좌우로 계산 주석 #54 (Cx = Cx-1 + Cx)
                sum[r][c] += sum[r][c - 1];
            }
        }

        int answer = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (board[i][j] + sum[i][j] > 0) answer++;
            }
        }
        return answer;
    }
}

// 누적합
// 행렬 방식으로 아래 방향으로 행 인덱스 증가, 오른쪽 방향으로 열 인덱스 증가

// 만약 (0, 0)을 sum하면 sum 배열은 아래와 같음
// 4 -4
// -4 4

// Ry = Ry-1 + Ry (단, y는 1 이상, R은 행, y = 0이라면 Ry = Ry) 으로 업데이트
// 4 -4
// 0  0

// Cx = Cx-1 + Cx (단, x는 1 이상, C는 컬럼, x = 0이라면 Cx = Cx) 으로 업데이트
// 4 0
// 0 0

 

 

2. 문제 접근 과정: 

 

해당 문제는 for문으로 범위 내 모든 탐색을 하게 된다면, skill의 행 길이 25만 개 중 모든 skill이 (0,0) ~ (1000, 1000) 범위를 업데이트를 하게 된다면 25만 * 1000 * 1000 = 250,000,000,000 번 계산을 하게 됩니다. 즉 최대 K * N * M 번을 계산해야 합니다.

 

이를 해결하기 위한 개념이 누적합 개념입니다. 처음에 이를 활용하지 못하여, 다른 분의 풀이를 보고 문제를 해결하였습니다.

해결과정은 다음과 같습니다.

 

skill의 하나의 행은 [type, r1, c1, r2, c2, degree]로 되어 있습니다. 

이때, r1 ~ r2, c1 ~ c2 범위로 바로 degree를 업데이트하지 않고 각 꼭짓점을 누적합을 위한 범위로 설정합니다.

 

주석과 마찬가지로 하나의 예를 들면 다음과 같습니다.

board : [[1, 1], [1, 1], [1, 1]]

skill : [[2, 0, 0, 0, 0, 4], [1, 0, 0, 1, 0, 2]]

 

board의 누적합을 위해 int [][] sum이라는 2차원 배열을 생성합니다.

누적합은 특정 x에 대한 범위 계산을 위해 하나의 열과 하나의 행이 더 필요합니다. 따라서 sum의 크기는

board.length + 1, board[0].length + 1입니다.

 

누적합에 입력은 x1, y1 이 시작점, x2, y2가 끝 점이라고 하면

 

(x1, y1) += n

(x1, y2 + 1) += -n  

(x2 + 1, y1) += -n

(x2 + 1, y2 + 1) += n입니다.  

 

먼저 skill의 0번째 인덱스에 대한 결과를 보면 (0, 0), (0, 0) 범위에 4를 더해야 합니다. 

 

 

이어서 skill의 1번째 인덱스에 대한 누적합을 합니다. 

 

(0, 0) , (1, 0)에 대한 -2의 누적합을 위해 아래처럼 계산을 합니다.

(0, 0) += -2

(0, 1) += -(-2)

(2, 0) += -(-2)

(2, 1) += -2

 

 

 

이제 누적합에 대한 상하, 좌우 공식으로 누적합에 대한 결과를 계산합니다.

Ry = Ry-1 + Ry(y는 1 이상, R은 행, y = 0 일 때, Ry = Ry)

Cx = Cx-1 + Cx(x는 1 이상, C는 열, x = 0 일 때, Cx = Cx)

 

 

상하 계산

 

 

 

좌우 계산

 

 

이 결과를 기존에 board 이차원 배열 각 원소에 대응하는 값을 더해주면 값을 얻을 수 있습니다.

 

 

누적 합에 대한 개념을 이해하는데 많이 헷갈렸습니다.

실제 로그를 찍어보고 그려보니 어떠한 방식으로 적용되는지 이해할 수 있었습니다.

이상으로 파괴되지 않은 건물 풀이를 마치도록 하겠습니다.

 

감사합니다.!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

 

이번 포스팅은 프로그래머스 표 편집을 자바로 풀이한 과정을 정리하고자 합니다.

 

문제 출처: https://school.programmers.co.kr/learn/courses/30/lessons/81303#

 

프로그래머스

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

programmers.co.kr

 

 

1. 풀이 소스

 

import java.util.*;

class Solution {
    public String solution(int n, int k, String[] cmd) {
        Graph graph = new Graph(n, k);
        graph.run(cmd);
        return graph.getStatus();
    }
}

class Graph {

    Point point; // 현재 위치한 포인트를 기록
    Node start; // 시작 노드 (삭제하거나 다시 복구하는 과정에서 살아있는 맨 앞의 노드)
    Node last; // 마지막 노드 (삭제하거나 다시 복구하는 과정에서 살아있는 맨 뒤의 노드)
    Node[] nodes; // 노드를 설정할 입력 배열
    Stack<Node> dels = new Stack<>(); // 후입선출 복구를 위한 스택

    class Node {
        boolean isDel; // 출력을 위해 삭제 여부 기록
        Node pre; // 이전 노드
        Node next; // 마지막 노드
        Node () {}
    }

    class Point {
        Node p; // 현재 포인트의 노드
        Point(Node p) {
            this.p = p;
        }
    }

    Graph(int n, int k) {
        nodes = new Node[n];

        for (int i = 0; i < n; i++) nodes[i] = new Node();
        nodes[0].next = nodes[1];
        for (int i = 1; i < n - 1; i++) {
            nodes[i].pre = nodes[i - 1]; // nodes 배열에 i 인덱스에 있는 노드에 노드 연결
            nodes[i].next = nodes[i + 1];
        }
        
        nodes[n - 1].pre = nodes[n - 2];
        point = new Point(nodes[k]);
        start = nodes[0]; // start 초기화
        last = nodes[n - 1]; // last 초기화
    }

    void up(int dx) {
        Node node = point.p; // 현재 위치한 노드
        Node pre = node;
        int p = 0;
        while(p < dx) {
            pre = pre.pre; // up 은 행을 올려야 하므로 이전 노드로 탐색
            p++; 
        }

        point.p = pre;
    }

    void down(int dx) {
        Node node = point.p;
        Node next = node;
        int p = 0;
        while(p < dx) {
            next = next.next; // down은 행을 내려야 하므로 다음 노드로 탐색
            p++;
        }

        point.p = next;
    }

    // 위치한 노드에서 제거
    void del() {
        Node node = point.p;

        if (node == start) { // 만약 맨 앞의 노드가 삭제될 경우 해당 노드의 다음를 start로 설정
            start = node.next; 
            start.pre = null;
            point.p = start;
        } else if (node == last) {
            last = node.pre;
            last.next = null;
            point.p = last; // 마지막 인덱스의 경우 앞으로 이동
        } else {
            node.next.pre = node.pre; // 삭제되는 노드의 이전 노드와 다음 노드 연결
            node.pre.next = node.next;
            point.p = node.next; // 삭제되는 노드의 다음 노드로 포인트 변경
        }

        node.isDel = true; // 삭제 여부 표시
        dels.push(node); // 스택에 넣기
    }

    void restore() {
        Node node = dels.pop(); // 삭제된 노드 후입선출
        node.isDel = false; // 복구 설정

        if (node.pre != null && node.next != null) { // 중간에 위치한 노드인경우
            Node pre = node.pre; 
            Node next = node.next;
            pre.next = node; // 이전 노드의 다음 노드를 node로 복구
            next.pre = node; // 다음 노드의 이전 노드를 node로 복구
        } else if (node.pre != null) { // next == null
            Node pre = node.pre; 
            pre.next = node;
            last = node; // 마지막 노드 업데이트
        } else if (node.next != null) {
            Node next = node.next;
            next.pre = node;
            start = node; // 첫번째 노드 업데이트
        }
    }

    void run(String[] cmds) {
        for (String cmd : cmds) {
            char command = cmd.charAt(0);
            if (cmd.length() > 1) {
                int num = Integer.parseInt(cmd.split(" ")[1]);
                if (command == 'D') down(num);
                else up(num);
            }

            else {
                if (command == 'C') del();
                else restore();
            }
        }

    }

    String getStatus() {
        StringBuilder sb = new StringBuilder();
        for (Node node : nodes) {
            if (node.isDel) sb.append("X");
            else sb.append("O");
        }

        return sb.toString();
    }
}

 

 

 

2. 풀이 과정

 

해당 문제를 읽고 LinkedList를 사용하거나 ArrayList를 사용할 경우 삭제한 노드를 복구하는 과정에서 시간초과가 발생할 것 같다는 생각을 하였습니다. 따라서, LinkedList를 직접 구현하여, 만약 삭제 후 복구를 해야 한다면, 스택에서 노드를 가져온 후,
연결관계를 기억한 노드에 다시 연결관계를 설정해주는 것이 좋을 것 같다는 생각을 하였습니다. 따라서, 노드를 저장할 배열을 설정한 후, 연결관계를 설정하고 pre와 next로 인스턴스를 설정하여 문제를 해결하였습니다.

 

삭제 및 복구하는 과정에 대한 자세한 내용을 정리하면 다음과 같습니다.

  • 만약 OOOOO 상황에서 포인트가 인덱스 2에 위치한 경우, OOXOO가 설정되게 됩니다.
  • 이 과정에서 2번 인덱스의 이전, 다음 노드인 1번 3번 노드는 서로 연결 시켜 주어야 합니다.
  • 따라서, 1번 노드.next = 3번 노드, 3번 노드. pre = 1번 노드로 설정할 수 있습니다.
  • 복구하는 과정에서는 '후입선출'이므로, 2번 노드가 삭제되는 시점에 이전, 이후 노드는
    반드시 2번 노드가 다시 복구된다면 두 노드도 살아있게 됩니다.

    삭제: OOXOO -> OXXOO -> OXXXO 상태
    복구: OXXOO -> OOXOO (후입선출)

 

따라서, 삭제되는 시점에는 삭제되는 노드의 연관관계를 바꾸지 않는다면 다시 복구하는 과정을 설정할 수 있습니다.

기타 다른 부분들은 1번 풀이에 자세한 주석으로 작성하였습니다.

 

 

 

3. 더 좋은 코드 공유

 

 

테스트는 통과하였지만, 최대 약 325ms가 걸렸습니다.

다른 블로그에 더 좋은 풀이로 작성하신 코드가 있어 공유드립니다.

 

https://moonsbeen.tistory.com/294

 

[프로그래머스]표 편집 - JAVA

** 풀이가 추가되었습니다. 추가된 풀이가 정확한 풀이입니다. ** [프로그래머스]표 편집 코딩테스트 연습 - 표 편집 8 2 ["D 2","C","U 3","C","D 4","C","U 2","Z","Z"] "OOOOXOOO" 8 2 ["D 2","C","U 3","C","D 4","C","U 2","

moonsbeen.tistory.com

 

해당 블로거분의 코드를 실행하면 효율성 테스트 기준 무려 100ms 평균으로 통과할 수 있는 코드입니다.

 

이상으로 해당 문제에 대한 리뷰를 마치도록 하였습니다. 감사합니다.!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

 

이번 포스팅은 프로그래머스 양과 늑대에 대한 자바 풀이를 작성하도록 하겠습니다.

 

문제 출처: https://school.programmers.co.kr/learn/courses/30/lessons/92343

 

프로그래머스

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

programmers.co.kr

 

 

1. 풀이 소스

 

import java.util.*;

class Solution {
    
    int maxSheepCnt; // 최대 양의 수
    int totalSheepCnt; // 총 양의 수
    ArrayList<Integer>[] nodes; // A 노드의 자식 노드(B, C, D)와 부모(R)를 포함한 조상 노드(X, Y)의 자식 노드 중 방문해야하는 노드들
    
    public int solution(int[] info, int[][] edges) {
        
        nodes = new ArrayList[info.length]; 
        
        for (int i = 0; i < nodes.length; i++) nodes[i] = new ArrayList<>();
        
        for (int i = 0; i < info.length; i++) {
            if (info[i] == 0) totalSheepCnt++; // 총 양의 수를 구하여, 만약 최대 양의 수가 총 양의 수와 같다면 무조건 리턴
        }
        
        if (totalSheepCnt == 0) return 0; // 만약 총 양의 수가 0 이라면 바로 종료
        
        for (int[] edge : edges) {
            int parent = edge[0]; 
            int child = edge[1];
            nodes[parent].add(child); // 단방향 간선을 설정 (부모 -> 자식으로만 이동)
        }
     
        dfs(0, 0, 0, new ArrayList<>(), info);
        
        return maxSheepCnt;
    }
    
    void dfs(int sheepCnt, int wolfCnt, int node, List others, int[] info) {
        if (maxSheepCnt == totalSheepCnt) return; // 총 양의 수와 최대 방문한 양의 개수가 같다면 dfs 종료
        
        if (info[node] == 0) sheepCnt++;
        else wolfCnt++;
        maxSheepCnt = Math.max(maxSheepCnt, sheepCnt);
        
        if (sheepCnt <= wolfCnt) return; // 문제에서 늑대가 양의 개수보다 더 크다면 현재 dfs 종료
        
        List<Integer> remains = new ArrayList<>();
        remains.addAll(others); // 재귀로 남아 있는 리스트를 그대로 새로운 리스트에 추가
        // 예제 1 기준 루트 노드부터 선택되지 않았던 자식노드 ex)8, 현재 노드가 4번이라면 방문해야하는 노드2 추가
        
        if (!nodes[node].isEmpty()) {
            remains.addAll(nodes[node]); // 현재 노드의 자식 노드 추가
        }
        
        remains.remove(Integer.valueOf(node)); // remove에 레퍼타입을 적용하면 해당 값은 index가 아닌 value로 제거 가능
        
        for (int nextNode : remains) {
            dfs(sheepCnt, wolfCnt, nextNode, remains, info);
        }
    }
}

 

 

2. 주의할 점

 

 

  • 일반적인 dfs를 적용하게 된다면 다음과 같이 자식 노드를 기준으로 탐색하게 됩니다. 하지만, 늑대의 개수가 양보다 큰 경우 8번을 먼저 방문할 수 없습니다. 즉, 루트 노드(0)의 자식 노드 8번을 거치지 않게 됩니다. 따라서, 이 문제를 극복하기 위해 특정 A 노드로 방문하고자 할 때, 부모노드와 조상 노드에서 방문하지 않은 노드를  아래의 사진처럼 추가하여 재방문할 수 있도록 해야 합니다.

 

  • 문제 되는 부분은 방문하지 않은 노드를 선택할 때, 부모 노드의 자식 노드를 전부 추가하게 되면 양 2가 중복되게 됩니다. 
  • 자바의 list.remove()를 활용할 때 레퍼 변수 Integer 변수를 사용하게 되면 인덱스가 아닌 값을 제거할 수 있습니다.

 

나머지는 주석으로 처리하였습니다! 이상으로 해당 문제 리뷰를 마치겠습니다.

 

감사합니다.!!!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

 

이번 포스팅은 프로그래머스 등산코스 정하기 문제를 해결한 과정을 정리하도록 하겠습니다.

 

문제 출처: https://school.programmers.co.kr/learn/courses/30/lessons/118669#

 

프로그래머스

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

programmers.co.kr

 

 

1. 문제 해결 방안

 

해당 문제는 n번 노드로 도착하는 과정에서 하나의 특정 경로가 거친 가중치들 중 가장 큰 가중치를 intensity로 설정합니다.
만약 n번 노드로 도착할 수 있는 경로가 여러 개 일 때, 특정 경로들의 intensity를 비교하여 가장 작은 intensity를 선정합니다.

 

즉 to 노드는 from 노드의 intensity와 from과 to 노드 사이의 간선의 가중치 중 더 큰 값을 distance로 설정한 후,
distance = max(intensity[from.노드 번호], edge(from.노드번호, to.노드 번호))

 

to 노드의 노드 번호에 있는 intensity의 값보다 distance가 더 작다면 intensity를 distance로 업데이트해야 합니다.

if intensity[to.노드 번호] > distance: intensity[to.노드 번호] = distance

 

시작은 gates 배열에서 gate를 큐에넣는 방식으로 다익스트라 알고리즘을 활용할 수 있습니다.

 

 

 

2. 소스

 

import java.util.*;

class Solution {
    public int[] solution(int n, int[][] paths, int[] gates, int[] summits) {
        Graph graph = new Graph(n);
        graph.setGate(gates);
        graph.setSummit(summits);
        
        for (int[] path : paths) {
            int s = path[0];
            int d = path[1];
            int w = path[2];
            
            graph.addEdge(s, d, w);
        }

        return graph.dijkstra(n, gates, summits);
    }
}


class Graph {
    
    Set<Integer> gateSet = new HashSet<>();
    Set<Integer> summitSet = new HashSet<>();
    List<List<Node>> nodes = new ArrayList<>(); // nodes의 i번 인덱스에는 i번이 갈 수 있는 adjacent 
    
    class Node {
        int d; // 만약 새로 출발하는 노드 == queue에 넣어야 하는 노드(to) 라면 도착해야하는 노드의 주소, 이미 도착했다면 == queue에서 나온 노드(from) 도착한 현재 노드 번호
        int w; // 도착지 노드로 가는데 필요한 가중치, 만약 이 노드가 출발 노드라면 이 노드로 도착하는데 필요했던 가중치
        
        Node (int d, int w) {
            this.d = d;
            this.w = w;
        }
    }
    
    Graph(int n) {
        for (int i = 0; i <= n; i++) nodes.add(new ArrayList<>());
    };
    
    void setGate(int[] gates) {
        for (int gate : gates) {
            gateSet.add(gate);
        }
    }
    
    void setSummit(int[] summits) {
        for (int summit : summits) {
            summitSet.add(summit);
        }
    }
    
    void addEdge(int s, int d, int w) {        
        if (gateSet.contains(s) || summitSet.contains(d)) {
            nodes.get(s).add(new Node(d, w)); // 만약 s가 게이트라면 출발 노드만 존재 or d가 정상이라면 도착 노드만 존재
        } else if (gateSet.contains(d) || summitSet.contains(s)) {
            nodes.get(d).add(new Node(s, w)); 
        } else {
            nodes.get(d).add(new Node(s, w)); 
            nodes.get(s).add(new Node(d, w));   
        }
    }
    
    int[] dijkstra(int n, int[] gates, int[] summits) {
        
        Queue<Node> queue = new ArrayDeque<>();
        int[] intensity = new int[n + 1]; 
        
        Arrays.fill(intensity, Integer.MAX_VALUE);
        
        for (int gate : gates) {
            queue.add(new Node(gate, 0)); // gate로 도착하는 가중치는 0 (출발지이므로)
            intensity[gate] = 0;
        }
        
        while(!queue.isEmpty()) {
            Node from = queue.poll(); // 현재 도착한 노드는 출발 노드로 변경

            for (int i = 0; i < nodes.get(from.d).size(); i++) {
                Node to = nodes.get(from.d).get(i);
                
                int distance = Math.max(intensity[from.d], to.w);
                if (intensity[to.d] > distance) {
                    intensity[to.d] = distance;
                    queue.add(new Node(to.d, distance));
                }
            }            
        }
        
        int summit = Integer.MAX_VALUE; 
        int intense = Integer.MAX_VALUE;
        
        Arrays.sort(summits); // 만약 intense가 같다면 가장 작은 정상 번호를 구해야함
        
        for (int sm : summits) {
            if (intensity[sm] < intense) {
                summit = sm;
                intense = intensity[summit];
            }
        }
        
        return new int[]{summit, intense};
        
    }
    
}

 

 

3. 부분 해결 과정

 

while(!queue.isEmpty()) {
            Node from = queue.poll(); // 현재 도착한 노드는 출발 노드로 변경

            for (int i = 0; i < nodes.get(from.d).size(); i++) {
                Node to = nodes.get(from.d).get(i);
                
                int distance = Math.max(intensity[from.d], to.w);
                if (intensity[to.d] > distance) {
                    intensity[to.d] = distance;
                    queue.add(new Node(to.d, distance));
                }
            }            
        }

 

이 코드는 queue에서 나온 노드는 from, queue에 넣을 노드를 to로 설정하였습니다.

즉, 다음 목적지로 가야 할 노드에 설정된 w와 출발지까지 오는 과정에 설정된 노드의 intensity를 비교하여
더 큰 값을 distance로 설정합니다. (distance는 문제의 요구사항에 맞춰 현재까지 온 경로 중에 가장 큰 값으로 설정해야 함)

 

만약 다음 목적지로 가야할 곳의 intensity값 보다 새로 온 경로의 distance가 더 작다면 문제의 요구사항에 의해
더 작은 intensity를 설정해야 합니다.

 

이 코드의 진행 순서를 정리하면 다음과 같습니다.

 

  • from gate: 1, summit: 4
    edge => (1, 2, 3), (1, 3, 1)
    edge => (2, 4, 7), (3, 4, 1)
  • gate.d: 1, gate.w: 0 설정
  • while 루프 시작       

    nodes.get(1) -> [2, 3]

    1) if from d = 1, w = 0, 도착지 2로 갈 때,
    2) Node to => d = 2, w = 3
    3) distance = max(intensity[from.d], to.w)
    -----> intensity[from.d] => intensity[1] = 0, to.w = 3, 고로 distance = 3
    4) intensity[2] > distance 이므로, intensity[2] = 3
    5) queue (new Node(2, 3))
            
    6) if from d = 1, w = 0, 도착지 3으로 갈 때,
    7) Node to => d =  3, w = 1 
    8) 앞의 도착지 2로 갈 때와 같은 방법으로 distance 계산 -> distance = 1
    9) 앞의 intensity[] > distance 비교와 같은 방법으로 계산 -> intensity[3] = 1
    10) queue (3, 1)
            
    11) if from d = 2, w = 3, 도착지 4로 갈 때
    12) Node to = nodes.get(2) -> d =  4 w = 7
    13) intensity[2] =  3, to.w = 7 이므로 distance = 7
    14) intensity[4] = distance = 7
    15) queue (4, 7) ----> graph.get(4).size() == 0 (도착 노드이므로) --> 종료
            
    16) if from d = 3, w = 1, 도착지 4로 갈 때
    17) Node to = nodes.get(3) -> d = 4, w = 1
    18) intensity[3] = 1, to.w = 1로 distance = 1
    19) intensity[4] > distance (앞에서 7로 업데이트 되었으므로 ) 성립하므로  intensity[4] = distacne = 1
    20) queue (4, 1) ----> graph.get(4).size() == 0 ---> 종료

    따라서, summit 4, intensity[4] = 1가 됩니다.

 

이상으로 프로그래머스 등산코스 정하기 정리를 마치도록 하겠습니다.

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 스케줄링에 대해서 정리하는 시간을 가지도록 하겠습니다.!

 

 

1. 처리기 스케줄링 유형

처리기의 스케줄링은 응답 시간이나 처리량 효율성을 증대시키기 위해 처리기가 다음에 실행할 프로세스를 선택하는 것으로 일반적으로 스케줄링은 3단계로 이루어져 있습니다.

 

장기 스케줄링

프로세스가 CPU에 의해 실행될 수 있는 자격을 부여할지 여부를 결정하는 역할을 수행합니다. 장기 스케줄링은 프로세스를 시스템으로 진입시키는 여부를 판단하므로 멀티프로그래밍을 제어하는 역할을 한다고 볼 수 있습니다. 장기 스케줄링은 일괄 처리 큐에서 소정의 규정에 따라 작업들을 골라 프로세스로 만들어줍니다.

 

일괄 처리 큐

  • 일괄 처리 큐(Batch Queue)는 여러 작업들이 한 번에 처리되도록 대기열에 모아두는 방식
  • 일괄 처리 큐의 주요 역할은 시스템의 효율성을 높이고 자원을 최적화하기 위해 관련 작업을 그룹화하고 순차적으로 처리
  • 주로 백그라운드에서 실행되는 작업에 사용될 수 있으며, 작업들을 우선순위에 따라 정렬하여 작업을 처리할 수 있고 유사한 작업을 그룹화하여 한 번에 처리하는 기능을 수행

 

장기 스케줄러가 프로세스를 선택하는 알고리즘

  • FCFS(First com first Service)
  • 우선순위 기반
  • 예상되는 실행 시간의 길이
  • 입출력 우대

 

중기 스케줄링

중기 스케줄링은 스와핑 기능의 일부라고 할 수 있습니다. 장기 스케줄링에서 프로세스를 생성했을 때, 주메모리에 프로세스를 스왑인 해야하는 상황이 생길 수 있습니다. (반대로 스왑 아웃 상황) 이 경우 중기 스케줄링은 이러한 스와핑 과정을 수행하는 것을 의미할 수 있습니다. 


단기 스케줄링

단기 스케줄링은 CPU 스케줄러라고 불리며 프로세스들 사이에서 CPU를 할당하는 역할을 수행합니다. 단기 스케줄러는 프로세스들 사이에서 CPU를 우선순위 혹은 요구사항을 고려하여 실행 순서를 결정하고, 컨텍스트 스위칭을 관리합니다. 이때, 디스패쳐와 함께 작동하여 컨텍스트 스위칭을 수행합니다. 

 

디스패쳐란

프로세스 간의 컨텍스트 스위칭을 수행하는 역할을 담당하며 단기 스케줄러와 함께 작동합니다. 디스패처는 단기 스케줄러가 선택한 프로세스를 실행하기 위해 현재 실행 중인 프로세스의 상태를 저장하고 새로 선택된 프로세스의 상태를 불러오는 작업을 수행합니다. 

 

단기 스케줄링 호출

  • 새로운 프로세스가 생성되어 실행 대기 상태에 들어감
  • 실행 중인 프로세스가 입출력을 요청, 시스템 호출 등을 인해 대기 상태로 전환
  • 실행 중인 프로세스가 종료되거나 시간 할당량이 만료되어 다른 프로세스에게 CPU를 넘겨줌

 

 

2. 스케줄링  평가 척도

 

사용자 중심의 성능 관련 평가척도

  • 반환시간: 프로세스가 시스템으로 진입한 후로부터 종료할 때까지 걸린 시간을 의미하며, 실제 실행 시간뿐만 아니라, CPU나 다른 자원들을 사용하기 위해 대기한 시간 모두 포함합니다.
  • 응답시간: 대화형 프로세스가 시스템에 요구를 한 후, 이에 대한 시스템으로부터 첫 번째 응답이 올 때까지의 시간을 의미합니다. 프로세스는 사용자에게 실행 결과를 출력해 주는 사이에 시스템에 또 다른 요청을 하는 경우가 종종 있습니다. 따라서, 사용자의 입장에서 보면 시스템의 반응 속도를 가늠하기에는 반환 시간보다 응답 시간이 더 적합합니다.
  • 완료 기한: 프로세스가 완료되어야 하는 시점에 기한이 있다면, 스케줄러는 다른 평가 척도가 희생당하더라도 완료 기한을 만족시킬 수 있는 프로세스의 수를 최대화하는 방향으로 설계해야 합니다.

 

사용자 중심의 기타 평가척도

  • 예측 가능성: 같은 작업이라면 실행될 때마다 동일한 기간 동안 실행되어야 하고, 시스템의 부하 정도에 상관없이 일한 비용으로 실행되어야 합니다. 만약 실행할 때마다 응답 시간이나 반환 시간이 많이 차이 난다면 사용자들은 시스템을 신뢰하지 못하게 됩니다.

 

시스템 중심의 성능 관련 평가 척도

  • 처리량: 처리량을 중시하는 스케줄러는 단위 시간 내에 완료될 수 있는 프로세스의 수를 최대화하는 정책을 사용합니다. 이는 처리해야 할 작업의 양에 관련된 척도로써 각 프로세스 실행 시간의 길고 짧음에 따라 크게 좌우될 수  있습니다. 
  • 처리기 이용률: 처리기 이용률이란 전체 시간 중에 처리기가 바쁘게 실행을 한 시간의 비율을 의미합니다. 여러 사용자들이 공유하여 사용하는 고가의 시스템에서 처리기 이용률은 시스템을 효율적으로 사용하고 있느냐 아니냐가 매우 중요한 성능 척도입니다. 

 

시스템 중심의 기타 평가 척도

  • 공정성: 시스템이 어떤 특정 목적을 위해 공표하지 않은 이상 어떤 프로세스도 스케줄러로부터 차별을 받아서는 안됩니다. 즉 프로세스가 처리기를 할당받지 못하는 기아 상태 발생을 줄여야 합니다.
  • 우선순위: 프로세스들에게 우선순위가 부여되면, 스케줄러는 높은 우선순위의 프로세스를 우대해야 합니다.
  • 균형 있는 자원: 활용 스케줄러는 시스템 내의 자원들이 가능한 최대로 활용되도록 해야 합니다.

 

 

3. 스케줄링 정책

 

준비 큐에서 대기 중인 프로세스 중 하나를 선택하는 알고리즘을 함수 형태로 표현한 것은 선택 함수라고 부릅니다. 스케줄링이 선택함수를 적용하는 과정에 사용하는 용어는 다음과 같습니다.

  • w: 대기한 시간과 실행한 시간을 모두 합쳐 시스템이 들어온 후 지금까지 경과한 시간
  • e: 지금까지 실행하는 데에만 걸린 시간
  • s: 프로세스가 시작해서 종료하기까지 걸릴 총 서비스 시간(e를 포함). 이 값은 시스템에 의해 미리 계산되거나 프로세스가 이 정보를 미리 시스템에게 알려주어야 함. 

 

결정 모드는 선택 함수가 호출되는 시점이 언제인가 하는 것을 나타냅니다. 

  • Nonpreemptive(비선점) 모드: 프로세스가 일단 실행 상태에 진입하면 종료되거나 자발적으로 CPU를 놓을 때까지는 CPU를 빼앗기지 않습니다. 자발적으로 CPU를 놓는다는 것은 자기가 스스로 블록 시키거나 입출력 요구 및 시스템 서비스 호출 등으로 인해 스스로 준비 큐에서 빠지는 상황을 의미합니다.
  • Preemptive(선점) 모드: 현재 실행 중인 프로세스라 할지라도 운영체제에 의해 인터럽트가 걸려 비자발적으로 준비 큐로 이동될 수 있습니다. 현재 실행 중인 프로세스를 선점시킬 수 있는 시점으로는 새로운 프로세스가 진입, 하드웨어로부터 인터럽트가 걸려 이를 대기 중이던 선점된 프로세스가 다시 준비 큐로 들어오는 것, 또는 클록 인터럽트에 의해 주기적으로 스케줄러가 호출되는 시점 등을 들 수 있습니다. 

Preemptive 모드는 NonPreemptive 모드 스케줄링에 비해 프로세스 간 문맥 교환이 자주 발생하기 때문에 이로 인한 오버헤드가 크다고 할 수 있습니다. 하지만 최근 처리기는 문맥 교환에 드는 비용을 최소화하기 위해 하드웨어 수준에서 문맥 교환 연산을 제공하므로 오버헤드 문제가 해결되고 있습니다. 따라서, 기아 상태를 방지할 수 있는 Preemptive 모드가 나은 서비스를 제공합니다. 

 

 

스케줄링 정책의 특징

 

  선택 함수 결정 모드 처리량 응답 시간 문맥 교환
비용
프로세스들에게 미치는
영향
기아 발생
여부
FCFS max[w] 비선점   길어질 수
있음
최소 입출력 프로세스에 불리 X
Round
Robin
상수 선점 시간할당량이 낮으면 처리량이 낮음 짧은 프로세스에게 좋은 응답 시간 제공 최소 공정 X
SPN min[s] 비선점 높음 짧은 프로세스에게 좋은 응답 시간 커질 수 있음 긴 프로세스에 불리 O
SRT min[s - e] 선점 높음 좋은 응답 커질 수 있음 긴 프로세스에 불리 O
HRRN max
((w + s) / s)
비선점 높음 좋은 응답 커질 수 있음 어느 정도
공정
X
Feedback   선점     커질 수 있음 입출력 중심 
프로세스 우대
O

 

 

4. FCFS(First-Come-First-Served)

 

FCFS는 가장 단순한 형태의 스케줄링 정책으로 FIFO라고도 합니다. 큐잉 체계를 엄격하게 지키고 있으므로 다음의 절차에 따라 진행됩니다.

 

프로세스 도착 시각 서비스 시간(Ts) 시작 시각 종료 시각 반환 시간(Tr)
종료시간 - 도착시간
정규화된
반환시간 (Tr/Ts)
W 0 1 0 1 1 1
X 1 100 1 101 100 1
Y 2 1 101 102 100 100
Z 3 100 102 202 199 1.99
평균         100 26

 

이 표를 토대로 확인할 수 있는 부분은 긴 프로세스의 경우 상대적으로 짧은 프로세스보다 서비스 시간 자체가 길기 때문에 정규화된 반환시간이 적습니다. 하지만 Y 프로세스는 서비스 시간이 매우 짧지만 앞의 X 프로세스의 긴 시간으로 인해 반환시간이 늦어져 비효율적인 것을 확인할 수 있습니다. 

 

이를 처리기 중심 프로세스와 입출력 중심 프로세스로 대입하여 생각해 보면 다음과 같습니다.

  •  입출력 중심 프로세스는 무거운 작업이지만 실제 네트워크 및 디스크 처리 대기 시간이 긴 것이고 CPU의 실행 시간으로 보면 프로그램 카운터로부터 명령어를 입출력 컨트롤러에게 전달하고 제어하는 역할이므로 CPU 버스트는 짧습니다.
  • 반면 처리기 중심 프로세스는 실제 CPU가 연산해야 하는 작업이 많으므로 많은 시간이 소요됩니다.
  • 따라서, 빠른 시간 내 CPU 버스트를 마칠 수 있는 작업인 입출력 중심의 프로세스임에도 오랜 시간을 대기해야 하므로 비효율적이라고 볼 수 있습니다.

 

 

5. Round Robin

 

라운드 로빈 방식은 어떤 긴 프로세스가 일정 시간을 넘어가는 순간 실행을 강제로 선점(preemption)시키는 것을 의미합니다. 이때 클록 인터럽트가 주기적으로 발생하게 되는데 이 인터럽트 루틴으로 인해 현재 실행 중이던 프로세스는 준비 큐로 이동시키고 준비 큐에서 FCFS 방법으로 다음 프로세스를 골라 실행시킵니다.

 

여기서 사용되는 개념이 시간 할당량으로 모든 프로세스는 정해진 시간 조각만큼만 실행한 후 강제적으로 CPU를 빼앗습니다.

  • 시간할당량이 짧은 경우: 시간할당량이 짧은 경우, 짧은 프로세스에게 유리하지만, 짧은 할당 시간으로 인해 컨텍스트 스위칭이 빈번하게 발생하므로 오버헤드가 발생할 수 있습니다.
  • 시간할당량이 긴 경우: 긴 프로세스의 경우 끝날 때까지 다른 프로세스가 대기해야 하므로 정규화된 반환 시간이 매우 길었습니다. 만약 시간할당량이 긴 프로세스의 프로세스 시간과 비슷하거나 더 큰 경우 결국 짧은 프로세스는 긴 프로세스의 시간을 전부 기다려야 하게 되므로 FCFS의 문제점을 해결할 수 없습니다.

 

라운드 로빈 방식은 공평한 시간 할당량으로 프로세스를 처리하기 때문에 시분할 시스템이나 트랜잭션 처리 시스템에 매우 효과적일 수 있습니다. 하지만 라운드 로빈 방식에도 입출력 중심 프로세스를 처리하는데 한계가 있습니다. 이 절차를 정리하면 다음과 같습니다.

(공부한 내용이 명확하게 이해가 되지 않아서 제가 이해한 바를 바탕으로 정리하였습니다. 따라서, 사실과 다를 수 있습니다.)

 

  • 입출력 중심 프로세스는 처리기 중심 프로세스보다 상대적으로 짧은 CPU 버스트를 가집니다. 따라서 할당된 시간보다 빠르게 1번 요청을 수행하면 입출력 큐로 이동하여 입출력 완료까지 대기합니다. 입출력이 완료되면 준비 큐로 이동합니다.
  • 이 과정에서 만약 시간 할당량이 5초라면 2초만 사용하게 되고 3초는 사용하지 못하고 반납하게 됩니다.
  • 이어서 만약 처리기 중심 프로세스가 실행이 된다면 이 프로세스는 주어진 시간을 모두 활용한 후 다시 준비큐로 이동하게 됩니다.
  • 이 과정을 정리하면, 실제 CPU는 짧은 버스트를 가짐에도 불구하고 입출력 요청을 수행하게 되면 입출력 큐로 이동하고 다시 준비큐로 가게 되므로 실제 시간 할당량을 전부 채우지 못하게 되는 문제가 발생할 수 있습니다.

 

이를 해결하기 위한 방법이 개선된 라운드 가상 라운드 로빈(Virtual Round Robin)입니다. 수행 절차는 다음과 같습니다.

  • 입출력 중심 프로세스가 입출력 요청을 수행하면 입출력 큐로 가서 입출력 완료까지 대기합니다.
  • 이후, 다시 준비 큐로 이동하는 것이 아니라 보조 큐로 이동하여 현재 수행 중인 프로세스가 CPU를 반납하면 준비 큐보다 먼저 보조 큐에 있는 입출력 중심 프로세스를 스케줄링합니다.
  • 이때 만약 앞서 시간 할당량 5초를 기준으로 2초를 소모했다면 3초만 부여하여 다른 프로세스와 동일하게 5초만 수행될 수 있도록 합니다.

 

 

6. SPN(Shortest Process Next)

 

FCFS가 긴 프로세스에 효율적인 점을 극복하기 위해 짧은 프로세스에 우선순위를 두는 방식을 비선점 방식을 SPN이라고 합니다.

이 경우, 각 프로세스의 총 실행 시간을 미리 알아야 한다는 단점이 있는데, 이를 극복하기 위해 단순 평균 혹은 지수적 평균 방법을 적용합니다.

 

Sn+1 = aTn + (1 - a)Sn

 

여기서 알파는 0 < a < 1 사이의 가중치로 과거 측정치에 비해 상대적으로 최근 측정치에 어느 정도의 비중을 둘 것인가를 설정할 수 있는 파라미터입니다.

 

이를 적용하면, 일반적으로 반복되는 특정 프로세스에 대한 총 실행 시간을 예측하여 적용할 수 있습니다. 만약 짧을 프로세스가 있다면 우선순위를 얻게 되어 CPU를 할당받아 처리할 수 있습니다. 이 경우 긴 프로세스는 계속 대기하여 결국 CPU를 할당받지 못하게 되는 기아 상태에 빠질 수 있습니다.

 

 

7. SRT (Shortest Remaining Time)

 

SPN가 프로세스의 총 실행 시간에 따라 프로세스를 선택하는 스케줄링 방법이었다면, SRT는 프로세스가 남아있는 시간을 기준으로 선점 방식으로 프로세스를 선택하는 스케줄링입니다. 즉, 현재 실행 중인 프로세스가 남은 시간이 10초인데, 새로 들어온 프로세스의 남은 시간이 2초라면 선점 방식에 의해 프로세스 CPU를 빼앗고 준비 큐에 있는 프로세스를 선택할 수 있는 방법입니다.

이 역시, 긴 프로세스에 대해서는 기아 상태에 빠질 수 있다는 단점이 존재합니다.

 

 

8.HRRN(Hightest Response Ratio Next)

 

앞 서 정규화된 반환 시간이라는 평가 척도가 존재했었는데, 이러한 정규화된 반환 시간의 평균이 최소화시키는 방향으로 스케줄러를 최적화하는 방법을 HRRN이라고 합니다.

 

R = (w + s) / s
R = 응답 비율
w = 처리기를 기다리며 대기한 시간
s = 예상되는 서비스 시간

 

HRRN은 SPN, SRT처럼 프로세스들의 예상 실행시간에 대한 예측을 해야 한다는 점에서 단점이 있지만, HRRN의 장점은 대기 시간이 긴 프로세스에게도 우선권을 줄 수 있다는 점입니다.

 

해당 수식을 살펴보면, 짧은 프로세스의 경우 s가 매우 짧기 때문에 R의 값이 크게 나와 높은 우선권을 가질 수 있습니다. 하지만 s가 크더라도 만약 이를 상쇄시킬 정도로 w가 더 크다면 대기 시간이 그만큼 커져서 R이 커지게 됩니다. 따라서, 프로세스의 대기 시간을 고려할 수 있다는 점에서 장점을 가집니다.

 

 

9. FeedBack

 

FeedBack은 SPN, SRT, HRRN이 필요로 하는 프로세스의 총 실행시간을 예측할 수 없는 상황에 적용할 수 있습니다. FeedBack 방식은 현재 수행된 수행시간이 길어질수록 낮은 우선순위를 부여하는 방법으로 총 실행 시간을 예측할 수 없더라도 실행 시간이 길어진다면 곧 긴 프로세스임을 의미하므로 비슷한 효과를 얻을 수 있다는 개념입니다. 이를 구현하기 위해서는 우선순위 큐가 여러 개 구성되게 됩니다.(다단계 피드백)

 

수행 절차는 다음과 같습니다.

  • 프로세스 A가 시간 할당량을 채우지 못하고 5초의 누적된 수행시간을 기록합니다.
  • 프로세스 A는 우선순위 큐 RQ1에 입력이 되고 준비 큐에 있는 다른 프로세스들이 실행됩니다.
  • 프로세스 B는 시간 할당량 이내에 처리되어 CPU가 반납되었고(누적시간 2초) 프로세스 C는 RQ0에서 나와 시간 할당량을 채우지 못하고 5초의 누적된 수행시간을 기록합니다.
  • 이때,  프로세스 B가 입출력 요청을 완료하여 다시 RQ1으로 오게 되면 누적 시간이 적으므로 프로세스 B가 먼저 실행됩니다.
  • 우선순위 큐 RQ1에서 프로세스 A를 빼서 실행했는데 시간 할당량을 채우지 못하여 10초의 누적된 수행시간을 기록합니다.

이러한 절차대로 수행될 경우, 짧은 프로세스의 경우 우선권을 얻을 수 있습니다. 하지만, 긴 프로세스의 경우 계속 우선권에서 밀려 기아 상태에 돌입할 수 있게 됩니다.

 

따라서, 우선순위 큐 RQn에 시간 할당량 p를 다르게 부여하는 방법이 있습니다. 예를 들어 RQ1은 p = 1, RQ2는 p = 2 등으로,
우선순위가 계속 밀린 긴 프로세스가 선택되면 긴 시간 할당량이 적용될 수 있도록 하는 방법도 있습니다.

(하지만, 실제 우선순위를 보장하지는 않으므로 계속 기아상태에 머물 수 있다는 점도 있습니다.)

 

이상으로 단일 처리기 스케줄링에 대한 정리를 마치도록 하겠습니다.

감사합니다!!!

 

'OS' 카테고리의 다른 글

[OS] 가상메모리(상)  (0) 2023.04.25
[OS] 메모리 관리  (0) 2023.04.19
[OS] 상호 배제를 위한 모니터  (0) 2023.04.19
[OS] 세마포어(Semaphores)  (0) 2023.04.18

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 가상메모리에 대해서 정리하도록 하겠습니다.

 

 

1. 하드웨어와 제어구조

 

  • 프로세스의 모든 메모리 참조는 논리주소이고, 이 주소는 프로세스 수행 시간에 동적으로 물리주소로 변환됩니다. 
  • 프로세스는 스와핑으로 다시 주기억장치에 적재될 때, 이전 위치가 아닌 다른 위치에 저장될 수 있습니다.
  • 프로세스의 주소공간은 여러 블록으로 분할될 수 있고, 프로세스가 수행 중 이 블록들은 주기억장치의 연속된 영역에 위치할 필요가 없습니다.

 

 

2. 가상메모리 용어 정리

 

가상메모리 보조기억장치를 주기억장치처럼 주소지정 가능하게 만든 저장공간 할당체제
프로그램이 메모리를 참조할 때, 사용하는 주소는 메모리 시스템이 물리메모리의 특정 위치를 식별할 때 사용하는 주소와 구별
가상메모리의 크기는 주소지정체제와 보조기억장치의 가용 크기에 제한
가상주소 주기억장치처럼 참조될 수 있도록 가상메모리의 특정위치에 배정된 주소
가상주소공간 특정 프로세스에 할당된 가상 주소 영역
주소공간 특정 프로세스가 접근할 수 있는 메모리 주소 영역
실주소 주기억장치 상이 특정 저장위치의 주소

 

 

3. 주기억장치에 프로세스의 일부 블록(페이지, 세그먼트) 적재의 효과

 

수행 프로세스 과정

  • 프로세스의 적재집합은 프로세스의 코드나 데이터 중 임의 시점에 주기억장치에 적재되어 있는 부분을 의미합니다.
  • 처리기는 세그먼트테이블이나 페이지테이블을 이용하여 프로세스의 참조 주소가 적재집합에 포함되어 있는지를 파악합니다.
  • 주기억장치에 적재되지 않은 논리주소가 참조될 경우, 처리기는 메모리 접근 오류 인터럽트를 발생시킵니다.
  • 인터럽트 된 프로세스를 블록 상태로 둔 후, 디스크로부터 입출력 처리를 마친 후 블록된 상태를 재개합니다.

이러한 과정은, 보다 많은 프로세스를 주기억장치에 유지하고, 주기억장치보다 큰 프로세스를 수행할 수 있도록 합니다.

 

 

 

4. 페이징

 

가상 메모리는 전통적으로 프로세스별 페이지테이블이 설정됩니다. 페이지테이블 항목에는 해당 페이지가 메모리에 적재되어 있는지 구분할 수 있도록 비트(존재비트 P)가 존재해야 합니다. 페이지테이블 항목은  변경비트 M을 가지는데 해당 페이지가 메모리에 적재된 후 그 내용이 변경되었는지 여부를 나타냅니다.

 

페이지테이블 구조 

  • 프로세스가 수행될 때, 프로세스를 위한 페이지테이블의 시작 주소가 특정 레지스터에 저장
  • 가상주소의 페이지 번호를 테이블의 인덱스로 사용하여 페이지테이블의 항목 선정
  • 페이지가 적재된 프레임 번호를 얻음
  • 프레임번호 + 가상주소의 오프셋과 결합되어 물리주소 구성

 

전통적인 페이지 테이블 구조

 

32비트 주소 체계에 적용 가능한 2단계 구조의 페이지 처리 방식은 다음과 같습니다.

 

여기서, 4KB(2^10 * 2^2) * 4MB(2^20 * 2^2) = 4GB(2^32 * 2^2) 이므로 다음과 같은 2단계 구조를 만들 수 있으며 

전체 가상 주소 공간 2^32 / 페이지 크기 2^12를 하면 2^20의 페이지 수를 구할 수 있습니다.

 

2^20의 2^10(10 비트)은 루트 페이지 테이블 역할을 수행하고 나머지 2^10은 루트 페이지의 하위 페이지로 가상메모리 상에 유지됩니다.(프로세스가 적재되면 주기억장치로 루트 페이지 혹은 일부가 적재될 수 있습니다. -> 요구 페이징) 가상 페이지의 처음 10비트는 루트 페이지에 대한 인덱스로 쓰여 사용자 페이지테이블이 저장된 페이지를 위한 PTE를 찾아주며 (2^10), 여기서 찾은 값이 인덱스로 쓰여 가상주소에 참조될 실제 페이지를 위한 PTE를 찾게 합니다. 여기서 프레임 번호를 얻을 수 있으며 12비트의 오프셋과 합쳐져 주기억장치의 페이지프레임을 구할 수 있습니다.

 

 

이미지 출처:&nbsp;https://velog.io/@gndan4/OS-%EA%B0%80%EC%83%81-%EB%A9%94%EB%AA%A8%EB%A6%AC

 

 

 

역페이지 테이블 구조

 

앞서 정의한 전통적인 페이지테이블 구조에서는 가상 페이지수에 비례하여 PTE가 증가하게 되는데, 이를 주메모리에 적재해야 된다는 점에서 주기억장치에 많은 무리를 줄 수 있습니다. 이를 해결하기 위한 방법이 역페이지 테이블 구조입니다. 역페이지 테이블 구조의 저장 방식은 다음과 같습니다.

  • 가상주소 중 페이지번호 부분은 간단한 해시 함수를 통해 특정 해시 값으로 사상됩니다.
  • 해시값은 역페이지 테이블에 대한 인덱스로 쓰이고, 역페이지테이블은 페이지테이블의 항목들로 구성됩니다.
  • 페이지테이블 항목이 가상메모리의 페이지 당 하나씩이 아니라 실기억장치의 페이지프레임당 하나씩 설정되므로 프로세스의 수나 지원되는 가상 페이지 수와 상관없이 주기억장치의 일정 부분만이 테이블 설정에 쓰입니다.
  • 하나 이상의 가상주소들이 동일한 해시테이블 항목으로 사상될 수 있으므로 연결 기법을 통해 체인 기법을 활용합니다.

 

저는 역페이지 테이블 구조 2가지가 이해가 되지 않았습니다.ㅜㅜ
(하단에 게시되는 글을 제가 이해한 바를 정리한 내용이므로 사실과 다를 수 있습니다.)

먼저 실주소를 찾는 과정을 이해하는데 어려움을 겪었습니다.

예를 들어 운영체제가 프로세스A에 대한 가상 주소를 실주소로 매핑하길 원할 때, 단순히 페이지 번호로 어떻게 실주소를 매핑할 수 있는가입니다. 즉, 페이지 번호는 많은 프로세스에서 겹칠 수 있는데 역페이지테이블에서 프로세스 ID를 제공하더라도 여러 개의 체인으로 연결된 매핑값을 모두 실주소로 변환한 후 실주소에 접근하여 해당 프로세스가 운영체제가 요구하는 프로세스 A와 같은지 파악해야 하지 않는가입니다

이 문제에 대한 답변은 여러 블로그와 GPT를 통해 찾을 수 있었고 해당 절차는 다음과 같습니다.

운영체제는 현재 실행 중인 프로세스 A에 PID를 얻습니다. 가상 주소를 통해 페이지 번호를 획득한 후 해시함수에 프로세스 A의 PID와 가상 주소를 적용합니다. 만약 찾았다면 리턴하고, 해당 PID가 일치하지 않는 경우 체인을 활용하여 주소를 찾습니다. 역페이지테이블에서 해당 해시값을 바탕으로 실주소의 프레임 번호를 찾아 가상 주소의 오프셋과 결합하여 실주소를 얻습니다.

두번 째는 페이지테이블 항목이 가상메모리의 페이지 당 하나씩이 아니라 실기억장치의 페이지프레임당 하나씩 설정된다는 의미가 이해가 되지 않았습니다. 이에 대한 자료를 찾아본 후 제가 이해한 바는 다음과 같습니다.

페이징 작업을 수행할 때 주기억장치도 페이지 프레임을 적용합니다. 이때 가상 페이지 블록과 동일한 크기로 주기억장치도 물리 메모리를 분할하게 되는데, 분할된 각 페이지프레임에 대해 페이지테이블을 적용합니다. 이를 통해 다시 페이지테이블과 역페이지테이블을 이해하면, 페이지테이블의 경우 가상 주소 공간을 페이지 범위로 나누고 각 페이지에 대해 테이블 엔트리를 설정하기에 프로세스별로 PTE가 생성되게 됩니다.

역페이지테이블은 프로세스가 실행될 때, 필요한 가상 메모리 페이지들은 디스크 스왑 영역에 위치하고 프로세스가 해당 페이지에 액세스 하는 경우 페이지폴트가 발생하여 주기억장치에 스왑인 됩니다. 이 과정에서 역페이지 테이블에 페이지에 대한 정보, 프로세스 ID 등의 정보를 입력하게 됩니다. 따라서,  페이지 번호와 오프셋을 통해 가상 주소를 실주소로 매핑할 수 있게 됩니다. 즉, 프로세스 별 PTE를 적재하는 것이 아니라, 주메모리에 있는 페이지프레임에 역페이지테이블을 구성하여 적재하는 것입니다.

 

 

5. TLB(Translation Lookaside Buffer)

 

원칙적으로 모든 가상메모리 참조는 두번의 물리 메모리 참조를 수반하므로 두 배의 메모리 접근 시간을 갖게 합니다.

가상메모리 방식은 페이지테이블 항목들에 대한 특수 고속 캐시를 사용하는데, 이를 TLB라고 부릅니다. TLB는 가장 최근에 참조된 페이지테이블의 항목을 유지합니다.

 

TLB의 작동 구조

  • 시작
  • CPU가 TLB를 검사
  • TLB에 페이지 테이블 항목이 있다면 CPU가 물리 주소를 생성
  • 없다면, 페이지 테이블에 접근하여 주기억 장치에 페이지 테이블 확인 (존재비트 1 or 0)
  • 있다면(존재비트 1), TLB를 갱신한 후 CPU 물리 주소를 생성
  • 만약 주기억 장치에 페이지가 없다면 '페이지 폴트'가 발생하여 운영체제가 CPU에게 디스크로부터 페이지를 반입하도록 명령
  • CPU가 입출력 하드웨어를 활성화하고 드스크로부터 주기억장치로 페이지를 스왑인

 

이 부분에서 혼란이 왔던 부분은 실제 페이지테이블에서 페이지 번호를 조회하는 것과 TLB에서 페이지 번호를 조회하는 것이 결국 같은 메커니즘으로 적용되는 것이 아닌가라는 의문이었습니다.

하지만 실제 메커니즘은 많이 달랐습니다. 앞 서 페이지테이블은 일부가 주기억장치에 저장된다고 설명하였습니다. 만약 페이지폴트가 발생하지 않는 상황하에서 가상메모리의 실주소를 찾는 과정은 주기억장치에 접근해야 하므로 속도가 더딜 수 있습니다.

TLB는 CPU 하드웨어에 접근하여 만약 TLB 페이지테이블에 항목이 있다면 이미 주기억장치에 스왑인 하면서 TLB를 갱신한 것을 의미하기 때문에 물리주소를 바로 얻어올 수 있습니다.

만약, 페이지폴트가 발생한다면 디스크로부터 주기억장치로 필요한 페이지를 전송하여 적재하고 TLB를 갱신합니다. 이러한 과정을 통해 최근 참조된 페이지테이블에 값은 TLB에 적재되어 주기억장치를 매번 들려서 물리주소를 찾지 않더라도 빠른 고속 처리를 가능하도록 합니다.

 

 

6. 페이지 크기

 

페이지 크기는 그 양에 따라 페이지 폴트 발생률이 달라질 수 있습니다.

 

페이지 크기가 작은 경우

일반적으로 페이지 크기가 작다면 페이지폴트의 경우 지역성의 원리에 따라 한 프로세스가 활용할 수 있는 페이지들이 상대적으로 많아집니다. 이는 곧 페이지폴트 수를 줄이는 효과를 얻을 수 있습니다. 페이지 폴트의 수를 줄이는 이유는 다음과 같습니다.

  • 공간 지역성: 프로세스는 일반적으로 코드와 데이터의 일부 영역에 집중되어 실행됩니다. 페이지 크기가 작으면 주기억장치에 적재된 페이지가 필요한 데이터와 코드만 포함하게 되므로 더 좋은 공간 지역성을 달성합니다.
  • 메모리 사용의 효율성: 작은 페이지 크기를 사용하면 페이지 내부의 미사용 메모리 공간이 줄어들고 메모리 사용 효율성이 향상됩니다. (내부 단편화 감소)

 

< 페이지 크기가 작으면 주기억장치에 적재된 페이지가 필요한 데이터와 코드만 포함?>

페이지 크기가 작아지면, 각 페이지에 포함된 데이터와 코드가 더 밀집되어 있습니다. 이렇게 되면 주기억장치에 적재된 페이지가 프로세스가 필요로 하는 데이터와 코드만 포함하게 됩니다. 앞 서, 페이지 크기가 작으면 주기억장치에 적재된 페이지가 필요한 데이터와 코드만 포함하게 된다는 의미는 실제 프로세스가 주기억장치에 적재될 때 페이지 크기가 클 경우 불필요한 데이터도 함께 적재될 수 있지만, 페이지 크기가 작을 경우 반드시 필요한 소스만 적재될 수 있기 때문에 효율적으로 사용할 수 있다는 의미입니다.

 

 

하지만, 반드시 페이지폴트가 감소하는 것은 아닙니다.

 

페이지 크기가 작을 때는 페이지 수가 증가하게 됩니다. (32비트 가상 주소 공간을 고려하면, 2^12의 페이지 크기가 있다면 32 - 12 = 20 즉 2^20만큼의 페이지 수가 필요합니다)

이는 곧 페이지 테이블이 증가하는 문제를 발생시키고, 멀티프로그래밍 환경에서 큰 프로그램을 수행하는 활성 프로세스의 페이지테이블 중 일부가 주기억장치에 적재되지 못하고 가상메모리 상에 있어야 합니다. 즉 이 경우는 반대로 페이지 폴트 발생률을 높일 수 있습니다.

 

페이지 크기가 클 경우 

페이지 크기가 큰 경우 페이지 폴트 발생률은 증가할 수 있습니다. 페이지 크기에 따라 비례하여 페이지 수는 작아지므로 개별 페이지들은 최근 잠조로부터 멀리 떨어진 위치를 포함하게 됩니다. 이는 지역성의 원리에 따르면 공간 밀집도가 낮아지게 되므로 페이지 폴트 발생 확률을 높입니다.

 

페이지 크기가 프로세스 전체 크기와 근접해 갈 경우

이 경우 하나의 페이지에 프로세스 전체를 적재할 수 있으므로 페이지 폴트 발생 빈도가 감소하게 됩니다. 

 

 

 

7. 정리하며

 

가상 메모리를 사용할 경우 모든 주소 참조는 실행 중에 실주소로 변환되는 논리적 띄게 됩니다. 가상 메모리의 도입으로 주기억장치에 가해지는 부담을 효과적으로 줄일 수 있습니다. 이러한 가상 메모리의 용어, 페이지를 정리하며 정말 많은 점을 배울 수 있었고 단순히 논리적 참조로만 알고 있었던 과거에 저를 반성할 수 있게 되었습니다.

 

이해가 되지 않는 부분이 너무 많았고 하나씩 이해하는 과정에서 정말 많은 시간이 흘렀지만 가상 메모리가 어떠한 수행 절차로 실주소로 변환되는지 배울 수 있었고, TLB로 최근 참조한 페이지테이블에 대한 고속 캐시 효과를 얻을 수 있는 점도 정리할 수 있었습니다.

 

 

사실과 무관한 정보도 있을 수 있습니다. 이 부분은 계속 반복 학습하여 수정해 나가겠습니다.! 피드백 주시면 바로 배우겠습니다.

감사합니다.!

 

자료 출처: 운영체제 제 8판 내부구조 및 설계원리

그림 출처: https://velog.io/@gndan4/OS-%EA%B0%80%EC%83%81-%EB%A9%94%EB%AA%A8%EB%A6%AC

'OS' 카테고리의 다른 글

[OS] 단일처리기 스케줄링  (0) 2023.04.25
[OS] 메모리 관리  (0) 2023.04.19
[OS] 상호 배제를 위한 모니터  (0) 2023.04.19
[OS] 세마포어(Semaphores)  (0) 2023.04.18

+ Recent posts