https://programmers.co.kr/learn/courses/30/lessons/42893

 

코딩테스트 연습 - 매칭 점수

매칭 점수 프렌즈 대학교 조교였던 제이지는 허드렛일만 시키는 네오 학과장님의 마수에서 벗어나, 카카오에 입사하게 되었다. 평소에 관심있어하던 검색에 마침 결원이 발생하여, 검색개발팀

programmers.co.kr

 

문제 풀이

이 문제는 카카오답게 문자열을 다루는 스킬을 확인하는 문제인 것 같다. 이 문제에서 찾아야할 문자열은 url, 검색하고자 하는 단어, 링크가 걸린 url을 찾아야한다. 각각 다음과 같은 방식으로 찾는다.

 

1. url 검색 : url이 담긴 문자열은 다음과 같이 시작한다.

<meta property="og:url" content="https://

이 문자열로 시작하는 부분을 찾아 content="________"에 밑줄에 해당하는 부분을 저장한다.

 

2. 검색하고자 하는 단어 : 대문자로 한 번, 소문자로 한 번 찾는다.

대문자로 문자를 바꿔주는 toupper()와 소문자로 문자를 바꿔주는 tolower를 이용해 word의 첫 문자를 대문자, 소문자로 찾아 일치하는 단어가 있는지 비교한다. 비교는 모든 문자열을 대문자로 바꿔 비교한다.

 

3. 링크가 걸린 url : 링크 주소 문자열은 다음과 같이 시작한다.

<a href=\"https://

이 문자열로 시작하는 부분을 전부 찾아 자신을 가리키는 페이지의 인덱스를 저장하는 배열에 넣어주고 현재 페이지에서 가리키는 링크의 수를 증가시킨다.

 

위의 과정대로 짠 코드이다.

 

소스 코드

#include <string>
#include <iostream>
#include <vector>
#include <map>
#include <set>
using namespace std;

string url[21]; //index - url 매핑
double basic[21]; //기본점수
int linknum[21]; //각 노드의 외부링크
map<string, set<int>> links; //자기를 가리키는 링크 저장

bool isSame(string &a, string &b) { //문자열 비교 함수
    for(int i = 0; i < a.length(); i++) {
        if(!isalpha(b[i])) { // 알파벳이 아니면 false 리턴
            return false;
            break;
        }
        if(toupper(a[i]) != toupper(b[i])) { //같지 않으면 false 리턴
            return false;
            break;
        }
    }
    return true;
} 

int solution(string word, vector<string> pages) {
    int answer = 0;
    double mnum = 0;
    for(int i = 0; i < pages.size(); i++) {
        string &now = pages[i];
        //url찾기
        int start = now.find("<meta property=\"og:url\" content=\"https://"); //url이 담긴 문자열 검색
        start += 33; //url 시작 주소로 이동
        int end = now.find("\"", start); //"로 끝나는 위치 저장
        string nowurl = now.substr(start, end - start); //url 저장
        url[i] = nowurl;
        
        
        start = now.find("<html");
        string body = now.substr(start);
        int idx = 0;
        while(idx < body.size()) { //기본 점수 계산 소문자로 찾기
            int pos = body.find(tolower(word[0]), idx);
            if(pos == string::npos) break; //단어 없으면 종료
            if(pos + word.size() > body.size()) break; //범위 벗어나면 종료
            string tmp = body.substr(pos, word.size()); //길이를 벗어나면 break
            if(word.size() == tmp.size() && isSame(word, tmp)) {//길이가 같은 문자열 저장
                if(!isalpha(body[pos-1]) && !isalpha(body[pos + word.size()])) //앞 뒤로 알파벳이 없을 때 증가
                    basic[i]+=1;
            }
            idx = pos+1;
        }
        idx = 0;
        while(idx < body.size()) { //기본 점수 계산 대문자로 찾기
            int pos = body.find(toupper(word[0]), idx); //첫 글자가 같은 위치 검색
            if(pos == string::npos) break; //단어 없으면 종료
            if(pos + word.size() > body.size()) break; //길이를 벗어나면 break
            string tmp = body.substr(pos, word.size()); //길이가 같은 문자열 저장
            if(word.size() == tmp.size() && isSame(word, tmp)) { //사이즈도 같고 같은 문자일 때
                if(!isalpha(body[pos-1]) && !isalpha(body[pos + word.size()])) //앞 뒤로 알파벳이 없을 때 증가
                    basic[i]+=1;
            }
            idx = pos+1; //다음 위치부터 다시 검색
        }
        idx = 0;
        while(idx < body.size()) { //링크 주소 찾기
            int start = body.find("<a href=\"https://", idx);
            if(start == string::npos) break;
            start += 9;
            int end = body.find("\"", start);
            string link = body.substr(start, end - start); //링크 문자열
            links[link].insert(i); //자신을 가리키는 페이지 인덱스에 insert
            linknum[i]++; //현재 페이지의 링크 수 증가
            idx = end;
        }
    }

    for(int i = 0; i < pages.size(); i++) { //최고점 찾기
        double score = basic[i]; //기본 점수
        for(auto it = links[url[i]].begin(); it != links[url[i]].end(); it++) {
            score += ((basic[*it]) / linknum[*it]); //링크 점수 더하기
        }
        if(mnum < score) { //최대값 갱신
            mnum = score;
            answer = i;
        }
    }
    


    return answer;
}

index - url 매핑한 url 배열, 기본점수를 저장할 basic 배열, 각 노드의 외부 링크 수를 저장할 linknum배열, 자신을 가리키는 링크의 인덱스를 저장할 links map을 선언하였다.

 

