728x90

문제: https://school.programmers.co.kr/learn/courses/30/lessons/120844

 


1️⃣ 첫 번째 코드 (리스트 변환 사용)

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

class Solution {
    public int[] solution(int[] numbers, String direction) {
        List<Integer> list = Arrays.stream(numbers).boxed().collect(Collectors.toList());

        if (direction.equals("right")) {
            list.add(0, list.get(list.size() - 1)); // 마지막 원소를 맨 앞에 삽입
            list.remove(list.size() - 1); // 마지막 원소 제거
        } else {
            list.add(list.size(), list.get(0)); // 첫 번째 원소를 맨 뒤에 삽입
            list.remove(0); // 첫 번째 원소 제거
        }
        return list.stream().mapToInt(Integer::intValue).toArray();
    }
}

🔹 특징

  1. 배열을 List<Integer>로 변환한 후, 리스트 연산을 통해 이동을 수행.
  2. add(0, element) 또는 add(size, element)를 사용해 요소를 앞뒤로 이동.
  3. 마지막에 stream()을 사용하여 다시 int[]로 변환.

🟢 장점

  • 코드가 직관적이며 리스트의 메서드를 활용하여 쉽게 이해할 수 있음.

🔴 단점

  • 성능이 비효율적
    • Arrays.stream(numbers).boxed().collect(Collectors.toList()) → O(N)
    • add(index, element)와 remove(index) → O(N)
    • 최종적으로 list.stream().mapToInt(Integer::intValue).toArray() → O(N)
    • 전체 시간 복잡도: O(N) + O(N) + O(N) = O(N)
  • 불필요한 오토박싱 & 언박싱 발생
    • int를 Integer로 변환(boxing), 다시 int로 변환(unboxing) → 성능 저하 가능성 있음.

2️⃣ 두 번째 코드 (배열 인덱스 이동 사용)

class Solution {
    public int[] solution(int[] numbers, String direction) {
        int len = numbers.length;
        int[] answer = new int[len];
        boolean rightMove = "right".equals(direction);
        
        for (int i = 0; i < len; i++) {
            answer[rightMove ? (i + 1) % len : (i - 1 + len) % len] = numbers[i];
        }
        return answer;
    }
}

🔹 특징

  1. 새로운 배열 answer을 생성하고 수학적 연산을 이용해 인덱스를 조정하여 값을 삽입.
  2. 오른쪽 이동(right) → answer[(i + 1) % len] = numbers[i];
  3. 왼쪽 이동(left) → answer[(i - 1 + len) % len] = numbers[i];

🟢 장점

  • 성능이 매우 우수함 (O(N))
    • 단순 반복문과 인덱스 연산만 사용하여 추가적인 리스트 변환 없이 처리.
    • 리스트 변환, 박싱/언박싱이 없어 불필요한 성능 손실 없음.
  • 메모리 사용량이 적음
    • int[]만 사용하므로 추가적인 객체 생성이 없음.

🔴 단점

  • answer[rightMove ? (i + 1) % len : (i - 1 + len) % len] = numbers[i];
    → 가독성이 살짝 떨어질 수 있음.
    → 하지만 수학적 연산이므로 이해하면 훨씬 효율적임.

🏆 최종 결론: 두 번째 코드가 더 좋음!

첫 번째 코드 (리스트 변환) 두 번째 코드 (배열 인덱스 연산)

시간 복잡도 O(N) + O(N) + O(N) = O(N) O(N)
메모리 사용 추가적으로 List<Integer> 사용 int[] 배열만 사용 (메모리 절약)
성능 리스트 변환과 박싱/언박싱으로 성능 저하 빠른 배열 인덱스 계산
가독성 직관적이지만 리스트 변환 과정이 필요 다소 수학적이지만 최적화됨
추천 여부 ❌ 비효율적 ✅ 최적

💡 결론:

두 번째 코드(배열 인덱스 연산 방식)더 빠르고 메모리 효율적이며 최적화됨.

728x90
728x90

문제: https://school.programmers.co.kr/learn/courses/30/lessons/120835

 

 

1️⃣ 첫 번째 코드 분석 (이중 반복문)

class Solution {
    public int[] solution(int[] emergency) {
        int[] answer = new int[emergency.length];

        for(int i = 0; i < answer.length; i++){
            if(answer[i] != 0){
                continue;
            }
            int idx = 1;
            for(int j = 0; j < answer.length; j++){
                if(emergency[i] < emergency[j]){
                    idx++;
                }
            }
            answer[i] = idx;
        }
        return answer;
    }
}

