1. 문제 / 난이도 : Level 4

Summer/Winter Coding(2019)

programmers.co.kr/learn/courses/30/lessons/62050

 

코딩테스트 연습 - 지형 이동

[[1, 4, 8, 10], [5, 5, 5, 5], [10, 10, 10, 10], [10, 10, 10, 20]] 3 15 [[10, 11, 10, 11], [2, 21, 20, 10], [1, 20, 21, 11], [2, 1, 2, 1]] 1 18

programmers.co.kr


2. 풀이 및 코드 설명

처음에는 대충 원하는 대로 구현했다가 참패... 20%빼고 런타임에러를 맞는 기적을 경험했다. 유력한 가설은 아마 시간초과나 메모리 초과가 아닐까...

그 다음 어떻게 짜야할 지 감이 잡히지 않아서 결국 검색의 도움을 빌렸다. 다른 분들의 풀이를 참고해서 아래와 같이 해결하였다.

 

1. 같은 그룹으로 묶일 수 있는 칸끼리 묶기 (BFS)

2. 각 그룹간의 이동 거리 구하기

3. 최소 이동 거리 구하기 (MST)

 

(1) 초기화 및 설정

N = len(land) 
check = [[0 for _ in range(N)] for _ in range(N)] # 각 칸의 그룹

(2) BFS

group = 1
    for i in range(N):
        for j in range(N):
            if not check[i][j]: # 아직 그룹이 정해지지 않은 칸이라면
                BFS(land, height, check, [i, j], group, N)
                group += 1
  • N*N의 칸을 모두 돌면서 아직 그룹이 정해지지 않은 칸이 있다면 해당 칸을 기점으로 BFS를 수행
def BFS(land, height, check, loc, group, N):
    direction = [(0, -1), (0, 1), (1, 0), (-1, 0)]
    q = deque()
    q.append(loc)
    check[loc[0]][loc[1]] = group

    while q:
        x, y = q.popleft()
        for dx, dy in direction:
            nx, ny = x + dx, y + dy
            if nx < 0 or nx >= N or ny < 0 or ny >= N or check[nx][ny]: continue
            if abs(land[nx][ny] - land[x][y]) <= height: # 이동하려는 칸과의 높이 차가 작다면
                q.append([nx, ny]) # 큐에 넣기
                check[nx][ny] = group # 그룹 표시

(3) 각 그룹간의 이동거리 계산

def find_ladder(check, land, N):
    direction = [(0, -1), (0, 1), (1, 0), (-1, 0)]
    ladders = defaultdict(lambda: math.inf) # 최대값으로 초기화
    
    for i in range(N):
        for j in range(N):
            current = check[i][j]
            for dx, dy in direction:
                nx, ny = i + dx, j + dy
                if nx < 0 or nx >= N or ny < 0 or ny >= N or check[nx][ny] == current: continue # 같은 그룹에 속해 있다면 건너뜀
                dist = abs(land[i][j] - land[nx][ny])
                ladders[(current, check[nx][ny])] = min(dist, ladders[(current, check[nx][ny])]) # 최소거리로 갱신
    return ladders
  • ladders : {(x, y): value} 형태로 되어있는 딕셔너리. x그룹에서 y그룹으로 이동할 때 필요한 사다리의 비용을 가지고 있음

 

(4) MST

def kruskal(ladders, group):
    sum = 0
    roots = {_:_ for _ in range(1, group)} # 정점들 root 표시
    for (x, y), value in ladders:
       if find_root(x, roots) != find_root(y, roots): # root가 같지 않을 경우
           union_find(x, y, roots) # root 연결
           sum += value # 값 갱신
       if len(roots.items()) == 1: return sum # 노드들이 전부 연결된 경우 리턴
    return sum
  • 크루스칼 알고리즘으로 MST 구현
  • Union-find를 적용해서 각 노드의 루트로 올라가 루트가 같지 않을 경우에 값을 갱신해주었다!

3. 전체 코드

from collections import deque, defaultdict
import math

def find_root(x, root):
    if x == root[x]: return x
    else:
        r = find_root(root[x], root)
        root[x] = r
        return r

def union_find(x, y, root):
    x_root = find_root(x, root)
    y_root = find_root(y, root)
    root[y_root] = x_root

def BFS(land, height, check, loc, group, N):
    direction = [(0, -1), (0, 1), (1, 0), (-1, 0)]
    q = deque()
    q.append(loc)
    check[loc[0]][loc[1]] = group

    while q:
        x, y = q.popleft()
        for dx, dy in direction:
            nx, ny = x + dx, y + dy
            if nx < 0 or nx >= N or ny < 0 or ny >= N or check[nx][ny]: continue
            if abs(land[nx][ny] - land[x][y]) <= height: 
                q.append([nx, ny])
                check[nx][ny] = group