주의할 점과 실수한 부분

이 문제는 문자열을 파싱하는 부분이 조금 까다로웠고 점수가 double형으로 계산되어야 했다. 처음 int형으로 basic을 저장하고 캐스팅하여 점수를 계산하였을 때 몇 개의 테스트케이스를 통과하지 못했었다. 그 다음 최대값을 저장한 mnum 변수도 int형으로 선언되어있었는데 이것 때문에 8번, 17번만 통과를 하지 못하고 있었다. 이 실수를 모르고 계속 다른 부분에서 오류를 찾아 시간낭비를 많이 했다. mnum도 double로 바꾸는 순간 모든 테스트 케이스도 통과할 수 있었다.

 

채점 결과

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

 

2110번: 공유기 설치

첫째 줄에 집의 개수 N (2 ≤ N ≤ 200,000)과 공유기의 개수 C (2 ≤ C ≤ N)이 하나 이상의 빈 칸을 사이에 두고 주어진다. 둘째 줄부터 N개의 줄에는 집의 좌표를 나타내는 xi (0 ≤ xi ≤ 1,000,000,000)가

www.acmicpc.net

문제

도현이의 집 N개가 수직선 위에 있다. 각각의 집의 좌표는 x1, ..., xN이고, 집 여러개가 같은 좌표를 가지는 일은 없다.

도현이는 언제 어디서나 와이파이를 즐기기 위해서 집에 공유기 C개를 설치하려고 한다. 최대한 많은 곳에서 와이파이를 사용하려고 하기 때문에, 한 집에는 공유기를 하나만 설치할 수 있고, 가장 인접한 두 공유기 사이의 거리를 가능한 크게 하여 설치하려고 한다.

C개의 공유기를 N개의 집에 적당히 설치해서, 가장 인접한 두 공유기 사이의 거리를 최대로 하는 프로그램을 작성하시오.

 

예제

풀이

    이 문제는 이분 탐색으로 접근해야 하는 문제이다. 이분 탐색으로 푸는 문제는 문제 내용에서 힌트를 얻을 수 있다. 이분 탐색 문제는 최대나 최소를 구하는 문제가 많이 나온다. 이 문제도 "C개의 공유기를 N개의 집에 적당히 설치해서, 가장 인접한 두 공유기 사이의 거리를 최대로 하는 프로그램을 작성하시오."라고 나와있다. 이 부분에서 이분탐색을 이용해야한다는 것을 알아차릴 수 있다.

    이 문제에서 mid로 설정해야할 값은 공유기 사이의 거리이다. 즉 mid보다 큰 차이가 나면 공유기를 하나 설치해야한다는 것이다. 따라서 설치해야하는 공유기의 수를 매번 계산하여 C개의 공유기가 설치될 때의 최대값을 구할 수 있다.

 

소스 코드

#include <string>
#include <vector>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

vector<int> arr;

int main() {
    int N, C; cin >> N >> C;
    for(int i = 0; i < N; i++) {
        int a; cin >> a;
        arr.push_back(a);
    }
    sort(arr.begin(), arr.end());
    int left = 1, right = arr[arr.size() - 1] + 1;
    for(int i = 0; i < 30; i++) {
        int mid = (left + right) / 2;
        int pos1 = arr.front();
        int pos2;
        int wifi = 1;
        for(int j = 1; j < arr.size(); j++) {
            pos2 = arr[j];
            int dist = pos2 - pos1;
            if(dist >= mid) {
                wifi++;
                pos1 = pos2;
            }
        }
        if(wifi >= C) 
            left = mid;
        else
            right = mid;
    }
    cout << left;
}

문제에서 죄표의 최대 값은 10^9이다. 이 값은 2^30보다 작은 값이므로 이분 탐색을 하였을 때 30번만 반복한다면 충분히 커버할 수 있다. 초기 left 값은 1, right값은 가장 큰 인덱스에 1을 더한 값이다. 1을 더한 이유는 정답은 left인데 만약 마지막 인덱스가 정답일 경우 left가 도달하지 못할 수 있기 때문이다. 이 문제는 조건을 만족하는 최대값을 구하는 문제이므로 공유기의 수가 C와 같을 경우에도 left에 mid값을 넣어준다. 그렇다면 C와 같을 때에도 left값은 이를 만족하는 조건에서의 최대 값을 가질 수 있다.

 

채점 결과

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

 

13460번: 구슬 탈출 2

첫 번째 줄에는 보드의 세로, 가로 크기를 의미하는 두 정수 N, M (3 ≤ N, M ≤ 10)이 주어진다. 다음 N개의 줄에 보드의 모양을 나타내는 길이 M의 문자열이 주어진다. 이 문자열은 '.', '#', 'O', 'R', 'B'

www.acmicpc.net

문제

스타트링크에서 판매하는 어린이용 장난감 중에서 가장 인기가 많은 제품은 구슬 탈출이다. 구슬 탈출은 직사각형 보드에 빨간 구슬과 파란 구슬을 하나씩 넣은 다음, 빨간 구슬을 구멍을 통해 빼내는 게임이다.

보드의 세로 크기는 N, 가로 크기는 M이고, 편의상 1×1크기의 칸으로 나누어져 있다. 가장 바깥 행과 열은 모두 막혀져 있고, 보드에는 구멍이 하나 있다. 빨간 구슬과 파란 구슬의 크기는 보드에서 1×1크기의 칸을 가득 채우는 사이즈이고, 각각 하나씩 들어가 있다. 게임의 목표는 빨간 구슬을 구멍을 통해서 빼내는 것이다. 이때, 파란 구슬이 구멍에 들어가면 안 된다.