📌 특징

  • 각 요소에 대해 다른 모든 요소와 비교하여 순위를 매김.
  • 시간 복잡도: O(N²) (이중 반복문)

🔴 단점

  • 성능이 좋지 않음
    → emergency의 길이가 길어질수록 성능이 급격히 저하됨.
  • 불필요한 if(answer[i] != 0) 검사가 포함됨 (이 코드에서는 불필요한 로직).
  • 가독성이 떨어짐 (배열을 두 번 순회하여 index를 계산하는 것이 직관적이지 않음).

2️⃣ 두 번째 코드 분석 (정렬 + 해시맵)

import java.util.*;

class Solution {
    public int[] solution(int[] emergency) {
        int[] arr = emergency.clone();
        Arrays.sort(arr);
        Map<Integer, Integer> indexMap = new HashMap<>();
        for(int i = 0; i < arr.length; i++) {
            indexMap.put(arr[i], arr.length - i);
        }
        int[] answer = new int[emergency.length];
        for(int i = 0; i < emergency.length; i++) {
            answer[i] = indexMap.get(emergency[i]);
        }
        return answer;
    }
}

📌 특징

  • emergency 배열을 정렬하여 작은 값부터 큰 값까지 순위를 매김.
  • 해시맵(indexMap)을 이용하여 각 값의 순위를 빠르게 찾음.
  • 시간 복잡도: O(N log N) (정렬) + O(N) (해시맵 탐색) = O(N log N)

✅ 장점

  • 성능이 훨씬 좋음
    → O(N²) 대신 O(N log N)으로 성능 개선.
  • 가독성이 높음
    → 정렬 후 순위를 매기는 과정이 명확하고 직관적.
  • 불필요한 연산을 줄임
    → 중복 비교 없이 한 번의 정렬과 해시맵 탐색만으로 순위 결정.

3️⃣ 세 번째 코드 분석 (정렬)

import java.util.Arrays;

class solution {
    public int[] solution(int[] emergency) {
        int n = emergency.length;
        int[] answer = new int[n];
        Integer[] sortedIndex = new Integer[n];
        for( int i = 0; i < n; i++ ) {
            sortedIndex[i] = i;
        }

        Arrays.sort(sortedIndex, (i1, i2)-> emergency[i2] - emergency[i1]);

        for( int i = 0; i < n; i++ ) {
            answer[sortedIndex[i]] = i + 1;
        }

        return answer;
    }
}

📌 특징

  • emergency 배열의 Index를 정렬.
  • 정렬된 Index를 활용하여 각 값의 Rank를 매김.
  • 시간 복잡도: O(N) (Index 적용) + O(N log N) (정렬) + O(N) (answer작성) = O(N log N)

✅ 장점

  • 성능이 훨씬 좋음
    → O(N²) 대신 O(N log N)으로 성능 개선.
  • 가독성이 높음
    → 정렬 후 순위를 매기는 과정이 명확하고 직관적.

4️⃣ 네 번째 코드 분석 (우선순위 큐)

import java.util.Comparator;
import java.util.PriorityQueue;

public class Solution {
	public int[] solution(int[] emergency) {
        int n = emergency.length;
        int[] answer = new int[n];
        PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(o -> o[0]));
        for( int i = 0; i < n; i++ ) {
            pq.add(new int[]{emergency[i], i});
        }

        for( int i = 0; i < n; i++ ) {
            answer[pq.poll()[1]] = n - i;
        }

        return answer;
    }
}

📌 특징

  • 우선순위 큐를 활용하여 정렬된 데이터를 사용.
  • 시간 복잡도: O(log n) ( PriorityQueue 요소 추가) + O(N log N) ( PriorityQueue 요소 추출) = O(N log N)

🔴 단점

  • 우선순위 큐 사용시 큐 값을 꺼내는 데 추가적인 시간 오버헤드 발생

🏆 결론: 세 번째 코드가 더 낫다!

  • 성능: O(N log N) vs O(N²) → 두번째와 세번째 코드가 훨씬 빠름.
  • 가독성: 정렬된 배열을 활용함으로써 가독성 향상
  • 유지보수성: 코드가 짧고 이해하기 쉬움.
