문제 설명


[본 문제는 정확성과 효율성 테스트 각각 점수가 있는 문제입니다.]

오지 탐험가인 프로도는 탐험 도중 n개의 방으로 이루어진 지하 동굴을 탐험하게 되었습니다. 모든 방에는 0부터 n - 1 까지 번호가 붙어있고, 이 동굴에 들어갈 수 있는 유일한 입구는 0번 방과 연결되어 있습니다. 각 방들은 양방향으로 통행이 가능한 통로로 서로 연결되어 있는데, 서로 다른 두 방을 직접 연결하는 통로는 오직 하나입니다. 임의의 서로 다른 두 방 사이의 최단경로는 딱 한 가지만 있으며, 또한 임의의 두 방 사이에 이동이 불가능한 경우는 없습니다.

탐험에 앞서 이 지하 동굴의 지도를 손에 넣은 프로도는 다음과 같이 탐험 계획을 세웠습니다.

  1. 모든 방을 적어도 한 번은 방문해야 합니다.
  2. 특정 방은 방문하기 전에 반드시 먼저 방문할 방이 정해져 있습니다.
    2-1. 이는 A번 방은 방문하기 전에 반드시 B번 방을 먼저 방문해야 한다는 의미입니다.
    2-2. 어떤 방을 방문하기 위해 반드시 먼저 방문해야 하는 방은 없거나 또는 1개 입니다.
    2-3. 서로 다른 두 개 이상의 방에 대해 먼저 방문해야 하는 방이 같은 경우는 없습니다.
    2-4. 어떤 방이 먼저 방문해야 하는 방이면서 동시에 나중에 방문해야 되는 방인 경우는 없습니다.

