문제

  • 문제 링크
  • 문제 설명: A, B 두 사람이 s에서 출발해서 각각의 도착 지점(a, b)까지 택시를 타고 간다고 가정할 때, 최저 예상 택시요금을 계산해서 반환하는 함수를 작성
    • 두 사람이 함께 택시를 탈 경우 비용은 1인분으로 낸다. 만약, 아예 합승을 하지 않고 각자 이동하는 경우의 예상 택시요금이 더 낮다면, 합승을 하지 않아도 된다.
  • 입력
    • fares[i] = [c,d,f] 두 지점 c 와 d 사이의 비용 f
    • 지점갯수 n은 3 이상 200 이하인 자연수
    • 지점 s, a, b는 1 이상 n 이하인 자연수이며, 각기 서로 다른 값
    • 즉, 출발지점, A의 도착지점, B의 도착지점은 서로 겹치지 않습니다.
    • 요금 f는 1 이상 100,000 이하인 자연수

 

 

  • 출력: 최저 택시 요금
n s a b fares result
6 4 6 2 [[4, 1, 10], [3, 5, 24], [5, 6, 2], [3, 1, 41], [5, 1, 24], [4, 6, 50], [2, 4, 66], [2, 3, 22], [1, 6, 25]] 82
7 3 4 1 [[5, 7, 9], [4, 6, 4], [3, 6, 1], [3, 2, 3], [2, 1, 6]] 14
6 4 5 6 [[2,6,6], [6,3,7], [4,6,7], [6,5,11], [2,5,12], [5,3,20], [2,4,8], [4,3,9]] 18

풀이

  • n개의 지점 중 합승 후 a와 b가 따로 갈 때 비용을 가장 적게 내는 지점을 찾아야 함
  • a와 b가 s에서부터 합승해서 도착한 지점을 x 라고 할 때 cost[s][x] + cost[x][a] + cost[x][b] 가 최소가 되는 x를 찾으면 된다.
  • x가 s일 경우 두 사람은 완전히 합승하지 않는 것이고 x가 a나 b일 경우 둘 중 한 명의 도착지점까지의 거리 중에 다른 한 명의 도착지점이 있는 것 (경로가 겹쳐진 것)
  • 결국 모든 지점 쌍에 대해 최소 비용을 계산해야 하는데 n의 최대 크기가 200이기 때문에 플로이드-와샬로 모든 쌍에 대해 비용을 계산할 수 있음
  • 주의할 점: 요금 f는 1 이상 100,000 이하인 자연수이므로 200 * 200 * 100000 = 4 * 1e9 가 되는데 중간 계산 결과가 정수형을 넘을 수 있으므로 자료형으로 long을 써야 함
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

int solution(int n, int s, int a, int b, vector<vector<int>> fares) {
    long cost[n][n];
    const long INF = 1e9;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            cost[i][j] = i == j ? 0 : INF; // 비용 배열 초기화
    
    for (auto& v : fares)
        cost[v[0]-1][v[1]-1] = cost[v[1]-1][v[0]-1] = v[2];
    
    // 플로이드-와샬 알고리즘
    for (int k = 0; k < n; k++) { // 매 거쳐갈(via) 지점마다 비용을 갱신
        for (int u = 0; u < n; u++) {
            if (cost[u][k] == INF) continue;
            for (int v = 0; v < n; v++)
                cost[u][v] = min(cost[u][v], cost[u][k] + cost[k][v]);
        }
    }
    
    // 최소 비용 계산
    long answer = INF;
    for (int i = 0; i < n; i++)
        answer = min(answer, cost[s-1][i] + cost[a-1][i] + cost[b-1][i]);
    return answer;
}

 

문제

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

 

  • 입력
    • play_time := 00:00:01 이상 99:59:59 이하
    • adv_time <= play_time
    • logs := "H1:M1:S1-H2:M2:S2"
    • logs 크기 <= 300,000
  • 출력: 광고가 재생될 가장 빠른 시작 시각
play_time adv_time logs result
"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"

풀이

  • logs 배열의 크기가 최대 300,000 이므로 O(N) 만에 문제를 해결해야 함
  • 간단하게 누적 재생시간을 계산하려면 해당 시간대에 재생된 사람 수를 카운트 해야 함
  • play_time의 최대 시간을 초단위로 계산해보면 99*3600 + 59*60 + 59 = 359,999 로 배열의 크기가 크지 않음을 알 수 있음
  • 따라서 모든 시간을 초 단위로 바꾼 뒤 매 로그마다 재생된 구간에 사람 수를 카운트해서 슬라이딩 윈도우로 O(N)만에 가장 빠른 시각을 구할 수 있음
  • 주의할 점: 최대 로그 개수만큼 모든 구간이 재생되면 360000 * 300000 으로 32비트를 초과하므로 long long 자료형을 써야 함
#include <cstring>
#include <string>
#include <vector>
using namespace std;
typedef long long ll;

// 시간 문자열을 초 단위로 변환하는 함수
int timeToSeconds(const string& time) {
    int hours = (time[0]-'0')*10 + (time[1]-'0');
    int minutes = (time[3]-'0')*10 + (time[4]-'0');
    int seconds = (time[6]-'0')*10 + (time[7]-'0');
    return hours*3600 + minutes*60 + seconds;
}

string solution(string play_time, string adv_time, vector<string> logs) {
    int seconds[360000];
    memset(seconds, 0, sizeof(seconds));
    for (auto& l : logs) {
        int start = timeToSeconds(l);
        int end = timeToSeconds(l.substr(9));
        for (int i = start; i < end; i++)
            seconds[i]++; // 끝 시간은 포함되지 않으므로 제외하고 재생 사람 수를 카운트
    }
    
    int playTime = timeToSeconds(play_time);
    int advTime = timeToSeconds(adv_time);
    int startTime = 0;
    ll sum = 0, maxSum;
    // 첫 광고시간동안 재생한 사람 수 누적합 계산
    for (int i = 0; i < advTime; i++) sum += seconds[i];
    maxSum = sum;
    // 한 칸씩 윈도우를 옮기면서 최댓값 탐색
    for (int i = advTime; i < playTime; i++) {
        sum = sum - seconds[i-advTime] + seconds[i];
        if (sum > maxSum) {
            startTime = i-advTime+1; // 최댓값이면 시작 시각 저장
            maxSum = sum;
        }
    }
    
    // 초 단위를 다시 문자열 포맷으로 변환
    string answer;
    int times[3] = {startTime / 3600, (startTime % 3600) / 60, (startTime % 3600) % 60};
    for (int i = 0; i < 3; i++) {
        int a = times[i] / 10, b = times[i] % 10;
        answer.push_back(a + '0');
        answer.push_back(b + '0');
        if (i < 2) answer.push_back(':');
    }
    
    return answer;
}

문제

  • 문제 링크
  • 문제 설명: 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;
}

 

 

 

+ Recent posts