문제 설명


리틀 프렌즈 사천성

언제나 맛있는 음식들이 가득한 평화로운 푸드 타운. 푸드 타운에서 행복하게 사는 리틀 프렌즈들은 마을에 있는 매직 스푼을 보물처럼 보관하고 있다. 매직 스푼은 재료만 준비해서 냄비에 넣고 휘젓기만 하면 순식간에 최고의 요리로 만들어주는 신비의 아이템. 어느 날 매직 스푼을 호시탐탐 노리는 악당들이 보물을 훔쳐간다. 매직 스푼을 되찾고 다시 마을에 평화를 가져오기 위해 프렌즈의 대모험이 시작되는데...

리틀 프렌즈 사천성은 프렌즈 사천성과 유사한 게임이다. 게임은 2차원 배열에서 진행되는데, 여러 가지 무늬로 구성된 타일이 배치되어 있으며 같은 모양의 타일은 두 개씩 배치되어 있다. 게임의 목적은 배치된 모든 타일을 제거하는 것으로, 같은 모양의 타일을 규칙에 따라 제거하면 된다. 타일을 제거할 수 있는 경우는 다음과 같다.

다음 조건을 만족하는 경로가 있을 때 두 타일을 제거할 수 있다.

  • 경로의 양 끝은 제거하려는 두 타일이다.
  • 경로는 두 개 이하의 수평/수직 선분으로 구성되어 있고, 이들은 모두 연결되어 있다. (즉, 경로를 한 번 이하로 꺾을 수 있다)
    • 참고: 프렌즈 사천성은 경로가 세 개 이하의 선분으로 구성되어야 한다는 점이 다르다. (즉, 경로를 두 번 이하로 꺾을 수 있다)
  • 경로 상에는 다른 타일 또는 장애물이 없어야 한다.

위의 배열에서 어피치 타일은 직선의 경로로 이을 수 있으므로 제거 가능하다. 라이언 타일 역시 한 번 꺾인 경로로 연결되므로 제거 가능하다. 무지 타일의 경우 다른 타일을 지나지 않는 경로는 두 번 꺾여야 하므로 제거할 수 없는 타일이며, 튜브 타일 역시 직선의 경로 사이에 장애물이 있으므로 제거 가능하지 않다.

타일 배열이 주어졌을 때, 규칙에 따라 타일을 모두 제거할 수 있는지, 그 경우 어떤 순서로 타일을 제거하면 되는지 구하는 프로그램을 작성해보자.

입력 형식

입력은 게임판의 크기를 나타내는 m과 n, 그리고 배치된 타일의 정보를 담은 문자열 배열 board로 주어진다. 이 배열의 크기는 m이며, 각각의 원소는 n글자의 문자열로 구성되어 있다. 입력되는 값의 제한조건은 다음과 같다.

  • 1 <= m, n <= 100
  • board의 원소는 아래 나열된 문자로 구성된 문자열이다. 각 문자의 의미는 다음과 같다.
    • .: 빈칸을 나타낸다.
    • *: 막힌 칸을 나타낸다.
    • 알파벳 대문자(A-Z): 타일을 나타낸다. 이 문제에서, 같은 글자로 이루어진 타일은 한 테스트 케이스에 항상 두 개씩만 존재한다.
    • board에는 알파벳 대문자가 항상 존재한다. 즉, 타일이 없는 입력은 주어지지 않는다.

출력 형식

해가 존재하는 경우 타일을 제거하는 순서대로 한 글자씩 이루어진 문자열을, 그렇지 않은 경우 IMPOSSIBLE을 리턴한다. 해가 여러 가지인 경우, 알파벳 순으로 가장 먼저인 문자열을 리턴한다.

예제 입출력

mnboardanswer

3 3 ["DBA", "C*A", "CDB"] "ABCD"
2 4 ["NRYN", "ARYA"] "RYAN"
4 4 [".ZI.", "M.**", "MZU.", ".IU."] "MUZI"
2 2 ["AB", "BA"] "IMPOSSIBLE"

예제에 대한 설명

첫 번째 테스트 케이스에서 처음으로 제거 가능한 타일은 A와 C이다. 그리고 모든 가능한 경우를 나열하면 ABCD, ACBD, ACDB, CABD, CADB, CDAB이다. 이 중 알파벳 순으로 가장 먼저인 ABCD가 정답이다.

네 번째 테스트 케이스는 초기 상태에서 제거할 수 있는 타일이 없으므로 타일을 모두 제거하는 것이 불가능하다. 따라서 정답은 IMPOSSIBLE이다.

 

 

 

 

접근 방법


문제 풀이를 떠올리는 것은 어렵지 않았습니다. 하지만 치명적 실수 때문에 시간을 꽤 오래 잡았고 큰 깨달음을 얻게 해준 문제였습니다.

 

(!!!혹시 테스트 케이스는 다 맞는데 틀리는 분들을 위해 일단 제 실수에 대해 설명드리겠습니다.)

저는 문제를 보자마자 짝을 찾는 방법에 대해 BFS를 떠올렸습니다.

BFS를 사용할 때 visited로 방문체크를 하였는데 이게 가장 치명적인 실수였습니다.

 

만약 아래와 같이 입력이 주어졌을 때

 

BFS탐색 순서에 따라 아래와 같은 경로가 가능함에도 불구하고

 

좌측 사진과 같은 순서로 먼저 탐색이 된다면 우측 사진의 경로로 탐색이 불가능 하다는 점이었습니다.

 

왜냐하면 1,2 지점에 visited[1][2] = true가 되기 때문에 1,2지점으로 갈 수 없기 때문입니다.

 

 즉, 탐색을 할 경우 BFS의 경우 약간의 변형을 통해 visited를 사용하지 않거나, DFS(모든 지점 방문이 목적)의 경우 backTracking(모든 경로 탐색이 목적)을 사용한다면 이러한 문제점을 극복할 수 있습니다.

 

 

문제풀이는 아래와 같습니다.

1. (A~Z) 짝의 좌표를 저장 (동시에 제거 해야할 짝의 수를 증가->나중에 모든 짝이 제거되었는지 확인하기 위한 목적)

2. A~Z까지 제거 가능한 짝이 있는지 검사

3. 2에서 제거 가능한 짝이 있다면 제거 후, A부터 다시 검사

(A부터 검사하는 이유는 가능한 경우에 대해 사전 순 정렬 목적)

4. 제거할 수 있는 짝이 없다면 완전히 제거가 되었는지 검사

 

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

 

 

소스 코드


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

using namespace std;

typedef struct qinfo{
//행, 열, 방향, 꺾은 횟수
    int x,y,dir,cnt;
};
typedef struct ainfo{
    int x,y;
};

int pairCnt = 0;
int N = 0,M = 0;

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