위 계획 중 2-2, 2-3, 2-4는 순서를 지켜 방문해야 하는 두 방의 쌍이 A → B(A를 먼저 방문하고 B를 방문함) 형태로 유일함을 의미합니다. 즉, 프로도는 아래와 같은 형태로 방문순서가 잡히지 않도록 방문 계획을 세웠습니다.

  • A → B, A → C (방문순서 배열 order = [...,[A,B],...,[A,C],...]) 형태로 A를 방문 후에 방문해야 할 방이 B와 C로 두 개 또는 그 이상인 경우
  • X → A, Z → A (방문순서 배열 order = [...,[X,A],...,[Z,A],...]) 형태로 A를 방문하기 전에 방문해야 할 방이 X와 Z로 두 개 또는 그 이상인 경우
  • A → B → C (방문순서 배열 order = [...,[A,B],...,[B,C],...) 형태로 B처럼 A 방문 후이면서 동시에 C 방문 전인 경우

그리고 먼저 방문해야 할 방과 나중에 방문할 방을 반드시 연속해서 방문해야 할 필요는 없어 A방을 방문한 후 다른 방을 방문한 후 B방을 방문해도 좋습니다.

방 개수 n, 동굴의 각 통로들이 연결하는 두 방의 번호가 담긴 2차원 배열 path, 프로도가 정한 방문 순서가 담긴 2차원 배열 order가 매개변수로 주어질 때, 프로도가 규칙에 맞게 모든 방을 탐험할 수 있을 지 return 하도록 solution 함수를 완성해주세요.

[제한사항]

  • n은 2 이상 200,000 이하입니다.
  • path 배열의 세로(행) 길이는 n - 1 입니다.
    • path 배열의 원소는 [방 번호 A, 방 번호 B] 형태입니다.
    • 두 방 A, B사이를 연결하는 통로를 나타냅니다.
    • 통로가 연결하는 두 방 번호가 순서없이 들어있음에 주의하세요.
  • order 배열의 세로(행) 길이는 1 이상 (n / 2) 이하입니다.
    • order 배열의 원소는 [방 번호 A, 방 번호 B] 형태입니다.
    • A번 방을 먼저 방문한 후 B번 방을 방문해야 함을 나타냅니다.

입출력 예

npathorderresult

9 [[0,1],[0,3],[0,7],[8,1],[3,6],[1,2],[4,7],[7,5]] [[8,5],[6,7],[4,1]] true
9 [[8,1],[0,1],[1,2],[0,7],[4,7],[0,3],[7,5],[3,6]] [[4,1],[5,2]] true
9 [[0,1],[0,3],[0,7],[8,1],[3,6],[1,2],[4,7],[7,5]] [[4,1],[8,7],[6,5]] false

입출력 예에 대한 설명

 

 

접근 방법


 결론부터 이야기하자면 DFS를 사용하여 구현하였습니다. 효율성 10번 테스트케이스에서 런타임에러가 발생하는데 Java 환경에서는 BFS를 사용해야만 통과할 수 있을 거 같습니다. C++의 경우 DFS를 사용해도 무난히 통과할 수 있습니다.

 

이런 유형의 문제는 처음이여서 다른 사람들의 풀이를 참조하였습니다.

*order에서 [A -> B]가 존재한다고 할 때 A를 B의 사전노드, B를 A의 사후노드라고 표현하겠습니다.

 

1. 현재 방문하고자 하는 노드가 사전 노드를 가지고 있는지 확인합니다.

2. 사전 노드가 방문이 된 상태라면 DFS를 계속 진행합니다.

3. 사전 노드가 방문이 되지 않은 상태라면 현재 노드와 사전 노드를 reserve에 기록하고 DFS진행을 멈춥니다.

4. 이후 현재노드가 reserve에 기록되어 있다면, 즉시 사후노드부터 DFS를 진행시킵니다.

 

자세한 내용은 소스코드에 주석처리 하겠습니다.

 

소스 코드


import java.util.*;

class Solution {
    
    ArrayList<ArrayList<Integer>> tree = new ArrayList<ArrayList<Integer>>(); //인접 리스트
    HashMap<Integer, Integer> before = new HashMap<Integer, Integer>(); //(사후노드, 사전노드)
    HashMap<Integer, Integer> reserve = new HashMap<Integer, Integer>(); //(사전노드, 사후노드)
    boolean visited[] = new boolean[200000];
    int cnt = 0; //노드의 방문 횟수
    
    public void init(int n, int[][] path, int[][] order){
        
        for(int i=0;i<n;i++)
            tree.add(new ArrayList<Integer>());
        for(int i=0;i<order.length;i++)
            before.put(order[i][1], order[i][0]);
        
        for(int i=0;i<path.length;i++){
            tree.get(path[i][0]).add(path[i][1]);
            tree.get(path[i][1]).add(path[i][0]);
        }    
    }
    
    public void DFS(int now){
        
        //사전노드를 가지고 있는지 검사
        if(before.get(now) != null){
        	//사전 노드가 방문된 상태가 아니라면 기록 후 진행을 멈춤
            if(!visited[before.get(now)]){
                reserve.put(before.get(now), now);
                return;
            }
        }
        
        visited[now] = true;
        ++cnt; //노드의 방문 횟수
        
        //만약 사후노드가 방문되었다면 사전 노드부터 탐색
        if(reserve.get(now) != null)
            DFS(reserve.get(now));
        
        //인접한 노드들로 DFS진행
        for(int i=0;i<tree.get(now).size();i++){
            int next = tree.get(now).get(i);
            if(!visited[next])
                DFS(next);
        }
        return;
    }
    
    public boolean solution(int n, int[][] path, int[][] order) {
        boolean answer = true;
        init(n, path, order); DFS(0);
        //모든 노드를 방문했다면 true, 아니면 false
        return answer = (cnt == n) ? true : false;
    }
}

문제 설명


각 점에 가중치가 부여된 트리가 주어집니다. 당신은 다음 연산을 통하여, 이 트리의 모든 점들의 가중치를 0으로 만들고자 합니다.

  • 임의의 연결된 두 점을 골라서 한쪽은 1 증가시키고, 다른 한쪽은 1 감소시킵니다.

하지만, 모든 트리가 위의 행동을 통하여 모든 점들의 가중치를 0으로 만들 수 있는 것은 아닙니다. 당신은 주어진 트리에 대해서 해당 사항이 가능한지 판별하고, 만약 가능하다면 최소한의 행동을 통하여 모든 점들의 가중치를 0으로 만들고자 합니다.

트리의 각 점의 가중치를 의미하는 1차원 정수 배열 a와 트리의 간선 정보를 의미하는 edges가 매개변수로 주어집니다. 주어진 행동을 통해 트리의 모든 점들의 가중치를 0으로 만드는 것이 불가능하다면 -1을, 가능하다면 최소 몇 번만에 가능한지를 찾아 return 하도록 solution 함수를 완성해주세요. (만약 처음부터 트리의 모든 정점의 가중치가 0이라면, 0을 return 해야 합니다.)


제한사항

  • a의 길이는 2 이상 300,000 이하입니다.
    • a의 모든 수는 각각 -1,000,000 이상 1,000,000 이하입니다.
    • a[i]는 i번 정점의 가중치를 의미합니다.
  • edges의 행의 개수는 (a의 길이 - 1)입니다.
    • edges의 각 행은 [u, v] 2개의 정수로 이루어져 있으며, 이는 u번 정점과 v번 정점이 간선으로 연결되어 있음을 의미합니다.
    • edges가 나타내는 그래프는 항상 트리로 주어집니다.

입출력 예

aedgesresult

[-5,0,2,1,2] [[0,1],[3,4],[2,3],[0,3]] 9
[0,1,0] [[0,1],[1,2]] -1

입출력 예 설명

입출력 예 #1

  • 다음 그림은 주어진 트리의 모든 정점의 가중치를 0으로 만드는 과정을 나타낸 것입니다.

  1. 2번 정점과 3번 정점을 선택하여 2번 정점은 1 감소시키고, 3번 정점은 1 증가시킵니다. (2번 반복)
  2. 3번 정점과 4번 정점을 선택하여 4번 정점은 1 감소시키고, 3번 정점은 1 증가시킵니다. (2번 반복)
  3. 0번 정점과 3번 정점을 선택하여 3번 정점은 1 감소시키고, 0번 정점은 1 증가시킵니다. (5번 반복)
  • 모든 정점의 가중치를 0으로 만드는 데 필요한 최소 행동 횟수는 9번이므로, 9를 return 해야 합니다.

입출력 예 #2

  • 주어진 트리는 모든 정점의 가중치를 0으로 만드는 것이 불가능하므로, -1을 return 해야 합니다.

 

접근 방법


1. 가장 먼저 확인해야할 부분은 모두 0으로 만들 수 있는지 아닌지 입니다.

 

 모든 노드의 가중치의 합이 0이라면 모두 0으로 만드는 것이 가능하고, 반대의 경우는 모든 가중치를 0으로 만드는 것이 불가능합니다.

 

2. 어떻게 해야 최소의 횟수로 모든 가중치를 0으로 만들 수 있을까? 

 

 가장 최소의 횟수로 모든 가중치를 0으로 만드려면 반복되는 연산(가중치 교환)과정이 있어서는 안됩니다. 즉 일방향으로 연산을 적용한다면 노드의 어떤 부분부터 시작하더라도 모두 같은 최솟값을 구할 수 있습니다. 

 

 

 노드의 연결 정보는 무조건 트리의 형태로 주어진다는 것을 알 수 있습니다. 즉 어떤 임의의 노드를 루트로 잡고 DFS를 사용하여 자식의 방향으로 향하게 설계 후, 리프(말단)노드로부터 부모 노드의 방향으로 연산을 적용하여 해결할 수 있습니다.

 

소스 코드


import java.util.*;

class Solution {
    
    ArrayList<ArrayList<Integer>> info;
    boolean[] visited;
    long[] a;
    long cnt = 0;
    
    void solve(int now){
        
        //부모노드로 올라가지 못하게 방문노드 check
        visited[now] = true;
        
        for(int i=0;i<info.get(now).size();i++){
            int next = info.get(now).get(i);
            if(visited[next] == false){
                solve(next);
                a[now] += a[next];
            }
        }
        //cnt는 연산이 일어나는 횟수
        cnt += Math.abs(a[now]);
    }
    
    
    void makeInfo(int[][] edges){
        
        info = new ArrayList<ArrayList<Integer>>();
        
        for(int i=0;i<this.a.length;i++)
            info.add(new ArrayList<Integer>());
        
        for(int i=0;i<edges.length;i++){
            info.get(edges[i][0]).add(edges[i][1]);
            info.get(edges[i][1]).add(edges[i][0]);
        }
    }
    
    public long solution(int[] a, int[][] edges) {
        
        long sum = 0;
        
        this.a = new long[a.length];
        this.visited = new boolean[a.length];
        
        for(int i=0;i<a.length;i++){
            this.a[i] = a[i];
            sum += this.a[i];
        }
        //합이 0이 아니라면 가중치를 0으로 만들 수 없음
        if(sum != 0)
            return -1;
        
        //인접리스트로 표현
        makeInfo(edges);
        
        solve(0);
        return cnt;
    }
}

문제 설명


후보키

프렌즈대학교 컴퓨터공학과 조교인 제이지는 네오 학과장님의 지시로, 학생들의 인적사항을 정리하는 업무를 담당하게 되었다.

그의 학부 시절 프로그래밍 경험을 되살려, 모든 인적사항을 데이터베이스에 넣기로 하였고, 이를 위해 정리를 하던 중에 후보키(Candidate Key)에 대한 고민이 필요하게 되었다.

후보키에 대한 내용이 잘 기억나지 않던 제이지는, 정확한 내용을 파악하기 위해 데이터베이스 관련 서적을 확인하여 아래와 같은 내용을 확인하였다.

  • 관계 데이터베이스에서 릴레이션(Relation)의 튜플(Tuple)을 유일하게 식별할 수 있는 속성(Attribute) 또는 속성의 집합 중, 다음 두 성질을 만족하는 것을 후보 키(Candidate Key)라고 한다.
    • 유일성(uniqueness) : 릴레이션에 있는 모든 튜플에 대해 유일하게 식별되어야 한다.
    • 최소성(minimality) : 유일성을 가진 키를 구성하는 속성(Attribute) 중 하나라도 제외하는 경우 유일성이 깨지는 것을 의미한다. 즉, 릴레이션의 모든 튜플을 유일하게 식별하는 데 꼭 필요한 속성들로만 구성되어야 한다.

제이지를 위해, 아래와 같은 학생들의 인적사항이 주어졌을 때, 후보 키의 최대 개수를 구하라.

위의 예를 설명하면, 학생의 인적사항 릴레이션에서 모든 학생은 각자 유일한 "학번"을 가지고 있다. 따라서 "학번"은 릴레이션의 후보 키가 될 수 있다.
그다음 "이름"에 대해서는 같은 이름("apeach")을 사용하는 학생이 있기 때문에, "이름"은 후보 키가 될 수 없다. 그러나, 만약 ["이름", "전공"]을 함께 사용한다면 릴레이션의 모든 튜플을 유일하게 식별 가능하므로 후보 키가 될 수 있게 된다.
물론 ["이름", "전공", "학년"]을 함께 사용해도 릴레이션의 모든 튜플을 유일하게 식별할 수 있지만, 최소성을 만족하지 못하기 때문에 후보 키가 될 수 없다.
따라서, 위의 학생 인적사항의 후보키는 "학번", ["이름", "전공"] 두 개가 된다.

릴레이션을 나타내는 문자열 배열 relation이 매개변수로 주어질 때, 이 릴레이션에서 후보 키의 개수를 return 하도록 solution 함수를 완성하라.

제한사항

  • relation은 2차원 문자열 배열이다.
  • relation의 컬럼(column)의 길이는 1 이상 8 이하이며, 각각의 컬럼은 릴레이션의 속성을 나타낸다.
  • relation의 로우(row)의 길이는 1 이상 20 이하이며, 각각의 로우는 릴레이션의 튜플을 나타낸다.
  • relation의 모든 문자열의 길이는 1 이상 8 이하이며, 알파벳 소문자와 숫자로만 이루어져 있다.
  • relation의 모든 튜플은 유일하게 식별 가능하다.(즉, 중복되는 튜플은 없다.)

입출력 예

relationresult

[["100","ryan","music","2"],["200","apeach","math","2"],["300","tube","computer","3"],["400","con","computer","4"],["500","muzi","music","3"],["600","apeach","music","2"]] 2

입출력 예 설명

입출력 예 #1
문제에 주어진 릴레이션과 같으며, 후보 키는 2개이다.

 

 

접근 방법


 

문제 풀이 순서

1. 크기가 1인 조합부터 ~ n인 조합까지 순차적으로 진행

2. backTracking을 통해 크기가 i인 한 조합이 완성될 때, 최소성과 유일성을 검사한다.

3. 최소성을 보장하려면 자신보다 크기가 작은 후보키 중 자신의 부분집합이 존재하면 안된다.

4. 유일성을 보장하려면 중복되는 tuple이 존재해선 안된다.

5. 3,4번을 모두 만족한다면 후보키 목록에 현재 조합을 추가한다. 동시에 return할 값을 1증가 시킴문제 설명

후보키

프렌즈대학교 컴퓨터공학과 조교인 제이지는 네오 학과장님의 지시로, 학생들의 인적사항을 정리하는 업무를 담당하게 되었다.

 

그의 학부 시절 프로그래밍 경험을 되살려, 모든 인적사항을 데이터베이스에 넣기로 하였고, 이를 위해 정리를 하던 중에 후보키(Candidate Key)에 대한 고민이 필요하게 되었다.

 

후보키에 대한 내용이 잘 기억나지 않던 제이지는, 정확한 내용을 파악하기 위해 데이터베이스 관련 서적을 확인하여 아래와 같은 내용을 확인하였다.

 

관계 데이터베이스에서 릴레이션(Relation)의 튜플(Tuple)을 유일하게 식별할 수 있는 속성(Attribute) 또는 속성의 집합 중, 다음 두 성질을 만족하는 것을 후보 키(Candidate Key)라고 한다.

유일성(uniqueness) : 릴레이션에 있는 모든 튜플에 대해 유일하게 식별되어야 한다.

최소성(minimality) : 유일성을 가진 키를 구성하는 속성(Attribute) 중 하나라도 제외하는 경우 유일성이 깨지는 것을 의미한다. 즉, 릴레이션의 모든 튜플을 유일하게 식별하는 데 꼭 필요한 속성들로만 구성되어야 한다.

제이지를 위해, 아래와 같은 학생들의 인적사항이 주어졌을 때, 후보 키의 최대 개수를 구하라.

 

위의 예를 설명하면, 학생의 인적사항 릴레이션에서 모든 학생은 각자 유일한 "학번"을 가지고 있다. 따라서 "학번"은 릴레이션의 후보 키가 될 수 있다.

그다음 "이름"에 대해서는 같은 이름("apeach")을 사용하는 학생이 있기 때문에, "이름"은 후보 키가 될 수 없다. 그러나, 만약 ["이름", "전공"]을 함께 사용한다면 릴레이션의 모든 튜플을 유일하게 식별 가능하므로 후보 키가 될 수 있게 된다.

물론 ["이름", "전공", "학년"]을 함께 사용해도 릴레이션의 모든 튜플을 유일하게 식별할 수 있지만, 최소성을 만족하지 못하기 때문에 후보 키가 될 수 없다.

따라서, 위의 학생 인적사항의 후보키는 "학번", ["이름", "전공"] 두 개가 된다.

 

릴레이션을 나타내는 문자열 배열 relation이 매개변수로 주어질 때, 이 릴레이션에서 후보 키의 개수를 return 하도록 solution 함수를 완성하라.

 

제한사항

 

relation은 2차원 문자열 배열이다.

relation의 컬럼(column)의 길이는 1 이상 8 이하이며, 각각의 컬럼은 릴레이션의 속성을 나타낸다.

relation의 로우(row)의 길이는 1 이상 20 이하이며, 각각의 로우는 릴레이션의 튜플을 나타낸다.

relation의 모든 문자열의 길이는 1 이상 8 이하이며, 알파벳 소문자와 숫자로만 이루어져 있다.

relation의 모든 튜플은 유일하게 식별 가능하다.(즉, 중복되는 튜플은 없다.)

입출력 예

 

relationresult

 

[["100","ryan","music","2"],["200","apeach","math","2"],["300","tube","computer","3"],["400","con","computer","4"],["500","muzi","music","3"],["600","apeach","music","2"]] 2

입출력 예 설명

 

입출력 예 #1

문제에 주어진 릴레이션과 같으며, 후보 키는 2개이다.

 

 

 

 

 

접근 방법

문제 풀이 순서

 

1. 크기가 1인 조합부터 ~ n인 조합까지 순차적으로 진행

 

2. backTracking을 통해 크기가 i인 한 조합이 완성될 때, 최소성과 유일성을 검사한다.

 

3. 최소성을 보장하려면 자신보다 크기가 작은 후보키 중 자신의 부분집합이 존재하면 안된다.

 

4. 유일성을 보장하려면 중복되는 tuple이 존재해선 안된다.

 

5. 3,4번을 모두 만족한다면 후보키 목록에 현재 조합을 추가한다. 동시에 return할 값을 1증가 시킴

 

자세한 내용은 소스코드에 주석 처리 하겠습니다.

 

 

소스 코드


Java

import java.util.*;

class Solution {
    
    int ans = 0;
    String relation[][];
    HashMap <String, Boolean> visited = new HashMap<String, Boolean>(); //후보키 목록
    
    // 최소성을 만족하는지 검사
    public boolean checkMin(String seqstr){
        
        for(String elem : visited.keySet()){
            boolean flag = false;
            for(int i=0;i<elem.length();i++){
                if(seqstr.indexOf(elem.substring(i, i+1)) == -1)
                    flag = true;
            }
            if(flag == false) // 현재 조합이 후보키를 부분집합으로 가진다면
                return false;
        }
        return true;
    }
    
    // 유일성을 만족하는지 검사
    public boolean checkOnly(ArrayList<Integer> seq){
        
        HashMap <String, Boolean> map = new HashMap<String, Boolean>(); //튜플 목록
    
        for(int i=0;i<relation.length;i++){
            String key = "";
            
            for(int j=0;j<seq.size();j++)
                key += (relation[i][seq.get(j)] + ","); //i번째 튜플에, seq[j]번째 속성
            
            if(map.containsKey(key) == false) //해당 값이 없다면
                map.put(key, new Boolean(true));
            else  //중복되는 값이 있다면
                return false;
        }
        return true;
    }
    
    public void search(int n, int cnt, int now, int attr, ArrayList<Integer> seq){
        
        if(n == cnt){
        //indexOf 메소드로 포함여부를 검사하기 위해 String으로 변환
        //set이나 map 구조를 사용해도 상관없음
            String seqstr = "";
            for(int i=0; i<seq.size();i++)
                seqstr += seq.get(i).toString();
            
            if(checkMin(seqstr) && checkOnly(seq)){
                visited.put(seqstr, new Boolean(true));
                ans++;
            }
            return;
        }
        for(int i = now; i<attr; i++){
            seq.add(i);
            search(n, cnt+1, i+1, attr, seq);
            seq.remove(seq.size() - 1);
        }
    }
    
    public void solve(){
        
        ArrayList<Integer> seq = new ArrayList<Integer>();
        int attr = relation[0].length;
        int tup = relation.length;
        
        //속성의 수를 증가시킴, 작은 수 부터 확인해야 최소성을 확인하기 편함
        for(int i=1;i<=attr;i++){ 
            search(i,0,0,attr, seq);
        }
    }
    
    public int solution(String[][] relation) {
        int answer = 0;
        this.relation = relation;
        solve();
        return answer = ans;
    }
}

 

C++

과거에 c++로 풀었던 풀이입니다. Java와 풀이가 살짝 다릅니다.

#include <string>
#include <algorithm>
#include <string.h>
#include <vector>
#include <map>

using namespace std;

bool visited[8] = { false };
vector<vector<int>> combination;
int result = 0;

bool isCandidate(string &list, vector<vector<string>> &relation) {

	map<string, bool> duplicateCheck;
	for (int i = 0; i < relation.size(); i++) {
		vector<string> row = relation[i];
		string elem;
		for (int j = 0; j < list.size(); j++) {
			elem += (row[int(list[j]) - int('0')]+" ");
		}
		if (duplicateCheck.count(elem) == 0) {
			duplicateCheck.insert({elem, true});
		}
		else { //중복되는 값이 있다면
			return false;
		}
	}
	return true;
}

bool comp(vector<int> list1, vector<int> list2) {
	if (list1.size() < list2.size())
		return true;
	return false;
}

void solve(int size, int cnt, vector<int> &list, vector<vector<string>> &relation, int start) {
	if (size == cnt) {
		return;
	}
	for (int i = start; i < size; i++) {
		if (!visited[i]) {
			visited[i] = true;
			list.push_back(i);
			solve(size, cnt + 1, list, relation, i);
			combination.push_back(list);
			list.pop_back();
			visited[i] = false;
		}
	}
}

int solution(vector<vector<string>> relation) {
	
	int answer = 0;
	int col = relation[0].size();
	vector<int> list;

	result = 0;
	memset(visited, 0, sizeof(visited));
	
    //combination은 크기가 1~n까지 모든 조합이며, 크기가 작은 순부터 큰 순서로 정렬
	solve(col, 0, list, relation, 0);
	sort(combination.begin(), combination.end(), comp);

	vector<string> comb(combination.size());
	for (int i = 0; i < combination.size(); i++) {
		for (int j = 0; j < combination[i].size(); j++) {
			comb[i].push_back(char(int('0') + combination[i][j]));
		}
	}
	
	for (int i = 0; i < comb.size(); i++) {
		if (isCandidate(comb[i], relation)) {
			string tmp = comb[i];
			for (int j = 0; j < comb.size(); j++) {
				bool find = true;
				for (auto t : tmp) {
					if (comb[j].find(t) == string::npos) { // 모든요소가포함되는지 = 모두 찾는다면 = 하나라도 못찾는다면
						find = false;
						break;
					}
				}
				if (find) {
					comb.erase(comb.begin() + j);
                    j--;
				}
			}
			i--; //자기 자신도 삭제가 되므로
			result++;
		}
	}
	answer = result;
	return answer;
}

문제 설명


문제 설명

게임 개발자인 베로니는 개발 연습을 위해 다음과 같은 간단한 카드 짝맞추기 보드 게임을 개발해 보려고 합니다.
게임이 시작되면 화면에는 카드 16장이 뒷면을 위로하여 4 x 4 크기의 격자 형태로 표시되어 있습니다. 각 카드의 앞면에는 카카오프렌즈 캐릭터 그림이 그려져 있으며, 8가지의 캐릭터 그림이 그려진 카드가 각기 2장씩 화면에 무작위로 배치되어 있습니다.
유저가 카드를 2장 선택하여 앞면으로 뒤집었을 때 같은 그림이 그려진 카드면 해당 카드는 게임 화면에서 사라지며, 같은 그림이 아니라면 원래 상태로 뒷면이 보이도록 뒤집힙니다. 이와 같은 방법으로 모든 카드를 화면에서 사라지게 하면 게임이 종료됩니다.

게임에서 카드를 선택하는 방법은 다음과 같습니다.

  • 카드는 커서를 이용해서 선택할 수 있습니다.
    • 커서는 4 x 4 화면에서 유저가 선택한 현재 위치를 표시하는 굵고 빨간 테두리 상자를 의미합니다.
  • 커서는 [Ctrl] 키와 방향키에 의해 이동되며 키 조작법은 다음과 같습니다.
    • 방향키 ←, ↑, ↓, → 중 하나를 누르면, 커서가 누른 키 방향으로 1칸 이동합니다.
    • [Ctrl] 키를 누른 상태에서 방향키 ←, ↑, ↓, → 중 하나를 누르면, 누른 키 방향에 있는 가장 가까운 카드로 한번에 이동합니다.
      • 만약, 해당 방향에 카드가 하나도 없다면 그 방향의 가장 마지막 칸으로 이동합니다.
    • 만약, 누른 키 방향으로 이동 가능한 카드 또는 빈 공간이 없어 이동할 수 없다면 커서는 움직이지 않습니다.
  • 커서가 위치한 카드를 뒤집기 위해서는 [Enter] 키를 입력합니다.
    • [Enter] 키를 입력해서 카드를 뒤집었을 때
      • 앞면이 보이는 카드가 1장 뿐이라면 그림을 맞출 수 없으므로 두번째 카드를 뒤집을 때 까지 앞면을 유지합니다.
      • 앞면이 보이는 카드가 2장이 된 경우, 두개의 카드에 그려진 그림이 같으면 해당 카드들이 화면에서 사라지며, 그림이 다르다면 두 카드 모두 뒷면이 보이도록 다시 뒤집힙니다.

베로니는 게임 진행 중 카드의 짝을 맞춰 몇 장 제거된 상태에서 카드 앞면의 그림을 알고 있다면, 남은 카드를 모두 제거하는데 필요한 키 조작 횟수의 최솟값을 구해 보려고 합니다. 키 조작 횟수는 방향키와 [Enter] 키를 누르는 동작을 각각 조작 횟수 1로 계산하며, [Ctrl] 키와 방향키를 함께 누르는 동작 또한 조작 횟수 1로 계산합니다.

다음은 카드가 몇 장 제거된 상태의 게임 화면에서 커서를 이동하는 예시입니다.
아래 그림에서 빈 칸은 이미 카드가 제거되어 없어진 칸을 의미하며, 그림이 그려진 칸은 카드 앞 면에 그려진 그림을 나타냅니다.


예시에서 커서는 두번째 행, 첫번째 열 위치에서 시작하였습니다.


[Enter] 입력, ↓ 이동, [Ctrl]+→ 이동, [Enter] 입력 = 키 조작 4회


[Ctrl]+↑ 이동, [Enter] 입력, [Ctrl]+← 이동, [Ctrl]+↓ 이동, [Enter] 입력 = 키 조작 5회


[Ctrl]+→ 이동, [Enter] 입력, [Ctrl]+↑ 이동, [Ctrl]+← 이동, [Enter] 입력 = 키 조작 5회

위와 같은 방법으로 커서를 이동하여 카드를 선택하고 그림을 맞추어 카드를 모두 제거하기 위해서는 총 14번(방향 이동 8번, [Enter] 키 입력 6번)의 키 조작 횟수가 필요합니다.


[문제]

현재 카드가 놓인 상태를 나타내는 2차원 배열 board와 커서의 처음 위치 r, c가 매개변수로 주어질 때, 모든 카드를 제거하기 위한 키 조작 횟수의 최솟값을 return 하도록 solution 함수를 완성해 주세요.

[제한사항]

  • board는 4 x 4 크기의 2차원 배열입니다.
  • board 배열의 각 원소는 0 이상 6 이하인 자연수입니다.
    • 0은 카드가 제거된 빈 칸을 나타냅니다.
    • 1 부터 6까지의 자연수는 2개씩 들어있으며 같은 숫자는 같은 그림의 카드를 의미합니다.
    • 뒤집을 카드가 없는 경우(board의 모든 원소가 0인 경우)는 입력으로 주어지지 않습니다.
  • r은 커서의 최초 세로(행) 위치를 의미합니다.
  • c는 커서의 최초 가로(열) 위치를 의미합니다.
  • r과 c는 0 이상 3 이하인 정수입니다.
  • 게임 화면의 좌측 상단이 (0, 0), 우측 하단이 (3, 3) 입니다.

[입출력 예]

boardrcresult

[[1,0,0,3],[2,0,0,0],[0,0,0,2],[3,0,1,0]] 1 0 14
[[3,0,0,2],[0,0,1,0],[0,1,0,0],[2,0,0,3]] 0 1 16

 

접근 방법


삼성 유형과 비슷한 문제였는데 상당히 복잡하게 느껴졌습니다.

 

일단 문제의 큰 틀을 잡아보면 4가지 과정으로 나눌 수 있습니다. 

1. 어떤 짝 먼저 맞춰갈지에 대한 순서 정하기

2. 순서를 정했다면 한 쌍에서 어떤 카드부터 선택 할지 정하기

3. 2번 과정까지 완료 됬다면 각각 지점사이에 이동 횟수를 알아내어 전체 경로의 이동횟수를 구하기

4. 3번 과정을 반복하며 최소값 갱신 

 

1. 어떤 짝 먼저 맞춰갈지에 대한 순서 정하기

 만약 짝이 1,2,3이 있다면 1->2->3 순서로 해결할지 1->3->2의 순서로 해결할지

3->1->2 등등의 순서를 정해주어야 합니다. 즉 모든 가능한 순열을 구해야 합니다.

void combinations(vector<bool> &visited, vector<vector<int>> &combs, vector<int> comb, vector<int> &card){
    
    if(comb.size() == card.size()){
        combs.push_back(comb);
        return;
    }
    for(int i=0;i<card.size();i++){
        if(!visited[i]){
            visited[i] = true;
            comb.push_back(card[i]);
            combinations(visited, combs, comb, card);
            comb.pop_back();
            visited[i] = false;
        }
    }
}

 combs = 순열들의 집합, comb = 한 순열, visited = 방문여부, card = 짝(ex, {1,2,3})

 순열은 백트래킹 기법을 사용하여 구할 수 있습니다.

 

이 때 card는 아래와 같은 방법으로 구할 수 있습니다. 또한 카드의 좌표정보를 갖고있는 info를 갱신하여 줍니다. info[카드번호] = {{x1,y1}, {x2, y2}}

//map == board
vector<int> findcard(){
    
    vector<bool> check_card(7, false);
    
    for(int i=0;i<4;i++)
        for(int j=0;j<4;j++)
            if(map[i][j]!=0){
                check_card[map[i][j]] = true;
                info[map[i][j]].push_back({i, j});
            }
    
    vector<int> card;
    for(int i=0;i<check_card.size();i++)
        if(check_card[i])
            card.push_back(i);
    
    return card;
}

 

 

2. 순서를 정했다면 한 쌍에서 어떤 카드부터 선택 할지 정하기

 각각의 짝은 2장의 카드로 이루어져 있습니다. 만약 [a->b->c->d]라는 순서를 결정지었을 때

a라는 짝이 a1, a2 두 장의 카드로 이루어져 있다면 a1을 먼저 방문하고 a2를 방문할지, a2를 먼저 방문하고 a1을 방문할지 결정하고 b, c, d에 대해서도 각각의 카드의 방문 순서를 지정해주어야 합니다.  

ex 1) a1 -> a2 -> b1 -> b2 -> c2 -> c1 -> d2 -> d1

