문제 설명


블록게임

프렌즈 블록이라는 신규 게임이 출시되었고, 어마어마한 상금이 걸린 이벤트 대회가 개최 되었다.

이 대회는 사람을 대신해서 플레이할 프로그램으로 참가해도 된다는 규정이 있어서, 게임 실력이 형편없는 프로도는 프로그램을 만들어서 참가하기로 결심하고 개발을 시작하였다.

프로도가 우승할 수 있도록 도와서 빠르고 정확한 프로그램을 작성해 보자.

게임규칙

아래 그림과 같이 1×1 크기의 블록을 이어 붙여 만든 3 종류의 블록을 회전해서 총 12가지 모양의 블록을 만들 수 있다.

1 x 1 크기의 정사각형으로 이루어진 N x N 크기의 보드 위에 이 블록들이 배치된 채로 게임이 시작된다. (보드 위에 놓인 블록은 회전할 수 없다). 모든 블록은 블록을 구성하는 사각형들이 정확히 보드 위의 사각형에 맞도록 놓여있으며, 선 위에 걸치거나 보드를 벗어나게 놓여있는 경우는 없다.

플레이어는 위쪽에서 1 x 1 크기의 검은 블록을 떨어뜨려 쌓을 수 있다. 검은 블록은 항상 맵의 한 칸에 꽉 차게 떨어뜨려야 하며, 줄에 걸치면 안된다.
이때, 검은 블록과 기존에 놓인 블록을 합해 속이 꽉 채워진 직사각형을 만들 수 있다면 그 블록을 없앨 수 있다.

예를 들어 검은 블록을 떨어뜨려 아래와 같이 만들 경우 주황색 블록을 없앨 수 있다.

빨간 블록을 가로막던 주황색 블록이 없어졌으므로 다음과 같이 빨간 블록도 없앨 수 있다.

그러나 다른 블록들은 검은 블록을 떨어뜨려 직사각형으로 만들 수 없기 때문에 없앨 수 없다.

따라서 위 예시에서 없앨 수 있는 블록은 최대 2개이다.

보드 위에 놓인 블록의 상태가 담긴 2차원 배열 board가 주어질 때, 검은 블록을 떨어뜨려 없앨 수 있는 블록 개수의 최댓값을 구하라.

제한사항

  • board는 블록의 상태가 들어있는 N x N 크기 2차원 배열이다.
    • N은 4 이상 50 이하다.
  • board의 각 행의 원소는 0 이상 200 이하의 자연수이다.
    • 0 은 빈 칸을 나타낸다.
    • board에 놓여있는 각 블록은 숫자를 이용해 표현한다.
    • 잘못된 블록 모양이 주어지는 경우는 없다.
    • 모양에 관계 없이 서로 다른 블록은 서로 다른 숫자로 표현된다.
    • 예를 들어 문제에 주어진 예시의 경우 다음과 같이 주어진다.

 

 

접근 방법


특별하게 사용할 알고리즘이 따로 생각나지 않는 시뮬레이션 문제였습니다.

 

접근 방법은 아래와 같습니다.

1. board의 전체에 1개씩 검은 블록을 떨어트린다.

 

2. 제거 할 수 있는 블록이 있는지 확인한다. 제거할 수 있는 블록이 있다면 제거한다.

 

3. 2에서 블록이 제거 되었다면 board에 남아있는 모든 검은 블록을 제거한다. 어떤 블록도 제거되지 않았다면 놔둔다.

 

4. 1번부터 반복 (만약 3번 연속 블록이 제거되지 않았다면 더 이상 제거할 블록이 없다는 것이므로 반복을 종료한다.)

 

int solve(vector<vector<int>> &board){
    
    int total = 0;
    int zeroCnt = 0;
    
    while(zeroCnt < 3){
        
        stackBlack(board); //1번: 검은 블록을 전체에 떨어트림
        int cnt = check(board); //2번: 제거 할 수 있는 블록을 count
        
        //3번
        if(cnt == 0) 
            zeroCnt++;
        else{
            eraseBlack(board);
            zeroCnt = 0;  
        } 
        
        total += cnt;    
    }
    
    return total;
}

 

 

1. board의 전체에 1개씩 검은 블록을 떨어트린다.

 

한 열의 가장 위 부터 확인하며 어떤 블록이 발견되면 검은 블록을 쌓습니다. 저와 같은 경우 검은 블록은 -1로 표기 하였습니다. 이 때 주의할 점은 블록이 board의 최상단까지 쌓여있다면 다음 열로 넘어가야 한다는 것입니다.

void stackBlack(vector<vector<int>> &board){
    
    for(int i=0;i<board.size();i++){ //i가 가로
        for(int j=0;j<board[i].size();j++){ //j가 높이
            if(board[j][i] != 0){
                if(j == 0) //보드의 가장 위에 블록이 있다면
                    break;
                board[j - 1][i] = -1;
                break;
            }
        }    
    }
}

 

2. 제거 할 수 있는 블록이 있는지 확인한다. 제거할 수 있는 블록이 있다면 제거한다.

 

 일단 board의 처음부터 끝까지 한 칸씩 스캔을 하다 어떤 블록이 발견되면 그 위치에서 제거할 수 있는 블록이 있는지 확인을 합니다. 이 때 제거할 수 있는 블록의 형태는 3x2또는 2x3이므로 두 가지 형태에 대해 검사를 합니다.

int check(vector<vector<int>> &board){
    
    int cnt = 0;
    
    for(int i=0;i<board.size();i++)
        for(int j=0;j<board.size();j++)
            if(board[i][j] != 0)
                if(checkOneArea(i,j,2,3,board) || checkOneArea(i,j,3,2,board)) // 2*3모양 3*2모양에 대해서만 확인하면 됨
                    cnt++;       
    
    return cnt;
}

 

 

checkOneArea()함수에서는 매개변수로 확인해야할 지점과 범위를 매개변수로 주어 제거할 수 있는 블록이 있다면 제거를 한뒤 true를 반환하고, 제거할 수 있는 블록이 없다면 false를 반환합니다.

 

 이 때 제거할 수 있는 블록의 조건으로는 모든 기본 블록은 4개로 이루어져 있기 때문에 검은블록(-1)이 무조건 2개만 포함이 되어야 한다는 것이고 검은 블록과 제거할 블록의 색 외에 다른 어떠한 색도 포함이 되면 안된다는 것입니다.

bool checkOneArea(int x, int y, int x_len, int y_len, vector<vector<int>> &board){
    
    int color = 0;
    int blackCnt = 0;
    
    //검사 범위를 벗어나는 경우
    if(board.size() < x + x_len || board.size() < y + y_len) 
        return false;
    
    for(int i = x; i < x + x_len; i++){
        for(int j = y; j < y + y_len; j++){
            if(board[i][j] == 0)
                return false;
            if(board[i][j] == -1)
                blackCnt++;
            if(board[i][j] != -1 && color == 0) //검은 블록이 아닌 블록의 색을 color에 지정
                color = board[i][j];
            if(board[i][j] != -1 && board[i][j] != color) //지정된 color와 다른 색이라면 제거 불가능
                return false;
        }    
    }
    
    if(blackCnt != 2) 
        return false;
    
    //블록 제거
    for(int i = x; i < x + x_len; i++)
        for(int j = y; j < y + y_len; j++)
            board[i][j] = 0;
    
    return true;
}

 

3번과 4번의 과정은 간단하므로 자세한 설명은 생략하겠습니다.

 

소스 코드


#include <string>
#include <vector>

using namespace std;

void stackBlack(vector<vector<int>> &board){
    
    for(int i=0;i<board.size();i++){ //i가 가로
        for(int j=0;j<board[i].size();j++){ //j가 높이
            if(board[j][i] != 0){
                if(j == 0)
                    break;
                board[j - 1][i] = -1;
                break;
            }
        }    
    }
}

bool checkOneArea(int x, int y, int x_len, int y_len, vector<vector<int>> &board){
    
    int color = 0;
    int blackCnt = 0;
    
    //검사범위를 벗어나는 경우
    if(board.size() < x + x_len || board.size() < y + y_len) 
        return false;
    
    for(int i = x; i < x + x_len; i++){
        for(int j = y; j < y + y_len; j++){
            if(board[i][j] == 0)
                return false;
            if(board[i][j] == -1)
                blackCnt++;
            if(board[i][j] != -1 && color == 0) //검은 블록이 아닌 블록의 색을 color에 지정
                color = board[i][j];
            if(board[i][j] != -1 && board[i][j] != color) //지정된 color와 다른 색이라면 제거 불가능
                return false;
        }    
    }
    
    if(blackCnt != 2) 
        return false;
    
    //블록 제거
    for(int i = x; i < x + x_len; i++)
        for(int j = y; j < y + y_len; j++)
            board[i][j] = 0;
    
    return true;
}

int check(vector<vector<int>> &board){
    
    int cnt = 0;
    
    for(int i=0;i<board.size();i++)
        for(int j=0;j<board.size();j++)
            if(board[i][j] != 0)
                if(checkOneArea(i,j,2,3,board) || checkOneArea(i,j,3,2,board)) // 2*3모양 3*2모양에 대해서만 확인하면 됨
                    cnt++;       
    
    return cnt;
}

void eraseBlack(vector<vector<int>> &board){
    
    for(int i=0;i<board.size();i++)
        for(int j=0;j<board.size();j++)
            if(board[i][j] == -1)
                board[i][j] = 0;
}

int solve(vector<vector<int>> &board){
    
    int total = 0;
    int zeroCnt = 0;
    
    while(zeroCnt < 3){
        
        stackBlack(board); //검은 블록을 전체에 떨어트림
        int cnt = check(board); //제거 할 수 있는 블록을 count
        
        if(cnt == 0)
            zeroCnt++;
        else{
            eraseBlack(board);
            zeroCnt = 0;  
        } 
        
        total += cnt;    
    }
    
    return total;
}

int solution(vector<vector<int>> board) {
    
    int answer = solve(board);
    return answer;
}

문제 설명


매칭 점수

프렌즈 대학교 조교였던 제이지는 허드렛일만 시키는 네오 학과장님의 마수에서 벗어나, 카카오에 입사하게 되었다.
평소에 관심있어하던 검색에 마침 결원이 발생하여, 검색개발팀에 편입될 수 있었고, 대망의 첫 프로젝트를 맡게 되었다.
그 프로젝트는 검색어에 가장 잘 맞는 웹페이지를 보여주기 위해 아래와 같은 규칙으로 검색어에 대한 웹페이지의 매칭점수를 계산 하는 것이었다.

  • 한 웹페이지에 대해서 기본점수, 외부 링크 수, 링크점수, 그리고 매칭점수를 구할 수 있다.
  • 한 웹페이지의 기본점수는 해당 웹페이지의 텍스트 중, 검색어가 등장하는 횟수이다. (대소문자 무시)
  • 한 웹페이지의 외부 링크 수는 해당 웹페이지에서 다른 외부 페이지로 연결된 링크의 개수이다.
  • 한 웹페이지의 링크점수는 해당 웹페이지로 링크가 걸린 다른 웹페이지의 기본점수 ÷ 외부 링크 수의 총합이다.
  • 한 웹페이지의 매칭점수는 기본점수와 링크점수의 합으로 계산한다.

예를 들어, 다음과 같이 A, B, C 세 개의 웹페이지가 있고, 검색어가 hi라고 하자.

