문제

  • 문제 링크
  • 문제 설명: MxM 2차원 배열인 열쇠를 회전/이동 하여 NxN 2차원 배열인 자물쇠의 모든 홈 부분과 열쇠의 돌기가 딱 맞게 매칭 시킬 수 있으면 true, 아니면 false를 반환하는 함수 작성.
    • 자물쇠의 돌기와 열쇠의 돌기가 마주치면 안됌.
    • 열쇠의 범위가 자물쇠 범위 밖에 있을 경우 영향을 주지 않음.
  • 입력
    • key는 M x M(3 ≤ M ≤ 20, M은 자연수)크기 2차원 배열
    • lock은 N x N(3 ≤ N ≤ 20, N은 자연수)크기 2차원 배열
    • M은 항상 N 이하
    • key와 lock의 원소는 0 또는 1로 이루어져 있습니다.
      • 0은 홈 부분, 1은 돌기 부분을 나타냅니다.
  • 출력: 매칭이 되면 true, 아니면 false
key lock result
[[0, 0, 0],
[1, 0, 0],
[0, 1, 1]]
[[1, 1, 1],
[1, 1, 0],
[1, 0, 1]]
true

풀이

  • 2차원 배열의 크기가 크지 않기 때문에 naive하게 열쇠를 돌리는 함수(rotate)와 1칸씩 움직이면서 열쇠와 자물쇠가 겹치는 부분에 대해 모든 원소가 매칭이 되는지 검사하는 함수(canLock)를 호출
    • 말은 쉬운데 구현하는데 시간이 오래 걸렸던 문제
  • rotate 함수를 작성할 때 열쇠의 배열 크기 M이 홀수이면 맨 중앙에 있는 값은 그대로 복사해주어야 함
  • canLock 함수를 최대한 간단하게 구현하기 위해 각 열쇠와 자물쇠가 겹치는 위치의 좌표만 받음.
    • canLock(pii& ks, pii& ls, int yGap, int xGap) := 열쇠의 좌측상단 좌표 ks, 자물쇠의 좌측상단 좌표 ls, 그리고 y축 오프셋과 x축 오프셋.
  • 움직일 때는 열쇠의 맨 끝 원소[(N-1,N-1) 좌표]와 자물쇠의 첫번째 원소[(0,0) 좌표]를 맞물리는 것을 시작으로 x축으로 끝까지 이동하고 다 이동하면 y축으로 한 칸씩 늘리면서 진행 M 은 항상 N 이하이기 때문에 M이 N에 포함될 때 열쇠의 겹치는 구간은 변함이 없지만 자물쇠의 구간은 계속 바꾸어 주어야 하는데 주의
  • 이런 문제는 단정문(assertion)이 불가피하다.
  • 움직이면서 canLock 이 1번이라도 true이면 true를 반환하도록 구현함
#include <string>
#include <vector>
#include <cassert>
#define Y first
#define X second
using namespace std;
typedef pair<int,int> pii;

// 자물쇠를 시계 방향으로 90도 회전하는 함수
void rotate(vector<vector<int>>& old_key) {
    int n = old_key.size(), i = 0;
    vector<vector<int>> new_key(n, vector<int> (n, 0));
    for (; i < n >> 1; i++) { // 가장자리에서부터 회전할 때 최대 깊이는 길이의 절반
        for (int j = i; j < n-i; j++) {
            // top (i,i)~(i,n-i-1) -> right-side (i,n-i-1)~(n-i-1,n-i-1)
            new_key[j][n-i-1] = old_key[i][j];
            // right-side (i,n-i-1)~(n-i-1,n-i-1) -> bottom (n-i-1,n-i-1)~(n-i-1,i)
            new_key[n-i-1][n-j-1] = old_key[j][n-i-1];
            // bottom (n-i-1,i)~(n-i-1,n-i-1) -> left-side (i,i)~(n-i-1,i)
            new_key[j][i] = old_key[n-i-1][j];
            // left-side (i,i)~(n-i-1,i) -> top (i,n-i-1)~(i,i)
            new_key[i][n-j-1] = old_key[j][i];
        }
    }
    if (n & 1) new_key[i][i] = old_key[i][i]; // 길이가 홀수이면 맨 중앙값은 그대로 복사
    old_key = new_key;
}

int numZerosOfLock;