이때, 구슬을 손으로 건드릴 수는 없고, 중력을 이용해서 이리 저리 굴려야 한다. 왼쪽으로 기울이기, 오른쪽으로 기울이기, 위쪽으로 기울이기, 아래쪽으로 기울이기와 같은 네 가지 동작이 가능하다.

각각의 동작에서 공은 동시에 움직인다. 빨간 구슬이 구멍에 빠지면 성공이지만, 파란 구슬이 구멍에 빠지면 실패이다. 빨간 구슬과 파란 구슬이 동시에 구멍에 빠져도 실패이다. 빨간 구슬과 파란 구슬은 동시에 같은 칸에 있을 수 없다. 또, 빨간 구슬과 파란 구슬의 크기는 한 칸을 모두 차지한다. 기울이는 동작을 그만하는 것은 더 이상 구슬이 움직이지 않을 때 까지이다.

보드의 상태가 주어졌을 때, 최소 몇 번 만에 빨간 구슬을 구멍을 통해 빼낼 수 있는지 구하는 프로그램을 작성하시오.

 

입력 예시

풀이

이 문제는 방문 빨간 공과 파란 공의 위치가 바뀌었을 때 정확히 어디에 위치하는지를 잘 계산하여야한다. 문제를 본 순간 DFS를 이용하여 풀어야겠다고 생각하였다. 하지만 BFS를 이용하면 더 간단한 풀이가 가능했다. 직접 구현한 코드는 DFS를 이용하였다. DFS를 이용하여 빨간 공, 파란 공이 상하좌우로 기울어졌을 때 멈추는 지점을 계산하였고 공이 동시에 빠지는 지를 판단하였다. 파란 공만 빠지는 경우를 생각해야하므로 파란 공을 먼저 생각하였다. 코드의 실행은 다음과 같다.

 

  1. 파란 공이 멈출 때까지 한 방향으로 이동한다.
  2. 만약 파란 공이 구멍에 빠진 경우 다른 방향으로 계속한다.
  3. 만약 빨간 공이 부딪힌 경우 체크한다. (만약 빨간공이 구멍에 빠질 경우 같이 빠질 수 있고 빨간 공이 멈춘 경우 빨간 공 옆에 멈출 것이기 때문에 지금 위치를 판단할 수 없다)
  4. 파란공이 멈춘 위치를 저장하고 빨간공과 부딪히지 않은 경우에만 위치를 배열에서 수정(부딪힌 경우 빨간공에 의해 위치가 결정되므로)
  5. 빨간 공이 멈출 때까지 같은 방향으로 이동한다.
  6. 만약 구멍에 빠졌을 경우
    • 아까 체크한 파란공이 빨간공과 부딪힌 경우는 파란 공을 원래 위치로 되돌리고 계속 무시
    • 체크되지 않을 경우 depth가 최소값인 경우 정답을 갱신하고 다른 방향으로 계속
  7. 빨간 공이 멈춘 위치를 저장하고 빨간공 위치를 배열에서 수정
  8. 만약 파란 공이 빨간 공과 부딪힌 경우 파란공 위치를 다 빨간공 옆으로 수정하고 배열에서도 위치 수정
  9. DFS로 수정된 빨간 공과 파란 공을 탐색 계속
  10. 만약 현재 빨간 공과 파란 공의 위치가 이전에 방문했을 때의 회전 횟수보다 많은 경우는 리턴하여 불필요한 재탐색을 하지 않는다.
  11. 그렇지 않은 경우 현재 빨간 공, 파란 공의 위치일 때의 회전 횟수를 갱신하고 탐색을 계속한다.
  12. 탐색이 끝난 경우 빨간 공, 파란 공 위치를 다시 되돌리고 다른 방향으로 계속한다.

위 실행 순서를 구현한 코드이다.

코드

#include <string>
#include <iostream>
#include <algorithm>
#include <vector>
#include <map>
using namespace std;

const int MN = 11;

string arr[MN];

int dx[4] = {1, 0, 0, -1};
int dy[4] = {0, 1, -1, 0};

int visit[MN][MN][MN][MN];

int ans = 11;