def find_ladder(check, land, N):
    direction = [(0, -1), (0, 1), (1, 0), (-1, 0)]
    ladders = defaultdict(lambda: math.inf)
    
    for i in range(N):
        for j in range(N):
            current = check[i][j]
            for dx, dy in direction:
                nx, ny = i + dx, j + dy
                if nx < 0 or nx >= N or ny < 0 or ny >= N or check[nx][ny] == current: continue
                dist = abs(land[i][j] - land[nx][ny])
                ladders[(current, check[nx][ny])] = min(dist, ladders[(current, check[nx][ny])])
    return ladders

def kruskal(ladders, group):
    sum = 0
    roots = {_:_ for _ in range(1, group)}
    for (x, y), value in ladders:
       if find_root(x, roots) != find_root(y, roots):
           union_find(x, y, roots)
           sum += value
       if len(roots.items()) == 1: return sum
    return sum

def solution(land, height):
    answer = 0
    N = len(land)
    check = [[0 for _ in range(N)] for _ in range(N)]
    
    group = 1
    for i in range(N):
        for j in range(N):
            if not check[i][j]: 
                BFS(land, height, check, [i, j], group, N)
                group += 1
    
    ladders = find_ladder(check, land, N)
    ladders = sorted(ladders.items(),key=lambda x:x[1])
    
    answer = kruskal(ladders, group)
    return answer

1. 문제 / 난이도 : Level 2

programmers.co.kr/learn/courses/30/lessons/42885

 

코딩테스트 연습 - 구명보트

무인도에 갇힌 사람들을 구명보트를 이용하여 구출하려고 합니다. 구명보트는 작아서 한 번에 최대 2명씩 밖에 탈 수 없고, 무게 제한도 있습니다. 예를 들어, 사람들의 몸무게가 [70kg, 50kg, 80kg, 5

programmers.co.kr


2. 풀이 및 코드 설명

모든 사람들을 몸무게 순으로 정렬했을 때, 가장 몸무게가 가벼운 사람 + 가장 몸무게가 무거운 사람이 무게 제한보다 크다면 가장 몸무게가 무거운 사람은 아무와도 구명보트를 같이 탈 수 없다.

따라서, 그 경우에는 구명보트의 카운트를 1 증가 시켜주고 가장 몸무게가 가벼운 사람 + 2번째로 가장 몸무게가 무거운 사람과 무게제한을 비교하는 식으로 계속 반복해나가면 답을 얻을 수 있다.

 

간단한 코드라, 코드 블럭 단위의 설명은 생략.


3. 전체 코드

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


int solution(vector<int> people, int limit) {
	int answer = 0;
	sort(people.begin(), people.end()); // people 정렬
	int lo = 0; // 가장 몸무게가 가벼운 사람의 index
	int hi = people.size() - 1; // 가장 몸무게가 무거운 사람의 index

	while (lo <= hi) {
		if (people[lo] + people[hi] <= limit) { // 가장 몸무게가 가벼운 사람과 가장 무거운 사람이 같이 구명보트를 탈 수 있으므로
			lo++; 
			hi--;
		}
		else { // 그 외에는 가장 무거운 사람 혼자 구명보트를 타야 함
			hi--;
		}
		answer++;
	}

	return answer;
}

1. 문제 / 난이도 : Level 2

programmers.co.kr/learn/courses/30/lessons/42860?language=cpp

 

코딩테스트 연습 - 조이스틱

조이스틱으로 알파벳 이름을 완성하세요. 맨 처음엔 A로만 이루어져 있습니다. ex) 완성해야 하는 이름이 세 글자면 AAA, 네 글자면 AAAA 조이스틱을 각 방향으로 움직이면 아래와 같습니다. ▲ - 다

programmers.co.kr


2. 풀이 및 코드

처음에는 굉장히 단순하게 오른쪽으로 이동하는 것보다 왼쪽으로 이동하는 게 적은 경우는 _A______형태로 name[1] == 'A' 일 때 뿐이라고 생각했다.

그래서 호다닥 풀어서 냈으나, 결과는 당연히 실패....

정확하게 풀기 위해서는 다음과 같은 반례를 고려해야 한다.

name = "BBBAAAB" / answer = 9
name = "ABABAAAAABA" / answer = 11

요점은 쭉 오른쪽으로 가다가 어느 순간 왼쪽으로 되돌아갈 때가 최적인 경우도 있다는 것!

결국 최적의 경우를 항상 선택해야하므로 Greedy 다.

 

(1) 초기화 및 설정

answer = 0;
int N = name.length(); // name 길이
int a_num = 'A' - '0'; // 'A'의 ASCII
string compare(N, 'A'); // 바꾸고자 하는 문자열
  • a_num : 알파벳을 바꿀 때 위, 아래 버튼을 몇 번 눌러야하는지를 간편히 계산하기 위해 ASCII 코드를 이용.
    • ex ) 'A' = 48, 'B' = 49 이므로 ASCII 코드의 차를 이용해 버튼을 몇 번 눌러야하는지 계산할 수 있음.
  • compare : 'A'를 N만큼 가지고 있는 문자열. 나중에 While 종료조건(compare == name)을 위해 사용.
    • ex ) name = 'JAZ'. 'compare' = 'AAA