ex 2) a2 -> a1 -> b1 -> b2 -> c1 -> c2 -> d2 -> d1

void decide_path(vector<vector<pair<int, int>>> &paths, vector<pair<int, int>> path, vector<int> &comb, int idx){
    
    if(idx == comb.size()){
        paths.push_back(path);
        return;
    }
    
    int num = comb[idx];
    path.push_back(info[num][0]);
    path.push_back(info[num][1]);
    decide_path(paths, path, comb, idx+1);
    path.pop_back();
    path.pop_back();
    path.push_back(info[num][1]);
    path.push_back(info[num][0]);
    decide_path(paths, path, comb, idx+1);
    path.pop_back();
    path.pop_back();
}

paths = path들의 집합, path = 이동할 좌표의 순열, comb = 한 순열

이동할 좌표의 순열 또한 백트래킹을 사용하여 구할 수 있습니다. 

 

3. 2번 과정까지 완료가 되었다면 각각 지점사이에 이동 횟수를 알아내어 전체 경로의 이동횟수를 구해야 합니다.

 

 한 좌표에서 다른 좌표로 이동할 수 있는 경우는 총 8가지가 있습니다. 상하좌우 한 칸씩 이동하는 방법 4개, ctrl키를 누른뒤 상하좌우로 이동하는 방법이 4개 있습니다. 이 때 저는 BFS를 통해 해결을 하였습니다.