728x90
728x90

레드-블랙 트리란?

레드-블랙 트리란 BTS의 일종으로, BTS의 최악의 시간복잡도 O(n)을 방지하기 위해 균형을 유지하게 하는 균형 이진 탐색트리 중 하나입니다.

FunctionSearchInsertDelete

Function Amortized Worst Case
Select O(log⁡n) O(log⁡n)
Insert O(log⁡n) O(log⁡n)
Delete O(log⁡n) O(log⁡n)

 

레드-블랙 트리의 속성

레드-블랙 트리의 노드들은 red 또는 black색을 가지며, 아래와 같은 5가지 특징을 준수하여 균형을 이룹니다.

  1. 모든 노드는 red 혹은 black이다.
  2. 루트 노드는 black이다.
  3. 모든 리프 노드( NIL )는 black이다.
  4. red 노드의 자식 노드는 항상 black이다.
  5. 모든 노드에서 리프 노드들 까지 가는 경로에 존재하는 black노드의 수는 같다. ( 검색 시작 노드 제외 )

데이터 삽입

우선 레드-블랙 트리에서 삽입하는 노드의 색은 항상 red입니다.

 

데이터 삽입 과정 중 레드 - 블랙 트리의 속성에 위배하는 경우 Rebalancing과정을 거치게 됩니다.

Rebalancing은 색변경(Recoloring)회전(Rotation) 과정으로 이루어져 있으며, 케이스에 따라 두 가지 방법을 혼합하여 적용하면 레드 - 블랙 트리의 속성을 위반하지 않게 만들 수 있습니다.

 

아래에 삽입 시 발생할 수 있는 케이스에 대해 알아보고 또한 Rebalancing과정이 어떻게 이루어질 수 있는지 알아보도록 하겠습니다.

Case1. Root 삽입

맨 처음 Root에 데이터를 삽입하는 Case입니다.

 

데이터 삽입 시 들어오는 노드의 색은 항상 red로 고정되어 있기 때문에 삽입된 노드의 색상은 red입니다.

이때, 레드-블랙 트리 2번째 속성 'root는 black이다'를 위반하기 때문에 Recoloring과정을 통해 black으로 변경하여 줍니다.


Case2. 삽입한 노드의 부모 노드가 black인 경우

데이터 삽입 시 삽입되는 노드의 색상은 red이고, 위반하는 규칙은 존재하지 않습니다.


Case3. 부모 노드가 red이고, 한쪽으로 쏠린 경우

1을 삽입하게 되면 위 사진의 첫번째와 같이 red하위에 red가 존재하는 양상의 띄게 되며 그와 동시에 4번 속성을 위반하게 됩니다.

 

그럴경우 부모노드 색을 black으로 조부모 색을 red로 변경해줍니다.

그 다음 왼쪽으로 쏠림현상이 존재하므로 5를 기준으로 오른쪽으로 회전하여 2가 root에 위치하도록 합니다.

 

수정 후 모습을 보면 4번 속성을 위반하지 않고, 2번 속성도 위반하지 않는 것을 확인 할 수 있었습니다.


Case4. 부모노드가 red이고, 한쪽으로 몰려있으며, 오른쪽으로 꺾인 경우

3을 삽입하게 되면 오른쪽으로 꺾이게 됩니다.

이때 삽입한 노드를 기준으로 좌로 회전합니다.

 

이후 root와 삽입한 노드의 색을 바꿔주고, root를 기준으로 오른쪽으로 회전합니다.


Case5. 부모노드가 red이고, 삼촌노드도 red인 경우

6을 삽입하였습니다.

우선 4번 속성을 위반하기 때문에 부모와 삼촌 노드의 색을 black으로, 조상 노드의 색을 red로 변경합니다.

 

이후 2번 속성을 위반하였기 때문에 루트 노드의 색을 black으로 변경해줍니다.

 

이때 black 하위 black인 경우가 생기게 되는데, 이는 속성을 위반하는 사항이 아니므로 괜찮습니다.


이후 많은 다양한 상황들이 나올 수 있지만, 위 케이스들을 기반으로 비교하면 레드-블랙 트리의 속성을 지키며, 작업을 이어나갈 수 있게 됩니다.


레드-블랙 트리  VS AVL 트리