bool BFS(int start_x, int start_y, int end_x, int end_y, vector<string> &boards){
    
    pair<int, int> nextdir[4] = {{1,0},{0,1},{-1,0},{0,-1}};
    
    queue<qinfo> q;
    
    //어떤 방향으로 출발해도 상관 없다는 표시로 dir = -1;
    q.push({start_x, start_y, -1, 0});
    
    while(!q.empty()){
        
        int now_x = q.front().x; int now_y = q.front().y;
        int cnt = q.front().cnt; int dir = q.front().dir;
        q.pop();
        
        for(int i=0;i<4;i++){
            
            //dir이 -1이 아닐 때 역 방향이라면 continue
            if(dir != -1 && abs(dir - i) == 2)
                continue;
            
            int next_x = now_x + nextdir[i].first;
            int next_y = now_y + nextdir[i].second;
            int next_cnt = (dir == i || dir == -1) ? cnt:cnt+1; //방향이 다르다면 꺾어야만 함
            
            if(inRange(next_x, next_y)){
                //어디로 가던지 꺾은 횟수가 1번 이하여야 함
                if(next_cnt <= 1){
                    if(boards[next_x][next_y] == '*'){
                        continue;
                    }
                    else if(boards[next_x][next_y] == '.'){
                        q.push({next_x, next_y, i, next_cnt});
                    }
                    else {
                        //제거 할 수 있는 패를 찾았다면
                        if(next_x == end_x && next_y == end_y)
                            return true;
                    }    
                }
            }
        }
    }
    return false;
}

string solve(vector<vector<ainfo>> &arr, vector<string> &boards){
    
    string ans = "";
    bool flag = true;
    
    while(flag){
        
        flag = false;
        
        //2번 과정, A~Z까지 검사
        for(int i=0;i<arr.size();i++){
            //해결 해야할 요소가 있다면
            if(arr[i].size() > 1){
            	//BFS를 통해 제거 가능한 짝이 있는지 검사
                if(BFS(arr[i][0].x, arr[i][0].y, arr[i][1].x, arr[i][1].y, boards)){
                	//3번 과정, 제거한 부분을 빈 공간으로 변경
                    boards[arr[i][0].x][arr[i][0].y] = boards[arr[i][1].x][arr[i][1].y] = '.'; //빈칸으로 만듬
                    arr[i].clear(); //배열 정보 삭제
                    pairCnt -= 2; //제거 해야할 짝의 수 감소
                    ans += char(i + int('A')); 
                    flag = true;
                    break;
                }    
            }
        }   
    }
    
    //4번 과정
    if(pairCnt == 0) //모든 짝을 제거한 경우
        return ans;
    else //더 이상 해결을 못하는 경우
        return "IMPOSSIBLE";
}

// 전역 변수를 정의할 경우 함수 내에 초기화 코드를 꼭 작성해주세요.
string solution(int m, int n, vector<string> board) {
    
    string answer = "";
    vector<vector<ainfo>> arr(26, vector<ainfo>());
    
    //전역 변수 초기화
    pairCnt = 0;
    M=m; N=n;
    
    //1번 과정, 모든 짝의 위치를 저장
    for(int i=0;i<m;i++){
        for(int j=0;j<n;j++){
            if('A' <= board[i][j] && board[i][j] <= 'Z'){
            	//alphabet을 int로 변환 후 위치 저장
                arr[int(board[i][j]) - int('A')].push_back({i,j});
                pairCnt++; //제거 해야 할 짝의 수 증가
            }
        }
    }
                
    return answer = solve(arr,board);
}

문제 설명


단체사진 찍기

가을을 맞아 카카오프렌즈는 단체로 소풍을 떠났다. 즐거운 시간을 보내고 마지막에 단체사진을 찍기 위해 카메라 앞에 일렬로 나란히 섰다. 그런데 각자가 원하는 배치가 모두 달라 어떤 순서로 설지 정하는데 시간이 오래 걸렸다. 네오는 프로도와 나란히 서기를 원했고, 튜브가 뿜은 불을 맞은 적이 있던 라이언은 튜브에게서 적어도 세 칸 이상 떨어져서 서기를 원했다. 사진을 찍고 나서 돌아오는 길에, 무지는 모두가 원하는 조건을 만족하면서도 다르게 서는 방법이 있지 않았을까 생각해보게 되었다. 각 프렌즈가 원하는 조건을 입력으로 받았을 때 모든 조건을 만족할 수 있도록 서는 경우의 수를 계산하는 프로그램을 작성해보자.

입력 형식

입력은 조건의 개수를 나타내는 정수 n과 n개의 원소로 구성된 문자열 배열 data로 주어진다. data의 원소는 각 프렌즈가 원하는 조건이 N~F=0과 같은 형태의 문자열로 구성되어 있다. 제한조건은 아래와 같다.

  • 1 <= n <= 100
  • data의 원소는 다섯 글자로 구성된 문자열이다. 각 원소의 조건은 다음과 같다.
    • 첫 번째 글자와 세 번째 글자는 다음 8개 중 하나이다. {A, C, F, J, M, N, R, T} 각각 어피치, 콘, 프로도, 제이지, 무지, 네오, 라이언, 튜브를 의미한다. 첫 번째 글자는 조건을 제시한 프렌즈, 세 번째 글자는 상대방이다. 첫 번째 글자와 세 번째 글자는 항상 다르다.
    • 두 번째 글자는 항상 ~이다.
    • 네 번째 글자는 다음 3개 중 하나이다. {=, <, >} 각각 같음, 미만, 초과를 의미한다.
    • 다섯 번째 글자는 0 이상 6 이하의 정수의 문자형이며, 조건에 제시되는 간격을 의미한다. 이때 간격은 두 프렌즈 사이에 있는 다른 프렌즈의 수이다.

출력 형식

모든 조건을 만족하는 경우의 수를 리턴한다.

예제 입출력

ndataanswer

2 ["N~F=0", "R~T>2"] 3648
2 ["M~C<2", "C~M>1"] 0

예제에 대한 설명

첫 번째 예제는 문제에 설명된 바와 같이, 네오는 프로도와의 간격이 0이기를 원하고 라이언은 튜브와의 간격이 2보다 크기를 원하는 상황이다.

두 번째 예제는 무지가 콘과의 간격이 2보다 작기를 원하고, 반대로 콘은 무지와의 간격이 1보다 크기를 원하는 상황이다. 이는 동시에 만족할 수 없는 조건이므로 경우의 수는 0이다.

 

 

 

접근 방법


백트래킹 기법을 사용하여 해결할 수 있었습니다.

 

1. 백트래킹을 사용하여 8명이 서있을 수 있는 모든 경우의 수에 대한 순열을 구합니다. (이 문제의 경우 8!개)

 

2. 구해진 한 조합에 대해 각각의 프렌즈 들이 원하는 조건(data)가 전부 만족하는지 확인합니다.

 

3. 한 조건에 대해 검사를 하는 방법은 아래와 같습니다.

 

3-1. 조건의 0번째 인덱스와 2번째 인덱스인 두 명과, 3번째 인덱스인 오퍼레이션(<, >, =) 마지막으로 4번째 인덱스인  오퍼레이션에 대한 수를 parsing합니다.

 

3-2. 아래와 같은 코드로 각 조건이 만족하는지 검사합니다.

if(operation == '='){
	if(Math.abs(first - second) == num + 1)
		return true;
}
else if(operation == '>'){
	if(Math.abs(first-second) > num + 1)
		return true;
}
else{
	if(Math.abs(first-second) < num + 1)
		return true;
}
return false;

 

 

소스 코드


import java.util.*;
import java.lang.*;

class Solution {
    
    boolean visited[] = new boolean[8];
    String friends = "ACFJMNRT";
    String batch = "";
    String[] data;
    int ans = 0;
    