void dfs(pair<int,int> red, pair<int,int> blue, int depth) {
    if(depth > 10) return; //10번 이상 움직인 경우 리턴
    int redx = red.first, redy = red.second;
    int bluex = blue.first, bluey = blue.second;
    if(visit[redx][redy][bluex][bluey] <= depth) return; //이미 방문한 경우 리턴
    
    visit[redx][redy][bluex][bluey] = depth;

    for(int d = 0; d < 4; d++) { //네 가지 방향으로
        pair<int,int> nblue; //파란공 이동
        int nx = bluex + dx[d]; //다음 파란공 위치
        int ny = bluey + dy[d];
        bool chk = false; //빨간공과 부딪히는지 여부 판단
        while(arr[nx][ny] == '.') { //멈출 때까지
            nx += dx[d];
            ny += dy[d];
        }
        if(arr[nx][ny] == 'O') continue; //파란 공이 구멍에 빠진 경우 계속
        if(arr[nx][ny] == 'R') { //만약 빨간 공을 부딪힌 경우
            chk = true; //부딪혔다고 표시
        }
        nx -= dx[d]; //멈춘 곳으로 위치
        ny -= dy[d];
        nblue = {nx, ny}; //파란공 위치 저장
        
        if(!chk) { //부딪히지 않았을 경우에만 수정
            arr[bluex][bluey] = '.'; //파란공 위치 수정
            arr[nx][ny] = 'B';
        }

        pair<int,int> nred; //빨간공 이동
        nx = redx + dx[d]; //다음 빨간공 위치
        ny = redy + dy[d];
        
        while(arr[nx][ny] == '.') { //멈출 때까지
            nx += dx[d];
            ny += dy[d];
        }
        if(arr[nx][ny] == 'O') { //구멍에 빠진 경우
            if(!chk) {//파란 공이 같이 빠지지 않을 경우에만 정답 갱신
                ans = min(ans, depth+1);
                arr[nblue.first][nblue.second] = '.'; //파란 공 위치 원래대로 돌리고 계속
                arr[bluex][bluey] = 'B';
            }
            continue;
        }
        nx -= dx[d]; //빨간공 멈춘 곳으로 저장
        ny -= dy[d];
        nred = {nx, ny}; //빨간공 위치 저장
        arr[redx][redy] = '.'; //빨간공 위치 수정
        arr[nx][ny] = 'R';

        if(chk) { //만약 파란공이 빨간공과 부딪힌 경우
            nblue.first = nx - dx[d]; //파란공 위치를 빨간공 옆으로 수정
            nblue.second = ny - dy[d];
            arr[nblue.first][nblue.second] = 'B'; //파란공 위치 표시
            arr[bluex][bluey] = '.';
        }
        dfs(nred, nblue, depth+1); //dfs로 탐색 계속
        arr[nred.first][nred.second] = '.'; //빨강, 파란공 위치 처음으로 되돌리기
        arr[nblue.first][nblue.second] = '.';
        arr[redx][redy] = 'R';
        arr[bluex][bluey] = 'B';
    }
}

int main() {
    int N, M; cin >> N >> M;
    fill(&visit[0][0][0][0], &visit[MN-1][MN-1][MN-1][MN], 11); //방문 배열 초기화
    pair<int,int> red;
    pair<int,int> blue;
    for(int i = 0; i < N; i++) {
        cin >> arr[i];
        for(int j = 0; j < arr[i].size(); j++) {
            if(arr[i][j] == 'R') { //빨간공 위치 저장
                red = {i, j};
            }
            if(arr[i][j] == 'B') { //파란공 위치 저장
                blue = {i, j};
            }
        }
    }
    dfs(red, blue, 0);

    if(ans > 10) cout << -1; //10번 이상 걸린 경우 -1출력
    else cout << ans; //그렇지 않은 경우 걸린 횟수 출력
}

복잡해보이고 눈에 확 들어오는 코드는 아니지만 스스로 판단하기에 메모이제이션을 이용해 불필요한 탐색을 줄였다고 생각한다.

생각하지 못한 반례들이 꽤 있어서 질문검색에 있는 반례들을 많이 참고하였다. 빨간공의 좌표, 파란공의 좌표를 출력해보며 잘못 된 부분을 고쳐보며 스스로 풀 수 있었다. 이 문제의 티어는 골드2로 꽤 높은 편이다. 이러한 문제를 많이 풀어봐야할 것 같다.

 

채점 결과

 

https://programmers.co.kr/learn/courses/30/lessons/72412

 

코딩테스트 연습 - 순위 검색