레드 블랙 트리를 왜 사용하는지, 그리고 AVL 트리와의 차이는 어떤 점들이 존재하는지 알아보도록 하겠습니다.

  레드-블랙 트리 AVL 트리
삽입/삭제 속도 더 빠름 (재조정이 덜 복잡함) 느림 (더 엄격한 균형 유지)
검색 속도 약간 느림 ( 완벽한 균형 x )  더 빠름 (더 균형 잡힘)
구현 난이도 상대적으로 단순 더 빠름(더 균형 잡힘)
적합한 용도 삽입/삭제가 잦은 경우 일기 성능이 중요한 경우
사용처 TreeMap, TreeSet,... Index, Cache,...

Reference

https://en.wikipedia.org/wiki/Red%E2%80%93black_tree

https://www.youtube.com/watch?v=2MdsebfJOyM

 

 

 

728x90
728x90

들어가기에 앞서

해당 Post는 이진 트리에 대한 기초 지식을 요구합니다.

만약 이진트리를 모른다면 이 글을 읽고 난 이후 봐주시기 바랍니다.

이진 탐색 트리 값 삽입

이진트리 중 중복 값은 허용하지 않고, 왼쪽은 부모 노드보다 작은 값을 오른쪽 자식노드는 부모노드 보다 큰 조건을 충족하는 트리를 이진 탐색 트리라고 합니다.

 

위와 같이 데이터가 삽입되며, 왼쪽은 currnet보다 작은 값, 오른쪽은 큰 값을 배치하게 됩니다.

public class BinaryTree {
    TreeNode<Integer> root;

    public void add(Integer value) {
        root = add(root, value);
    }

    private TreeNode<Integer> add(TreeNode<Integer> current, Integer value) {
        if(current == null) {
            return new TreeNode<>(value);
        }

        if(current.getValue() < value) {
            current.setRight(add(current.getRight(), value));
        } else {
            current.setLeft(add(current.getLeft(), value));
        }

        return current;
    }
}

이진 탐색 트리의 삭제

이진 탐색 트리의 삭제는 어떤 상태를 가진 노드를 삭제하느냐로 3가지로 나눠볼 수 있습니다.

Leaf Node 삭제

가장 생각할 게 없는 단순한 삭제입니다.

 

자식 노드가 존재하지 않기 때문에 균형 이진트리가 아니라면 따로 큰 작업이 이뤄지지 않는 삭제입니다.

자식이 1개인 부모 Node 삭제

Leaf Node삭제보다 살짝 작업이 추가된 삭제라고 생각합니다.

 

자식이 1개일 경우 삭제할 노드의 부모 노드에 자식 노드의 Instance정보를 연결해 주고, 본인은 참조되지 않는 방법으로 간단하게 삭제처리할 수 있습니다.

자식이 2개인 부모 Node 삭제

자식이 2개인 경우 어떤 정책을 통해 부모 노드를 대체할 것인지를 고민해야 합니다.

  • 중위 전임자(In-order Predecessor): 삭제할 노드의 왼쪽 서브트리에서 가장 큰 값을 가진 노드.
  • 중위 후속자(In-order Successor): 삭제할 노드의 오른쪽 서브트리에서 가장 작은 값을 가진 노드.

위와 같이 정책을 정하여 삭제를 진행할 경우 데이터의 쏠림 현상으로 인해 시간복잡도가 차이 날 수 있는데,

평균적으로 O(log n)의 시간복잡도를 가지며, 최악의 경우 O(n)의 시간복잡도를 가지게 됩니다.

 

중위 전임자 정책을 사용하여 삭제하였을 경우

public class BinaryTree {
    TreeNode<Integer> root;

    private TreeNode<Integer> add(TreeNode<Integer> current, TreeNode<Integer> prevNode) {
        if(current == null) {
            return prevNode;
        }

        if(current.getValue() < prevNode.getValue()) {
            current.setRight(add(current.getRight(), prevNode));
        } else {
            current.setLeft(add(current.getLeft(), prevNode));
        }

        return current;
    }

    public void remove(Integer value) {
        root = remove(root, value);
    }

    private TreeNode<Integer> remove(TreeNode<Integer> current, Integer value) {
        if(current == null) return null;
        // 중위 전임자(In-order Predecessor)
        if(Objects.equals(current.getValue(), value)) {
            TreeNode<Integer> predecessor = predecessor(current.getLeft());
            predecessor.setRight(current.getRight());
            if(current.getLeft() != null)
                add(predecessor, current.getLeft());
            return predecessor;
        }

        current.setLeft(remove(current.getLeft(), value));
        current.setRight(remove(current.getRight(), value));

        return current;
    }