int BFS(pair<int, int> start, pair<int, int> end){
    
    queue<pair<pair<int, int>, int>> q; //좌표와 레벨
    vector<vector<bool>> visited(4, vector<bool>(4,false));
    
    q.push({start, 0});
    visited[start.first][start.second] = true;
    
    while(!q.empty()){
     
        int x = q.front().first.first;
        int y = q.front().first.second;
        int level = q.front().second;
        q.pop();
        
        for(int i=0;i<8;i++){ //8가지 방향에 대해
            pair<int, int> next_xy = findNext(i,x,y); //다음 좌표!!!
            int next_x = next_xy.first;
            int next_y = next_xy.second;
            if(inRange(next_x, next_y) && !visited[next_x][next_y]){
                visited[next_x][next_y] = true;
                if(next_x == end.first && next_y == end.second)
                    return level + 1;
                q.push({{next_x, next_y}, level+1});    
            }
        }
    }
    return 0;
}

 위와 같이 BFS를 사용하여 start(출발 좌표)와 end(도착 좌표)까지의 최소 이동횟수를 구할 수 있는데 findNext()함수를  통해 다음 좌표를 구할 수 있습니다.

 

findNext(int dir, int x, int y)

pair<int, int> findNext(int dir, int x, int y){
    
    vector<pair<int, int>> nextdir = {{1,0},{0,1},{-1,0},{0,-1}};
    
    //처음 4방향은 1칸만 이동
    if(dir < 4)
        return { x + nextdir[dir].first, y + nextdir[dir].second};
    else{ //나머지 4방향은 끝 또는 카드가 있는 곳까지 이동(ctrl + 방향키)
        dir -= 4;
        while(inRange(x + nextdir[dir].first, y + nextdir[dir].second)){
         
            x += nextdir[dir].first;
            y += nextdir[dir].second;    
            if(tmp[x][y] != 0)
                break;
        }
        return {x, y};
    }
}

 

 