    public boolean checkOne(String condition){
        
        // 2명의 위치, 연산자, 숫자
        int first = batch.indexOf(condition.substring(0, 1)); 
        int second = batch.indexOf(condition.substring(2, 3));
        char operation = condition.charAt(3);
        Integer num = Integer.parseInt(condition.substring(condition.length() - 1));
        
        if(operation == '='){
            if(Math.abs(first - second) == num + 1)
                return true;
        }
        else if(operation == '>'){
            if(Math.abs(first-second) > num + 1)
                return true;
        }
        else{
            if(Math.abs(first-second) < num + 1)
                return true;
        }
        return false;
    }
    
    //전체 데이터에 대한 검사
    public boolean check(){
        
        for(int i=0;i<data.length;i++){
            if(!checkOne(data[i])) //하나라도 만족하지 않는다면
                return false;
        }
        return true;
    } 
    
    public void backTracking(int cnt){
        
        //8명의 순열이 완성되었을 때
        if(cnt == 8){
            if(check()) //전체 조건을 검사
                ans++;  //만족한다면 ans++
            return;
        }
        
        for(int i=0;i<8;i++){
            if(visited[i] == false){
                visited[i] = true;
                batch += friends.substring(i,i+1);
                backTracking(cnt + 1);
                batch = batch.substring(0, batch.length() - 1);
                visited[i] = false;
            }    
        }
    }
    
    public int solve(int n, String[] data){
        
        this.data = data;
        backTracking(0);
        return ans;
    }
    
    public int solution(int n, String[] data) {
        int answer = 0;
        return answer = solve(n, data);
    }
}

문제 설명


후보키

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

그의 학부 시절 프로그래밍 경험을 되살려, 모든 인적사항을 데이터베이스에 넣기로 하였고, 이를 위해 정리를 하던 중에 후보키(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;
}

문제 설명


데이터 처리 전문가가 되고 싶은 "어피치"는 문자열을 압축하는 방법에 대해 공부를 하고 있습니다. 최근에 대량의 데이터 처리를 위한 간단한 비손실 압축 방법에 대해 공부를 하고 있는데, 문자열에서 같은 값이 연속해서 나타나는 것을 그 문자의 개수와 반복되는 값으로 표현하여 더 짧은 문자열로 줄여서 표현하는 알고리즘을 공부하고 있습니다.
간단한 예로 "aabbaccc"의 경우 "2a2ba3c"(문자가 반복되지 않아 한번만 나타난 경우 1은 생략함)와 같이 표현할 수 있는데, 이러한 방식은 반복되는 문자가 적은 경우 압축률이 낮다는 단점이 있습니다. 예를 들면, "abcabcdede"와 같은 문자열은 전혀 압축되지 않습니다. "어피치"는 이러한 단점을 해결하기 위해 문자열을 1개 이상의 단위로 잘라서 압축하여 더 짧은 문자열로 표현할 수 있는지 방법을 찾아보려고 합니다.

예를 들어, "ababcdcdababcdcd"의 경우 문자를 1개 단위로 자르면 전혀 압축되지 않지만, 2개 단위로 잘라서 압축한다면 "2ab2cd2ab2cd"로 표현할 수 있습니다. 다른 방법으로 8개 단위로 잘라서 압축한다면 "2ababcdcd"로 표현할 수 있으며, 이때가 가장 짧게 압축하여 표현할 수 있는 방법입니다.

다른 예로, "abcabcdede"와 같은 경우, 문자를 2개 단위로 잘라서 압축하면 "abcabc2de"가 되지만, 3개 단위로 자른다면 "2abcdede"가 되어 3개 단위가 가장 짧은 압축 방법이 됩니다. 이때 3개 단위로 자르고 마지막에 남는 문자열은 그대로 붙여주면 됩니다.

압축할 문자열 s가 매개변수로 주어질 때, 위에 설명한 방법으로 1개 이상 단위로 문자열을 잘라 압축하여 표현한 문자열 중 가장 짧은 것의 길이를 return 하도록 solution 함수를 완성해주세요.

제한사항

  • s의 길이는 1 이상 1,000 이하입니다.
  • s는 알파벳 소문자로만 이루어져 있습니다.

입출력 예

sresult

"aabbaccc" 7
"ababcdcdababcdcd" 9
"abcabcdede" 8
"abcabcabcabcdededededede" 14
"xababcdcdababcdcd" 17

입출력 예에 대한 설명

입출력 예 #1

문자열을 1개 단위로 잘라 압축했을 때 가장 짧습니다.

입출력 예 #2

문자열을 8개 단위로 잘라 압축했을 때 가장 짧습니다.

입출력 예 #3

문자열을 3개 단위로 잘라 압축했을 때 가장 짧습니다.

입출력 예 #4

문자열을 2개 단위로 자르면 "abcabcabcabc6de" 가 됩니다.
문자열을 3개 단위로 자르면 "4abcdededededede" 가 됩니다.
문자열을 4개 단위로 자르면 "abcabcabcabc3dede" 가 됩니다.
문자열을 6개 단위로 자를 경우 "2abcabc2dedede"가 되며, 이때의 길이가 14로 가장 짧습니다.

입출력 예 #5

문자열은 제일 앞부터 정해진 길이만큼 잘라야 합니다.
따라서 주어진 문자열을 x / ababcdcd / ababcdcd 로 자르는 것은 불가능 합니다.
이 경우 어떻게 문자열을 잘라도 압축되지 않으므로 가장 짧은 길이는 17이 됩니다.

 

 

접근 방법


1. 일단 몇개의 문자열로 잘라야 최소가 되는지 모르므로 (1개~ [문자열길이/2]개)만큼 잘라보며 비교를 해야된다.

(문자열의 길이의 절반보다 큰 수로 자르게 되는 것은 압축을 할 수 없기 때문에 의미가 없다.)

int answer = s.length();
        
for(int i=1;i <= s.length()/2; i++)
	answer = Math.min(answer, findLength(s, i));

 

2. 문자열을 i개로 자른 단위를 s라고 한다면, 문자열  s와 s가 연속적으로 반복된 수 cnt를 저장할 수 있는 구조가 필요하다. 

  class Info{
        String s;
        Integer cnt;
        
        public Info(String s, int cnt){
            this.s = s;
            this.cnt = cnt;
        }
    }

 

3-1. ArrayList<Info> arr형태의 자료구조에 전체 문자열을 i개 단위로 잘라가며 저장을한다.

3-2. 만약 arr의 가장 마지막에 저장된 문자열과 현재 잘려진 문자열이 동일하다면 arr의 가장 마지막 요소의 cnt를 증가

3-3. 만약 arr의 가장 마지막 저장된 문자열과 다르다면, arr에 add 메소드로 삽입을 한다.

 

4. 완성된 arr의 정보를 토대로 문자열의 길이를 계산한다.

 

 

소스 코드


Java

import java.util.*;

class Solution {
    
    class Info{
        String s;
        Integer cnt;
        
        public Info(String s, int cnt){
            this.s = s;
            this.cnt = cnt;
        }
    }
    
    public int findLength(String s, int n){
        
        ArrayList<Info> arr = new ArrayList<Info>();
        
        int idx = 0;
        
        //n개 단위 문자열 처리
        while(idx + n <= s.length()){
            
            String sub = s.substring(idx, idx + n);
            
            if(arr == null || arr.size() == 0)
                arr.add(new Info(sub, 1));
            else{
                
                Info back = arr.get(arr.size() - 1);
                
                if(back.s.equals(sub))
                    back.cnt++;
                else
                    arr.add(new Info(sub, 1));
            }
            
            idx += n;
        }
        
        //압축된 문자열 길이 구하기
        int result = 0;
        
        for(int i=0;i<arr.size();i++){
            Info info = arr.get(i);
            result += info.s.length();
            if(info.cnt > 1)
                result += info.cnt.toString().length();
        }
        return result + (s.length() - idx);
    }
    