    // 중위 전임자(In-order Predecessor): 삭제할 노드의 왼쪽 서브트리에서 가장 큰 값을 가진 노드.
    private TreeNode<Integer> predecessor(TreeNode<Integer> current) {
        if(current.getRight() == null)
            return current;

        TreeNode<Integer> predecessor =  predecessor(current.getRight());
        if(Objects.equals(current.getRight().getValue(), predecessor.getValue()))
            current.setRight(null);

        return predecessor;
    }
}


중위 후속자 정책을 사용하여 삭제하였을 경우

public class BinaryTree {
    TreeNode<Integer> root;

    private TreeNode<Integer> add(TreeNode<Integer> current, TreeNode<Integer> prevNode) {
        if(current == null) {
            return prevNode;
        }

        if(current.getValue() < prevNode.getValue()) {
            current.setRight(add(current.getRight(), prevNode));
        } else {
            current.setLeft(add(current.getLeft(), prevNode));
        }

        return current;
    }

    public void remove(Integer value) {
        root = remove(root, value);
    }

    private TreeNode<Integer> remove(TreeNode<Integer> current, Integer value) {
        if(current == null) return null;

        // 중위 후속자(In-order Successor)
        if(Objects.equals(current.getValue(), value)) {
            TreeNode<Integer> successor = successor(current.getRight());
            successor.setLeft(current.getLeft());
            if(current.getRight() != null)
                add(successor, current.getRight());
            return successor;
        }

        current.setLeft(remove(current.getLeft(), value));
        current.setRight(remove(current.getRight(), value));

        return current;
    }

    // 중위 후속자(In-order Successor): 삭제할 노드의 오른쪽 서브트리에서 가장 작은 값을 가진 노드.
    private TreeNode<Integer> successor(TreeNode<Integer> current) {
        if(current.getLeft() == null)
            return current;

        TreeNode<Integer> successor =  successor(current.getLeft());
        if(Objects.equals(current.getLeft().getValue(), successor.getValue()))
            current.setLeft(null);
        return successor;
    }
}


최종 코드

public class BinaryTree {
    TreeNode<Integer> root;

    public BinaryTree() {
        root = null;
    }

    public void add(Integer value) {
        root = add(root, value);
    }

    private TreeNode<Integer> add(TreeNode<Integer> current, Integer value) {
        if(current == null) {
            return new TreeNode<>(value);
        }

        if(current.getValue() < value) {
            current.setRight(add(current.getRight(), value));
        } else {
            current.setLeft(add(current.getLeft(), value));
        }

        return current;
    }

    private TreeNode<Integer> add(TreeNode<Integer> current, TreeNode<Integer> prevNode) {
        if(current == null) {
            return prevNode;
        }

        if(current.getValue() < prevNode.getValue()) {
            current.setRight(add(current.getRight(), prevNode));
        } else {
            current.setLeft(add(current.getLeft(), prevNode));
        }

        return current;
    }

    public void remove(Integer value) {
        root = remove(root, value);
    }

    private TreeNode<Integer> remove(TreeNode<Integer> current, Integer value) {
        if(current == null) return null;

        if(!Objects.equals(current.getValue(), value)) {
            current.setLeft(remove(current.getLeft(), value));
            current.setRight(remove(current.getRight(), value));
            return current;
        }

        // Leaf일 때 삭제
        if(current.getRight() == null && current.getLeft() == null)
            return null;

        if(current.getLeft() == null) return current.getRight();
        if(current.getRight() == null) return current.getLeft();

//        // 중위 전임자(In-order Predecessor)
//        TreeNode<Integer> predecessor = predecessor(current.getLeft());
//        predecessor.setRight(current.getRight());
//        if(current.getLeft() != null)
//           add(predecessor, current.getLeft());
//        return predecessor;

        // 중위 후속자(In-order Successor)
        TreeNode<Integer> successor = successor(current.getRight());
        successor.setLeft(current.getLeft());
        if(current.getRight() != null)
            add(successor, current.getRight());

        return successor;

    }