// 겹치는 구간에 모든 자물쇠의 홈이 문제없이 매칭된다면 true 반환
bool canLock(vector<vector<int>>& key,
             vector<vector<int>>& lock,
             pii& ks, pii& ls, int yGap, int xGap)
{
    int numMatches = 0;
    for (int dy = 0; dy <= yGap; dy++) {
        for (int dx = 0; dx <= xGap; dx++) {
            auto& kVal = key[ks.Y + dy][ks.X + dx];
            auto& lVal = lock[ls.Y + dy][ls.X + dx];
            if (kVal == lVal) return false;
            if (kVal && !lVal) numMatches++;
        }
    }
    return numMatches == numZerosOfLock;
}

bool solution(vector<vector<int>> key, vector<vector<int>> lock) {
    int n = lock.size();
    int m = key.size();
    numZerosOfLock = 0;
    for (int y = 0; y < n; y++)
        for (int x = 0; x < n; x++)
            if (lock[y][x] == 0)
                numZerosOfLock++; // 맨 처음에 자물쇠의 홈의 개수 계산
    // 매 회전마다 시도
    for (int i = 0; i < 4; i++) {
        rotate(key);
        pii ks, ke, ls, le;
        ks.Y = ke.Y = m-1;
        ls.Y = le.Y = 0;
        while (ke.Y >= 0) {
            ls.X = le.X = 0;
            ks.X = ke.X = m-1;
            while (ke.X >= 0) { // X축으로 1칸씩 이동하면서 겹치는 구간을 검사
                if (canLock(key, lock, ks, ls, ke.Y-ks.Y, ke.X-ks.X))
                    return true;
                if (ks.X > 0) ks.X--; // 열쇠는 자물쇠 배열에서 왼->오 이동할 때 x좌표가 0으로 수렴
                else ls.X++; // 열쇠가 자물쇠의 끝에 도달하면 열쇠의 구간의 시작 좌표 x값을 늘려줌
                if (le.X < n-1) le.X++; // 열쇠가 왼->오 이동할 때 자물쇠 구간의 끝 좌표 x값을 늘려줌
                else ke.X--; // 열쇠가 자물쇠의 끝에 도달하면 열쇠의 구간의 끝 좌표 x값을 줄여줌
                assert (le.X - ls.X == ke.X - ks.X);
            }
            if (ks.Y > 0) ks.Y--; // 열쇠가 자물쇠 배열에서 위->아래 이동할 때 y좌표는 0으로 수렴
            else ls.Y++; // 열쇠가 자물쇠의 끝에 도달하면 열쇠의 구간의 시작 좌표 y값을 늘려줌
            if (le.Y < n-1) le.Y++; // 열쇠가 이동하면 자물쇠 배열의 구간의 끝 좌표 y값을 늘려줌
            else ke.Y--; // 열쇠가 자물쇠의 끝에 도달하면 열쇠의 구간의 끝 좌표 y값을 줄여줌
            assert (le.Y - ls.Y == ke.Y - ks.Y);
        }
    }
    return false;
}

 

 

문제

  • 문제 링크
  • 문제 설명: 릴레이션에 모든 row 가 유니크하도록 하는 컬럼 조합의 개수 반환
    • 어떤 컬럼 조합에 대해 데이터들 중 같은 데이터가 없으면 그 컬럼 조합은 후보키가 될 수 있다.
    • 어떤 컬럼 조합을 포함하는 컬럼 조합은 후보키가 될 수 없다. -> 최소성
  • 입력
    • relation은 2차원 문자열 배열
    • relation의 컬럼(column)의 길이는 1 이상 8 이하이며, 각각의 컬럼은 릴레이션의 속성을 나타냄
    • relation의 로우(row)의 길이는 1 이상 20 이하이며, 각각의 로우는 릴레이션의 튜플을 나타냄
    • relation의 모든 문자열의 길이는 1 이상 8 이하이며, 알파벳 소문자와 숫자로만 이루어져 있음
    • relation의 모든 튜플은 유일하게 식별 가능 (즉, 중복되는 튜플은 없다.)
  • 출력: 컬럼 조합의 개수