4. 3번 과정을 반복하며 최소값 갱신 

int processOneComb(vector<int> &comb, int r, int c){
    
    vector<vector<pair<int, int>>> paths;
    vector<pair<int, int>> path;
    
    decide_path(paths, path, comb, 0);
    int mindist = 0xFFFFFFF;
    
    for(int i=0;i<paths.size();i++){
        
        tmp = map; //한 순열에 대한 board상태 초기화
        int dist = 0;
        
        for(int j=0;j<paths[i].size();j++){ //한 경로 조합에 대한 거리
            //각각 dist에 +1하는 이유는 엔터동작 때문임
            if(j==0) dist += BFS({r,c}, paths[i][j]) + 1;
            else dist += BFS(paths[i][j - 1], paths[i][j]) + 1;
            tmp[paths[i][j].first][paths[i][j].second] = 0; //이동한 좌표의 카드 삭제
        }    
        mindist = min(mindist, dist);
    }
    return mindist;
}

 

 

소스 코드


#include <string>
#include <vector>
#include <algorithm>
#include <queue>

using namespace std;

vector<pair<int,int>> info[7];
vector<vector<int>> map;
vector<vector<int>> tmp;

bool inRange(int x, int y){
    return (0 <= x && x < 4 && 0 <= y && y < 4);
}

void combinations(vector<bool> &visited, vector<vector<int>> &combs, vector<int> comb, vector<int> &card){
    
    if(comb.size() == card.size()){
        combs.push_back(comb);
        return;
    }
    for(int i=0;i<card.size();i++){
        if(!visited[i]){
            visited[i] = true;
            comb.push_back(card[i]);
            combinations(visited, combs, comb, card);
            comb.pop_back();
            visited[i] = false;
        }
    }
}

vector<vector<int>> find_combs(vector<int> &card){
    
    vector<bool> visited(card.size(), false);
    vector<vector<int>> combs;
    vector<int> comb;
    combinations(visited, combs, comb, card);
    return combs;
}

vector<int> findcard(){
    
    vector<bool> check_card(7, false);
    
    for(int i=0;i<4;i++)
        for(int j=0;j<4;j++)
            if(map[i][j]!=0){
                check_card[map[i][j]] = true;
                info[map[i][j]].push_back({i, j});
            }
    
    vector<int> card;
    for(int i=0;i<check_card.size();i++)
        if(check_card[i])
            card.push_back(i);
    
    return card;
}

void decide_path(vector<vector<pair<int, int>>> &paths, vector<pair<int, int>> path, vector<int> &comb, int idx){
    
    if(idx == comb.size()){
        paths.push_back(path);
        return;
    }
    
    int num = comb[idx];
    path.push_back(info[num][0]);
    path.push_back(info[num][1]);
    decide_path(paths, path, comb, idx+1);
    path.pop_back();
    path.pop_back();
    path.push_back(info[num][1]);
    path.push_back(info[num][0]);
    decide_path(paths, path, comb, idx+1);
    path.pop_back();
    path.pop_back();
}

pair<int, int> findNext(int dir, int x, int y){
    
    vector<pair<int, int>> nextdir = {{1,0},{0,1},{-1,0},{0,-1}};
    
    if(dir < 4)
        return { x + nextdir[dir].first, y + nextdir[dir].second};
    else{
        dir -= 4;
        while(inRange(x + nextdir[dir].first, y + nextdir[dir].second)){
         
            x += nextdir[dir].first;
            y += nextdir[dir].second;    
            if(tmp[x][y] != 0)
                break;
        }
        return {x, y};
    }
}