    // 중위 전임자(In-order Predecessor): 삭제할 노드의 왼쪽 서브트리에서 가장 큰 값을 가진 노드.
    private TreeNode<Integer> predecessor(TreeNode<Integer> current) {
        if(current.getRight() == null)
            return current;

        TreeNode<Integer> predecessor =  predecessor(current.getRight());
        if(Objects.equals(current.getRight().getValue(), predecessor.getValue()))
            current.setRight(null);

        return predecessor;
    }

    // 중위 후속자(In-order Successor): 삭제할 노드의 오른쪽 서브트리에서 가장 작은 값을 가진 노드.
    private TreeNode<Integer> successor(TreeNode<Integer> current) {
        if(current.getLeft() == null)
            return current;

        TreeNode<Integer> successor =  successor(current.getLeft());
        if(Objects.equals(current.getLeft().getValue(), successor.getValue()))
            current.setLeft(null);
        return successor;
    }


    // 전위 순회 (Pre-order Traversal) 출력 메서드
    public void traversePreOrder(TreeNode<Integer> node) {
        if (node != null) {
            System.out.print(" " + node.getValue());
            traversePreOrder(node.getLeft());
            traversePreOrder(node.getRight());
        }
    }

    // 중위 순회 (In-order Traversal) 출력 메서드
    public void traverseInOrder(TreeNode<Integer> node) {
        if (node != null) {
            traverseInOrder(node.getLeft());
            System.out.print(" " + node.getValue());
            traverseInOrder(node.getRight());
        }
    }

    // 후위 순회 (Post-order Traversal) 출력 메서드
    public void traversePostOrder(TreeNode node) {
        if (node != null) {
            traversePostOrder(node.getLeft());
            traversePostOrder(node.getRight());
            System.out.print(" " + node.getValue());
        }
    }

    // 레벨 순회 (Level-order Traversal) 출력 메서드
    public void traverseLevelOrder(TreeNode root) {
        if (root == null) return;

        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);

        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            System.out.print(" " + node.getValue());

            if (node.getLeft() != null) queue.add(node.getLeft());
            if (node.getRight() != null) queue.add(node.getRight());
        }
    }

    public static void main(String[] args) {
        BinaryTree bt = new BinaryTree();
        bt.add(5);
        bt.add(2);
        bt.add(3);
        bt.add(1);
        bt.add(0);
        bt.add(7);
        bt.add(8);
        bt.add(6);

        bt.remove(5);

        // 순회 결과 출력
        System.out.print("Pre-order traversal:");
        bt.traversePreOrder(bt.root);
        System.out.println();

        System.out.print("In-order traversal:");
        bt.traverseInOrder(bt.root);
        System.out.println();

        System.out.print("Post-order traversal:");
        bt.traversePostOrder(bt.root);
        System.out.println();

        System.out.print("Level-order traversal:");
        bt.traverseLevelOrder(bt.root);
        System.out.println();
    }
}
728x90

'알고리즘 문제풀이 > 자료구조' 카테고리의 다른 글

[Tree] 이진 트리 순회  (1) 2024.11.08
[Tree] 이진 트리 ( Binary Tree )  (0) 2024.11.07
[자료구조] 스택(Stack)  (0) 2024.10.21
728x90

 

바보같이 너무 많이 생각해서 어려웠던 문제였습니다.

 

문제자체는 단순합니다.

parent 값들을 주고 해당 값으로 생성된 node들 중 하나를 삭제하고, 이후 leaf의 갯수를 알아내면 되는 문제입니다.

 

parent와 index값을 저장을 하고,

index를 부모로 가지는 node들을 child로 저장,

삭제된 노드들의 정보를 가지게 된다면 간단하게 해결할 수 있는 문제입니다.

 

다만 주의할 점은 현재 삭제된 노드들만 가지고 있는 노드 또한 leaf라는 점입니다.