    public int solution(String s) {
        int answer = s.length();
        
        for(int i=1;i <= s.length()/2; i++)
            answer = Math.min(answer, findLength(s, i));
        
        return answer;
    }
}

 

C++

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

using namespace std;

int strLength(string s, int psize) {
	
	vector <pair<int, string>> patterns;
	int result = 0;
	int nonepattern = s.size();

	int i = 0;
	while (i < s.size() - psize){
		if (s.substr(i, psize).compare(s.substr(i + psize, psize)) == 0) {
			if (patterns.empty() || patterns.back().second.compare(s.substr(i, psize)) != 0)
				patterns.push_back({ 2, s.substr(i, psize) });
			else
				patterns.back().first += 1;

			if (s.substr(i + psize, psize).compare(s.substr(i + 2 * psize, psize)) == 0)
				i += psize;
			else
				i += 2 * psize;
		}
		else
			i+= psize;
	}
	
	for (int i = 0; i < patterns.size(); i++) {
		pair<int, string> pattern = patterns[i];
		int cnt = to_string(pattern.first).size();
		result += (cnt + psize);
		nonepattern -= (pattern.first*psize);
	}
	return result + nonepattern;
}

int solution(string s) {
	int answer = 0;
	int minimum = s.size();
	for (int i = 1; i < s.size()/2 + 1; i++) {
		minimum = min(minimum, strLength(s, i));
	}
	return answer = minimum;
}

문제 설명


레스토랑을 운영하던 스카피는 코로나19로 인한 불경기를 극복하고자 메뉴를 새로 구성하려고 고민하고 있습니다.
기존에는 단품으로만 제공하던 메뉴를 조합해서 코스요리 형태로 재구성해서 새로운 메뉴를 제공하기로 결정했습니다. 어떤 단품메뉴들을 조합해서 코스요리 메뉴로 구성하면 좋을 지 고민하던 "스카피"는 이전에 각 손님들이 주문할 때 가장 많이 함께 주문한 단품메뉴들을 코스요리 메뉴로 구성하기로 했습니다.
단, 코스요리 메뉴는 최소 2가지 이상의 단품메뉴로 구성하려고 합니다. 또한, 최소 2명 이상의 손님으로부터 주문된 단품메뉴 조합에 대해서만 코스요리 메뉴 후보에 포함하기로 했습니다.

예를 들어, 손님 6명이 주문한 단품메뉴들의 조합이 다음과 같다면,
(각 손님은 단품메뉴를 2개 이상 주문해야 하며, 각 단품메뉴는 A ~ Z의 알파벳 대문자로 표기합니다.)

손님 번호주문한 단품메뉴 조합

1번 손님 A, B, C, F, G
2번 손님 A, C
3번 손님 C, D, E
4번 손님 A, C, D, E
5번 손님 B, C, F, G
6번 손님 A, C, D, E, H

가장 많이 함께 주문된 단품메뉴 조합에 따라 "스카피"가 만들게 될 코스요리 메뉴 구성 후보는 다음과 같습니다.

코스 종류메뉴 구성설명

요리 2개 코스 A, C 1번, 2번, 4번, 6번 손님으로부터 총 4번 주문됐습니다.
요리 3개 코스 C, D, E 3번, 4번, 6번 손님으로부터 총 3번 주문됐습니다.
요리 4개 코스 B, C, F, G 1번, 5번 손님으로부터 총 2번 주문됐습니다.
요리 4개 코스 A, C, D, E 4번, 6번 손님으로부터 총 2번 주문됐습니다.

[문제]

각 손님들이 주문한 단품메뉴들이 문자열 형식으로 담긴 배열 orders, "스카피"가 추가하고 싶어하는 코스요리를 구성하는 단품메뉴들의 갯수가 담긴 배열 course가 매개변수로 주어질 때, "스카피"가 새로 추가하게 될 코스요리의 메뉴 구성을 문자열 형태로 배열에 담아 return 하도록 solution 함수를 완성해 주세요.

[제한사항]

  • orders 배열의 크기는 2 이상 20 이하입니다.
  • orders 배열의 각 원소는 크기가 2 이상 10 이하인 문자열입니다.
    • 각 문자열은 알파벳 대문자로만 이루어져 있습니다.
    • 각 문자열에는 같은 알파벳이 중복해서 들어있지 않습니다.
  • course 배열의 크기는 1 이상 10 이하입니다.
    • course 배열의 각 원소는 2 이상 10 이하인 자연수가 오름차순으로 정렬되어 있습니다.
    • course 배열에는 같은 값이 중복해서 들어있지 않습니다.
  • 정답은 각 코스요리 메뉴의 구성을 문자열 형식으로 배열에 담아 사전 순으로 오름차순 정렬해서 return 해주세요.
    • 배열의 각 원소에 저장된 문자열 또한 알파벳 오름차순으로 정렬되어야 합니다.
    • 만약 가장 많이 함께 주문된 메뉴 구성이 여러 개라면, 모두 배열에 담아 return 하면 됩니다.
    • orders와 course 매개변수는 return 하는 배열의 길이가 1 이상이 되도록 주어집니다.

[입출력 예]

orderscourseresult

["ABCFG", "AC", "CDE", "ACDE", "BCFG", "ACDEH"] [2,3,4] ["AC", "ACDE", "BCFG", "CDE"]
["ABCDE", "AB", "CD", "ADE", "XYZ", "XYZ", "ACD"] [2,3,5] ["ACD", "AD", "ADE", "CD", "XYZ"]
["XYZ", "XWY", "WXA"] [2,3,4] ["WX", "XY"]

입출력 예에 대한 설명


입출력 예 #1
문제의 예시와 같습니다.

입출력 예 #2
AD가 세 번, CD가 세 번, ACD가 두 번, ADE가 두 번, XYZ 가 두 번 주문됐습니다.
요리 5개를 주문한 손님이 1명 있지만, 최소 2명 이상의 손님에게서 주문된 구성만 코스요리 후보에 들어가므로, 요리 5개로 구성된 코스요리는 새로 추가하지 않습니다.

입출력 예 #3
WX가 두 번, XY가 두 번 주문됐습니다.
3명의 손님 모두 단품메뉴를 3개씩 주문했지만, 최소 2명 이상의 손님에게서 주문된 구성만 코스요리 후보에 들어가므로, 요리 3개로 구성된 코스요리는 새로 추가하지 않습니다.
또, 단품메뉴를 4개 이상 주문한 손님은 없으므로, 요리 4개로 구성된 코스요리 또한 새로 추가하지 않습니다.

 

 

 

접근 방법


 HashMap과 backTracking을 적절히 활용하여 해결할 수 있는 문제였습니다.

 

1. 문제를 해결하기에 앞서 orders를 정렬 해야합니다. 한 예시로 xy와 yx는 같은 조합으로 볼 수 있고, 이러한 경우를 수월하게 계산하기 위해 사전순으로 정렬합니다.

 

2. 최소 2번 이상 주문된 음식의 조합(backTracking)을 HashMap에 저장합니다. (HashMap<String 음식조합, Integer 주문횟수>) 

 