int BFS(pair<int, int> start, pair<int, int> end){
    
    queue<pair<pair<int, int>, int>> q; //좌표와 레벨
    vector<vector<bool>> visited(4, vector<bool>(4,false));
    
    q.push({start, 0});
    visited[start.first][start.second] = true;
    
    while(!q.empty()){
     
        int x = q.front().first.first;
        int y = q.front().first.second;
        int level = q.front().second;
        q.pop();
        
        for(int i=0;i<8;i++){
            pair<int, int> next_xy = findNext(i,x,y);
            int next_x = next_xy.first;
            int next_y = next_xy.second;
            if(inRange(next_x, next_y) && !visited[next_x][next_y]){
                visited[next_x][next_y] = true;
                if(next_x == end.first && next_y == end.second)
                    return level + 1;
                q.push({{next_x, next_y}, level+1});    
            }
        }
    }
    return 0;
}

int processOneComb(vector<int> &comb, int r, int c){
    
    vector<vector<pair<int, int>>> paths;
    vector<pair<int, int>> path;
    
    decide_path(paths, path, comb, 0);
    int mindist = 0xFFFFFFF;
    
    for(int i=0;i<paths.size();i++){
        
        tmp = map; //한 순열에 대한 board상태 초기화
        int dist = 0;
        
        for(int j=0;j<paths[i].size();j++){ //한 경로 조합에 대한 거리
            //각각 dist에 +1하는 이유는 엔터동작 때문임
            if(j==0) dist += BFS({r,c}, paths[i][j]) + 1;
            else dist += BFS(paths[i][j - 1], paths[i][j]) + 1;
            tmp[paths[i][j].first][paths[i][j].second] = 0; //이동한 좌표의 카드 삭제
        }    
        mindist = min(mindist, dist);
    }
    return mindist;
}

int solution(vector<vector<int>> board, int r, int c) {
    
    int answer = 0xFFFFFFF;
    map = board;
    vector<int> card = findcard();
    vector<vector<int>> combs = find_combs(card);
    
    for(int i=0;i<combs.size();i++){
        answer = min(answer, processOneComb(combs[i], r, c));
    }
    
    return answer;
}

문제 링크


www.acmicpc.net/problem/19236

 

19236번: 청소년 상어

첫째 줄부터 4개의 줄에 각 칸의 들어있는 물고기의 정보가 1번 행부터 순서대로 주어진다. 물고기의 정보는 두 정수 ai, bi로 이루어져 있고, ai는 물고기의 번호, bi는 방향을 의미한다. 방향 bi는

www.acmicpc.net

 

접근 방법


바보같은 실수 하나로 오랜 시간을 잡아먹은 문제다. 

 필자가 했던 실수는 물고기를 이동시키지 않은 상태에서 상어가 먹을 수 있는 물고기가 있는지 확인하고 물고기를 이동시켰다는 점이다. 이렇게 구현을 한 상태에서 문제에서 주어진 테스트 케이스가 모두 맞아 떨어져 오류를 수정하기 힘들었다.

 

문제 풀이는 단순하다.

1. 상어를 0,0에 배치시키고 상태를 업데이트 한다.

2. DFS를 실행하는데 현재 4X4의 물고기들 상태를 저장해 두고 물고기를 이동시킨다.

3. 상어가 먹을 수 있는 먹이가 있는지 확인한다. 

4. 먹을 수 있다면 DFS를 진행 시키고 아니면 최대 값을 갱신하고 저장 해 둔 상태로 다시 복원한다.

 

물고기를 이동시키는 함수를 풀이하자면

1. 맵을 스캔하며 n번 물고기의 좌표를 vector<pair<int, int>> fishInfo에 저장한다.

void moveAll(info &shark) {

	vector<pair<int, int>> fishInfo(17, {-1,-1}); //n번 물고기와 좌표를 매핑

	for (int i = 0; i < 4; i++) 
		for (int j = 0; j < 4; j++) 
			if (!(i == shark.x && j == shark.y))
				if (map[i][j].num != 0) 
					fishInfo[map[i][j].num] = { i,j }; 
	
	for (int i = 1; i < 17; i++) 
		if (fishInfo[i].first != -1) 
			move(fishInfo, shark, i);
}

 

2. fishInfo를 1번 부터 탐색하며 한 마리 씩 이동시킨다. 이 때 물고기가 이동하며 swap되는 정보가 fishInfo에 실시간으로 갱신되야 하므로 move()함수에 fishInfo를 참조 연산자로 넘겨주었다.

void move(vector<pair<int, int>> &fishInfo, info shark, int num) {

	int x = fishInfo[num].first;
	int y = fishInfo[num].second;
	int dir = map[x][y].dir;

	for (int i = 0; i < 8; i++) {

		int next_dir = (dir + i) % 8;
		int next_x = x + direction[next_dir].first;
		int next_y = y + direction[next_dir].second;
		
		if (inRange(next_x, next_y) && !(next_x == shark.x && next_y == shark.y)) { //이동가능 하다면 swap
			
			fish tmp = map[next_x][next_y];
			map[next_x][next_y] = { num, next_dir }; 
			map[x][y] = tmp;
			fishInfo[num] = { next_x, next_y };
			fishInfo[tmp.num] = { x,y };

			break;
		}
	}
}

 

자세한 내용은 하단의 소스코드에 주석을 남겨두었음

 

소스 코드


#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

typedef struct fish {
	int num, dir;
};

typedef struct info {
	int x, y, dir, cost;
};

pair<int, int> direction[8] = { {-1, 0}, {-1,-1}, {0,-1}, {1,-1}, {1,0}, {1,1}, {0,1}, {-1,1} };
fish map[4][4];
info shark;
int result = 0;

bool inRange(int x, int y) {
	return (0 <= x && x < 4 && 0 <= y && y < 4);
}

vector<vector<fish>> copy() {

	vector<vector<fish>> repos(4, vector<fish>(4));

	for (int i = 0; i < 4; i++)
		for (int j = 0; j < 4; j++) 
				repos[i][j] = map[i][j]; 		
	return repos;
}

void restore(vector<vector<fish>> repos) {
	for (int i = 0; i < 4; i++)
		for (int j = 0; j < 4; j++)
			map[i][j] = repos[i][j];
}

void move(vector<pair<int, int>> &fishInfo, info shark, int num) { 
//한 마리의 물고기에 대한 처리
//실시간으로 fishInfo를 업데이트 해야 하므로 참조 연산자 사용

	int x = fishInfo[num].first;
	int y = fishInfo[num].second;
	int dir = map[x][y].dir;

	for (int i = 0; i < 8; i++) { //반 시계 방향으로 방향 전환

		int next_dir = (dir + i) % 8;
		int next_x = x + direction[next_dir].first;
		int next_y = y + direction[next_dir].second;
		
		if (inRange(next_x, next_y) && !(next_x == shark.x && next_y == shark.y)) { //이동가능 하다면 swap
			
			fish tmp = map[next_x][next_y];
			map[next_x][next_y] = { num, next_dir }; 
			map[x][y] = tmp;
			fishInfo[num] = { next_x, next_y };
			fishInfo[tmp.num] = { x,y };

			break; // 물고기 이동 후 종료
		}
	}
}

void moveAll(info &shark) { // 전체 물고기에 대해

	vector<pair<int, int>> fishInfo(17, {-1,-1}); //n번 물고기와 좌표를 매핑

	for (int i = 0; i < 4; i++) 
		for (int j = 0; j < 4; j++) 
			if (!(i == shark.x && j == shark.y))
				if (map[i][j].num != 0) 
					fishInfo[map[i][j].num] = { i,j }; 
	
	for (int i = 1; i < 17; i++) 
		if (fishInfo[i].first != -1) 
			move(fishInfo, shark, i); //1번 부터 1마리 씩 이동
}

bool goHome(info &shark) { 

	int x = shark.x;
	int y = shark.y;

	while (inRange(x, y)) {  //범위 안에서 물고기가 있는지 확인
		
		if (map[x][y].num != 0)
			return false;
		x += direction[shark.dir].first;
		y += direction[shark.dir].second;
	}
	return true;
}

void solve(info shark) { 

	//repos를 전역변수로 사용한다면 다른 dfs에서 repos의 상태를 변화시킬 수 있으므로 불가능
	vector<vector<fish>> repos = copy();  
	moveAll(shark); // 물고기 전부 이동

	if (goHome(shark)) { //먹을 먹이가 없다면
		result = max(shark.cost, result); //결과 갱신
		restore(repos); //map복원
		return;
	}
	else {

		int x = shark.x + direction[shark.dir].first;
		int y = shark.y + direction[shark.dir].second;

		while (inRange(x, y)) {

			if (map[x][y].num != 0) {
				fish tmp = map[x][y];
				map[x][y] = { 0, 0 }; //해당 좌표의 물고기를 먹음
				solve({ x, y, tmp.dir, shark.cost + tmp.num }); //next
				map[x][y] = tmp; //back
			}
			x += direction[shark.dir].first;
			y += direction[shark.dir].second;
		}
		restore(repos); //map 복원
	}
}