(2) 최적 방향 찾기

int check_move(int N, int i, string name, string compare) {
	int left = 0; int right = 0; int index = 0;
	for (int j = i + 1; j < i + N; j++) { // 오른쪽으로 이동하는 경우
		if (name[j % N] != compare[j % N]) { // name과 compare가 달라질 때까지 이동
			right = j - i; // 오른쪽으로 몇 번 이동했는지
			index = j % N; // 그때의 인덱스
			break;
		}
	}

	for (int j = i - 1; j > i - N; j--) { // 왼쪽으로 이동하는 경우
		if (name[(N + j) % N] != compare[(N + j) % N]) {
			left = i - j;
			if(left < right) index = (N + j) % N; // 왼쪽으로 이동하는 경우가 더 최적이면, 해당 위치로 인덱스 갱신
			break;
		}
	}
	answer += min(left, right); // 왼쪽 이동과 오른쪽 이동 중에 더 작은 값 더하기
	return index; // 그 때의 인덱스 반환
}
  • 왼쪽으로 이동하는 것과 오른쪽으로 이동하는 것 중 어느 방향으로 이동하는 것이 더 최적인지를 계산하기 위한 함수.
  • 맨 끝 문자에서 +1 을 하면 맨 처음으로, 맨 처음 문자에서 -1을 하면 맨 끝으로 이동해야하기 때문에 모듈러 연산 사용.
    • ex ) N = 7, j = 8 이라면, name[j % N] = name[8 % 7] = name[1]
  • 최적의 이동 수만큼 answer에 더해주고, 그 때 도착한 인덱스 반환

(3) 원하는 문자열이 될 때까지 갱신하며 반복

int i = 0;
	while (name != compare) {
		if(name[i] == compare[i]) i = check_move(N, i, name, compare); // 현재 인덱스의 name과 compare가 같으면 이동
		if (name[i] != 'A') {
			int move = (name[i] - '0') - a_num; // 얼마나 위 버튼을 눌러야 하는지
			if (move > 13) move = 26 - move; // 중간값인 13보다 크다면 아래 버튼을 누르는 게 이득
			answer += move;
			compare[i] = name[i]; // compare 갱신
		}
	}
	return answer;

3. 전체 코드

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

int answer;

int check_move(int N, int i, string name, string compare) {
	int left = 0; int right = 0; int index = 0;
	for (int j = i + 1; j < i + N; j++) {
		if (name[j % N] != compare[j % N]) {
			right = j - i;
			index = j % N;
			break;
		}
	}

	for (int j = i - 1; j > i - N; j--) {
		if (name[(N + j) % N] != compare[(N + j) % N]) {
			left = i - j;
			if(left < right) index = (N + j) % N;
			break;
		}
	}
	answer += min(left, right);
	return index;
}

int solution(string name) {
	answer = 0;
	int N = name.length();
	int a_num = 'A' - '0';
	string compare(N, 'A');

	int i = 0;
	while (name != compare) {
		if(name[i] == compare[i]) i = check_move(N, i, name, compare);
		if (name[i] != 'A') {
			int move = (name[i] - '0') - a_num;
			if (move > 13) move = 26 - move;
			answer += move;
			compare[i] = name[i];
		}
	}
	return answer;
}

1. 문제 / 난이도 : Level 3

programmers.co.kr/learn/courses/30/lessons/59042

 

코딩테스트 연습 - 없어진 기록 찾기

ANIMAL_INS 테이블은 동물 보호소에 들어온 동물의 정보를 담은 테이블입니다. ANIMAL_INS 테이블 구조는 다음과 같으며, ANIMAL_ID, ANIMAL_TYPE, DATETIME, INTAKE_CONDITION, NAME, SEX_UPON_INTAKE는 각각 동물의 아이디

programmers.co.kr

2. 풀이 및 전체 코드

SQL문제들은 책정된 난이도보다 체감난이도는 훨씬 평이한 편. 해당 문제도 어렵지 않게 풀 수 있었음.

JOIN 카테고리로 분류되어있었는데 결과적으로는 JOIN을 사용하지 않고 NOT IN을 이용해서 풀었다.

SELECT 
  ANIMAL_ID, 
  NAME
FROM ANIMAL_OUTS
WHERE 
  ANIMAL_ID NOT IN (
    SELECT ANIMAL_ID 
    FROM ANIMAL_INS
  );
  • 입양은 간 기록은 있는데, 보호소에 들어온 기록이 없다 => ANIMAL_OUTS 테이블에는 있지만, ANIMAL_INS에는 없다.

+ Recent posts