3. HashMap의 원소를 각각 탐색하면서 음식조합의 수가 course의 한 요소와 일치한다면 가장 많이 주문된 횟수를 비교 갱신하며 음식조합 또한 추가 혹은 갱신을 합니다. 이 때 아래 2개의 HashMap을 사용하였습니다.

 (1) HashMap<Integer, ArrayList<String>> maxMap = new HashMap<Integer, ArrayList<String>>(); 
 (2) HashMap<Integer, Integer> maxCnt = new HashMap<Integer, Integer>();  

 

maxMap은 음식조합의 수(course)에 따른 음식조합을 저장합니다. 만약 한 음식 조합의 주문된 횟수가 최대 주문 횟수랑 일치하는 경우 요소를 추가해야 하기 때문에 value는 List의 형태를 가져야 합니다.

maxCnt는 음식조합의 수(course)에  따른 주문된 최대 횟수를 저장합니다.

 

자세한 내용은 아래 소스코드에 주석으로 설명하겠습니다.

 

 

소스 코드


import java.util.*;
import java.lang.*;

class Solution {
    
    HashMap<String, Integer> map = new HashMap<String, Integer>();
    HashMap<Integer, ArrayList<String>> maxMap = new HashMap<Integer, ArrayList<String>>(); 
    HashMap<Integer, Integer> maxCnt = new HashMap<Integer, Integer>(); 
    
    public void backTracking(int n, int cnt, int now, String order, String comb){
        
        if(cnt == n){
            if(map.containsKey(comb)){ //이미 주문된 적 있는 조합이라면
                int orderCnt = map.get(comb);
                map.put(comb, orderCnt + 1); // 주문횟수 1증가
            }
            else
                map.put(comb, 1);
        }
        for(int i = now; i<order.length(); i++){
            comb += order.charAt(i);
            backTracking(n, cnt+1, i + 1, order, comb);
            comb = comb.substring(0, comb.length() - 1);
        }
    }
    
    public ArrayList<String> solve(String[] orders, int[] course){
        
        ArrayList<String> ans = new ArrayList<String>();
        
        //1번 과정, 수월한 풀이를 위해 문자열 사전순 정렬
        for(int i=0;i<orders.length;i++){
            char[] tmp = orders[i].toCharArray();
            Arrays.sort(tmp);
            orders[i] = new String(tmp);
        }
        
        //2번 과정, i번째 주문에 대해 ,j개(최소 2개) 이상의 조합을 구함
        for(int i=0;i<orders.length;i++)
            for(int j=2;j<=orders[i].length();j++)
                backTracking(j,0,0,orders[i], "");    
        
        //course를 map에 미리 매핑시킴, 이후 course의 요소인지 판별하기에 쉽게 이용 가능
        for(int i=0;i<course.length;i++){
            maxMap.put(course[i], new ArrayList<String>()); 
            maxCnt.put(course[i], 0); 
        }
        
        //3번 과정
        for(Map.Entry<String, Integer> elem : map.entrySet()){
            String foods = elem.getKey();
            int cnt = elem.getValue();
            if(cnt >= 2){
                if(maxMap.containsKey(foods.length())){ //course의 요소라면
                    if(maxCnt.get(foods.length()) == cnt){ //최대 주문 횟수와 일치한다면
                        maxMap.get(foods.length()).add(foods); //추가
                    }
                    else if(maxCnt.get(foods.length()) < cnt){ //최대 주문 횟수 보다 더 크다면
                        maxCnt.put(foods.length(), cnt); //최댓값 갱신
                        ArrayList<String> newOne = new ArrayList<String>(Arrays.asList(foods)); 
                        maxMap.put(foods.length(), newOne); //새롭게 갱신
                    }
                }
            }   
        }
        
        //결과들을 ArrayList에 담는다.
        for(ArrayList<String> elem : maxMap.values())
            for(int i=0;i<elem.size();i++)
                ans.add(elem.get(i));    
        
        //사전순 정렬
        Collections.sort(ans);
        
        return ans;
    }
    
    public String[] solution(String[] orders, int[] course) {
        ArrayList<String> ans = solve(orders, course);
        String[] answer = ans.toArray(new String[ans.size()]);
        return answer;
    }
}

문제 설명


자동완성

포털 다음에서 검색어 자동완성 기능을 넣고 싶은 라이언은 한 번 입력된 문자열을 학습해서 다음 입력 때 활용하고 싶어 졌다. 예를 들어, go 가 한 번 입력되었다면, 다음 사용자는 g 만 입력해도 go를 추천해주므로 o를 입력할 필요가 없어진다! 단, 학습에 사용된 단어들 중 앞부분이 같은 경우에는 어쩔 수 없이 다른 문자가 나올 때까지 입력을 해야 한다.
효과가 얼마나 좋을지 알고 싶은 라이언은 학습된 단어들을 찾을 때 몇 글자를 입력해야 하는지 궁금해졌다.

예를 들어, 학습된 단어들이 아래와 같을 때

go gone guild

  • go를 찾을 때 go를 모두 입력해야 한다.
  • gone을 찾을 때 gon 까지 입력해야 한다. (gon이 입력되기 전까지는 go 인지 gone인지 확신할 수 없다.)
  • guild를 찾을 때는 gu 까지만 입력하면 guild가 완성된다.

이 경우 총 입력해야 할 문자의 수는 7이다.

라이언을 도와 위와 같이 문자열이 입력으로 주어지면 학습을 시킨 후, 학습된 단어들을 순서대로 찾을 때 몇 개의 문자를 입력하면 되는지 계산하는 프로그램을 만들어보자.

입력 형식

학습과 검색에 사용될 중복 없는 단어 N개가 주어진다.
모든 단어는 알파벳 소문자로 구성되며 단어의 수 N과 단어들의 길이의 총합 L의 범위는 다음과 같다.

  • 2 <= N <= 100,000
  • 2 <= L <= 1,000,000

출력 형식

단어를 찾을 때 입력해야 할 총 문자수를 리턴한다.

입출력 예제

wordsresult

["go","gone","guild"] 7
["abc","def","ghi","jklm"] 4
["word","war","warrior","world"] 15

입출력 설명

  • 첫 번째 예제는 본문 설명과 같다.
  • 두 번째 예제에서는 모든 단어들이 공통된 부분이 없으므로, 가장 앞글자만 입력하면 된다.
  • 세 번째 예제는 총 15 자를 입력해야 하고 설명은 아래와 같다.
    • word는 word모두 입력해야 한다.
    • war는 war 까지 모두 입력해야 한다.
    • warrior는 warr 까지만 입력하면 된다.
    • world는 worl까지 입력해야 한다. (word와 구분되어야 함을 명심하자)

 

 

접근 방법


 Trie 자료구조를 공부해봤다면 문제를 봤을 때 Trie를 사용해 해결할 수 있다는 것을 바로 떠올릴 수 있었을 것입니다.

 

 저는 문제를 풀기 위해 Trie의 한 노드가 가지는 자료를 3가지를 정의하였습니다.

1. child HashMap<Character, Node> child = new HashMap();

2. boolean isEnd;

3. int branchSum;

 

1번 child는 노드의 child노드를 연결하기 위한 링크 역할을 합니다.

2번 isEnd는 단어의 끝인지의 여부를 나타냅니다.

3번 branchSum은 뻗어있는 가지의 수를 의미합니다. (isEnd == true)인 구간, 즉 단어의 끝을 가지는 부분도 하나의 가지로 계산합니다.

 