이점은 코드를 확인해보며 찾아보시기 바랍니다.

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

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

        int[] parents = new int[N];
        boolean[] isDeleted = new boolean[N];
        List<List<Integer>> graph = new ArrayList<>();

        for(int i = 0; i <= N; i++) {
            graph.add(new ArrayList<>());
        }

        int root = -1;
        StringTokenizer st = new StringTokenizer(br.readLine());

        for(int i = 0; i < N; i++) {
            parents[i] = Integer.parseInt(st.nextToken());
            if(parents[i] == -1) {
                root = i;
            } else {
                graph.get(parents[i]).add(i);
            }
        }
        int deleteNode = Integer.parseInt(br.readLine());
        if(parents[deleteNode] > -1) {
            delete(graph.get(parents[deleteNode]), isDeleted, deleteNode);
        } else {
            isDeleted[root] = true;
        }
        bw.write(countLeafNode(graph, isDeleted, root) + "");
        bw.flush();
        br.close();
        bw.close();
    }

    public static void delete(List<Integer> graph, boolean[] isDeleted, int node) {
        for(int child : graph) {
            if(child == node) {
                isDeleted[child] = true;
                return;
            }
        }
    }

    public static int countLeafNode(List<List<Integer>> graph, boolean[] isDeleted, int node) {
        if(isDeleted[node]) return 0;
        if(graph.get(node).isEmpty()) return 1;
        int count = 0;

        for(int child : graph.get(node)) {
            count += countLeafNode(graph, isDeleted, child);
        }

        return count == 0 ? 1 : count;
    }
}
728x90
728x90

문제를 보면 모든 노드들은 연결이 되어있고, 모든 노드들을 돌아볼 때 가장 적은 종류의 비행기( 간선 정보 )를 타고 움직이는 수를 알아내야하는 것이 문제의 의도 입니다.

 

이때 우리는 모든 노드가 연결되어있고, 최소한의 경로로 모든 노드를 방문해야한다는 조건에서 최소신장 트리를 떠올릴 수 있어야 합니다.

 

최소신장 트리의 모든 노드를 연결하면서 가중치의 합을 구하면 되겠는데, 이때 값은 N - 1로써 가장 간단하게 구할 수 있습니다.

(N은 노드 수입니다.)

 

import java.io.*;
import java.util.*;

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

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

        StringBuilder sb = new StringBuilder();
        while (T-- > 0) {
            st = new StringTokenizer(br.readLine());
            int n = Integer.parseInt(st.nextToken());
            int m = Integer.parseInt(st.nextToken());
            for (int i = 0; i < m; i++) {
                br.readLine();
            }
            sb.append(n - 1).append("\n");
        }
        bw.write(sb.toString());
        bw.flush();
        br.close();
        bw.close();
    }

}
  • 시간복잡도: O(1)
  • 공간복잡도: O(1)
728x90
728x90

 

입력 예제를 확인해보면 N개의 노드와 N - 1개의 간선 정보가 주어지는 것을 확인해 볼 수 있습니다.

그림으로 표현하면 위와 같이 표현할 수 있으며, 해당 정보를 통해 2번부터 N번 까지 노드들의 부모정보를 출력하면 되는 문제입니다.

 

즉 2번의 부모 4, 3번의 부모 6, 4번의 부모 1, 5번의 부모 3, 6번의 부모 1, 7번의 부모 4번 해서 결과는 4, 6, 1, 3, 1, 4가 나오면 되는 것 입니다.

 

여기서 주의해야하는 점은 해당 트리는 자식 노드가 최대 2개가 주어진다는 말이 없으니 일반 트리이며, 자식 노드는 N개가 될 수 있다는 말입니다.

 

문제 풀이는 생각보다 간단합니다. 

문제에서 트리의 루트를 1번으로 잡는다 하였으니, 1번의 자식들의 부모를 Bfs 탐색을 통해 확인한 다음 2번노드 부터 부모 번호를 출력해주면 됩니다.

public class Main {

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

        List<Integer>[] arr = new List[N + 1];
        for( int i = 1; i <= N; i++ ) {
            arr[i] = new ArrayList<>();
        }

        for (int i = 1; i < N; i++) {
            st = new StringTokenizer(br.readLine());
            int from = Integer.parseInt(st.nextToken());
            int to = Integer.parseInt(st.nextToken());

            arr[from].add(to);
            arr[to].add(from);
        }

        int[] parent = new int[N + 1];
        Queue<Integer> queue = new LinkedList<>();
        queue.add(1);
        parent[1] = 1;

        while (!queue.isEmpty()) {
            int current = queue.poll();
            for(int child: arr[current]){
                if (parent[child] == 0) {
                    parent[child] = current;
                    queue.add(child);
                }
            }
        }

        StringBuilder sb = new StringBuilder();
        for (int i = 2; i <= N; i++) {
            sb.append(parent[i]);
            sb.append("\n");
        }
        System.out.println(sb);
    }
}
  • 시간복잡도: O(N)
  • 공간복잡도: O(N)