int main() {

	ios_base::sync_with_stdio(false);
	cout.tie(0);
	cin.tie(0);

	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 4; j++) {
			int num, dir;
			cin >> num >> dir;
			map[i][j] = { num, dir - 1 };
		}
	}

	shark = { 0,0, map[0][0].dir, map[0][0].num };
	map[0][0] = { 0,0 };
	result = shark.cost;
	solve(shark);

	cout << result << "\n";
}

문제 링크


www.acmicpc.net/problem/17281

 

17281번: ⚾

⚾는 9명으로 이루어진 두 팀이 공격과 수비를 번갈아 하는 게임이다. 하나의 이닝은 공격과 수비로 이루어져 있고, 총 N이닝 동안 게임을 진행해야 한다. 한 이닝에 3아웃이 발생하면 이닝이 종

www.acmicpc.net

 

접근 방법


문제 풀이보다 문제를 이해하는데 어려운 문제였다.

 

문제를 간략히 정리하자면

1. 타자들의 순번을 정한다.

2. 1번 선수는 항상 4번째 순서에 와야한다.

3. 아웃이 3번 될 때까지 현재 순열을 반복하여 점수를 더한다.

4. 아웃이 3번 된다면 다음 타자의 순번을 구한다.

5. 모든 이닝이 끝날 때 까지 반복하여 게임의 총 점수를 구한다.

6. 점수의 최대 값을 갱신하며 위의 과정을 반복한다.

 

코드를 단계 별로 보자면

1. DFS를 통해 순열을 구한다. 이 때 cnt=3(4번 타자)는 고정이므로 pass하고, 순열이 완성 되었을 때 게임의 점수를 구하는 play_game()함수를 실행한다.

int solve(int cnt) {

	int maximum = 0;

	if (cnt == 3) //4번 타자는 pass
		cnt++;
	if (cnt == 9) {
		return play_game();
	}
	for (int i = 0; i < 9; i++) {
		if (!visited[i]) { //방문 여부
			visited[i] = true;
			seq[cnt] = i;
			maximum = max(maximum, solve(cnt + 1));
			visited[i] = false;
		}
	}
	return maximum;
}

2. while문을 통해 한 이닝이 끝났을 때 점수(sum)를 더해주며, 다음 이닝의 선두주자(now)를 갱신한다. 

int play_game() { //순서가 결정 되었을 때

	int inning = -1;
	int now = 0;
	int sum = 0;

	while (++inning < N) {
		pair<int, int> info = oneInning(inning, now);
		sum += info.first;
		now = info.second;
	}
	return sum;
}

3. base는 홈 부터 1,2,3루를 의미하며, 아웃(info[inning][hitter] == 0)인 경우 변수out을 증가시켜 반복한다. out이 아니라면 각각의 타자가 친 정보를 통해 move()함수를 실행하여 base를 갱신하며 점수를 구한다.

pair<int, int> oneInning(int inning, int idx) { 
	
	int out = 0;
	int score = 0;

	vector<bool> base = {1,0,0,0};

	while(out < 3) {

		int hitter = seq[idx];
		base[0] = true; //타자 투입

		if (info[inning][hitter] == 0)
			out++;
		else
			score += move(base, info[inning][hitter]);

		idx = (idx + 1) % 9;
	}
		
	return { score, idx };

}

4. 타자의 n루타의 정보를 통해 점수를 얻을 수 있는 상황(next >= 4)에는 score를 획득하며, 아닌 경우에는 루의 위치를 변경시켜준다. 

int move(vector<bool> &base, int scoretype) { 

	int score = 0;

	for (int i = 3; i >= 0; i--) {
		if (base[i]) {
			int next = i + scoretype;
			if (next >= 4) {
				score++;
				base[i] = false;
			}				
			else {
				base[next] = true;
				base[i] = false;
			}
		}
	}
	return score;
}

 

소스 코드


#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

bool visited[9];
int seq[10];
int info[51][9];
int N;

int move(vector<bool> &base, int scoretype) { 

	int score = 0;

	for (int i = 3; i >= 0; i--) {
		if (base[i]) {
			int next = i + scoretype;
			if (next >= 4) {
				score++;
				base[i] = false;
			}				
			else {
				base[next] = true;
				base[i] = false;
			}
		}
	}
	return score;
}

pair<int, int> oneInning(int inning, int idx) { 
	
	int out = 0;
	int score = 0;

	vector<bool> base = {1,0,0,0};

	while(out < 3) {

		int hitter = seq[idx];
		base[0] = true; //타자 투입

		if (info[inning][hitter] == 0)
			out++;
		else
			score += move(base, info[inning][hitter]);

		idx = (idx + 1) % 9;
	}
		
	return { score, idx };

}

int play_game() { //순서가 결정 되었을 때

	int inning = -1;
	int now = 0;
	int sum = 0;

	while (++inning < N) {
		pair<int, int> info = oneInning(inning, now);
		sum += info.first;
		now = info.second;
	}
	return sum;
}

int solve(int cnt) {

	int maximum = 0;

	if (cnt == 3)
		cnt++;
	if (cnt == 9) {
		return play_game();
	}
	for (int i = 0; i < 9; i++) {
		if (!visited[i]) {
			visited[i] = true;
			seq[cnt] = i;
			maximum = max(maximum, solve(cnt + 1));
			visited[i] = false;
		}
	}
	return maximum;
}

int main() {

	ios_base::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);

	cin >> N;

	for (int i = 0; i < N; i++) 
		for (int j = 0; j < 9; j++) 
			cin >> info[i][j];

	visited[0] = true;
	seq[3] = 0;

	cout << solve(0);
}

개발 환경 : VS2017

질문, 지적 환영합니다!

'알고리즘 > 백준' 카테고리의 다른 글

[C++] 백준 20061 모노미노도미노  (0) 2021.01.05
[C++] 백준 17472 다리만들기2  (0) 2021.01.04
[C++] 백준 17406 배열돌리기4  (0) 2021.01.03
[C++] 백준 3190 뱀  (0) 2021.01.03
[C++] 백준 17136 색종이 붙이기  (0) 2021.01.02

문제 링크


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

 

14502번: 연구소

인체에 치명적인 바이러스를 연구하던 연구소에서 바이러스가 유출되었다. 다행히 바이러스는 아직 퍼지지 않았고, 바이러스의 확산을 막기 위해서 연구소에 벽을 세우려고 한다. 연구소는 크�

www.acmicpc.net

접근 방법


 위 문제는 벽 3개를 세울 수 있는 모든 경우에 대해 벽을 세운 후 바이러스를 퍼뜨려 안전영역의 최댓값을 구하면 된다. 

 

 벽을 3개를 세우는 방법은 백트래킹으로 가능하나 for문을 3중으로 사용하는 것도 가능하다. 필자는 3중 for문을 사용했으며, 취향에 따라 방법을 달리하여도 된다. 벽을 3개를 세운 뒤 바이러스를 퍼뜨리는 방법은 BFS를 사용하면 된다.

 

1. 일단 main()에서 입력을 받는 동시에 빈 공간의 위치와 바이러스의 위치의 정보를 저장해 두었다.

for (int i = 0; i < n; i++) 
	for (int j = 0; j < m; j++) {
    cin >> room[i][j];
    	if (room[i][j] == 0)
    		zeros.push_back({ i,j }); //빈공간
    	else if (room[i][j] == 2)
    		virus.push_back({ i,j }); //바이러스 초기위치
    }

2. 3중 for문을 사용하여 벽 3개를 세울 위치를 각각 first, second, third로 받았으며 해당 위치를 map에서 1로 변경 후 bfs를 사용하여 바이러스를 퍼뜨려 바이러스가 퍼진 공간을 cnt로 반환받았으며 안전영역의 수는 아래와 같이 구할 수 있다.

int safe = zeros.size() - 3 - cnt;

safe(바이러스가 퍼진 후 안전영역) =

zeros.size()(바이러스가 퍼지기 이전의 안전영역 수) - 3(벽을 새로 세운 공간) - cnt(바이러스가 퍼진 영역)

 

 

 