만약 단어가 go, gone, guild가 주어진다면 branchSum의 상태는 아래의 그림과 같습니다.

아래와 같은 코드로 branchSum을 계산할 수 있습니다.

public int makeBranchSum(Node now){

	if(now.isEnd && now.child.isEmpty()) //단어의 끝이면서 더 나아갈 깊이가 없는 경우
		return now.branchSum = 1;

	for(Node node : now.child.values())
		now.branchSum += makeBranchSum(node);

	//단어의 끝이지만 더 나아갈 깊이가 있는 경우, 위 그림에서 노드(o)에 해당
	if(now.isEnd && !now.child.isEmpty()) 
		now.branchSum += 1;
	return now.branchSum;
} 

 

 

 isEnd와 branchSum을 적절히 활용하면 문제를 해결할 수 있습니다.

1. 일단 처음으로 (branchSum == 1)인 구간에 도달한다면 더 이상의 가지가 뻗어있지 않다는 의미가 되므로 그 구간까지의 깊이를 반환하고, 더 깊은 곳으로 진입하지 않습니다.

2. 만약 branchSum != 1이고 isEnd == true인 경우면 더 나아갈 수 있는 가지는 남아있으나, 여기가 어떠한 단어의 끝이라는 의미가 되므로 그 구간까지의 깊이를 포함하여 더해줍니다.

public int search(Node now, int cnt){ //cnt는 깊이를 의미
        
	int ret = 0;

	if(now.branchSum == 1) //1번 항목 검사
		return cnt;

	for(Node node : now.child.values())
		ret += search(node, cnt + 1);
	if(now.isEnd) //2번 항목 검사
		ret += cnt;

	return ret;
}

 

 

소스 코드


import java.util.*;

class Solution {
    
    public class Node {
        HashMap<Character, Node> child = new HashMap();
        boolean isEnd = false;
        int branchSum = 0;
    }
    
    public class Trie {
        
        Node root;
        
        public Trie(){
            root = new Node();
        }
        
        public void insert(String word){
            
            Node current = root;
            
            for(Character c : word.toCharArray()){
                if(current.child.get(c) == null){
                    Node node = new Node();
                    current.child.put(c, node);
                    current = node;
                }
                else
                    current = current.child.get(c);
            }
            current.isEnd = true;
        }
    }
    
    public int makeBranchSum(Node now){
        
        if(now.isEnd && now.child.isEmpty())
            return now.branchSum = 1;
        
        for(Node node : now.child.values())
            now.branchSum += makeBranchSum(node);
        
        if(now.isEnd && !now.child.isEmpty())
            now.branchSum += 1;
        return now.branchSum;
    } 
    
    public int search(Node now, int cnt){
        
        int ret = 0;
        
        if(now.branchSum == 1)
            return cnt;
        
        for(Node node : now.child.values())
            ret += search(node, cnt + 1);
        if(now.isEnd)
            ret += cnt;
        
        return ret;
    }
    
    public int solve(String[] words){
        
        Trie trie = new Trie();
        
        for(int i=0;i<words.length;i++){
            String word = words[i];
            trie.insert(word);
        }
        makeBranchSum(trie.root);
        return search(trie.root, 0);
    }
    
    public int solution(String[] words) {
        int answer = 0;
        return answer = solve(words);
    }
}

문제 설명


캠핑

무지를 돌보느라 지친 콘은 한적한 시골의 한 캠핑장에 놀러 갔다. 캠핑장은 텐트를 칠 수 있는 넓은 평지를 제공하고 있는데, 이 평지에는 이미 캠핑장에서 설치해 놓은 n개의 쐐기가 박혀 있다. 캠핑장 이용 고객은 이 쐐기들 중 한 쌍을 골라 다음과 같은 조건을 만족하도록 텐트를 설치해야 한다.

  • 텐트는 직사각형 형태여야 한다.
  • 텐트의 네 면이 정확하게 동, 서, 남, 북을 향해야 한다.
  • 대각에 위치하는 텐트의 두 꼭짓점이 정확하게 선택한 두 개의 쐐기에 위치해야 한다.
  • 텐트가 점유하는 직사각형 영역의 넓이는 0보다는 커야 한다.
  • 텐트가 점유하는 직사각형 영역 내부에 다른 쐐기를 포함하면 안 된다. (다른 쐐기가 경계에 위치하는 경우는 허용함)

캠핑장에서는 위와 같은 조건을 만족하는 위치라면 어디든 고객이 텐트를 설치할 수 있도록 정확한 크기의 텐트를 모두 구비하여 대여해준다고 한다.
당신은 위와 같은 조건을 만족하는 텐트를 설치할 수 있는 쐐기의 쌍의 개수는 총 몇 가지가 될지 궁금해졌다.
n개의 쐐기의 위치가 좌표로 주어질 때, 위의 조건을 만족하는 쐐기의 쌍의 개수를 계산하는 프로그램을 작성하시오. 단, 동서 방향은 x축, 남북 방향은 y축과 평행하다고 가정한다.

입력 형식

입력은 쐐기의 개수를 의미하는 정수 n과, n × 2 크기의 2차원 배열 data로 주어지며, 배열의 각 행은 캠핑장에 설치된 쐐기의 x좌표와 y좌표를 의미한다. 제한 조건은 다음과 같다.

  • 2 <= n <= 5,000
  • n개의 쐐기는 모두 x좌표 0 이상 2^31-1 이하, y좌표 0 이상 2^31-1 이하에 위치한다.
  • 입력되는 n개의 쐐기 중 x좌표와 y좌표가 모두 같은 경우는 없다.

출력 형식

입력에 주어진 각 케이스에 대해 가능한 텐트의 쐐기의 쌍의 개수를 정수 형태로 리턴한다.

예제 입출력

ndataanswer

4 [[0, 0], [1, 1], [0, 2], [2, 0]] 3

예제에 대한 설명

예제에는 총 4개의 쐐기가 있으며 이 중 (0,0)-(1,1), (0,2)-(1,1), (1,1)-(2,0)의 세 가지 위치에만 텐트를 설치할 수 있다. (0,0)-(0,2)와 (0,0)-(2,0)의 경우에는 직사각형 영역의 넓이가 0이 되기 때문에 조건을 만족하지 못하며, (0,2)-(2,0)의 경우 (1,1) 위치의 쐐기가 직사각형의 내부에 포함되므로 조건을 만족하지 못한다.

 

 

접근 방법


 문제 풀이의 핵심은 부분합과 좌표압축입니다. 문제를 처음보았을 때 부분합은 바로 떠올랐으나 문제에서 주어지는 좌표가 너무 커 어떻게 해결해야 하나 고민을 하였습니다. 

 이 때 좌표의 값과 상관없이 좌표의 크기 순서대로 좌표를 압축한다면 부분합을 사용하여 문제를 해결할 수 있습니다.

 

1. 좌표 압축

저와 같은 경우는 Hash Map을 사용하여 압축을 진행하였습니다.

(만약 데이터가 [100,121,999999999999]가 있다면 [0,1,2]로 변환가능)

 

1. map구조에 문제에서 주어진 좌표값들을 저장을 합니다.

2. map을 for문을 사용하여 데이터를 탐색하면 자동적으로 크기가 작은 값부터 탐색이 됩니다.(Hash 함수에 의해 value가 정렬되어 있음)