relation result
[["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

풀이

  • relation을 이용해서 컬럼마다 데이터를 저장 -> 인덱스: 컬럼명, 값: 해당 컬럼의 데이터 목록
  • 컬럼은 최대 8개 있으므로 DFS 를 이용해서 컬럼별 데이터를 조합해서 중복되는 데이터가 없으면 후보키 목록에 추가
    • "1번 컬럼 -> 2번 컬럼" 순서로 조합하는 것과 "2번 컬럼 -> 1번 컬럼" 순서로 조합하면 결과가 같으므로 인자로 현재 조합할 시작 컬럼을 추가해서 중복을 방지
    • 컬럼 개수가 8개이므로 정수형 변수에 비트로 조합한 컬럼 목록 저장 가능
  • 최소성을 만족하는지 찾기 위해, 현재 중복 데이터가 없는 후보키가 이전에 후보키 목록들 중에 포함되는지 검사
    • 후보키 목록들 중 현재 후보키를 포함하는 것들이 있으면 모두 제거
    • 예를 들어, 0 1 2 … 순서로 조합을 만들다 보니 뒤에 5 7 만 있으면 후보키가 되는 경우 앞에 만들어진 후보키들을 없애야 함. 예를 들어 0 1 5 7 이 있고 0 2 3 5 7 이 있으면 둘 다 없애고 5 7만 저장
  • 끝으로 후보키 목록의 크기를 반환
#include <string>
#include <vector>
#include <set>
using namespace std;

vector<string> relation_by_col[8]; // 컬럼별 데이터
int num_cols, num_rows;
set<int> candidates; // 후보키 목록

// c := 컬럼 인덱스
// cand := 조합된 컬럼 인덱스를 비트로 저장한 정수형 변수
// comb := 조합된 컬럼의 데이터들
void dfs(int c, int cand, vector<vector<string>>& comb) {
    if (comb.size() > 0) { // 하나라도 조합된 컬럼이 있으면
        set<string> s;
        for (int i = 0; i < num_rows; i++) {
            string data;
            for (int j = 0; j < comb.size(); j++)
                data += comb[j][i] + " ";
            s.insert(data);
        }
        // 데이터가 중복이 없는지 검사
        if (s.size() == num_rows) {
            // 현재 후보키를 포함하는 후보키들은 모두 제거
            while (1) {
                bool minimality = true;
                for (auto& c : candidates) {
                    if ((c & cand) == cand) {
                        candidates.erase(c);
                        minimality = false;
                        break;
                    }
                }
                // 최소성을 만족하면 break
                if (minimality) break;
            }
            candidates.insert(cand); // 후보키에 추가
            return;
        }
    }
    
    for (int i = c; i < num_cols; i++) {
        comb.push_back(relation_by_col[i]);
        dfs(i+1, cand | (1 << i), comb); // 컬럼 조합 생성
        comb.pop_back();
    }
}

int solution(vector<vector<string>> relation) {
    int answer = 0;
    num_rows = relation.size();
    num_cols = relation[0].size();
    for (int i = 0; i < relation.size(); i++)
        for (int j = 0; j < num_cols; j++)
            relation_by_col[j].push_back(relation[i][j]);
    vector<vector<string>> comb;
    dfs(0, 0, comb);
    answer = candidates.size();
    return answer;
}

 

 

 

문제

  • 문제 링크
  • 문제 설명: 지원자가 지원서에 입력한 4가지의 정보(개발언어, 직군, 경력, 소울푸드)와 획득한 코딩테스트 점수를 하나의 문자열로 구성한 값의 배열 info, 개발팀이 궁금해하는 문의조건이 문자열 형태로 담긴 배열 query가 매개변수로 주어질 때, 각 문의조건에 해당하는 사람들의 숫자를 순서대로 배열에 담아 반환하는 함수 작성
  • 입력
    • info 배열의 크기는 1 이상 50,000 이하
    • 개발언어는 cpp, java, python 중 하나
    • 직군은 backend, frontend 중 하나
    • 소울푸드는 chicken, pizza 중 하나
    • 점수는 코딩테스트 점수를 의미하며, 1 이상 100,000 이하인 자연수
    • query 배열의 크기는 1 이상 100,000 이하
    • query의 각 문자열은 "[조건] X" 형식
    • '-' 표시는 해당 조건을 고려하지 않겠다는 의미
    • X는 코딩테스트 점수를 의미하며 조건을 만족하는 사람 중 X점 이상 받은 사람은 모두 몇 명인 지를 의미
  • 출력: 매 query마다 조건을 만족하는 사람 수를 배열에 담아 반환
info query result
["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]

풀이

  • query 배열의 크기가 최대 10만이기 때문에 매 쿼리에 info 배열을 순회하는 것은 시간초과가 발생한다. 따라서 O(logN) 으로 조건을 만족하는 사람을 찾도록 info 배열을 전처리 해주어야 한다.
  • logN 의 시간으로 탐색하는 방법은 이진 탐색이 있다. 이진 탐색은 탐색 대상이 오름차순으로 정렬되었음을 가정한다.
  • 4가지 지원자의 정보로 조합 가능한 조건들은 최대 (3+1) * (2+1) * (2+1) * (2+1) = 4 * 3 * 3 * 3 = 108가지이다. 1을 더한 것은 '-'을 포함한 것이다.
  • map<string, vector<int>> 자료형을 이용해서 108가지의 조건 각각을 키로 가지고 조건에 해당되는 지원자의 점수를 값으로 가지게 하면 정렬 후 이진 탐색으로 원소의 위치를 알아내서 컨테이너 끝하고 오프셋으로 사람 수를 계산할 수 있게 된다.
    • 시간 절약을 위해 unordered_map 을 활용하기로 했다.
#include <string>
#include <sstream>
#include <vector>
#include <set>
#include <unordered_map>
#include <algorithm>
using namespace std;

// 각 조건과 해당되는 점수 목록을 저장할 자료구조
unordered_map<string, vector<int>> m;

// 지원자 정보로 가능한 조건을 조합
void dfs(int pos, vector<string>& key, int& score, string s) {
    if (pos == key.size()) {
        m[s].push_back(score); // 생성된 조건마다 지원자의 점수 저장
        return;
    }
    dfs(pos + 1, key, score, s + key[pos]);
    dfs(pos + 1, key, score, s + "-");
}

vector<int> solution(vector<string> info, vector<string> query) {
    vector<int> answer;
    vector<string> key(4);
    int score;
    for (auto& i : info) {
        istringstream iss(i);
        iss >> key[0] >> key[1] >> key[2] >> key[3] >> score;
        dfs(0, key, score, ""); // 가지고 있는 조건들로 가능한 조건 조합을 생성
    }
    
    for (auto it = m.begin(); it != m.end(); it++)
        sort(it->second.begin(), it->second.end()); // 각 조건마다 지원자의 점수들을 오름차순 정렬
    
    for (auto& q : query) {
        istringstream iss(q);
        string tmp; // and
        iss >> key[0] >> tmp >> key[1] >> tmp >> key[2] >> tmp >> key[3] >> score;
        auto it = m.find(key[0] + key[1] + key[2] + key[3]);
        if (it == m.end()) answer.push_back(0); // 없는 조건이면 0명
        else { // 있는 조건이면 이진 탐색으로 오프셋 계산
            // lower_bound는 score보다 이상인 값들 중 가장 작은 값(하한)의 위치를 반환
            auto vit = lower_bound(it->second.begin(), it->second.end(), score);
            int num_applicants = it->second.end() - vit;
            answer.push_back(num_applicants);
        }
    }
    return answer;
}

 

문제

  • 문제 링크
  • 문제 설명: 각 손님이 이전에 주문한 메뉴 조합들(orders)과 앞으로 만들 메뉴 조합들 각각의 단품 메뉴 개수 목록(course)이 주어질 때, 이전에 2번 이상 주문된 단품 메뉴들 중 원하는 개수만큼 메뉴를 조합할 때 각 코스별 가장 많이 주문된 것들을 반환하는 함수를 작성하기
  • 입력
    • orders 배열의 크기는 2 이상 20 이하
    • orders 배열의 각 원소는 크기가 2 이상 10 이하인 문자열
    • 각 문자열은 알파벳 대문자로만 구성
    • 각 문자열에는 같은 알파벳이 중복해서 들어있지 않음
    • course 배열의 크기는 1 이상 10 이하
    • course 배열의 각 원소는 2 이상 10 이하인 자연수가 오름차순으로 정렬
    • course 배열에는 같은 값이 중복해서 들어있지 않음
  • 출력
    • 정답은 각 코스요리 메뉴의 구성을 문자열 형식으로 배열에 담아 사전 순으로 오름차순 정렬해서 반환
    • 배열의 각 원소에 저장된 문자열 또한 알파벳 오름차순으로 정렬되어야 함
    • 만약 가장 많이 함께 주문된 메뉴 구성이 여러 개라면, 모두 배열에 담을 것
orders course result
["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"]

풀이

  • 먼저 단품 메뉴마다 몇 명이 주문했는지 알기 위해 단품 메뉴별 주문한 사람 수(freq)를 구함
  • course 목록에 있는 개수대로 메뉴를 조합. (조합을 구현할 때는 항상 DFS)
    • dfs(pos, comb, ...) 에서 pos가 필요한 이유: 메뉴 A -> 메뉴 B 와 메뉴 B -> 메뉴 A 는 동일하며 중복을 없애기 위해 다음 호출 시 pos 를 1씩 증가시켜준다. 게다가 오름차순 정렬도 자연스레 된다.
  • 메뉴를 조합한 후 결과가 여러 개일 때 가장 많이 주문한 것들을 구해야 함.
    • orders의 원소는 알파벳 대문자만 포함하기 때문에 단품 메뉴는 최대 26개
    • orders 배열의 크기는 최대 20이므로 각 메뉴를 주문한 최대 사람 수도 20이 된다.
    • 32보다 작은 숫자이므로 32비트 정수형 변수에 비트로 저장하면 AND 연산으로 메뉴 A와 메뉴 B를 주문한 사람을 쉽게 알아낼 수 있게 된다.
      • freq[i] 는 i번 메뉴 (=메뉴 i + 'A')를 주문한 사람의 인덱스번째 비트를 1로 가짐
      • orders를 순회할 때마다 OR 연산으로 해당 단품 메뉴를 주문한 사람 목록을 쉽게 구할 수 있다.
      • __builtin_popcount() 를 활용하여 1-bit의 개수를 얻으면 주문한 사람 수도 쉽게 얻을 수 있다.
    • course 배열마다 dfs를 호출할 필요 없이 dfs를 1번 호출해서 단품 메뉴의 조합 개수를 늘릴 때마다 course에 있는 개수인지 검사해서 있으면 해당 메뉴 조합을 주문한 사람 수를 저장
      • 메뉴 A와 메뉴 B를 주문한 사람의 목록은 freq[0] & freq[1] 을 해서 구하면 된다. dfs 를 재귀호출할 때마다 인자로 넘기기
    • 메뉴 조합의 개수별로 dfs로 구한 전체 메뉴 조합을 나눈다. set 을 쓰면 삽입하면서 주문한 사람수 오름차순으로 자동 정렬할 수 있다.
    •  각 조합 개수마다 가장 많이 주문한 것들을 answer 배열에 저장한다.
  • 정답 코드
#include <string>
#include <vector>
#include <set>
#include <algorithm>
using namespace std;

void dfs(int pos, string comb, int found, vector<int>& freq,
         vector<int>& course, vector<pair<int, string>>& ret)
{
    // course 에 있는 메뉴 조합의 개수이면
    if (binary_search(course.begin(), course.end(), (int)comb.size())) {
        // 해당 조합에 포함된 단품메뉴를 주문한 사람 수 계산
        int num_found = __builtin_popcount(found);
        // set에 오름차순을 위해 -1을 곱함
        if (num_found > 1) ret.emplace_back(-num_found, comb);
    }
    
    for (int i = pos; i < 26; i++)
        if (__builtin_popcount(freq[i]) > 1) // 단품 메뉴 주문 수 > 1 일 때만 포함
            dfs(i+1, comb + char(i+'A'), found & freq[i], freq, course, ret);
}

vector<string> solution(vector<string> orders, vector<int> course) {
    vector<string> answer;
    
    // 주문한 사람 목록 구하기
    vector<int> freq(26, 0);
    for (int i = 0; i < orders.size(); i++)
        for (int j = 0; j < orders[i].size(); j++)
            freq[orders[i][j]-'A'] |= (1 << i);
    
    // 모든 가능한 메뉴 조합 구하기
    vector<pair<int, string>> ret;
    dfs(0, "", (1<<21)-1, freq, course, ret);
    
    // 조합 메뉴 개수별로 조합 목록 나누기
    set<pair<int,string>> combinations[11];
    for (int i = 0; i < ret.size(); i++)
        combinations[ret[i].second.length()].insert(ret[i]);
    
    // 개수마다 최대 주문량을 가진 메뉴 조합 저장
    for (int i = 2; i < 11; i++) {
        if (combinations[i].size() == 0) continue;
        auto it = combinations[i].begin();
        int max_orders = it->first;
        while (it != combinations[i].end() and it->first == max_orders) {
            answer.push_back(it->second);
            it++;
        }
    }
    
    // 정렬해서 반환
    sort(answer.begin(), answer.end());
    return answer;
}

 

+ Recent posts