이때 A 웹페이지의 매칭점수는 다음과 같이 계산할 수 있다.

  • 기본 점수는 각 웹페이지에서 hi가 등장한 횟수이다.
    • A,B,C 웹페이지의 기본점수는 각각 1점, 4점, 9점이다.
  • 외부 링크수는 다른 웹페이지로 링크가 걸린 개수이다.
    • A,B,C 웹페이지의 외부 링크 수는 각각 1점, 2점, 3점이다.
  • A 웹페이지로 링크가 걸린 페이지는 B와 C가 있다.
    • A 웹페이지의 링크점수는 B의 링크점수 2점(4 ÷ 2)과 C의 링크점수 3점(9 ÷ 3)을 더한 5점이 된다.
  • 그러므로, A 웹페이지의 매칭점수는 기본점수 1점 + 링크점수 5점 = 6점이 된다.

검색어 word와 웹페이지의 HTML 목록인 pages가 주어졌을 때, 매칭점수가 가장 높은 웹페이지의 index를 구하라. 만약 그런 웹페이지가 여러 개라면 그중 번호가 가장 작은 것을 구하라.

제한사항

  • pages는 HTML 형식의 웹페이지가 문자열 형태로 들어있는 배열이고, 길이는 1 이상 20 이하이다.
  • 한 웹페이지 문자열의 길이는 1 이상 1,500 이하이다.
  • 웹페이지의 index는 pages 배열의 index와 같으며 0부터 시작한다.
  • 한 웹페이지의 url은 HTML의 <head> 태그 내에 <meta> 태그의 값으로 주어진다.
  • 한 웹페이지에서 모든 외부 링크는 <a href=https://careers.kakao.com/index>의 형태를 가진다.
    • <a> 내에 다른 attribute가 주어지는 경우는 없으며 항상 href로 연결할 사이트의 url만 포함된다.
    • 위의 경우에서 해당 웹페이지는 https://careers.kakao.com/index 로 외부링크를 가지고 있다고 볼 수 있다.
  • 모든 url은 https:// 로만 시작한다.
  • 검색어 word는 하나의 영어 단어로만 주어지며 알파벳 소문자와 대문자로만 이루어져 있다.
  • word의 길이는 1 이상 12 이하이다.
  • 검색어를 찾을 때, 대소문자 구분은 무시하고 찾는다.
    • 예를들어 검색어가 blind일 때, HTML 내에 Blind라는 단어가 있거나, BLIND라는 단어가 있으면 두 경우 모두 해당된다.
  • 검색어는 단어 단위로 비교하며, 단어와 완전히 일치하는 경우에만 기본 점수에 반영한다.
    • 단어는 알파벳을 제외한 다른 모든 문자로 구분한다.
    • 예를들어 검색어가 aba 일 때, abab abababa는 단어 단위로 일치하는게 없으니, 기본 점수는 0점이 된다.
    • 만약 검색어가 aba 라면, aba@aba aba는 단어 단위로 세개가 일치하므로, 기본 점수는 3점이다.
  • 결과를 돌려줄때, 동일한 매칭점수를 가진 웹페이지가 여러 개라면 그중 index 번호가 가장 작은 것를 리턴한다
    • 즉, 웹페이지가 세개이고, 각각 매칭점수가 3,1,3 이라면 제일 적은 index 번호인 0을 리턴하면 된다.

 

접근 방법


문제를 해결하기 위해 크게 5가지 방법으로 나눌 수 있습니다.

 

1. 대문자를 모두 소문자로 전처리하기

2. 자신의 페이지 이름 파악하기

3. 페이지의 기본 점수 구하기

4. 자신의 페이지를 A, 외부로 나가는 링크를 B라 한다면 A에서 외부로 나가는 링크 B를 저장하고, B는 자신으로 들어오는 링크 A를 저장하기

5. 점수 구하기

 

일단 위의 과정을 설명하기 이전에 문제를 해결하기 위해 전역변수로 사용한 4개의 HashMap에 대해 설명하겠습니다.

1. map<int, string> name;  =>  name[페이지index] = "페이지 주소"
2. map<string, int> score;  =>  score[페이지 주소] = 기본 점수
3. map<string, vector<string>> outdegree; => outdegree["페이지 주소"] = {외부로 나가는 링크들}
4. map<string, vector<string>> indegree;  => indegree["페이지 주소"] = {자신으로 들어오는 외부 링크들}

 

 

1. 대문자를 모두 소문자로 전처리하기

 

아스키 코드 표를 사용하여 어렵지 않게 소문자로 변환할 수 있습니다.

void toSmall(string &page){
        
    for(int j=0;j<page.size();j++)
        if(65 <= int(page[j]) && int(page[j]) <= 90)
            page[j] = char(int(page[j]) + 32);       
}

 

2. 자신의 페이지 이름 파악하기

 

 string 헤더의 find함수를 적절히 사용하여 구할 수 있었습니다. 자신의 페이지 주소 뿐만 아니라 용도에 맞게 어떠한 페이지의 주소라도 파악할 수 있도록 pattern이라는 매개변수를 추가하였습니다.

만약 자신의 페이지 주소를 파악하기 위해서는 pattern = "<meta property=\"og:url\" content="가 될 것이며

외부로 링크의 페이지 주소를 파악하기 위해서는  pattern = "<a href=" 가 될 것입니다. 