3. for문을 통해 탐색을 함과 동시에 value를 압축한 값으로 저장을 합니다.

 

void coordCompress(vector<vector<int>> &data){
    
    map<int, int> x_mapper;
    map<int, int> y_mapper;
    
    //1. 데이터를 hashmap에 저장(x와 y는 독립적임)
    for(int i=0;i<data.size();i++){
        int x = data[i][0];
        int y = data[i][1];
        if(x_mapper.count(x) == 0)
            x_mapper.insert({x, true});
        if(y_mapper.count(y) == 0)
            y_mapper.insert({y, true});
    }
    
    //좌표 압축을 진행
    int cnt = 0;
    for(auto &elem : x_mapper){ //2. 자동적으로 value가 작은 값부터 탐색됨
        int num = elem.first;
        x_mapper[num] = cnt++; //3. 좌표압축을 진행
    }
    cnt = 0;
    for(auto &elem : y_mapper){ //2. 자동적으로 value가 작은 값부터 탐색됨
        int num = elem.first;
        y_mapper[num] = cnt++; //3. 좌표압축을 진행
    }
    
    //변환된 좌표를 벡터에 저장하지 않고 map에서 계속해서 읽어오는 것은 속도저하를 유발하므로 
    //벡터로 저장을 해야함(map은 tree구조 이므로 데이터를 탐색하는 속도가 vector보다 느림)
    for(int i=0;i<data.size();i++){
        data[i][0] = x_mapper[data[i][0]];
        data[i][1] = y_mapper[data[i][1]];
    }
    M = x_mapper.size();
    N = y_mapper.size();
}

 

2. 부분합

 부분합을 사용하는 이유는 선택한 두 쐐기 사이에 포함되는 쐐기를 쉽게 식별할 수 있기 때문입니다. 

부분합 arr[x][y] = {0,0} ~ {x,y}사이에 있는 쐐기의 수를 의미합니다.

void make_arr(vector<vector<int>> &data){
    
    //압축된 좌표들의 정보로 arr에 좌표를 표시
    for(int i=0;i<data.size();i++){
        int x = data[i][0];
        int y = data[i][1];
        arr[x][y] = 1;
    }
    
    // arr[x][y] = {0,0} ~ {x,y}까지 존재하는 쐐기의 수 
    //태두리의 부분합을 구함
    for(int x = 1; x < M; x++)
        arr[x][0] += arr[x - 1][0];
    for(int y = 1; y < N; y++)
        arr[0][y] += arr[0][y - 1];
    
    //내부의 부분합을 구함
    for(int x = 1; x < M; x++){
        for(int y = 1; y < N; y++){
            arr[x][y] += arr[x - 1][y] + arr[x][y - 1] - arr[x - 1][y - 1];
        }    
    }
}

 

위의 과정 까지 진행했다면 2개의 쐐기를 선택하는 모든 경우에 대해 3가지 조건을 검사합니다.

1. 두 쐐기의 x좌표가 같은지

2. 두 쐐기의 y좌표가 같은지

3. 두 쐐기 사이에 다른 쐐기가 있는지(태두리에 쐐기가 있는 것은 무관함)

int solve(vector<vector<int>> &data){
    
    int cnt = 0;
    
    for(int i = 0; i < data.size(); i++){
        for(int j = i + 1; j < data.size(); j++){
            
            int h_x = max(data[i][0], data[j][0]);
            int h_y = max(data[i][1], data[j][1]);
            int l_x = min(data[i][0], data[j][0]);
            int l_y = min(data[i][1], data[j][1]);
            
            //x가 같거나, y가 같거나, 사이에 쐐기가 있는 경우
            if(h_x == l_x || h_y == l_y || 0 < arr[h_x - 1][h_y - 1] - arr[h_x - 1][l_y] - arr[l_x][h_y - 1] + arr[l_x][l_y])
                continue;  
            cnt++;
        }    
    }
    return cnt;
}

 

 

 

소스 코드


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

using namespace std;

int arr[5001][5001];
int M, N;

void coordCompress(vector<vector<int>> &data){
    
    map<int, int> x_mapper;
    map<int, int> y_mapper;
    
    for(int i=0;i<data.size();i++){
        int x = data[i][0];
        int y = data[i][1];
        if(x_mapper.count(x) == 0)
            x_mapper.insert({x, true});
        if(y_mapper.count(y) == 0)
            y_mapper.insert({y, true});
    }
    
    int cnt = 0;
    for(auto &elem : x_mapper){
        int num = elem.first;
        x_mapper[num] = cnt++;
    }
    cnt = 0;
    for(auto &elem : y_mapper){
        int num = elem.first;
        y_mapper[num] = cnt++;
    }
    
    for(int i=0;i<data.size();i++){
        data[i][0] = x_mapper[data[i][0]];
        data[i][1] = y_mapper[data[i][1]];
    }
    M = x_mapper.size();
    N = y_mapper.size();
}

void make_arr(vector<vector<int>> &data){
    
    for(int i=0;i<data.size();i++){
        int x = data[i][0];
        int y = data[i][1];
        arr[x][y] = 1;
    }
    
    for(int x = 1; x < M; x++)
        arr[x][0] += arr[x - 1][0];
    for(int y = 1; y < N; y++)
        arr[0][y] += arr[0][y - 1];
    
    for(int x = 1; x < M; x++){
        for(int y = 1; y < N; y++){
            arr[x][y] += arr[x - 1][y] + arr[x][y - 1] - arr[x - 1][y - 1];
        }    
    }
}

int solve(vector<vector<int>> &data){
    
    int cnt = 0;
    
    for(int i = 0; i < data.size(); i++){
        for(int j = i + 1; j < data.size(); j++){
            
            int h_x = max(data[i][0], data[j][0]);
            int h_y = max(data[i][1], data[j][1]);
            int l_x = min(data[i][0], data[j][0]);
            int l_y = min(data[i][1], data[j][1]);
            
            //x가 같거나, y가 같거나, 사이에 쐐기가 있는 경우
            if(h_x == l_x || h_y == l_y || 0 < arr[h_x - 1][h_y - 1] - arr[h_x - 1][l_y] - arr[l_x][h_y - 1] + arr[l_x][l_y])
                continue;  
            cnt++;
        }    
    }
    return cnt;
}

// 전역 변수를 정의할 경우 함수 내에 초기화 코드를 꼭 작성해주세요.
int solution(int n, vector<vector<int>> data) {
    
    int answer = 0;
    memset(arr, 0, sizeof(arr));
    M = 0; N = 0;
    
    coordCompress(data);
    make_arr(data);
    
    answer = solve(data);
    return answer;
}

문제 설명


셔틀버스

카카오에서는 무료 셔틀버스를 운행하기 때문에 판교역에서 편하게 사무실로 올 수 있다. 카카오의 직원은 서로를 '크루'라고 부르는데, 아침마다 많은 크루들이 이 셔틀을 이용하여 출근한다.

이 문제에서는 편의를 위해 셔틀은 다음과 같은 규칙으로 운행한다고 가정하자.

  • 셔틀은 09:00부터 총 n회 t분 간격으로 역에 도착하며, 하나의 셔틀에는 최대 m명의 승객이 탈 수 있다.
  • 셔틀은 도착했을 때 도착한 순간에 대기열에 선 크루까지 포함해서 대기 순서대로 태우고 바로 출발한다. 예를 들어 09:00에 도착한 셔틀은 자리가 있다면 09:00에 줄을 선 크루도 탈 수 있다.