["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","pyt

programmers.co.kr

이 문제는 스스로 시간초과 문제를 해결하지 못하여 풀이를 보고 해결할 수 있었다.

문제

카카오는 하반기 경력 개발자 공개채용을 진행 중에 있으며 현재 지원서 접수와 코딩테스트가 종료되었습니다. 이번 채용에서 지원자는 지원서 작성 시 아래와 같이 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점 이상 받은 사람은 모두 몇 명인가?
  • 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점 이상 받은 사람은 모두 몇 명인가?"를 의미합니다.

조건이 주어지는 query를 만족하는 인원이 몇명인지를 vector에 담아 리턴하는 문제이다.

 

예시

위 그림과 같이 info로 각 사람들의 지원 조건이 주어지면 query를 만족하는 인원이 각각 몇명인지를 구하는 문제이다.

 

이 문제는 각각 인원을 모두 구하고 겹치는 인원들을 찾아내어 인원을 세면 시간초과가 걸린다. 따라서 각 조건을 만족하는 사람을 전부 가지고 유지하고 있어야한다. 따라서 info에 주어지는 언어, 직군, 경력, 음식을 하나로 생각해서 그 조건을 만족하는 사람들의 점수를 가지고 있어야한다. 글쓴이는 언어, 직군, 경력,음식을 각가 1차원으로 생각하여 4차원 배열을 만들었다. 코드는 다음과 같다.

 

소스코드

#include <string>
#include <vector>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

vector<int> database[3][2][2][2]; //데이터베이스 구축
//cpp = 0, java = 1, python = 2;
//backend = 0, frontend = 1;
//junior = 0, senior = 1;
//chicken = 0, pizza = 1;


vector<int> solution(vector<string> info, vector<string> query) {
    vector<int> answer;
    for(int i =0; i < info.size(); i++) {
        int lan, end, career, food;
        int now = 0;
        string s = info[i]; //문자열을 나눔
        if(s[0] == 'c') {
            lan = 0;
            now = 4;
        }
        else if(s[0] == 'j') {
            lan = 1;
            now = 5;
        }
        else{
            lan = 2;
            now = 7;
        }
        
        if(s[now] == 'b') {
            end = 0;
            now += 8;
        }
        else {
            end = 1;
            now += 9;
        }
        
        if(s[now] == 'j') {
            career = 0;
        }
        else {
            career = 1;
        }
        now += 7;
        
        if(s[now] == 'c') {
            food = 0;
            now += 8;
        }
        else {
            food = 1;
            now += 6;
        }
        
        int num = stoi(s.substr(now));
        database[lan][end][career][food].push_back(num); //해당하는 데이터베이스에 점수 추가
    }
    
  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(database[i][j][k][l].begin(),database[i][j][k][l].end());
    
    for(string q : query) {
        int now = 0;
        int s1, s2, s3, s4, e1, e2, e3, e4;
        int lan = -1, end = -1, career = -1, food = -1;
        if(q[now] == 'c') { //언어 분류
            lan = 0;
            now += 8;
        }
        else if(q[now] == 'j') {
            lan = 1;
            now += 9;
        }
        else if(q[now] == 'p') {
            lan = 2;
            now += 11;
        }
        else {
            now += 6;
        }
        
        if(q[now] == 'b') {
            end = 0;
            now += 12;
        }
        else if(q[now] == 'f') {
            end = 1;
            now += 13;
        }
        else {
            now += 6;
        }
        
        if(q[now] == 'j') {
            career = 0;
            now += 11;
        }
        else if(q[now] == 's') {
            career = 1;
            now += 11;
        }
        else {
            now += 6;
        }
        
        if(q[now] == 'p') {
            food = 1;
            now += 6;
        }
        else if(q[now] == 'c') {
            food = 0;
            now += 8;
        }
        else {
            now += 2;
        }
        
        int num = stoi(q.substr(now));
        
        int people = 0;
        for(int i = 0; i <= 2; i++) {
            if(lan != i && lan != -1) continue;
            for(int j = 0; j <= 1; j++) {
                if(end != j && end != -1) continue;
                for(int k = 0; k <= 1; k++) {
                    if(career != -1 && career != k) continue;
                    for(int l = 0; l <= 1; l++) {
                        if(food != -1 && food != l) continue;
                        if(database[i][j][k][l].size()) {
                            auto lo = lower_bound(database[i][j][k][l].begin(), database[i][j][k][l].end(), num);
                            people += database[i][j][k][l].end() - lo;
                        }
                    }
                }
            }
        }
        answer.push_back(people);
    }
    
    return answer;
}

문자열을 분류하는 코드 때문에 상당히 복잡해보이지만 그 부분을 제외하면 간단한 코드이다. 각 조건에 따라서 언어, 직군, 경력, 음식에 해당하는 인덱스가 결정된다. 그 조건을 만족하는 벡터에 점수를 push한다. 그렇다면 같은 조건을 가진 사람들의 점수가 유지될 것이다. 그리고 조건이 되는 점수를 이분탐색하기 위하여 모두 정렬한다. 4중 for문이지만 3 * 2 * 2 * 2 = 24로 24번밖에 반복하지 않는다. 그 다음 query에서 주어지는 조건에 따라 탐색하고자 하는 인덱스를 결정한다. 모든 초기값을 -1로 주어 만약 -1인 경우 해당 조건의 모든 점수를 탐색할 수 있게 하였다. 위 코드를 보았을 때 

 if(lan != i && lan != -1) continue;

이 부분은 언어가 i번째 인덱스도 아니고 -1도 아닐 경우 건너 뛴다는 의미이다. 즉 -1인 경우 모든 인덱스를 탐색하고 그렇지 않은 경우 원하는 조건인 lan에 해당하는 인덱스만 탐색한다는 뜻이다. 그렇게 4가지 조건을 모두 탐색하여 lower_bound라는 이분탐색 알고리즘을 이용하여 조건을 만족하는 점수의 이터레이터를 얻는다. 그리고 end와의 차이를 이용해 조건으로 주어진 값보다 크거나 같은 값의 수를 구할 수 있다. 그렇게 구한 인원을 각 query마다 answer에 push_back하여 원하는 값을 구할 수 있다.

 

채점 결과

 

https://programmers.co.kr/learn/courses/30/lessons/77886

 

코딩테스트 연습 - 110 옮기기

0과 1로 이루어진 어떤 문자열 x에 대해서, 당신은 다음과 같은 행동을 통해 x를 최대한 사전 순으로 앞에 오도록 만들고자 합니다. x에 있는 "110"을 뽑아서, 임의의 위치에 다시 삽입합니다. 예를

programmers.co.kr

문제

0과 1로 이루어진 어떤 문자열 x에 대해서, 당신은 다음과 같은 행동을 통해 x를 최대한 사전 순으로 앞에 오도록 만들고자 합니다.

  • x에 있는 "110"을 뽑아서, 임의의 위치에 다시 삽입합니다.

예를 들어, x = "11100" 일 때, 여기서 중앙에 있는 "110"을 뽑으면 x = "10" 이 됩니다. 뽑았던 "110"을 x의 맨 앞에 다시 삽입하면 x = "11010" 이 됩니다.

변형시킬 문자열 x가 여러 개 들어있는 문자열 배열 s가 주어졌을 때, 각 문자열에 대해서 위의 행동으로 변형해서 만들 수 있는 문자열 중 사전 순으로 가장 앞에 오는 문자열을 배열에 담아 return 하도록 solution 함수를 완성해주세요.

 

풀이

    이 문제는 혼자 풀었을 때 시간초과를 해결하지 못하여 풀이를 보고 풀었다. 우선 110이 사라지고 난 다음 생기는 110도 처리를 해주어야된다는 것을 뒤늦게 알았다. 풀이를 보고 stack을 이용하면 쉽게 풀린다는 것을 알았다. 또 110을 삽입하는 위치가 오른쪽에서 시작하여 연속되는 1의 앞부분에 붙여 넣으면 된다는 것을 문제만 보고 떠올리기 쉽지 않았다. 이 부분만 알고 풀면 굉장히 쉽게 풀 수 있는 문제이다. 코드의 순서는 다음과 같다.

 

  1. 지워진 문자열의 문자를 하나씩 stack에 넣으며 110을 찾으면 지우고 카운트(110의 개수)를 센다.
  2. 110이 모두 지워진 문자열에서 뒤에서부터 1이 아닌 숫자가 나올 때까지 탐색한다. 즉 예를들어 001001111이 있을 경우 110들은 00100____1111 위치에 들어가야지 최소 숫자가 나오게 된다.
  3. 위치를 찾은 경우 삽입하려는 위치 앞부분의 문자열을 정답 문자열에 붙여넣는다.
  4. 110의 개수만큼 정답 문자열에 붙여넣는다.
  5. 삽임하려는 위치 뒷부분 문자열을 정답 문자열에 붙여넣는다.

코드

#include <string>
#include <vector>
#include <iostream>

using namespace std;

vector<string> solution(vector<string> s) {
    vector<string> answer;
    vector<char> st; //110을 지우고 남은 문자열
    int cnt = 0; //지워진 110의 개수
    for(string str : s) {
        cnt = 0;
        st.clear();
        for(int i = 0; i < str.size(); i++) { //110을 모두 지우기
            if(str[i] == '1') st.push_back('1'); //만약 0이 아니면 스택에 넣음
            else {
                if(st.size() >= 2 && st[st.size()-1] == '1' && st[st.size()-2] == '1') { //110을 찾으면 110개수를 늘리고 스택에서 지우기
                    cnt++;
                    st.pop_back();
                    st.pop_back();
                }
                else
                    st.push_back('0');
            }
        }
        int i;
        for(i = st.size()-1; i >= 0; i--) { //뒤에서 시작하여 연속된 1의 앞부분에 110붙이기
            if(st[i] == '1') continue;
            else break;
        }
        
        string ret = ""; 
        
        for(int k = 0; k <= i; k++) //연속된 1 앞부분 붙여넣기
            ret += st[k];
        
        for(int c = 0; c < cnt; c++) //110 붙여넣기
            ret += "110";
        
        for(int k = i+1; k < st.size(); k++) //연속된 1 뒷부분 붙여넣기
            ret += st[k];
        
        answer.push_back(ret);
    }
    
    return answer;
}

결과

 

 

풀이 방법만 알면 절대 어렵지 않은 문제였지만 이런 풀이과정을 생각해낸다는 것이 굉장히 어렵다. stack을 잘 다룬다고 생각하였지만 이렇게 실전에 적용하는 생각은 잘 하지 못하였다. 스택, 큐, 힙 등을 실제에 사용하는 문제를 많이 풀어봐야할 것 같다.

문제

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

 

1937번: 욕심쟁이 판다

n × n의 크기의 대나무 숲이 있다. 욕심쟁이 판다는 어떤 지역에서 대나무를 먹기 시작한다. 그리고 그 곳의 대나무를 다 먹어 치우면 상, 하, 좌, 우 중 한 곳으로 이동을 한다. 그리고 또 그곳에

www.acmicpc.net

 

이 문제는 DFS나 BFS로 간단히 풀 수 있는 문제처럼 보인다. 하지만 기본적인 DFS나 BFS로 풀 경우 시간초과가 발생한다. 따라서 DFS와 DP를 결합한 메모이제이션 방식을 이용해야한다. 그렇다면 메모이제이션을 어떻게 사용해야할까?

 

예시

입력 배열

만약 (0,0)인 5에서 시작한다면 5, 6, 7, 8, 9, 15까지 탐색 가능하며 2차원인 DP배열은 다음과 같을 것이다.

DP 배열

5에서 시작하면 총 7번까지 탐색 가능하다. 만약 그렇다면 (1,0)인 3에서 탐색을 시작한다면 어떻게 될까? 3에서는 5방향으로 탐색한다면 5에서 시작한 것과 똑같이 탐색할 것이다. 5부터는 결과도 똑같을 것이다. 그렇다면 3은 5에 이미 탐색한 결과가 있을 경우 DP배열의 (0,0)에 1을 더해주기만 하면 탐색 가능한 최대값을 얻을 수 있을 것이다. 만약 그렇지 않고 5부터 다시 탐색한다면 같은 반복을 여러번할 것이다. 만약 3부터 탐색하고 (1,1)인 2부터 탐색한다면 같은 작업을 3번 반복하는 것이다. 엄청난 시간낭비가 발생한다. 그렇기 때문에 이미 계산한 결과가 있는 경우 탐색을 멈추고 그 결과값을 이용해야한다. 코드로 나타내면 다음과 같다.

#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;

const int MN = 501;

int dp[MN][MN]; //DP 배열
int arr[MN][MN]; //입력 배열
bool visit[MN][MN]; //방문 배열
int N;
int di[4] = {1, 0, 0, -1}; //상하좌우 표시
int dj[4] = {0, 1, -1, 0};

int dfs(int i, int j) {
    if(visit[i][j]) return dp[i][j]; //만약 이미 계산한 결과값이 있는 경우 사용
    visit[i][j] = true; //방문 표시
    dp[i][j] = 1; //첫 방문인 경우 1로 초기화
    for(int d = 0; d < 4; d++) { //상하좌우를 탐색
        int ni = i + di[d];
        int nj = j + dj[d];
        if(ni >= 0 && ni < N && nj >= 0 && nj < N) { //배열 범위를 벗어나지 않는 조건
            if(arr[ni][nj] > arr[i][j]) //더 많은 대나무가 있는 곳으로
                dp[i][j] = max(dp[i][j],dfs(ni, nj) + 1); //탐색하고 최대값 갱신
        }
    }
    return dp[i][j]; //현 위치에서의 최대값 리턴
}

int main() {
    cin >> N;
    int ans = 1;
    for(int i = 0; i < N; i++) { //입력받기
        for(int j = 0; j < N; j++) {
            cin >> arr[i][j];
        }
    }
    for(int i = 0; i < N; i++) {
        for(int j = 0; j < N; j++) {
            ans = max(ans,dfs(i, j)); //각 위치에서 시작하여 최대값 갱신
        }
    }

    cout << ans; //최대값 출력
    return 0;
}

DFS와 DP를 이용하여 불필요한 재방문을 스킵하면 시간을 줄일 수 있다. 메모이제이션은 DP가 어떤 값을 기억하는지를 정확히 알고 이해해야지 사용할 수 있다. 카카오 코딩테스트에서도 가끔 메모이제이션을 사용하는 문제가 나오기 때문에 이런 문제를 많이 풀어보는 것이 중요하다.

https://programmers.co.kr/learn/courses/30/lessons/76503

 

코딩테스트 연습 - 모두 0으로 만들기

각 점에 가중치가 부여된 트리가 주어집니다. 당신은 다음 연산을 통하여, 이 트리의 모든 점들의 가중치를 0으로 만들고자 합니다. 임의의 연결된 두 점을 골라서 한쪽은 1 증가시키고, 다른 한

programmers.co.kr

문제 설명

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

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

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

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

 

문제 예시

문제 예시

위의 그림처럼 모든 하나의 노드는 증가, 하나의 노드는 감소시키면서 모든 노드의 가중치를 0으로 만들 경우 증가나 감소시키는 횟수를 리턴하면 된다.

 

문제 풀이

위 예시에서 보았듯이 이 문제는 리프노드 즉 가장 끝에 있는 노드부터 순서대로 0으로 만들어가다보면 풀리는 문제이다. 따라서 깊이우선 탐색을 하며 자식 노드부터 순서대로 0으로 만들어가면 문제를 풀 수 있다. 그렇다면 자식 노드부터 0으로 만들기 위해선 코드상에서 어떻게 해야할까?

#include <string>
#include <vector>
#include <iostream>
#include <cmath>

using namespace std;

vector<long long> weight; //각 노드의 현재 가중치 저장

vector<int> g[300001]; //인접리스트를 이용한 그래프 표현
bool visit[300001]; //방문여부 표시

long long answer;

void dfs(int now) {
    visit[now] = true; //현재 노드 방문 체크
    for(int child : g[now]) { //자식 노드를 돌며
        if(visit[child]) continue; //방문노드(부모노드)는 스킵
        dfs(child); //자식노드를 먼저 탐색하고
        answer += abs(weight[child]); //증가/감소하는 횟수 더하기
        weight[now] += weight[child]; //부모노드의 가중치 증가/감소
        weight[child] = 0; //자식노드 0으로 만들기
    }
}

long long solution(vector<int> a, vector<vector<int>> edges) {

    for(int e : a) {
        weight.push_back((long long)e); //long long형으로 가중치 배열 다시 만들기
    }
    
    for(vector<int> e : edges) { //인접리스트 그래프 만들기
        g[e[0]].push_back(e[1]);
        g[e[1]].push_back(e[0]);
    }
    
    dfs(0); //dfs탐색
    
    if(weight[0] != 0) return -1; //모두 0으로 만들지 못한 경우 -1리턴
    else return answer; //그렇지 않은 경우 정답 리턴
}

이 코드와 같이 자식노드를 먼저 탐색한 다음 부모노드의 가중치를 수정하면 된다. 위 코드의 19~22번줄과 같이 dfs(child)로 자식노드를 먼저 탐색하고서 부모노드의 가중치를 수정하고 자식노드를 0으로 만드는 것을 알 수 있다. 현재 노드의 자식노드가 있을 때 그 자식노드가 리프노드가 아닐 경우 리프노드부터 차례대로 0으로 만들어 올라오며 수정된 가중치를 가지고 현재 노드를 수정해야하기 때문이다. 이렇게 가중치를 계산하는 부분의 위치만 잘 생각하면 쉽게 풀 수 있는 문제였다.

 

채점 결과

 

채점 결과

https://programmers.co.kr/learn/courses/30/lessons/42892

 

코딩테스트 연습 - 길 찾기 게임

[[5,3],[11,5],[13,3],[3,5],[6,1],[1,3],[8,6],[7,2],[2,2]] [[7,4,6,9,1,8,5,2,3],[9,6,5,8,1,4,3,2,7]]

programmers.co.kr

문제 설명

이 문제는 좌표로 주어지는 노드들을 트리로 이진 트리로 만들어 전위 순회, 후위 순회 결과를 리턴하는 문제이다. 예시는 다음과 같다.

문제 예시

첫 번째 그림과 같이 주어진 좌표를 아래의 그림처럼 이진 트리 구조로 만드는 것이다. 트리를 구성하는 노드들의 규칙은 다음과 같다.

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

위 예시의 전위 순회와 후위 순회의 결과는 다음과 같이 나온다.

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

 

풀이

이 문제를 풀기 위해서 우선 트리를 구현해야한다. 연결리스트를 이용하여 구현하면 복잡해진다고 판단하여 배열을 이용하였다.

 

1. 트리의 표현

    pair<int,int>로 구성된 배열을 이용하여 first에는 왼쪽 자식, second에는 오른쪽 자식의 번호를 저장하였다. 트리를 사용하기 위해서는 트리를 초기화해야한다. 자식을 가리키는 인덱스 번호가 자기 자신과 같다면 리프노드로 판단하도록 초기화하였다.

 

2. 노드 순서

    입력으로 주어진 노드 정보들을 루트 노드부터 순서대로 정렬해야한다. 이때 주어진 배열의 (인덱스 번호 + 1)이 노드의 번호이기 때문에 주어진 배열 자체를 정렬시키면 안되고 인덱스 번호와 좌표를 유지해야한다. 따라서 각 노드의 번호를 통해 좌표를 쉽게 찾기 위해 map을 사용하였고 전위 순회 방식으로 노드를 정렬하기 위해 정렬을 위한 node라는 배열을 따로 선언하였다.

 

3. 트리 구현

    그 다음에는 정렬된 노드를 순서대로 트리에 붙여나가야한다. 트리를 만드는 find함수를 따로 구현하였다. find 함수는 다음과 같이 동작한다.

  1. 현재 노드의 x좌표보다  추가하는 노드의 x좌표가 작은 경우(왼쪽 자식인 경우)
    • 만약 왼쪽 자식이 자기 자신일 때(리프노드일 때) 왼쪽 자식에 붙인다.
    • 그렇지 않으면 재귀적으로 왼쪽 자식과 현재 노드를 방금 과정과 똑같이 비교한다.
  2. 현재 노드의 x좌표보다 추가하는 노드의 x좌표가 큰 경우(오른쪽 자식인 경우)
    • 만약 오른쪽 자식이 자기 자신일 때(리프노드일 때) 오른쪽 자식에 붙인다.
    • 그렇지 않으면 재귀적으로 오른쪽 자식과 현재 노드를 방금 과정과 똑같이 비교한다.

4. 전위 순회와 후위 순회

    전위 순회는 왼쪽 오른쪽 자식을 탐색하기 전에 자기 자신을 경로에 넣으면 된다. 반대로 후위 순회는 왼쪽 오른쪽 자식을 탐색하고 난 이후에 자기 자신을 경로에 넣으면 된다.

 

소스코드

 

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

using namespace std;

pair<int,int> tree[10001];
map<int,pair<int,int>> m;

vector<int> frontpath; //전위순회 경로
vector<int> endpath; //후위순회 경로

struct edge { //정렬을 위한 구조체
    int idx, x, y;
};

bool cmp(edge a, edge b) { //루트노드부터 순서대로 정렬하는 비교함수
    if(a.y == b.y) {
        return a.x < b.x;
    }
    else
        return a.y > b.y;
}

void find(int now, edge e) { //적절한 위치 찾기
    pair<int,int> nowpos = m[now];
    if(nowpos.first > e.x) { //왼쪽자식인 경우
        if(tree[now].first == now) { //왼쪽 자식 끝인 경우
            tree[now].first = e.idx; //왼쪽 자식에 붙이기
            return;
        }
        else { //끝이 아닌 경우
            find(tree[now].first, e); //재귀적으로 탐색
        }
    }
    else { //오른쪽 자식인 경우
        if(tree[now].second == now) { //오른쪽 자식 끝인 경우
            tree[now].second = e.idx;
            return;
        }
        else {
            find(tree[now].second, e);
        }
    }
    return;
}

void front(int now) { //전위순회
    frontpath.push_back(now); //경로 먼저 추가
    if(tree[now].first != now) { //리프노드가 아니라면
        front(tree[now].first); //왼쪽 자식 탐색
    }
    if(tree[now].second != now) {//리프노드가 아니라면
        front(tree[now].second); //오른쪽 자식 탐색
    }
}

void end(int now) { //후위순회
    if(tree[now].first != now) { //리프노드가 아니라면
        end(tree[now].first); //왼쪽 자식 탐색
    }
    if(tree[now].second != now) { //리프노드가 아니라면
        end(tree[now].second); //오른쪽 자식 탐색
    }
    endpath.push_back(now); //경로 나중에 추가
}

vector<vector<int>> solution(vector<vector<int>> nodeinfo) {
    vector<vector<int>> answer;
    vector<edge> node; //정렬을 위한 배열
    
    for(int i = 1; i <= nodeinfo.size(); i++) { //트리 초기화
        tree[i].first = i; //왼쪽 자식을 자기 자신으로
        tree[i].second = i; //오른쪽 자식을 자기 자신으로
    }
    
    for(int i = 0;i < nodeinfo.size(); i++) { 
        node.push_back({i+1, nodeinfo[i][0], nodeinfo[i][1]}); //정렬을 위한 배열
        m[i+1] = {nodeinfo[i][0], nodeinfo[i][1]}; //각 노드 번호별 x, y좌표 map에 저장
    }
    sort(node.begin(), node.end(), cmp); //y좌표가 큰순, x좌표가 작은순으로 정렬
    
    int root = node[0].idx; //루트 인덱스 번호
    
    for(int i = 1; i < node.size(); i++) {
        edge now = node[i]; //현재 노드
        find(root, now); //자식으로 붙이기
    }
    
    front(root); //전위순회
    end(root); //후위순회
    
    
    answer.push_back(frontpath);
    answer.push_back(endpath);
    
    return answer;
}

 

결과

 

+ Recent posts