728x90

'알고리즘 문제풀이' 카테고리의 다른 글

[ 백준 - 1068 ] 트리  (0) 2024.11.14
[ 백준 - 9372 ]상준이의 여행  (2) 2024.11.13
[ 백준 - 29701 ] 모스 부호  (0) 2024.11.11
[ 백준 - 25325 ] 학생 인기도 측정  (0) 2024.11.10
[ 백준 - 2358 ] 평행선  (2) 2024.11.09
728x90

모스 부호 문제입니다.

매우 간단한 문제이며, 우선 시간제한과 메모리 제한을 확인해 보겠습니다.

 

입력 예제를 보면  N은 최대 100 이하의 개수가 주어진다 합니다.

그럼 이 문제를 접할 때 최악의 경우 모스부호 100개를 문자열로 변환해야 하는 문제라고 볼 수 있습니다.

 

예제로 준 모스 부호표를 보면 41개를 명시한 표를 통해 답을 만들 수 있습니다.

최악의 경우 100개의 모스 부호를 41개 중 찾기 위해 반복문을 돌린다 하면 4100번 돌려 해결을 할 수 있습니다.

 

다만, 4100번 돌려 답을 찾는 것은 O(n)의 복잡도를 가지게 되지만 41이란 상수는 빅오표기법상 표기를 안 하니 딱히 효율적인 알고리즘은 아니라 생각합니다.

거기서 우리는 Map이라는 자료구조를 활용하여 O(1) 시간복잡도로 모스 부호를 검색할 수 있는 방법을 떠올릴 수 있습니다.

public class Problem29701 {

    public static Map<String, String> initMorseCodeMap() {
        Map<String, String> morseCodeMap = new HashMap<>();
        morseCodeMap.put(".-", "A");
        morseCodeMap.put("-...", "B");
        morseCodeMap.put("-.-.", "C");
        morseCodeMap.put("-..", "D");
        morseCodeMap.put(".", "E");
        morseCodeMap.put("..-.", "F");
        morseCodeMap.put("--.", "G");
        morseCodeMap.put("....", "H");
        morseCodeMap.put("..", "I");
        morseCodeMap.put(".---", "J");
        morseCodeMap.put("-.-", "K");
        morseCodeMap.put(".-..", "L");
        morseCodeMap.put("--", "M");
        morseCodeMap.put("-.", "N");
        morseCodeMap.put("---", "O");
        morseCodeMap.put(".--.", "P");
        morseCodeMap.put("--.-", "Q");
        morseCodeMap.put(".-.", "R");
        morseCodeMap.put("...", "S");
        morseCodeMap.put("-", "T");
        morseCodeMap.put("..-", "U");
        morseCodeMap.put("...-", "V");
        morseCodeMap.put(".--", "W");
        morseCodeMap.put("-..-", "X");
        morseCodeMap.put("-.--", "Y");
        morseCodeMap.put("--..", "Z");
        morseCodeMap.put(".----", "1");
        morseCodeMap.put("..---", "2");
        morseCodeMap.put("...--", "3");
        morseCodeMap.put("....-", "4");
        morseCodeMap.put(".....", "5");
        morseCodeMap.put("-....", "6");
        morseCodeMap.put("--...", "7");
        morseCodeMap.put("---..", "8");
        morseCodeMap.put("----.", "9");
        morseCodeMap.put("-----", "0");
        morseCodeMap.put("--..--", ",");
        morseCodeMap.put(".-.-.-", ".");
        morseCodeMap.put("..--..", "?");
        morseCodeMap.put("---...", ":");
        morseCodeMap.put("-....-", "-");
        morseCodeMap.put(".--.-.", "@");
        return morseCodeMap;
    }

    public static void main(String[] args) throws IOException {
        Map<String, String> morseCodeMap = initMorseCodeMap();
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(br.readLine());
        StringTokenizer st = new StringTokenizer(br.readLine());

        StringBuilder sb = new StringBuilder();
        while(N-- > 0) {
            String morseCode = st.nextToken();
            sb.append(morseCodeMap.get(morseCode));
        }
        System.out.println(sb);
    }
}
  • 시간 복잡도: O(n)
  • 공간 복잡도: O(n)

 

728x90

+ Recent posts