일찍 나와서 셔틀을 기다리는 것이 귀찮았던 콘은, 일주일간의 집요한 관찰 끝에 어떤 크루가 몇 시에 셔틀 대기열에 도착하는지 알아냈다. 콘이 셔틀을 타고 사무실로 갈 수 있는 도착 시각 중 제일 늦은 시각을 구하여라.

단, 콘은 게으르기 때문에 같은 시각에 도착한 크루 중 대기열에서 제일 뒤에 선다. 또한, 모든 크루는 잠을 자야 하므로 23:59에 집에 돌아간다. 따라서 어떤 크루도 다음날 셔틀을 타는 일은 없다.

입력 형식

셔틀 운행 횟수 n, 셔틀 운행 간격 t, 한 셔틀에 탈 수 있는 최대 크루 수 m, 크루가 대기열에 도착하는 시각을 모은 배열 timetable이 입력으로 주어진다.

  • 0 < n ≦ 10
  • 0 < t ≦ 60
  • 0 < m ≦ 45
  • timetable은 최소 길이 1이고 최대 길이 2000인 배열로, 하루 동안 크루가 대기열에 도착하는 시각이 HH:MM 형식으로 이루어져 있다.
  • 크루의 도착 시각 HH:MM은 00:01에서 23:59 사이이다.

출력 형식

콘이 무사히 셔틀을 타고 사무실로 갈 수 있는 제일 늦은 도착 시각을 출력한다. 도착 시각은 HH:MM 형식이며, 00:00에서 23:59 사이의 값이 될 수 있다.

입출력 예제

ntmtimetableanswer

1 1 5 ["08:00", "08:01", "08:02", "08:03"] "09:00"
2 10 2 ["09:10", "09:09", "08:00"] "09:09"
2 1 2 ["09:00", "09:00", "09:00", "09:00"] "08:59"
1 1 5 ["00:01", "00:01", "00:01", "00:01", "00:01"] "00:00"
1 1 1 ["23:59"] "09:00"
10 60 45 ["23:59","23:59", "23:59", "23:59", "23:59", "23:59", "23:59", "23:59", "23:59", "23:59", "23:59", "23:59", "23:59", "23:59", "23:59", "23:59"] "18:00"

 

 

 

접근 방법


가장 마지막에 타기 위해서는 가장 마지막 버스를 타면 됩니다. 이 때 2가지 경우가 있습니다.

1. 마지막 버스에 빈자리가 있는 경우

2. 마지막 버스에 빈자리가 없는 경우

 

1번의 경우 마지막 버스의 출발시간이 정답이 될 것입니다.

2번의 경우는 마지막 버스에 마지막으로 올라탄 사람보다 1분 일찍 도착한다면 마지막 버스를 탈 수 있게 될 것입니다.

 

문제 풀이

 

1. 위의 결과를 도출하기 위해 [버스에 타고있는 인원, 버스의 출발시간, 버스에 마지막으로 탄 사람의 도착시간] 3가지의 정보가 필요합니다. 이 3가지의 정보를 담고 있는 구조체를 가진 배열을 초기화 합니다.

2. 또한 timetable이 string의 형식이므로 계산을 편히 하기 위해 int로 바꾸어줄 필요가 있습니다. int형으로 바꾸어 주었다면 사람들을 도착한 순서대로 정렬을 시킵니다.  

typedef struct info{
    int cnt, start, last;
};

vector<int> table; //timetable을 int로 변환
vector<info> busTable; //버스 배열

void init(vector<string> &timetable, int n, int t, int m){
    
    //time table을 int로 변환하여 table에 입력
    for(int i=0;i<timetable.size();i++){
        string t = timetable[i];
        int h = atoi(t.substr(0,2).c_str()); 
        int m = atoi(t.substr(3,2).c_str()); 
        table.push_back(h*60 + m);
    }
    //도착한 순서대로 정렬(오름차순)
    sort(table.begin(), table.end());
    
    //버스의 정보를 담고 있는 배열 초기화
    int start = 9*60; //출발시간이 09:00이므로
    for(int i=0;i<n;i++){ //버스는 n번 운행 됨
        busTable.push_back({0, start, 0}); //탑승한 인원, 출발시간, 마지막에 탑승한 사람의 도착시간 
        start += t; //t분 간격으로 운행
    }
}

 

이러한 초기화 과정의 끝났다면 모든 승객을 버스에 배치 시켜 봅니다.

 

 사람의 도착시간이 버스의 출발시간 보다 빠르고 버스에 빈 자리가 있을 때에만 버스에 탑승이 가능합니다.

반대의 경우는 탑승 불가능 하며 다음 버스에 배치를 해야합니다.

string solve(int m, int n){
    
    int busNum = 0;
    string ans = "";
    
    for(int i=0;i<table.size();i++){
        //버스에 탈 수 있는 경우, 수용가능하며 시간이 맞을 때
        if(busTable[busNum].cnt < m && table[i] <= busTable[busNum].start){
            busTable[busNum].cnt++;
            busTable[busNum].last = table[i];
        } // 시간이 밀리거나 버스가 꽉 찾다면
        else{
            busNum++; //다음 버스
            i--;  //현재 인원이 아직 처리가 안됬기 때문임
        }
        
        //모든 버스가 운행 종료
        if(busNum >= n)
            break;
    }
    
    //마지막 버스
    info bus = busTable.back();
    //버스에 빈 자리가 있다면 버스의 출발시간, 아니라면 버스에 탑승한 마지막 인원 보다 1분 일찍
    return ans = (bus.cnt < m) ? intToStr(bus.start) : intToStr(bus.last - 1);
}

 

 

소스 코드


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

using namespace std;

typedef struct info{
    int cnt, start, last;
};

vector<int> table;
vector<info> busTable;

void init(vector<string> &timetable, int n, int t, int m){
    
    for(int i=0;i<timetable.size();i++){
        string t = timetable[i];
        int h = atoi(t.substr(0,2).c_str());
        int m = atoi(t.substr(3,2).c_str());
        table.push_back(h*60 + m);
    }
    sort(table.begin(), table.end());
    
    int start = 9*60;
    for(int i=0;i<n;i++){
        busTable.push_back({0, start, 0});
        start += t;
    }
}

string intToStr(int time){
    
    int h = time/60;
    int m = time%60;
    
    string H = to_string(h);
    string M = to_string(m);
    
    H = (H.size() == 1) ? "0" + H : H;
    M = (M.size() == 1) ? "0" + M : M;
    
    return H + ":" + M;
}

string solve(int m, int n){
    
    int busNum = 0;
    string ans = "";
    
    for(int i=0;i<table.size();i++){
        //버스에 탈 수 있는 경우, 수용가능하며 시간이 맞을 때
        if(busTable[busNum].cnt < m && table[i] <= busTable[busNum].start){
            busTable[busNum].cnt++;
            busTable[busNum].last = table[i];
        } // 시간이 밀리거나 버스가 꽉 찾다면
        else{
            busNum++;
            i--;
        }
        
        if(busNum >= n)
            break;
    }
    
    info bus = busTable.back();
    return ans = (bus.cnt < m) ? intToStr(bus.start) : intToStr(bus.last - 1);
}

string solution(int n, int t, int m, vector<string> timetable) {
    string answer = "";
    init(timetable,n,t,m);
    
    return answer = solve(m, n);
}

 

+ Recent posts