(참고로 문자열 안에 "를 사용하려면 \를 붙여 사용할 수 있습니다.)

//string myNamePattern = "<meta property=\"og:url\" content=";
// string outLinkPattern = "<a href=";
string getLinks(string &page, string pattern, int &idx){
    
    string url = "";
    idx = page.find(pattern, idx);
    
    if(idx == string::npos) return url;
    else idx += pattern.size();
    
    while(page[++idx] != '"')
        url += page[idx];
    
    return url;
}

 이 함수는 찾아낸 페이지의 주소의 문자열을 반환하며, idx를 참조로 받아 동시에 페이지 주소가 끝나는 지점의 index로 갱신됩니다.

 

 

3. 기본점수 구하기

 

기본점수는 페이지 문서 내에서 해당 단어가 몇번 등장하는지 찾을 수 있습니다. 단어의 앞뒤에 영어 철자가 존재하면 안된다는 규칙이 있어 단어를 찾아낸 뒤에 단어의 시작 지점보다 한 칸 앞과 단어의 끝 지점의 한 칸 뒤를 모두 확인해야 합니다. 이 때 cnt는 페이지에서 찾은 word의 수 입니다.

int getScore(string &word, string &page){
    
    int idx = page.find(word);
    int cnt = 0;
    
    while(page.find(word, idx) != string::npos){ //단어를 찾을 수 있는 동안에
        
        bool isWord = true;
        idx = page.find(word, idx);
        
        //단어의 앞 부분 확인
        if(0 <= idx - 1 && idx - 1 < page.size() && 'a' <= page[idx - 1] && page[idx - 1] <= 'z')    
            isWord = false;
        //단어의 뒷 부분 확인
        if(0 <= idx + word.size() && idx + word.size() < page.size() && 'a' <= page[idx + word.size()] && page[idx + word.size()] <= 'z')    
            isWord = false;
        if(isWord)
            cnt++;
        idx += word.size();
    }
    return cnt;
}

 

 

4. 자신의 페이지를 A, 외부로 나가는 링크를 B라 한다면 A에서 외부로 나가는 링크 B를 저장하고, B는 자신으로 들어오는 링크 A를 저장하기

 

2번에서 설명한 getLinks 함수를 사용하여 while문으로 문서의 끝까지 외부로 나가는 링크가 있는지 탐색을 하여 hashmap인 indegree와 outdegree에 등록합니다.

pos = 0;
while(pos != string::npos){

	string link = getLinks(page, outLinkPattern, pos);
	if(link.size() == 0)
		break;

	outdegree[myName].push_back(link);
	indegree[link].push_back(myName);
}

 

5. 점수 구하기

 

이제 점수를 구하기 위한 모든 정보를 저장했습니다. 매칭 점수는 아래의 수식으로 구할 수 있습니다.

매칭 점수 = 자신의 기본점수 + (외부로 나가는 링크들의 '링크 점수')

링크 점수 = (링크의 기본점수/링크의 외부로 나가는 링크)

(참고로 여기서 자료형을 float을 사용하면 통과하지 못하는 테스트 케이스가 있었습니다.)

double getMatchScore(int idx){
    
    string myName = name[idx];
    
    double baseScore = double(score[myName]);
    
    double linkScore = 0;
    vector<string> inLinks = indegree[myName];
    for(int i=0;i<inLinks.size();i++){
        string link = inLinks[i];
        linkScore += (double(score[link]) / double(outdegree[link].size()));
    }
    return baseScore + linkScore;
}

 

 

소스 코드


#include <string>
#include <vector>
#include <map>

using namespace std;

map<int, string> name;
map<string, int> score;
map<string, vector<string>> outdegree;
map<string, vector<string>> indegree;

void toSmall(string &page){
        
    for(int j=0;j<page.size();j++)
        if(65 <= int(page[j]) && int(page[j]) <= 90)
            page[j] = char(int(page[j]) + 32);       
}

string getLinks(string &page, string pattern, int &idx){
    
    string url = "";
    idx = page.find(pattern, idx);
    
    if(idx == string::npos) return url;
    else idx += pattern.size();
    
    while(page[++idx] != '"')
        url += page[idx];
    
    return url;
}

int getScore(string &word, string &page){
    
    int idx = page.find(word);
    int cnt = 0;
    
    while(page.find(word, idx) != string::npos){
        
        bool isWord = true;
        idx = page.find(word, idx);
        
        if(0 <= idx - 1 && idx - 1 < page.size() && 'a' <= page[idx - 1] && page[idx - 1] <= 'z')    
            isWord = false;
        if(0 <= idx + word.size() && idx + word.size() < page.size() && 'a' <= page[idx + word.size()] && page[idx + word.size()] <= 'z')    
            isWord = false;
        if(isWord)
            cnt++;
        idx += word.size();
    }
    return cnt;
}
    
void processing(string &word, string &page, int idx){
    
    string myNamePattern = "<meta property=\"og:url\" content=";
    string outLinkPattern = "<a href=";
    
    int pos = 0;
    string myName = getLinks(page, myNamePattern, pos); //함수 내부에 참조연산자를 사용하여 pos가 변할 수 있음
    name.insert({idx, myName});
    
    int cnt = getScore(word, page);
    score.insert({myName, cnt});
    
    pos = 0;
    while(pos != string::npos){
        
        string link = getLinks(page, outLinkPattern, pos);
        if(link.size() == 0)
            break;
        
        outdegree[myName].push_back(link);
        indegree[link].push_back(myName);
    }
}

double getMatchScore(int idx){
    
    string myName = name[idx];
    
    double baseScore = double(score[myName]);
    
    double linkScore = 0;
    vector<string> inLinks = indegree[myName];
    for(int i=0;i<inLinks.size();i++){
        string link = inLinks[i];
        linkScore += (double(score[link]) / double(outdegree[link].size()));
    }
    return baseScore + linkScore;
}

int solution(string word, vector<string> pages) {
    
    int answer = 0;
    
    toSmall(word);
    for(int i=0;i<pages.size();i++)
        toSmall(pages[i]);
    
    for(int i=0;i<pages.size();i++)
        processing(word, pages[i], i);
    
    double maxScore = 0;
    for(int i=0;i<pages.size();i++){
        double nowScore = getMatchScore(i);
        if(maxScore < nowScore){
            maxScore = nowScore;
            answer = i;
        }
    }
    
    return answer;
}

문제 설명


길 찾기 게임

전무로 승진한 라이언은 기분이 너무 좋아 프렌즈를 이끌고 특별 휴가를 가기로 했다.
내친김에 여행 계획까지 구상하던 라이언은 재미있는 게임을 생각해냈고 역시 전무로 승진할만한 인재라고 스스로에게 감탄했다.

라이언이 구상한(그리고 아마도 라이언만 즐거울만한) 게임은, 카카오 프렌즈를 두 팀으로 나누고, 각 팀이 같은 곳을 다른 순서로 방문하도록 해서 먼저 순회를 마친 팀이 승리하는 것이다.

그냥 지도를 주고 게임을 시작하면 재미가 덜해지므로, 라이언은 방문할 곳의 2차원 좌표 값을 구하고 각 장소를 이진트리의 노드가 되도록 구성한 후, 순회 방법을 힌트로 주어 각 팀이 스스로 경로를 찾도록 할 계획이다.

라이언은 아래와 같은 특별한 규칙으로 트리 노드들을 구성한다.

  • 트리를 구성하는 모든 노드의 x, y 좌표 값은 정수이다.
  • 모든 노드는 서로 다른 x값을 가진다.
  • 같은 레벨(level)에 있는 노드는 같은 y 좌표를 가진다.
  • 자식 노드의 y 값은 항상 부모 노드보다 작다.
  • 임의의 노드 V의 왼쪽 서브 트리(left subtree)에 있는 모든 노드의 x값은 V의 x값보다 작다.
  • 임의의 노드 V의 오른쪽 서브 트리(right subtree)에 있는 모든 노드의 x값은 V의 x값보다 크다.

아래 예시를 확인해보자.

라이언의 규칙에 맞게 이진트리의 노드만 좌표 평면에 그리면 다음과 같다. (이진트리의 각 노드에는 1부터 N까지 순서대로 번호가 붙어있다.)

이제, 노드를 잇는 간선(edge)을 모두 그리면 아래와 같은 모양이 된다.

위 이진트리에서 전위 순회(preorder), 후위 순회(postorder)를 한 결과는 다음과 같고, 이것은 각 팀이 방문해야 할 순서를 의미한다.

  • 전위 순회 : 7, 4, 6, 9, 1, 8, 5, 2, 3
  • 후위 순회 : 9, 6, 5, 8, 1, 4, 3, 2, 7

다행히 두 팀 모두 머리를 모아 분석한 끝에 라이언의 의도를 간신히 알아차렸다.

그러나 여전히 문제는 남아있다. 노드의 수가 예시처럼 적다면 쉽게 해결할 수 있겠지만, 예상대로 라이언은 그렇게 할 생각이 전혀 없었다.

이제 당신이 나설 때가 되었다.

곤경에 빠진 카카오 프렌즈를 위해 이진트리를 구성하는 노드들의 좌표가 담긴 배열 nodeinfo가 매개변수로 주어질 때,
노드들로 구성된 이진트리를 전위 순회, 후위 순회한 결과를 2차원 배열에 순서대로 담아 return 하도록 solution 함수를 완성하자.

제한사항

  • nodeinfo는 이진트리를 구성하는 각 노드의 좌표가 1번 노드부터 순서대로 들어있는 2차원 배열이다.
    • nodeinfo의 길이는 1 이상 10,000 이하이다.
    • nodeinfo[i] 는 i + 1번 노드의 좌표이며, [x축 좌표, y축 좌표] 순으로 들어있다.
    • 모든 노드의 좌표 값은 0 이상 100,000 이하인 정수이다.
    • 트리의 깊이가 1,000 이하인 경우만 입력으로 주어진다.
    • 모든 노드의 좌표는 문제에 주어진 규칙을 따르며, 잘못된 노드 위치가 주어지는 경우는 없다.

 

접근 방법


 제가 해결한 방법은 재귀함수를 통해 매개변수로 자식노드가 존재할 수 있는 x의 범위를 주었으며, 자식노드가 될 수 있는 후보를 선택하여 매개변수로 주어진 x의 범위안에 있는지 확인하며 트리를 구성하는 것입니다.

 

 왼쪽의 경계 값을 start, 오른쪽의 경계 값을 end, 현재 노드의 x좌표를 now_x라 가정합니다.

위의 그림에서 루트노드의 경우 start = -1 < now_x = 8 < end = 100001을 만족하므로 문제가 없습니다. 

이 상태에서 자식노드를 찾아 재귀함수를 진행하는 방법은 5가지의 순서로 구분지을 수 있습니다.

 

1. leftchild를 찾는 방법은 자신보다 y가 낮은 것 중에 가장 큰 y를 찾습니다. 

 

2. 1에서 찾은 y의 값을 갖는 것중 now_x보다 작은 x의 값을 가지면서 가장 큰 x를 갖는 노드가 후보가 될 수 있습니다.

이 때 찾아낸 후보 노드가 start ~ now_x사이의 x값을 가지고 있다면 그 후보 노드가 leftchild가 됩니다.

 

3. 2에서 leftchild를 찾았다면 end를 now_x로 변경하고, now = leftchild로 하여 재귀함수를 진행 시킵니다.

 

4. rightchild는 1에서 찾은 y의 값을 갖는 것중 now_x보다 큰 x의 값을 가지면서 가장 작은 x를 갖는 노드가 후보가 될 수 있습니다. 이 때 찾아낸 후보 노드가 now_x ~ end사이의 x값을 가지고 있다면 그 후보 노드가 rightchild가 됩니다.

 

5. 4에서 rightchild를 찾았다면 start를 now_x로 변경하고, now = rightchild로 하여 재귀함수를 진행시킵니다.

 

 

위의 방식을 진행시키기 이전에 문제에서 데이터를 전처리하여 treeinfo라는 벡터에 저장하였습니다.

typedef struct info{
    int x, num;
};

vector<info> treeinfo[100001];

treeinfo는 위와 같은 자료형을 가지며 treeinfo[y] = {x, 노드번호}를 의미합니다.

 

treeinfo는 높이에 따른 x와 노드번호를 가지며 x를 기준으로 오름차순으로 정렬하였습니다.

int init(vector<vector<int>> &nodeinfo){
   
    int maxh = -1;
    
    for(int i=0;i<nodeinfo.size();i++){
        treeinfo[nodeinfo[i][1]].push_back({nodeinfo[i][0], i+1});
        maxh = max(maxh, nodeinfo[i][1]);
    }
    for(int i=0;i<100001;i++)
        sort(treeinfo[i].begin(), treeinfo[i].end(), comp);
    
    return maxh;
}

maxh는 가장 상단의 노드의 y값이며 이를 구한 이유는 루트노드를 알아내기 위함입니다.

 

 

여기까지 이해가 되었다면 위에 설명한 1~5의 순서에 대해 소스코드로 설명하겠습니다. 

void makeTree(int start, int end, int now, vector<vector<int>> &nodeinfo){
    
    int now_x = nodeinfo[now - 1][0];
    int now_y = nodeinfo[now - 1][1];
    int next_y = findNextHeight(now_y); //1번 과정
    
    if(next_y < 0)
        return;
    
    int leftidx = findLeft(now_x, next_y);  //2번 과정
    int rightidx = findRight(now_x, next_y); //4번 과정
    
    if(start < treeinfo[next_y][leftidx].x && treeinfo[next_y][leftidx].x < now_x){ //3번 과정
        tree[now].left = treeinfo[next_y][leftidx].num; //leftchild연결
        makeTree(start, now_x, tree[now].left, nodeinfo);
    }    
    if(now_x < treeinfo[next_y][rightidx].x && treeinfo[next_y][rightidx].x < end){ //5번 과정
        tree[now].right = treeinfo[next_y][rightidx].num; //rightchild연결
        makeTree(now_x, end, tree[now].right, nodeinfo);
    }    
}

 

1번 과정에서 사용한 findNextHeight는 자신보다 높이가 작으면서 가장 큰 높이를 반환합니다.

//자신의 높이에서 1씩 내려가며 노드가 존재하는지 확인
int findNextHeight(int now){
    
    while(--now >= 0)
        if(treeinfo[now].size() > 0)
            break;
    return now;
}

 

2번과 4번 과정은 이분탐색을 응용하여 함수를 만들었습니다.

int findLeft(int val, int y){
    
    int start = 0;
    int end = treeinfo[y].size() - 1;
    
    while(start <= end){
        int mid = (start + end) / 2;
        if(treeinfo[y][mid].x >= val)
            end = mid - 1;
        else
            start = mid + 1;
    }
    return end = (end == -1) ? 0 : end;
}

int findRight(int val, int y){
    
    int start = 0;
    int end = treeinfo[y].size() - 1;
    
    while(start < end){
        int mid = (start + end) / 2;
        if(treeinfo[y][mid].x < val)
            start = mid + 1;
        else
            end = mid;
    }
    return end;
}

 

 1~5의 과정을 완료했다면 트리가 인접 리스트의 형태로 구성되었을 것입니다. 그렇다면 마지막으로 전위순회와 후위순회를 하여 정답을 구할 수 있습니다.

 

소스 코드


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

using namespace std;

typedef struct info{
    int x, num;
};
typedef struct child{
    int left, right;
};

vector<info> treeinfo[100001];
child tree[10001];

bool comp(info &a, info &b){
    return a.x < b.x;
}

int init(vector<vector<int>> &nodeinfo){
   
    int maxh = -1;
    
    for(int i=0;i<nodeinfo.size();i++){
        treeinfo[nodeinfo[i][1]].push_back({nodeinfo[i][0], i+1});
        maxh = max(maxh, nodeinfo[i][1]);
    }
    for(int i=0;i<100001;i++)
        sort(treeinfo[i].begin(), treeinfo[i].end(), comp);
    
    return maxh;
}

int findNextHeight(int now){
    
    while(--now >= 0)
        if(treeinfo[now].size() > 0)
            break;
    return now;
}

int findRight(int val, int y){
    
    int start = 0;
    int end = treeinfo[y].size() - 1;
    
    while(start < end){
        int mid = (start + end) / 2;
        if(treeinfo[y][mid].x < val)
            start = mid + 1;
        else
            end = mid;
    }
    return end;
}

int findLeft(int val, int y){
    
    int start = 0;
    int end = treeinfo[y].size() - 1;
    
    while(start <= end){
        int mid = (start + end) / 2;
        if(treeinfo[y][mid].x >= val)
            end = mid - 1;
        else
            start = mid + 1;
    }
    return end = (end == -1) ? 0 : end;
}

void makeTree(int start, int end, int now, vector<vector<int>> &nodeinfo){
    
    int now_x = nodeinfo[now - 1][0];
    int now_y = nodeinfo[now - 1][1];
    int next_y = findNextHeight(now_y);
    
    if(next_y < 0)
        return;
    
    int leftidx = findLeft(now_x, next_y);
    int rightidx = findRight(now_x, next_y);
    
    if(start < treeinfo[next_y][leftidx].x && treeinfo[next_y][leftidx].x < now_x){
        tree[now].left = treeinfo[next_y][leftidx].num;
        makeTree(start, now_x, tree[now].left, nodeinfo);
    }
    if(now_x < treeinfo[next_y][rightidx].x && treeinfo[next_y][rightidx].x < end){
        tree[now].right = treeinfo[next_y][rightidx].num;
        makeTree(now_x, end, tree[now].right, nodeinfo);
    }        
}

void preorder(int now, vector<int> &v){
    
    v.push_back(now);
    if(tree[now].left != 0)
        preorder(tree[now].left, v);
    if(tree[now].right != 0)
        preorder(tree[now].right, v);
}

void postorder(int now, vector<int> &v){
    
    if(tree[now].left != 0)
        postorder(tree[now].left, v);
    if(tree[now].right != 0)
        postorder(tree[now].right, v);
    v.push_back(now);
}

vector<vector<int>> solution(vector<vector<int>> nodeinfo) {

    vector<vector<int>> answer;
    vector<int> pre_v, post_v;
    
    int maxh = init(nodeinfo);
    int root = treeinfo[maxh][0].num;
    
    makeTree(-1, 100001, root, nodeinfo);
    preorder(root, pre_v);
    postorder(root, post_v);
    
    answer.push_back(pre_v);
    answer.push_back(post_v);
    
    for(int i=0;i<pre_v.size();i++)
        cout << pre_v[i] << " ";
    
    return answer;
}

문제 설명


로봇개발자 무지는 한 달 앞으로 다가온 카카오배 로봇경진대회에 출품할 로봇을 준비하고 있습니다. 준비 중인 로봇은 2 x 1 크기의 로봇으로 무지는 0 1로 이루어진 N x N 크기의 지도에서 2 x 1 크기인 로봇을 움직여 (N, N) 위치까지 이동 할 수 있도록 프로그래밍을 하려고 합니다. 로봇이 이동하는 지도는 가장 왼쪽, 상단의 좌표를 (1, 1)로 하며 지도 내에 표시된 숫자 0은 빈칸을 1은 벽을 나타냅니다. 로봇은 벽이 있는 칸 또는 지도 밖으로는 이동할 수 없습니다. 로봇은 처음에 아래 그림과 같이 좌표 (1, 1) 위치에서 가로방향으로 놓여있는 상태로 시작하며, 앞뒤 구분없이 움직일 수 있습니다.

로봇이 움직일 때는 현재 놓여있는 상태를 유지하면서 이동합니다. 예를 들어, 위 그림에서 오른쪽으로 한 칸 이동한다면 (1, 2), (1, 3) 두 칸을 차지하게 되며, 아래로 이동한다면 (2, 1), (2, 2) 두 칸을 차지하게 됩니다. 로봇이 차지하는 두 칸 중 어느 한 칸이라도 (N, N) 위치에 도착하면 됩니다.

로봇은 다음과 같이 조건에 따라 회전이 가능합니다.

위 그림과 같이 로봇은 90도씩 회전할 수 있습니다. 단, 로봇이 차지하는 두 칸 중, 어느 칸이든 축이 될 수 있지만, 회전하는 방향(축이 되는 칸으로부터 대각선 방향에 있는 칸)에는 벽이 없어야 합니다. 로봇이 한 칸 이동하거나 90도 회전하는 데는 걸리는 시간은 정확히 1초 입니다.

0 1로 이루어진 지도인 board가 주어질 때, 로봇이 (N, N) 위치까지 이동하는데 필요한 최소 시간을 return 하도록 solution 함수를 완성해주세요.

제한사항

  • board의 한 변의 길이는 5 이상 100 이하입니다.
  • board의 원소는 0 또는 1입니다.
  • 로봇이 처음에 놓여 있는 칸 (1, 1), (1, 2)는 항상 0으로 주어집니다.
  • 로봇이 항상 목적지에 도착할 수 있는 경우만 입력으로 주어집니다.

입출력 예

boardresult

[[0, 0, 0, 1, 1],[0, 0, 0, 1, 0],[0, 1, 0, 1, 1],[1, 1, 0, 0, 1],[0, 0, 0, 0, 0]] 7

입출력 예에 대한 설명

문제에 주어진 예시와 같습니다.
로봇이 오른쪽으로 한 칸 이동 후, (1, 3) 칸을 축으로 반시계 방향으로 90도 회전합니다. 다시, 아래쪽으로 3칸 이동하면 로봇은 (4, 3), (5, 3) 두 칸을 차지하게 됩니다. 이제 (5, 3)을 축으로 시계 방향으로 90도 회전 후, 오른쪽으로 한 칸 이동하면 (N, N)에 도착합니다. 따라서 목적지에 도달하기까지 최소 7초가 걸립니다.

 

 

접근 방법


 출발 지점으로 부터 도착지까지 최소 이동횟수를 구하는 문제로 BFS를 사용하여 구할 수 있습니다. 이 때 한 턴에서 이동하는 방법은 상하좌우 4가지,  회전하는 경우 4가지로 총 8가지의 방법이 있습니다.

 

1. BFS를 사용하기 위해 방문여부를 체크해야 하는데 이 때 visited[x][y][x][y]의 4차원 배열로 확인을 하였고, 로봇은 2개의 좌표로 이루어져 있기 때문에 visited[x1][y1][x2][y2], visited[x2][y2][x1][y1]을 같은 경우의 수로 생각해야 합니다. 

 

2. 로봇이 상하좌우로 이동할 때에는 이동하려는 곳에 벽이 있는지 범위를 벗어나지 않았는지에 대해 확인을 하면 어렵지 않게 구현을 할 수 있습니다.

 

3. 로봇이 회전을 하는 경우에 대해서는 조금 생각을 해야 하는데, 2번에서의 조건을 만족하면서 회전 동작을 진행 할 때 방해하는 벽이 있는지 먼저 확인을 해야합니다.

 

저와 같은 경우는 아래 그림과 같이 구현을 하였습니다.

 

만약 아래의 그림과 같이 로봇을 회전 시키고 싶다면 로봇아래 두칸(검은 선이 그려진 부분)에 벽이 있는지 확인을 합니다.

확인을 한 이후에 벽이 없다면 회전을 시키면 됩니다. 자세한 내용은 아래 소스코드에 주석으로 달아 놓았습니다.

 //dir이 4~7일 때는 회전에 대한 동작이며 그에 해당하는 경우에 대한 논리
 //coord nextDir[4] = {{1,0}, {0,1}, {-1,0}, {0,-1}};
 //tmp는 robot의 상태로 초기화 했으며 회전한 상태를 return하기 위한 변수
 //state가 true면 로봇이 가로로 놓여있는 상태, false면 새로로 놓여있는 상태
 bool state = (tmp[0].x == tmp[1].x) ? true:false; 
 	int dirtmp = dir - 4; //dir의 값을 미리 저장

	if(state) dir = (dir < 6) ? 0 : 2;
	else dir = (dir < 6) ? 1 : 3;
	
    //로봇은 좌표 2칸을 차지하기 때문에 robot.size()인 2개의 좌표를 모두 검사해야함
	for(int i=0;i<robot.size();i++){
        tmp[i].x = robot[i].x + nextDir[dir].x;
        tmp[i].y = robot[i].y + nextDir[dir].y;
        //범위를 벗어나거나 로봇이 회전하는데 방해하는 벽이 있다면
        if(!inRange(tmp[i]) || map[tmp[i].x][tmp[i].y]) 
        	return robot;    
    }

//회전을 할 수 있다면 방향에 맞게 회전
if(dirtmp == 0 || dirtmp == 2) tmp[0] = robot[1];
else tmp[1] = robot[0];

 

 

 

소스 코드


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

using namespace std;

typedef struct coord{
    int x, y;
};

coord nextDir[4] = {{1,0}, {0,1}, {-1,0}, {0,-1}};
bool visited[101][101][101][101];
vector<vector<int>> map;
int N;

bool inRange(coord &xy){
    return (0 <= xy.x && xy.x < N && 0 <= xy.y && xy.y < N);
}

bool isVisited(vector<coord> &robot){
    return (visited[robot[0].x][robot[0].y][robot[1].x][robot[1].y] || visited[robot[1].x][robot[1].y][robot[0].x][robot[0].y]);
}

bool isDest(vector<coord> &robot){
    for(int i=0;i<robot.size();i++)
        if(robot[i].x == N - 1 && robot[i].y == N - 1)
            return true;
    return false;
}

vector<coord> nextState(vector<coord> robot, int dir){
    
    vector<coord> tmp = robot;
    
    if(dir < 4){
        for(int i=0;i<robot.size();i++){
            tmp[i].x = robot[i].x + nextDir[dir].x;
            tmp[i].y = robot[i].y + nextDir[dir].y;
            if(!inRange(tmp[i]) || map[tmp[i].x][tmp[i].y]) //범위를 벗어나거나 벽이라면
                return robot;
        }
    }
    else{
        //state가 true면 가로로 놓여있는 상태, false면 새로로 놓여있는 상태
        bool state = (tmp[0].x == tmp[1].x) ? true:false; 
        int dirtmp = dir - 4;
        
        if(state) dir = (dir < 6) ? 0 : 2;
        else dir = (dir < 6) ? 1 : 3;
        
        for(int i=0;i<robot.size();i++){
            tmp[i].x = robot[i].x + nextDir[dir].x;
            tmp[i].y = robot[i].y + nextDir[dir].y;
            if(!inRange(tmp[i]) || map[tmp[i].x][tmp[i].y]) //범위를 벗어나거나 벽이라면
                return robot;    
        }
        
        if(dirtmp == 0 || dirtmp == 2) tmp[0] = robot[1];
        else tmp[1] = robot[0];
    }
    return tmp;
}

int BFS(){
    
    vector<coord> robot = {{0,0},{0,1}};
    queue<pair<vector<coord>, int>> q; //로봇과 이동시간
    
    q.push({robot, 0});
    visited[0][0][0][1] = true;
    visited[0][1][0][0] = true;
    
    while(!q.empty()){
        
        vector<coord> now = q.front().first;
        int cnt = q.front().second;
        q.pop();
        
        for(int i=0;i<8;i++){
        
            vector<coord> next = nextState(now, i);
            
            if(!isVisited(next)){
                if(isDest(next))
                    return cnt + 1;
                q.push({next, cnt + 1});
                visited[next[0].x][next[0].y][next[1].x][next[1].y] = true;
                visited[next[1].x][next[1].y][next[0].x][next[0].y] = true;
            }
        }
    }
    return 0;
}

int solution(vector<vector<int>> board) {
    
    N = board.size();
    map = board;
    int answer = BFS();
    return answer;
}

문제 설명


레스토랑을 운영하고 있는 스카피는 레스토랑 내부가 너무 낡아 친구들과 함께 직접 리모델링 하기로 했습니다. 레스토랑이 있는 곳은 스노우타운으로 매우 추운 지역이어서 내부 공사를 하는 도중에 주기적으로 외벽의 상태를 점검해야 할 필요가 있습니다.

레스토랑의 구조는 완전히 동그란 모양이고 외벽의 총 둘레는 n미터이며, 외벽의 몇몇 지점은 추위가 심할 경우 손상될 수도 있는 취약한 지점들이 있습니다. 따라서 내부 공사 도중에도 외벽의 취약 지점들이 손상되지 않았는 지, 주기적으로 친구들을 보내서 점검을 하기로 했습니다. 다만, 빠른 공사 진행을 위해 점검 시간을 1시간으로 제한했습니다. 친구들이 1시간 동안 이동할 수 있는 거리는 제각각이기 때문에, 최소한의 친구들을 투입해 취약 지점을 점검하고 나머지 친구들은 내부 공사를 돕도록 하려고 합니다. 편의 상 레스토랑의 정북 방향 지점을 0으로 나타내며, 취약 지점의 위치는 정북 방향 지점으로부터 시계 방향으로 떨어진 거리로 나타냅니다. 또, 친구들은 출발 지점부터 시계, 혹은 반시계 방향으로 외벽을 따라서만 이동합니다.

외벽의 길이 n, 취약 지점의 위치가 담긴 배열 weak, 각 친구가 1시간 동안 이동할 수 있는 거리가 담긴 배열 dist가 매개변수로 주어질 때, 취약 지점을 점검하기 위해 보내야 하는 친구 수의 최소값을 return 하도록 solution 함수를 완성해주세요.

제한사항

  • n은 1 이상 200 이하인 자연수입니다.
  • weak의 길이는 1 이상 15 이하입니다.
    • 서로 다른 두 취약점의 위치가 같은 경우는 주어지지 않습니다.
    • 취약 지점의 위치는 오름차순으로 정렬되어 주어집니다.
    • weak의 원소는 0 이상 n - 1 이하인 정수입니다.
  • dist의 길이는 1 이상 8 이하입니다.
    • dist의 원소는 1 이상 100 이하인 자연수입니다.
  • 친구들을 모두 투입해도 취약 지점을 전부 점검할 수 없는 경우에는 -1을 return 해주세요.

입출력 예

nweakdistresult

12 [1, 5, 6, 10] [1, 2, 3, 4] 2
12 [1, 3, 4, 9, 10] [3, 5, 7] 1

입출력 예에 대한 설명

입출력 예 #1

원형 레스토랑에서 외벽의 취약 지점의 위치는 다음과 같습니다.

친구들을 투입하는 예시 중 하나는 다음과 같습니다.

  • 4m를 이동할 수 있는 친구는 10m 지점에서 출발해 시계방향으로 돌아 1m 위치에 있는 취약 지점에서 외벽 점검을 마칩니다.
  • 2m를 이동할 수 있는 친구는 4.5m 지점에서 출발해 6.5m 지점에서 외벽 점검을 마칩니다.

그 외에 여러 방법들이 있지만, 두 명보다 적은 친구를 투입하는 방법은 없습니다. 따라서 친구를 최소 두 명 투입해야 합니다.

입출력 예 #2

원형 레스토랑에서 외벽의 취약 지점의 위치는 다음과 같습니다.

7m를 이동할 수 있는 친구가 4m 지점에서 출발해 반시계 방향으로 점검을 돌면 모든 취약 지점을 점검할 수 있습니다. 따라서 친구를 최소 한 명 투입하면 됩니다.

 

 

접근 방법


 백트래킹을 돌며 한 명씩 친구를 배치시킨 후 모든 외벽의 점검을 다했는지 확인하여 해결 할 수 있습니다. 이 때 친구들을 배치하는 순서는 최대한 1시간 내에 이동거리가 큰 친구부터 배치하는 것이 좋습니다.

 

 저는 외벽의 점검 상태를 알기 위해 vector<pair<int(번호), bool(점검여부)>> wall이라는 자료구조를 사용하였습니다.  아래의 코드로 초기화를 시킬 수 있습니다.

for (int i = 0; i < weak.size(); i++) {
	wall.push_back({weak[i], true});
}

 

백트래킹 함수를 살펴보자면 백트래킹의 종료시점은 2가지가 있습니다. 

1. 모든 벽의 점검이 완료되었을 때

2. dist(이동거리 정보에 대한 벡터)로 해결 할 수 없을 때(모든 벽 점검 불가능)

 

위의 두 조건에 걸리지 않는다면 현재 함수에 대한 처리를 합니다.

 

1. 아직 점검이 되지 않은 외벽이 있는지 찾습니다.

2. 점검이 되지 않은 외벽이 있다면 그 벽의 위치를 start라고 정의합니다.

3. 현재 이 벽을 처리해야될 친구가 idx라면 친구는 start ~ start + dist[idx] 사이에 있는 모든 벽을 점검할 수 있습니다. 

4. 이 때 end = start + dist[idx]가 n보다 크거나 같다면(끝 지점이 한 바퀴를 돌아버리는 경우) 점검해야 할 범위는 start ~ n 과 0 ~ end%n 사이가 될 것입니다.

 

 

소스 코드


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

using namespace std;

bool comp(int a, int b) {
	return a > b;
}

bool check(vector<pair<int, bool>> &wall) {

	for (int i = 0; i < wall.size(); i++) {
		if (wall[i].second)
			return false;
	}
	return true;
}

int solve(vector<pair<int, bool>> wall, vector<int> &dist, int idx, int n) {

	int ret = 101; //최대의 경우의 수가 100이기 때문에 101로 초기화

	if (check(wall)) 
		return idx;
    if(idx == dist.size())
        return ret;
    
	vector<pair<int, bool>> tmp = wall;
	for (int i = 0; i < wall.size(); i++) {
		if(wall[i].second){
            
            int start = wall[i].first;
            int end = wall[i].first + dist[idx];
            
            if(end >= n){ // 끝 지점이 한 바퀴를 넘어가는 경우
                end %= n;
                for(int j=0;j<wall.size();j++)
                    if((start <= wall[j].first && wall[j].first < n) || (0 <= wall[j].first && wall[j].first <= end))
                        wall[j].second = false;
            }
            else{ // 끝 지점이 범위 내에 있는 경우
                for(int j=0;j<wall.size();j++)
                    if(start <= wall[j].first && wall[j].first <= end)
                        wall[j].second = false;
            }
            ret = min(ret, solve(wall, dist, idx+1, n));
            wall = tmp;
        }
	}
	return ret;
}

int solution(int n, vector<int> weak, vector<int> dist) {

	int answer = 0;

	vector<pair<int, bool>> wall;
	for (int i = 0; i < weak.size(); i++) {
		wall.push_back({weak[i], true});
	}
    //이동거리를 내림차순으로 정렬 -> backTracking에서 idx를 1씩 증가시키며 참조하기 위함
	sort(dist.begin(), dist.end(), comp); 
	answer = solve(wall, dist, 0, n);
    // 101이라면 불가능한 경우
    answer = (answer == 101) ? -1 : answer;

	return answer;
}

문제 설명


유통전문회사 카카오상사의 오너인 제이지는 새로운 사업 아이템을 구상하기 위해 전문경영인(CEO)인 프로도에게 회사의 경영을 부탁하였습니다.
카카오상사는 직원들을 여러 개의 팀 단위로 조직을 구성하고 있으며 아래 그림은 CEO를 포함하여 10명의 직원과 4개의 팀으로 구성되어 있는 회사 조직도를 보여주고 있습니다.


그림의 조직도는 다음과 같이 설명할 수 있습니다.

  1. 그림의 각 원들은 각각의 직원 1명을 표시하고 있으며, CEO를 포함하여 총 10명의 직원을 표시하고 있습니다.
  2. 원 안에 적힌 두 개의 숫자는 직원의 정보를 담고 있습니다. 왼쪽 숫자는 직원번호이며 직원을 식별할 수 있도록 1번부터 순서대로 발급되는 일련번호이며, 오른쪽 숫자는 해당 직원의 하루평균 매출액을 나타냅니다. 위 그림에서 1번 직원은 14원을, 9번 직원은 28원의 하루평균 매출액을 기록하고 있습니다.
  3. CEO를 포함하여 모든 직원은 팀장 또는 팀원이라는 직위를 가지고 있으며 그림에서는 팀장과 팀원의 관계를 화살표로 표시하고 있습니다. 화살표가 시작되는 쪽의 직원은 팀장, 화살표를 받는 쪽의 직원은 팀원을 의미합니다.
    3-1. 직원번호 1번은 회사의 CEO로 고정되어 있으며, CEO는 항상 팀장이고 팀원일 수 없어 화살표를 받는 쪽이 될 수 없습니다.
    3-2. 반면에 CEO를 제외한 나머지 모든 직원들은 다른 누군가로부터 정확히 1개의 화살표를 받게 됩니다.
    3-3. 한 직원은 최대 2개의 팀에 소속될 수 있습니다. 만약 어떤 직원이 두 개의 팀에 소속되어 있다면, 반드시 하나의 팀에서는 팀장, 나머지 팀에서는 팀원이어야 합니다. 팀장을 겸임하거나, 두 개의 팀에서 팀원이 될 수는 없습니다. 예를들어 10번 직원은 D팀의 팀장이면서 동시에 5번 직원이 팀장으로 있는 C팀에 속한 팀원입니다.
    3-4. 5번, 9번, 10번 직원은 받는 쪽의 화살표와 시작하는 화살표가 모두 있으므로 팀장인 동시에 팀원입니다.
    3-5. 2번, 3번, 4번, 6번, 7번, 8번 직원은 시작하는 화살표가 없고 받는 쪽의 화살표만 있으므로 팀장이 아니며 오직 팀원입니다.
    3-6. 1번 직원인 CEO는 받는 쪽의 화살표가 없고 시작하는 화살표만 있으며 항상 팀원이 아닌 팀장입니다.
    3-7. 그림의 조직도에는 A, B, C, D 총 4개의 팀이 존재하며, 각각 1번, 9번, 5번, 10번 직원이 팀장 직위를 담당하게 됩니다.

제이지는 자신이 구상한 새로운 사업 아이템에 대해 직원들에게 설명하고자 하루 일정으로 워크숍을 계획하고 있습니다. 단, 모든 직원을 참석시킬 수 없어 아래와 같은 기준으로 워크숍에 참석할 직원들을 선발하려고 합니다.

  1. 워크숍에서 교육받은 내용은 전 직원들에게 공유되어야 하므로 모든 팀은 최소 1명 이상의 직원을 워크숍에 참석시켜야 합니다.
  2. 워크숍 기간 동안, 회사의 매출 손실을 최소화하는 것이 중요하므로 워크숍에 참석하는 직원들의 하루평균 매출액의 합이 최소가 되어야 합니다.

위 그림의 조직도에서 회색으로 색칠된 1번, 7번, 10번 직원을 워크숍에 참석시키면 모든 팀에서 최소 한 명 이상의 직원을 참석시킨 것이 되며, 해당 직원들의 하루평균 매출액의 합은 44(14+13+17)원 입니다. 10번 직원은 C팀과 D팀 모두에 속해 있으므로, 두 팀에서 모두 참석한 것으로 인정됩니다.


[문제]

직원들의 하루평균 매출액 값을 담은 배열 sales, 직원들의 팀장-팀원의 관계를 나타내는 2차원 배열 links가 매개변수로 주어집니다. 이때, 모든 팀에서 최소 한 명 이상 워크숍에 참석하면서, 참석하는 직원들의 하루평균 매출액의 합을 최소로 하려고 합니다. 그렇게 최소화된 매출액의 합을 구해서 return 하도록 solution 함수를 완성해 주세요.

[제한사항]

  • sales 배열의 크기는 2 이상 300,000 이하입니다. sales 배열의 크기는 CEO를 포함한 전체 직원 수와 같습니다.
    • sales 배열은 각 직원들의 하루평균 매출액을 담고 있으며, 1번 직원부터 직원번호 순서대로 주어집니다.
    • sales 배열의 각 원소의 값은 0 이상 10,000 이하인 정수입니다.
  • links 배열의 크기는 sales 배열의 크기 - 1 입니다. 즉, 전체 직원 수보다 1이 작습니다.
  • links 배열의 각 원소는 [a, b] 형식입니다.
    • a는 팀장의 직원번호, b는 a팀장이 관리하는 팀원의 직원번호이며, a와 b는 서로 다른 자연수입니다.
    • 1 ≤ a  sales 배열의 크기 입니다.
    • 2 ≤ b  sales 배열의 크기 입니다.
    • 직원번호 1은 CEO로 정해져 있고 CEO는 항상 팀장으므로 b ≠ 1 입니다.
    • links 배열로 만들어지는 조직도는 하나의 트리 구조 형태입니다.
  • 정답으로 return 되는 값은 231 - 1 이하인 자연수임이 보장됩니다.

[입출력 예]

saleslinksresult

[14, 17, 15, 18, 19, 14, 13, 16, 28, 17] [[10, 8], [1, 9], [9, 7], [5, 4], [1, 5], [5, 10], [10, 6], [1, 3], [10, 2]] 44
[5, 6, 5, 3, 4] [[2,3], [1,4], [2,5], [1,2]] 6
[5, 6, 5, 1, 4] [[2,3], [1,4], [2,5], [1,2]] 5
[10, 10, 1, 1] [[3,2], [4,3], [1,4]] 2

입출력 예에 대한 설명


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

입출력 예 #2
직원번호가 2인 직원 한 명을 워크숍에 참석시키는 것이 최선이며, 2번 직원의 하루평균 매출액은 6원입니다. 따라서 6을 return 해주어야 합니다.

입출력 예 #3
직원번호가 4, 5인 직원 두 명을 워크숍에 참석시키는 것이 최선이며, 4번, 5번 직원의 하루평균 매출액의 합은 5(1+4)원 입니다. 따라서 5를 return 해주어야 합니다.

입출력 예 #4
직원번호가 3, 4인 직원 두 명을 워크숍에 참석시키는 것이 최선이며, 3번, 4번 직원의 하루평균 매출액의 합은 2(1+1)원 입니다. 따라서 2를 return 해주어야 합니다.

 

 

접근 방법


트리 DP를 사용하여 해결할 수 있는 문제였습니다.

일반적으로 트리 DP는 2개의 차원을 가지며

DP[노드 번호][노드 포함 여부] = 최적해

의 구조를 가지고 있습니다.

 

문제의 풀이는 다음과 같습니다.

 

1. 문제에서 주어진 vector<vector<int>> links를 통해 트리를 구성한다.

 

links는 명확하게 부모노드에서 자식노드로 연결되는 순서를 가진 단방향의 구조입니다.

또한 문제에서 1이 최상위 노드라고 알려주었기 때문에 트리를 구성하는 것은 vector를 사용한 인접리스트로 구성을 할 수 있습니다.

void makeTree(vector<vector<int>> &links){
    
    for(int i=0; i<links.size(); i++){
        int from = links[i][0];
        int to = links[i][1];
        tree[from].push_back(to);
    }
}

 

2. 문제의 조건에 따라 DP를 채워나간다. 

 

DP 테이블을 채워나갈 때 크게 2가지 경우로 나눌 수 있습니다.

1. 자신이 포함되어 있을 때 (include == true)

2. 자신이 포함되지 않았을 때 (include == false)

 

1. 자신이 포함되어 있을 때 (include == true)

 

1번 경우에서 처리 과정은 트리 DP를 한번이라도 다뤄봤다면 어렵지 않게 생각해 낼 수 있었을 것입니다.

자신이 포함이 되어있다면 그룹 내에서 최소 한명이 포함이 되어있기 때문에 자식 노드는 포함을 시켜도 되고 안 시켜도 됩니다. 이 때 자식 노드의 번호 K에 대해 K를 포함시키는 것이 좋은지, K를 포함시키지 않는 것이 좋은지 생각하여야 합니다.

 즉 자식노드를 포함시키는 경우 dp[k][1]와 자식노드를 포함시키지 않는 경우 dp[k][0] 중 더 작은 값을 결정해야 합니다. 작은 값을 결정해야 하는 이유는 값이 작을 수록 매출액의 손해를 줄여줄 수 있기 때문이고 이것이 최적해를 만족하기 때문입니다.

 

//int &ref = dp[now][1];
if(include){ //나를 포함했다면, 하위 직원을 포함해도 되고 안해도 됨    

	long long ans = 0;

	for(int i=0;i<tree[now].size();i++){
		int next = tree[now][i];
		ans += min(solve(next, true), solve(next, false));
	}
	return ref = ans + salesInfo[now - 1];
} 

 

2. 자신이 포함되지 않았을 때 (include == false)

 

 이 경우에 대해 풀이를 생각하는 것은 쉽지 않았습니다. 

1번 경우와 마찬가지로 자식노드 k에 대해 dp[k][1]과 dp[k][0] 중 더 작은 값 만을 포함을 해야 합니다. 이 때 모든 자식노드가 참여를 하지 않는 것이 최적해 일 때 강제로 1명은 참여를 시켜야 합니다. 그렇다면 어떤 자식 노드를 참여 시킬지 결정을 해야 합니다. 

 

 결정을 하는 방법은 한 자식노드가 자신이 참여를 했을 때와 안 했을 때의 차이가 가장 적은 것이 가장 좋습니다. 차이가 가장 적은 것이 좋은 이유는 이런 상황에서는 기본적으로 모든 k에 대해 dp[k][1] > dp[k][0]일 것입니다. 즉 참여를 하는 것이 값이 크기 때문에 좋지 않은 상황이며, dp[k][1]와 dp[k][0]의 차이가 작을수록 참여를 했을 때 불참을 했을 때 보다 회사의 손실이 작아지기 때문입니다. (이 내용이 정확한 이해가 가지 않는다면 댓글 남겨주세요.)

 

 else{ //내가 포함되지 않았다면 하위 직원 중 한 명 이상은 무조건 포함이 되어야함

	long long ans = 0;
	long long diff = (tree[now].size() > 0) ? 0x7FFFFFFF : 0;
	bool flag = false; //모두가 불참여 한 것인지 확인 하기 위한 변수

	for(int i=0;i<tree[now].size();i++){

		int next = tree[now][i];
		long long case1 = solve(next, true);
		long long case2 = solve(next, false);

		//참여 케이스의 값이 작다는 것은, 참여를 했다는 것이다.
		if(case1 < case2) flag = true;
		else diff = min(diff, case1 - case2);

		ans += min(case1, case2);
	}
    //모든 자식 노드가 불참인 경우, 최소 한명은 참여를 시켜야함
    return ref = (!flag) ? ans + diff : ans;
}

 

 

소스 코드


C++

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

using namespace std;

vector<int> tree[300001];
long long dp[300001][2]; 
vector<int> salesInfo;

void makeTree(vector<vector<int>> &links){
    
    for(int i=0; i<links.size(); i++){
        int from = links[i][0];
        int to = links[i][1];
        tree[from].push_back(to);
    }
}

long long solve(int now, bool include){
    
    long long &ref = dp[now][include];
    
    if (ref != -1)
        return ref;
    
    ref = 0;
    if(include){ //나를 포함했다면, 하위 직원을 포함해도 되고 안해도 됨    

        long long ans = 0;

        for(int i=0;i<tree[now].size();i++){
            int next = tree[now][i];
            ans += min(solve(next, true), solve(next, false));
        }
        return ref = ans + salesInfo[now - 1];
    }   
    else{ //내가 포함되지 않았다면 하위 직원 중 한 명 이상은 무조건 포함이 되어야함

        long long ans = 0;
        long long diff = (tree[now].size() > 0) ? 0x7FFFFFFF : 0;
        bool flag = false;

        for(int i=0;i<tree[now].size();i++){

            int next = tree[now][i];
            long long case1 = solve(next, true);
            long long case2 = solve(next, false);

            //참여 케이스의 값이 작다는 것은, 참여를 했다는 것이다.
            if(case1 < case2) flag = true;
            else diff = min(diff, case1 - case2);

            ans += min(case1, case2);
        }
        //모든 자식 노드가 불참인 경우, 최소 한명은 참여를 시켜야함
        return ref = (!flag) ? ans + diff : ans;
    }
    
}

long long solution(vector<int> sales, vector<vector<int>> links) {
    
    long long answer = 0;
    
    salesInfo = sales;
    makeTree(links);
    memset(dp, -1, sizeof(dp));
    
    return answer = min(solve(1, false), solve(1, true));
}

 

Java

import java.util.*;

class Solution {
    
    int dp[][] = new int[300001][2];
    ArrayList<ArrayList<Integer>> info = new ArrayList();
    int salesInfo[];
    
    public void init(int[][] links, int[] sales){
        
        salesInfo = sales;
        for(int i=0;i<=sales.length;i++)
            info.add(new ArrayList<Integer>());
        for(int i=0;i<links.length;i++)
            info.get(links[i][0]).add(links[i][1]);
        for(int i=0;i<=sales.length;i++)
            dp[i][0] = dp[i][1] = -1;
    }
    
    public int solve(int now, int include){
        
        if(dp[now][include] != -1)
            return dp[now][include];
        
        dp[now][include] = 0;
        int ret = 0;
        
        if(include == 1){
            for(int i=0;i<info.get(now).size();i++){
                int next = info.get(now).get(i);
                ret += Math.min(solve(next, 1), solve(next, 0));
            }
            return dp[now][include] = (ret + salesInfo[now - 1]);
        }
        else{
            boolean flag = false;
            int diff = (info.get(now).size() > 0) ? 0x7FFFFFFF:0;
            for(int i=0;i<info.get(now).size();i++){
                int next = info.get(now).get(i);
                int case1 = solve(next, 1);
                int case2 = solve(next, 0);
                
                if(case1 < case2) flag = true;
                else diff = Math.min(case1 - case2, diff);
                
                ret += Math.min(case1, case2);
            }
            return dp[now][include] = (flag == true) ? ret:ret + diff;
        }
    }
    
    public int solution(int[] sales, int[][] links) {
        int answer = 0;
        init(links, sales);
        return answer = Math.min(solve(1, 1), solve(1, 0));
    }
}

 

문제 설명


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

카카오는 하반기 경력 개발자 공개채용을 진행 중에 있으며 현재 지원서 접수와 코딩테스트가 종료되었습니다. 이번 채용에서 지원자는 지원서 작성 시 아래와 같이 4가지 항목을 반드시 선택하도록 하였습니다.

  • 코딩테스트 참여 개발언어 항목에 cpp, java, python 중 하나를 선택해야 합니다.
  • 지원 직군 항목에 backend와 frontend 중 하나를 선택해야 합니다.
  • 지원 경력구분 항목에 junior와 senior 중 하나를 선택해야 합니다.
  • 선호하는 소울푸드로 chicken과 pizza 중 하나를 선택해야 합니다.

인재영입팀에 근무하고 있는 니니즈는 코딩테스트 결과를 분석하여 채용에 참여한 개발팀들에 제공하기 위해 지원자들의 지원 조건을 선택하면 해당 조건에 맞는 지원자가 몇 명인 지 쉽게 알 수 있는 도구를 만들고 있습니다.
예를 들어, 개발팀에서 궁금해하는 문의사항은 다음과 같은 형태가 될 수 있습니다.
코딩테스트에 java로 참여했으며, backend 직군을 선택했고, junior 경력이면서, 소울푸드로 pizza를 선택한 사람 중 코딩테스트 점수를 50점 이상 받은 지원자는 몇 명인가?

물론 이 외에도 각 개발팀의 상황에 따라 아래와 같이 다양한 형태의 문의가 있을 수 있습니다.

  • 코딩테스트에 python으로 참여했으며, frontend 직군을 선택했고, senior 경력이면서, 소울푸드로 chicken을 선택한 사람 중 코딩테스트 점수를 100점 이상 받은 사람은 모두 몇 명인가?
  • 코딩테스트에 cpp로 참여했으며, senior 경력이면서, 소울푸드로 pizza를 선택한 사람 중 코딩테스트 점수를 100점 이상 받은 사람은 모두 몇 명인가?
  • backend 직군을 선택했고, senior 경력이면서 코딩테스트 점수를 200점 이상 받은 사람은 모두 몇 명인가?
  • 소울푸드로 chicken을 선택한 사람 중 코딩테스트 점수를 250점 이상 받은 사람은 모두 몇 명인가?
  • 코딩테스트 점수를 150점 이상 받은 사람은 모두 몇 명인가?

즉, 개발팀에서 궁금해하는 내용은 다음과 같은 형태를 갖습니다.

* [조건]을 만족하는 사람 중 코딩테스트 점수를 X점 이상 받은 사람은 모두 몇 명인가?


[문제]

지원자가 지원서에 입력한 4가지의 정보와 획득한 코딩테스트 점수를 하나의 문자열로 구성한 값의 배열 info, 개발팀이 궁금해하는 문의조건이 문자열 형태로 담긴 배열 query가 매개변수로 주어질 때,
각 문의조건에 해당하는 사람들의 숫자를 순서대로 배열에 담아 return 하도록 solution 함수를 완성해 주세요.

[제한사항]

  • info 배열의 크기는 1 이상 50,000 이하입니다.
  • info 배열 각 원소의 값은 지원자가 지원서에 입력한 4가지 값과 코딩테스트 점수를 합친 개발언어 직군 경력 소울푸드 점수 형식입니다.
    • 개발언어는 cpp, java, python 중 하나입니다.
    • 직군은 backend, frontend 중 하나입니다.
    • 경력은 junior, senior 중 하나입니다.
    • 소울푸드는 chicken, pizza 중 하나입니다.
    • 점수는 코딩테스트 점수를 의미하며, 1 이상 100,000 이하인 자연수입니다.
    • 각 단어는 공백문자(스페이스 바) 하나로 구분되어 있습니다.
  • query 배열의 크기는 1 이상 100,000 이하입니다.
  • query의 각 문자열은 [조건] X 형식입니다.
    • [조건]은 개발언어 and 직군 and 경력 and 소울푸드 형식의 문자열입니다.
    • 언어는 cpp, java, python, - 중 하나입니다.
    • 직군은 backend, frontend, - 중 하나입니다.
    • 경력은 junior, senior, - 중 하나입니다.
    • 소울푸드는 chicken, pizza, - 중 하나입니다.
    • '-' 표시는 해당 조건을 고려하지 않겠다는 의미입니다.
    • X는 코딩테스트 점수를 의미하며 조건을 만족하는 사람 중 X점 이상 받은 사람은 모두 몇 명인 지를 의미합니다.
    • 각 단어는 공백문자(스페이스 바) 하나로 구분되어 있습니다.
    • 예를 들면, cpp and - and senior and pizza 500은 cpp로 코딩테스트를 봤으며, 경력은 senior 이면서 소울푸드로 pizza를 선택한 지원자 중 코딩테스트 점수를 500점 이상 받은 사람은 모두 몇 명인가?를 의미합니다.

[입출력 예]

infoqueryresult

["java backend junior pizza 150","python frontend senior chicken 210","python frontend senior chicken 150","cpp backend senior pizza 260","java backend junior chicken 80","python backend senior chicken 50"] ["java and backend and junior and pizza 100","python and frontend and senior and chicken 200","cpp and - and senior and pizza 250","- and backend and senior and - 150","- and - and - and chicken 100","- and - and - and - 150"] [1,1,1,1,2,4]

입출력 예에 대한 설명

지원자 정보를 표로 나타내면 다음과 같습니다.

언어직군경력소울 푸드점수

java backend junior pizza 150
python frontend senior chicken 210
python frontend senior chicken 150
cpp backend senior pizza 260
java backend junior chicken 80
python backend senior chicken 50
  • "java and backend and junior and pizza 100" : java로 코딩테스트를 봤으며, backend 직군을 선택했고 junior 경력이면서 소울푸드로 pizza를 선택한 지원자 중 코딩테스트 점수를 100점 이상 받은 지원자는 1명 입니다.
  • "python and frontend and senior and chicken 200" : python으로 코딩테스트를 봤으며, frontend 직군을 선택했고, senior 경력이면서 소울 푸드로 chicken을 선택한 지원자 중 코딩테스트 점수를 200점 이상 받은 지원자는 1명 입니다.
  • "cpp and - and senior and pizza 250" : cpp로 코딩테스트를 봤으며, senior 경력이면서 소울푸드로 pizza를 선택한 지원자 중 코딩테스트 점수를 250점 이상 받은 지원자는 1명 입니다.
  • "- and backend and senior and - 150" : backend 직군을 선택했고, senior 경력인 지원자 중 코딩테스트 점수를 150점 이상 받은 지원자는 1명 입니다.
  • "- and - and - and chicken 100" : 소울푸드로 chicken을 선택한 지원자 중 코딩테스트 점수를 100점 이상을 받은 지원자는 2명 입니다.
  • "- and - and - and - 150" : 코딩테스트 점수를 150점 이상 받은 지원자는 4명 입니다.

 

접근 방법


자료구조를 잘 설계하고 이분탐색을 활용하여 풀 수 있는 문제 였습니다.

 

1. 자료를 담을 수 있는 배열(vector<int> db[3][2][2][2] )을 4차원으로 선언합니다.

배열의 값으로는 4차원에 따른 점수의 값이 vector의 요소로 들어가게 됩니다.

 

1차원은 코딩테스트 참여 개발항목 언어인 cpp(0), java(1), python(2)로 3가지를 구분짓습니다.

2차원은 지원 직군 항목인 backend(0), frontend(1)로 2가지를 구분짓습니다.

3차원은 지원 경력구분인 junior(0), senior(1)로 2가지를 구분짓습니다.

4차원은 선호하는 소울푸드인 chicken(0), pizza(1)로 2가지를 구분짓습니다.

 

2. 구문분석을 할 수 있는 기능의 함수를 만들어 줍니다.

함수의 return값은 1번의 db[3][2][2][2]에서의 값을 조회하거나 삽입할 때 사용할 수 있는 index의 pair<처음, 끝>입니다.

vector<pair<int, int>> processing(vector<string> &v){
    
    pair<int, int> lang,job,career,food;
    
    if(v[0].compare("cpp") == 0) lang = {0,0};
    else if(v[0].compare("java") == 0) lang = {1,1};
    else if(v[0].compare("python") == 0) lang = {2,2};
    else if(v[0].compare("-") == 0) lang = {0,2};
    
    if(v[1].compare("backend") == 0) job = {0, 0};
    else if(v[1].compare("frontend") == 0) job = {1, 1};
    else if(v[1].compare("-") == 0) job = {0, 1};
    
    if(v[2].compare("junior") == 0) career = {0, 0};
    else if(v[2].compare("senior") == 0) career = {1, 1};
    else if(v[2].compare("-") == 0) career = {0, 1};
    
    if(v[3].compare("chicken") == 0) food = {0, 0};
    else if(v[3].compare("pizza") == 0) food = {1, 1};
    else if(v[3].compare("-") == 0) food = {0, 1};
    
    return {lang, job, career, food};
}

 

함수의 입력인자로 들어온 vector<string> &v는 strtok를 사용하여 만든 tokenizing()함수에서 문제의 입력인 info를 구분지어 저장한 vector입니다.

vector<string> tokenizing(string str){
    
    vector<string> v;
    char *buffer = new char[100];
	strcpy(buffer, str.c_str());
    
    char *ptr = strtok(buffer, " ");    
 
    while (ptr != NULL){
        //문자열이 and인 경우 pass
        if(string(ptr).compare("and") == 0){
            ptr = strtok(NULL, " ");
            continue;
        }
        v.push_back(string(ptr));        
        ptr = strtok(NULL, " ");
    }

    return v;
}

 

3. 배열의 각각 요소인 vector를 오름차순으로 정리 합니다.

오름차순으로 정리하는 이유는 추후에 query로 들어온 점수의 lower_bound를 이분탐색으로 찾아낼 수 있고, 이분탐색으로 찾아낸 lower_bound의 index를 통해 쉽게 query의 점수보다 크거나 같은 인원의 수를 계산할 수 있기 때문입니다.

 

 

4.  query를 2번에서 언급한 tokenizing()함수를 통해 vector로 변환 후, processing()함수를 통해 조회 해야 하는 구간을 알아내어 정답을 구합니다.

 

int search(string &query){
    
    vector<string> tokenV = tokenizing(query);
    vector<pair<int, int>> v = processing(tokenV);
    
    int num = stoi(tokenV.back());
    int cnt = 0;
    
    for(int i = v[0].first; i <= v[0].second; i++)
        for(int j = v[1].first; j <= v[1].second; j++)
            for(int k = v[2].first; k <= v[2].second; k++)
                for(int l = v[3].first; l <= v[3].second; l++){
                    if(db[i][j][k][l].size() == 0 || num > db[i][j][k][l].back()) continue;
                   	//bSearch는 lower_bound를 알아내기 위한 함수
                    cnt += (db[i][j][k][l].size() - bSearch(i,j,k,l,num)); 
                }
                    
    return cnt;
}

  bSearch는 lower_bound의 index를 알아내기 위한 이분탐색을 활용한 함수입니다.

int bSearch(int i, int j, int k, int l, int num){
    
    int start = 0;
    int end = db[i][j][k][l].size() - 1;
    
    while(start < end){
        
        int mid = (start + end)/2;
        
        if(db[i][j][k][l][mid] < num)
            start = mid + 1;
        else
            end = mid;
    }
    return end;
}

 

 

소스 코드


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

using namespace std;

vector<int> db[3][2][2][2];

void sorting(){
    
    for(int i=0;i<3;i++)
        for(int j=0;j<2;j++)
            for(int k=0;k<2;k++)
                for(int l=0;l<2;l++)
                    sort(db[i][j][k][l].begin(), db[i][j][k][l].end());
}

vector<string> tokenizing(string str){
    
    vector<string> v;
    char *buffer = new char[100];
	strcpy(buffer, str.c_str());
    
    char *ptr = strtok(buffer, " ");    
 
    while (ptr != NULL){
        
        if(string(ptr).compare("and") == 0){
            ptr = strtok(NULL, " ");
            continue;
        }
        v.push_back(string(ptr));        
        ptr = strtok(NULL, " ");
    }

    return v;
}

vector<pair<int, int>> processing(vector<string> &v){
    
    pair<int, int> lang,job,career,food;
    
    if(v[0].compare("cpp") == 0) lang = {0,0};
    else if(v[0].compare("java") == 0) lang = {1,1};
    else if(v[0].compare("python") == 0) lang = {2,2};
    else if(v[0].compare("-") == 0) lang = {0,2};
    
    if(v[1].compare("backend") == 0) job = {0, 0};
    else if(v[1].compare("frontend") == 0) job = {1, 1};
    else if(v[1].compare("-") == 0) job = {0, 1};
    
    if(v[2].compare("junior") == 0) career = {0, 0};
    else if(v[2].compare("senior") == 0) career = {1, 1};
    else if(v[2].compare("-") == 0) career = {0, 1};
    
    if(v[3].compare("chicken") == 0) food = {0, 0};
    else if(v[3].compare("pizza") == 0) food = {1, 1};
    else if(v[3].compare("-") == 0) food = {0, 1};
    
    return {lang, job, career, food};
}

void make_db(vector<string> &info){
    
    vector<vector<int>> data;
    for(int i=0;i<info.size();i++){
        vector<string> tokenV = tokenizing(info[i]);
        vector<pair<int, int>> v = processing(tokenV);
        db[v[0].first][v[1].first][v[2].first][v[3].first].push_back(stoi(tokenV.back()));
    }
}

int bSearch(int i, int j, int k, int l, int num){
    
    int start = 0;
    int end = db[i][j][k][l].size() - 1;
    
    while(start < end){
        
        int mid = (start + end)/2;
        
        if(db[i][j][k][l][mid] < num)
            start = mid + 1;
        else
            end = mid;
    }
    return end;
}

int search(string &query){
    
    vector<string> tokenV = tokenizing(query);
    vector<pair<int, int>> v = processing(tokenV);
    
    int num = stoi(tokenV.back());
    int cnt = 0;
    
    for(int i = v[0].first; i <= v[0].second; i++)
        for(int j = v[1].first; j <= v[1].second; j++)
            for(int k = v[2].first; k <= v[2].second; k++)
                for(int l = v[3].first; l <= v[3].second; l++){
                    if(db[i][j][k][l].size() == 0 || num > db[i][j][k][l].back()) continue;
                    cnt += (db[i][j][k][l].size() - bSearch(i,j,k,l,num));
                }
                    
    return cnt;
}

vector<int> solution(vector<string> info, vector<string> query) {
    
    vector<int> answer;
    make_db(info);
    sorting();
    
    for(int i=0;i<query.size();i++)
        answer.push_back(search(query[i]));
    
    return answer;
}

문제 설명


카카오TV에서 유명한 크리에이터로 활동 중인 죠르디는 환경 단체로부터 자신의 가장 인기있는 동영상에 지구온난화의 심각성을 알리기 위한 공익광고를 넣어 달라는 요청을 받았습니다. 평소에 환경 문제에 관심을 가지고 있던 죠르디는 요청을 받아들였고 광고효과를 높이기 위해 시청자들이 가장 많이 보는 구간에 공익광고를 넣으려고 합니다. 죠르디는 시청자들이 해당 동영상의 어떤 구간을 재생했는 지 알 수 있는 재생구간 기록을 구했고, 해당 기록을 바탕으로 공익광고가 삽입될 최적의 위치를 고를 수 있었습니다.
참고로 광고는 재생 중인 동영상의 오른쪽 아래에서 원래 영상과 동시에 재생되는 PIP(Picture in Picture) 형태로 제공됩니다.

 

 

다음은 죠르디가 공익광고가 삽입될 최적의 위치를 고르는 과정을 그림으로 설명한 것입니다.

 

 

  • 그림의 파란색 선은 광고를 검토 중인 죠르디 동영상의 전체 재생 구간을 나타냅니다.
    • 위 그림에서, 죠르디 동영상의 총 재생시간은 02시간 03분 55초 입니다.
  • 그림의 검은색 선들은 각 시청자들이 죠르디의 동영상을 재생한 구간의 위치를 표시하고 있습니다.
    • 검은색 선의 가운데 숫자는 각 재생 기록을 구분하는 ID를 나타냅니다.
    • 검은색 선에 표기된 왼쪽 끝 숫자와 오른쪽 끝 숫자는 시청자들이 재생한 동영상 구간의 시작 시각과 종료 시각을 나타냅니다.
    • 위 그림에서, 3번 재생 기록은 00시 25분 50초 부터 00시 48분 29초 까지 총 00시간 22분 39초 동안 죠르디의 동영상을 재생했습니다. 1
    • 위 그림에서, 1번 재생 기록은 01시 20분 15초 부터 01시 45분 14초 까지 총 00시간 24분 59초 동안 죠르디의 동영상을 재생했습니다.
  • 그림의 빨간색 선은 죠르디가 선택한 최적의 공익광고 위치를 나타냅니다.
    • 만약 공익광고의 재생시간이 00시간 14분 15초라면, 위의 그림처럼 01시 30분 59초 부터 01시 45분 14초 까지 공익광고를 삽입하는 것이 가장 좋습니다. 이 구간을 시청한 시청자들의 누적 재생시간이 가장 크기 때문입니다.
    • 01시 30분 59초 부터 01시 45분 14초 까지의 누적 재생시간은 다음과 같이 계산됩니다.
      • 01시 30분 59초 부터 01시 37분 44초 까지 : 4번, 1번 재생 기록이 두차례 있으므로 재생시간의 합은 00시간 06분 45초 X 2 = 00시간 13분 30초
      • 01시 37분 44초 부터 01시 45분 14초 까지 : 4번, 1번, 5번 재생 기록이 세차례 있으므로 재생시간의 합은 00시간 07분 30초 X 3 = 00시간 22분 30초
      • 따라서, 이 구간 시청자들의 누적 재생시간은 00시간 13분 30초 + 00시간 22분 30초 = 00시간 36분 00초입니다.

[문제]

죠르디의 동영상 재생시간 길이 play_time, 공익광고의 재생시간 길이 adv_time, 시청자들이 해당 동영상을 재생했던 구간 정보 logs가 매개변수로 주어질 때, 시청자들의 누적 재생시간이 가장 많이 나오는 곳에 공익광고를 삽입하려고 합니다. 이때, 공익광고가 들어갈 시작 시각을 구해서 return 하도록 solution 함수를 완성해주세요. 만약, 시청자들의 누적 재생시간이 가장 많은 곳이 여러 곳이라면, 그 중에서 가장 빠른 시작 시각을 return 하도록 합니다.

[제한사항]

  • play_time, adv_time은 길이 8로 고정된 문자열입니다.
    • play_time, adv_time은 HH:MM:SS 형식이며, 00:00:01 이상 99:59:59 이하입니다.
    • 즉, 동영상 재생시간과 공익광고 재생시간은 00시간 00분 01초 이상 99시간 59분 59초 이하입니다.
    • 공익광고 재생시간은 동영상 재생시간보다 짧거나 같게 주어집니다.
  • logs는 크기가 1 이상 300,000 이하인 문자열 배열입니다.

    • logs 배열의 각 원소는 시청자의 재생 구간을 나타냅니다.
    • logs 배열의 각 원소는 길이가 17로 고정된 문자열입니다.
    • logs 배열의 각 원소는 H1:M1:S1-H2:M2:S2 형식입니다.
      • H1:M1:S1은 동영상이 시작된 시각, H2:M2:S2는 동영상이 종료된 시각을 나타냅니다.
      • H1:M1:S1는 H2:M2:S2보다 1초 이상 이전 시각으로 주어집니다.
      • H1:M1:S1와 H2:M2:S2는 play_time 이내의 시각입니다.
  • 시간을 나타내는 HH, H1, H2의 범위는 00~99, 분을 나타내는 MM, M1, M2의 범위는 00~59, 초를 나타내는 SS, S1, S2의 범위는 00~59까지 사용됩니다. 잘못된 시각은 입력으로 주어지지 않습니다. (예: 04:60:24, 11:12:78, 123:12:45 등)

  • return 값의 형식

    • 공익광고를 삽입할 시각을 HH:MM:SS 형식의 8자리 문자열로 반환합니다.

[입출력 예]

play_timeadv_timelogsresult

"02:03:55""00:14:15"["01:20:15-01:45:14", "00:40:31-01:00:00", "00:25:50-00:48:29", "01:30:59-01:53:29", "01:37:44-02:02:30"]"01:30:59"
"99:59:59""25:00:00"["69:59:59-89:59:59", "01:00:00-21:00:00", "79:59:59-99:59:59", "11:00:00-31:00:00"]"01:00:00"
"50:00:00""50:00:00"["15:36:51-38:21:49", "10:14:18-15:36:51", "38:21:49-42:51:45"]"00:00:00"

 

 

접근 방법


부분합과 투포인터를 적절히 사용하면 어렵지 않게 해결할 수 있는 문제였습니다. 

 

접근 방법은 크게 4가지로 나눌 수 있습니다.

1. 시간정보에 관한 문자열을 초로 변환

2. arr[i]라는 배열을 i초에 보고 있던 시청자 수라 하였을 때, 1에서 변환된 초에 따라 arr배열에 기록

3. 광고를 0초부터 가능한 부분까지 1초씩 옮겨가며 arr배열의 정보를 사용하여 최댓값 갱신 

4. 3에서 구한 값을 문자열로 변환

 

 

1. 시간정보에 관한 문자열을 초로 변환

 

문자열에서 ':'를 기준으로 for문을 통해 시간, 분, 초의 정보를 초로 변경하였습니다.

int string_to_sec(string time){
    
    int sec = 0;
    string sub = "";
    
    for(int i=0;i<time.size();i++){
        
        if(time[i] == ':'){
            sec = (sec * 60) + (stoi(sub) * 60);
            sub = "";
            continue;
        }
        sub.push_back(time[i]);
    }
    sec += stoi(sub);
    return sec;
}

 

2. arr[i]라는 배열을 i초에 보고 있던 시청자 수라 하였을 때, 1에서 변환된 초에 따라 arr배열에 기록

 

1번에서 설명한 string_to_sec()함수를 사용하여 문제의 입력으로 주어진 logs를 변환하여 arr[] 배열에 기록합니다.

 for(int i=0;i<logs.size();i++){
        
        int start = string_to_sec(logs[i].substr(0, 8));
        int end = string_to_sec(logs[i].substr(9, 8));
        
        for(int i = start;i<end;i++) 
            arr[i]++;
}

 

3. 광고를 0초부터 가능한 부분까지 1초씩 옮겨가며 arr배열의 정보를 사용하여 최댓값 갱신 

 

광고시작시간이 0초일 때 시청자들의 누적 재생시간을 최대값의 초기값으로 설정하여 1초씩 시작시간을 증가시켜 최대값을 갱신합니다.

 예를 들어 광고의 재생시간이 5초라고 한다면 sum1 = arr[0] + arr[1] + arr[2] + arr[3] + arr[4]가 될 것입니다. 이 때 광고의 시작시간을 1초 뒤로 옮기게 된다면 sum2 = arr[1] + arr[2] + arr[3] + arr[4] + arr[5]가 될 것입니다. 이 때 광고의 길이는 항상 일정하기 때문에 sum을 간단하게 구할 수 있습니다. 

 

광고가 끝나는 시간을 i초, 광고의 길이를 a_time이라 하였을 때

sum(현재) = sum(과거) + arr[i] - arr[i - a_time]이라는 식을 유도할 수 있습니다.

즉 이전상태의 부분합을 안다면 다음상태에서 모든 요소를 참조하지 않고 현재상태의 부분합을 알아낼 수 있습니다.

//a_time은 광고길이, p_time은 영상길이
int accumulate(int a_time, int p_time){
    
    long long maxsum = 0;
    int result = 0;
    
    for(int i=0;i<a_time;i++)
        maxsum += arr[i];
    
    long long sum = maxsum;
    
    for(int i = a_time; i < p_time; i++){
        
        sum += arr[i] - arr[i - a_time];
        
        if(maxsum < sum) {
            maxsum = sum;
            result = i - a_time + 1;
        }
    }
    return result;
}

 

4. 3에서 구한 값을 문자열로 변환

 

int형의 초를 60으로 나누어 가며 시, 분, 초로 변환을 하여 문자열을 구성을 합니다.

이 때 주의할 점은 시간은 60으로 나눈 나머지를 구해서 사용하면 안됩니다.(코드 복붙하다 여기서 틀린줄 모르고 많이 해맸음)

string sec_to_string(int sec){
    
     
    string s = to_string(sec % 60);
    if(s.size() == 1)
        s = "0" + s;
    sec /= 60;
    
    string m = to_string(sec % 60);
    if(m.size() == 1)
        m = "0" + m;
    sec /= 60;
    
    string h = to_string(sec); //시간은 60으로 나눈 나머지를 사용하지 않는다.
    if(h.size() == 1)
        h = "0" + h;
    
    return h + ":" + m + ":" + s; 
}

 

 

소스 코드


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

using namespace std;

int arr[360000];

int string_to_sec(string time){
    
    int sec = 0;
    string sub = "";
    
    for(int i=0;i<time.size();i++){
        
        if(time[i] == ':'){
            sec = (sec * 60) + (stoi(sub) * 60);
            sub = "";
            continue;
        }
        sub.push_back(time[i]);
    }
    sec += stoi(sub);
    return sec;
}

string sec_to_string(int sec){
    
     
    string s = to_string(sec % 60);
    if(s.size() == 1)
        s = "0" + s;
    sec /= 60;
    
    string m = to_string(sec % 60);
    if(m.size() == 1)
        m = "0" + m;
    sec /= 60;
    
    string h = to_string(sec);
    if(h.size() == 1)
        h = "0" + h;
    
    return h + ":" + m + ":" + s; 
}

int accumulate(int a_time, int p_time){
    
    long long maxsum = 0;
    int result = 0;
    
    for(int i=0;i<a_time;i++)
        maxsum += arr[i];
    
    long long sum = maxsum;
    
    for(int i = a_time; i < p_time; i++){
        
        sum += arr[i] - arr[i - a_time];
        
        if(maxsum < sum) {
            maxsum = sum;
            result = i - a_time + 1;
        }
    }
    return result;
}

string solution(string play_time, string adv_time, vector<string> logs) {
    
    string answer = "";
    
    int p_time = string_to_sec(play_time);
    int a_time = string_to_sec(adv_time);
    
    for(int i=0;i<logs.size();i++){
        
        int start = string_to_sec(logs[i].substr(0, 8));
        int end = string_to_sec(logs[i].substr(9, 8));
        
        for(int i = start;i<end;i++) 
            arr[i]++;
    }
        
    int result = accumulate(a_time, p_time);
    
    answer = sec_to_string(result);
    
    return answer;
}

+ Recent posts