소스 코드


#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
#include <string.h>

using namespace std;

int room[8][8];
vector <pair<int, int>> zeros;
vector <pair<int, int>> virus;
pair<int, int> direction[4] = { {1,0}, {-1,0}, {0,1}, {0,-1} };
int n, m;
int max_ = 0;

int bfs(int x, int y) {

	int cnt = 0;
	queue < pair<int, int>> q;
	q.push({ x,y });
	while (!q.empty()) {
		auto elem = q.front();
		int i = elem.first;
		int j = elem.second;
		q.pop();
		for (int k = 0; k < 4; k++) {
			int next_x = i + direction[k].first;
			int next_y = j + direction[k].second;
			if (0 <= next_x && next_x < n && 0 <= next_y && next_y < m && room[next_x][next_y] == 0) {
				q.push({ next_x, next_y });
				room[next_x][next_y] = 2;
				cnt++;
			}
		}
	}
	return cnt;
}

void build_wall(int n, int m) {

	for (int i = 0; i < zeros.size(); i++) {
		for (int j = i + 1; j < zeros.size(); j++) {
			for (int k = j + 1; k < zeros.size(); k++) {
				int cnt = 0;
				auto first = zeros[i];
				auto second = zeros[j];
				auto third = zeros[k];
				room[first.first][first.second] = 1;
				room[second.first][second.second] = 1;
				room[third.first][third.second] = 1;
				for (auto &elem : virus) {
					cnt += bfs(elem.first, elem.second); //바이러스가 퍼진 영역의 수
				}
				int safe = zeros.size() - 3 - cnt;
				max_ = max(max_, safe);
				for (auto &elem : zeros) {
					room[elem.first][elem.second] = 0;
				}
				room[first.first][first.second] = 0;
				room[second.first][second.second] = 0;
				room[third.first][third.second] = 0;
			}
		}
	}
}

int main() {

	ios_base::sync_with_stdio(false);
	cin.tie(NULL);
	cout.tie(NULL);

	memset(room, -1, sizeof(room));

	cin >> n >> m;
	for (int i = 0; i < n; i++) 
		for (int j = 0; j < m; j++) {
			cin >> room[i][j];
			if (room[i][j] == 0)
				zeros.push_back({ i,j }); //빈공간
			else if (room[i][j] == 2)
				virus.push_back({ i,j }); //바이러스 초기위치
		}
	build_wall(n, m);
	cout << max_ << "\n";
}

개발 환경 : VS 2019

질문, 지적 환영입니다.

'알고리즘 > 백준' 카테고리의 다른 글

[C++] 백준 2580 스도쿠  (0) 2020.06.10
[C++] 백준 4195 친구네트워크  (0) 2020.06.09
[C++] 백준 17779 게리맨더링2  (0) 2020.05.29
[C++] 백준 17837 새로운 게임 2  (0) 2020.05.29
[C++] 백준 1520 내리막길  (0) 2020.05.26

문제 링크


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

 

15684번: 사다리 조작

사다리 게임은 N개의 세로선과 M개의 가로선으로 이루어져 있다. 인접한 세로선 사이에는 가로선을 놓을 수 있는데, 각각의 세로선마다 가로선을 놓을 수 있는 위치의 개수는 H이고, 모든 세로선

www.acmicpc.net

 

접근 방법


 이 문제는 사다리에 추가적으로 가로선을 0개 부터 최대 3개까지 놓는 가능한 모든 경우의 수를 다 보아야 해결 할 수 있다.  결국 조합 문제로 해석될 수 있으므로, 백트래킹을 하며 모든 조합에 대해 문제의 조건은 1번째부터 i번째에서 출발할 때 순서대로 1번째부터 i번째의 도착 지점에 도달하면 카운트하고, 그 카운트가 4이상이거나 가능한 방법이 없다면 -1을 출력하면 된다.

 

(1) 변수

map[h][l]는 h의 높이, l번째 라인이 h의 높이, l+1번째 라인에 가로선이 연결되어 있다면 1, 아니면 0으로 나타낸다.  

 

(2) go() 함수

bool go() {
	for (int i = 1; i <= N; i++) {
		int nowl = i; 
		for (int nowh = 0; nowh <= H; nowh++){
			if (map[nowh][nowl] == 1) 
				nowl++;
			else if (map[nowh][nowl - 1] == 1) 
				nowl--;
		}
		if (nowl != i)
			return false;
	}
	return true;
}

이 함수는 사다리의 출발지점과 도착지점이 같은지 따라가보는 함수이다.

 모든 사다리의 출발지점 1~N에 대해 확인 하여야 하기 때문에 for문에서 경우를 검사해보자. 현재 출발 라인을 nowl = i 라 하고, nowh(0~H)를 따라갈때, 우측에 사다리가 있는 경우 (if(map[nowh][nowl] == 1))   라인을 오른쪽으로 옮겨갈 것(nowl++)이고, 좌측에 사다리가 있는 경우 (if(map[nowh][nowl - 1] == 1))   라인을 왼쪽으로 옮겨갈 것(nowl--)이다.

 

(3) solve()함수

void solve(int cnt, int now_h, int now_l) {
	if (go()) {
		result = min(cnt, result);
		return;
	}
	if (cnt == 3) 
		return;

	for (int h = now_h; h <= H; h++) {
		for (int l = now_l; l < N; l++) {
			if (map[h][l] == 0 && map[h][l - 1] == 0 && map[h][l + 1] == 0) {
				map[h][l] = 1;
				solve(cnt + 1, h, l);
				map[h][l] = 0;
			}
		}
		now_l = 1;
	}
}

이 함수는 조합을 보기 위한 백트래킹(DFS) 함수이다. 중복되는 경우를 보지 않기 위해 매게변수로(nowl, nowh)를 사용하였으며, 위 함수가 호출되는 모든 경우에 대해 위에 설명한 go()함수로 조건을 만족하는지 검사한다. cnt가 3보다 커지는 경우는 고려하지 않으므로 cnt는 3에서 return을 하게 된다.

 

 가로선을 추가 할 수 있는 경우인,

양쪽에 가로라인이 존재하지 않을 때(if (map[h][l] == 0 && map[h][l - 1] == 0 && map[h][l + 1] == 0))

가로라인을 추가(map[h][l] = 1)하고, 함수를 진행 시킨뒤(solve(cnt + 1, h, 1)), 함수가 종료되면 해당 가로라인을 다시 삭제 (map[h][l] = 0)하면 된다. 

 

소스 코드


#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int N, M, H;
int map[32][12]; //height, line
int result = 4;	

bool go() {
	
	for (int i = 1; i <= N; i++) {
		int nowl = i; 
		for (int nowh = 0; nowh <= H; nowh++){
			if (map[nowh][nowl] == 1) 
				nowl++;
			else if (map[nowh][nowl - 1] == 1) 
				nowl--;
		}
		if (nowl != i)
			return false;
	}
	return true;
}

void solve(int cnt, int now_h, int now_l) {
	if (go()) {
		result = min(cnt, result);
		return;
	}
	if (cnt == 3) 
		return;

	for (int h = now_h; h <= H; h++) {
		for (int l = now_l; l < N; l++) {
			if (map[h][l] == 0 && map[h][l - 1] == 0 && map[h][l + 1] == 0) {
				map[h][l] = 1;
				solve(cnt + 1, h, l);
				map[h][l] = 0;
			}
		}
		now_l = 1;
	}
}

int main() {

	ios_base::sync_with_stdio(false);
	cin.tie(NULL);
	cout.tie(NULL);

	cin >> N >> M >> H;
	for (int i = 1; i <= M; i++) {
		int a, b;
		cin >> a >> b; //a높이 b라인
		map[a][b] = 1;
	}
	solve(0, 1, 1);
	
	if (result >= 4)
		cout << -1 << "\n";
	else
		cout << result << "\n";
}

개발 환경 : VS 2019

질문, 지적 환영입니다.

'알고리즘 > 백준' 카테고리의 다른 글

[C++] 백준 5373 큐빙  (0) 2020.05.26
[C++] 백준 13460 구슬 탈출 2  (0) 2020.05.26
[C++] 백준 14891 톱니바퀴  (0) 2020.05.25
[C++} 백준 14890 경사로  (0) 2020.05.25
[C++] 백준 15683 감시  (0) 2020.05.20

+ Recent posts