Jekyll2025-12-14T07:06:36+00:00https://seminkim.github.io/feed.xmlSemin KimHydejack is a boutique Jekyll theme for hackers, nerds, and academics, with a focus on personal sites that are meant to impress. Semin Kim<[email protected]>문제2022-05-04T08:40:00+00:002022-05-04T08:40:00+00:00https://seminkim.github.io/blog_old/%5BBOJ%5D10605링크

노드 $N$개, 엣지 $M$개인 그래프가 있다. 이 그래프에는 총 K마리의 드래곤이 사는데, 각 드래곤은 처음에 $S_i$개의 머리를 가지고 있으며 매 분마다 $N_i$개의 머리가 새로 생겨난다. 어떤 노드에는 여러 마리의 드래곤이 있을 수도 있고, 드래곤이 없을 수도 있다.

한편 워리어는 매 분마다 (1)드래곤의 머리 하나를 자르거나 (2) 이웃한 노드로 움직일 수 있다. 모든 드래곤을 죽일 수 있는(머리를 모두 자를 수 있는) 워리어의 최소 수를 구하는 문제이다.

접근

대략 생각해보면, 드래곤을 잡기 위해서는 (1)처음부터 드래곤을 기다렸다가 $S_i$명의 워리어가 1분에 머리를 자르는 방법 (2)$N_i+1$명 이상의 워리어를 고용하여 머리가 생기는 속도보다 빠르게 자르는 방법이 있을 것이다.

그렇다면 그 외에, (3)어느정도 드래곤 머리가 생기기를 기다렸다가 워리어가 모여서 처치하는 상황은 생각하지 않아도 될까? 이 부분이 사실 까다로운데, 결론만 보면 그렇다. 이는 다음과 같이 대략적으로 발견할 수 있다. (엄밀한 증명은 아니다)

초기 상태가 아닌 $t$분 후에, 어떤 드래곤의 머리 갯수 $S^t_i$는 항상 $S^t_i \ge N_i$이다(머리가 자라므로). 그렇다면 그 순간부터 드래곤을 잡기 위해서는 워리어 $x>N_i$명이 이 드래곤 앞에 모여있어야 하는데, 이는 앞에서의 (2)번 상황에 이미 포함된다. 즉, 초기 상태가 아닌 경우 드래곤을 잡기 위해서는 결국 $N_i+1$명 이상의 워리어가 필요하다.

이러한 사실을 바탕으로, 우선 $max(N_i)+1$명의 워리어가 있으면 드래곤을 잡을 수 있다는 사실을 발견할 수 있다. 또, 이로 인해 가능한 워리어 수의 범위 역시 $max(N_i)+1$ 이하이므로, $10^5+1$이하여야 함을 알 수 있다.

그렇다면 결국 워리어의 수 $w$라 할 때, $1 \le w \le 10^5+1$에 대해서 루프를 돌며 (1)번 방법으로 잡아야 하는 수보다 큰지 확인하면 된다. $N_i\lt w $인 경우는 어차피 언젠가는 잡을 수 있으므로 무시하고, $N_i\ge w $인 $i$에 대하여 $\Sigma S_i \le w$인지 확인하면 된다.

주의할 점

도로가 이 문제의 함정이다. 주어진 그래프가 연결 그래프라는 보장이 없다. 그래프를 연결요소로 분해하여 각 연결요소에 대한 부분 답을 모두 더하여 정답을 내야한다. 연결 요소에 드래곤이 한마리도 없을 수도 있다.

Code

링크

# https://www.acmicpc.net/problem/10605
# Greedy - kill dragon at start(xi>Si) or by crowd(sum(xi) > max(Ni))

from bisect import *
from collections import deque
from sys import stdin


def solve(n, m, k):
    graph = [[] for _ in range(n)]
    for _ in range(m):
        a, b = map(int, stdin.readline().split())
        graph[a - 1].append(b - 1)
        graph[b - 1].append(a - 1)

    dragons = [[] for _ in range(n)]
    for _ in range(k):
        ci, si, ni = map(int, stdin.readline().split())
        dragons[ci - 1].append([ni, si])  # change order for simpler implementation

    ans = 0
    visited = [False for _ in range(n)]
    for node in range(n):
        if not visited[node]:
            # Make partial solution for connected component
            target = dragons[node]
            Q = deque([node])
            visited[node] = True
            while Q:
                curr = Q.popleft()
                for child in graph[curr]:
                    if not visited[child]:
                        visited[child] = True
                        target.extend(dragons[child])
                        Q.append(child)
            if len(target) == 0:  # no dragon in this connected component.
                continue

            target.sort()  # Ni, Si
            si_partial = [0 for _ in range(len(target) + 1)]
            acc = 0
            for i in range(len(target)):
                acc += target[i][1]
                si_partial[i + 1] = acc
            curr_ans = 0

            for hero in range(1, 10 ** 5 + 1):
                start_idx = bisect_left(target, [hero, -1])
                if start_idx == len(target):  # num of hero exceeds sum of ni
                    curr_ans = hero
                    break
                if si_partial[-1] - si_partial[start_idx] <= hero:
                    curr_ans = hero
                    break
            ans += curr_ans
    print(ans)


while True:
    n, m, k = map(int, stdin.readline().split())
    if n == m == k == 0:
        break
    else:
        solve(n, m, k)
]]>
Semin Kim<[email protected]>
Codejam 2022 후기2022-04-30T15:30:30+00:002022-04-30T15:30:30+00:00https://seminkim.github.io/blog_old/codejam-2022코드잼 2022…

몇달 전 나는 호기롭게 티셔츠를 받아보겠다는 큰 야망을 품고 참가신청을 했더랬더랬다. 그러나 어제(오늘?) 치룬 1C라운드에서 커트에 한참 못미치게 풀어서 결과적으로는 1라운드 광탈로 마무리하게 되었다.

솔직히 말하자면 왠지 넘을 수 없을 것 같은 벽을 느끼기도 했고, 한편으로는 아쉬움도 남는다. 1라운드에서 이정도라면 뭐 티셔츠는 가시권에도 못 들어간 셈이지만ㅎㅎ;;

음…왠지 문제별로 review하는 것은 따로 다시 해보아야 할 것 같고, 전체를 놓고 보았을 때 떠오르는 점을 좀 적어보자면,

  • Qualification Round는 어렵지 않았다. 시간도 길고, 컷도 낮고, 채점도 다 open verdict라서 대부분의 사람들이 통과하는 것 같다. 다만 이때도 다 푸는 것은 못했고 4문제 중에서 3문제 풀고 마지막 문제는 작은 테스트케이스만 긁었다.

  • 1A의 경우 좀 망쳤다(0문제 풀고 1문제 긁음). 근데 한편으로는 그만큼 참가자도 많았고 문제도 어려웠던 것 같다. 또 이때 interactive 문제를 대비해두지 않아서 못 딴 점수도 있었다.

  • 1B의 경우 쉽게 나왔는데, 실수를 많이 했다. 문제를 잘못읽기도 했고 DP배열 초기값 설정을 잘못하기도 했다. 이 때가 그나마 2300등 언저리로 제일 컷에 가까웠는데, 그래서 아쉬움이 제일 많이 남는다. 1A를 망치고 좀 마음이 급해져서 실수가 많이 나왔던 것 같다.

  • 1C의 경우 좀 어려운 편이었는데, 딱 평소처럼 친 것 같아서 아쉬워하기는 좀 민망하다. 근데 구성적으로 푸는 문제 아이디어를 못 떠올린건 아쉽다.😥 다 푼거였는데..

  • 벽을 느끼게 된 부분: 풀이를 보고도 쉽게 이해하기 어려운 문제들이 있었다. 올솔브를 노린것은 당연히 아니었지만, 코포 Div.2 E번을 보는 느낌이랄까? 그리고 생각보다 이 세상에는 그런 문제들을 잘 푸는 똑똑한 사람들이 많았다…

  • 아쉬운 부분: 몇몇 문제들은 상당히 답에 근접했었는데, 끝내 마지막 한발짝을 내딛지 못한 것들이 있었다. 사실은 항상 이런식이기는 하다. 옛날부터 수학 문제를 풀어도 중요한 아이디어 같은거는 거의 찾아놓고 마지막 단추 하나를 못찾아서 빙빙 돌곤 했다ㅎㅎ..

어찌되었든 ABC를 다 쳐놓고 광탈하니까 좀 부끄럽긴 한데, 내년이든 내후년이든 다시 도전해보고 싶다. 티셔츠는…좀 어렵겠지만😂

]]>
Semin Kim<[email protected]>
문제2022-02-24T15:45:00+00:002022-02-24T15:45:00+00:00https://seminkim.github.io/blog_old/%5BBOJ%5D19585링크

문자열의 집합 두 개 $S_C, S_N$가 주어진다. 각각 크기는 $C$, $N$이다. $Q$개의 문자열 쿼리를 받아서 그 문자열 $q$를 두 부분문자열 $c\in S_C$와 $n\in S_N$으로 분할 할 수 있는지 출력하는 문제이다.

접근

시간 제한 3초에 메모리 1024MB로 제한이 아주 넉넉하다. 백준 온라인 저지는 Python의 경우 여기에다가 추가로 메모리와 시간 제한을 늘려주기 때문에 11초 안에만 풀면 된다🤣

그래서… 예전에 Trie를 알기 전에는 딕셔너리를 사용하여 야매로 풀려고도 했었다.ㅋㅋ

from sys import stdin

c, n = map(int, stdin.readline().split())
colors = {stdin.readline().strip() for _ in range(c)}
names = {stdin.readline().strip() for _ in range(n)}
queries = [stdin.readline().strip() for _ in range(int(stdin.readline().strip()))]
for query in queries:
    flag = False
    for i in range(1, len(query) - 1):
        if query[:i] in colors and query[i:] in names:
            flag = True
            break

    print('Yes' if flag else 'No')

어찌보면 당연하게도 문자열 슬라이싱을 너무 많이 하는 바람에 시간초과를 받았고, 결국 Trie를 공부해서 적용했다.

Trie의 경우 뭐 복잡한 것이 없다. 트리를 만드는데 타고 내려가면서 Prefix를 얻도록 잘 설계하자는 것이 끝이다. 딱 3개의 정보만 관리해주면 된다. 각 노드에 저장할 내용, 그리고 다음 노드가 무엇인지(트리 구조가 되도록), 이 노드에서 끝나는 문자열이 있는지이다.

풀이

나이브하게 딕셔너리를 만들고 잘라보면서 찾자! 는 생각은 시간에 걸렸고, Trie를 적용하기로 했으니 이제 풀이를 구체화해 보자. 쿼리가 들어오면 앞에서부터 한 글자씩 진행하면서 $S_C$로 구성한 Trie를 탐색한다…

그런데 그러면 뒷부분은 어떻게 검색해야 할까? $S_N$으로도 Trie를 구성해서 탐색해야 하나? 생각해보면 Prefix를 그대로 두면서 뒷부분을 붙인 경우를 탐색할때 빠른 것이 이 문제에서 사용하는 Trie의 장점이다. 헌데 뒷부분은 계속 Prefix가 바뀌므로 매번 새롭게 슬라이싱으로 복사해서 사용해야 하고, 그러면 사실상 딕셔너리를 사용하는 것과 다름이 없다.

해서! 나는 그냥 $S_N$에 대해서는 딕셔너리를 사용해버렸다. Best solution을 생각해보자면 아마 문자열을 뒤집어서 Trie를 만들고, 쿼리도 뒤집어서 처리하는 방법으로 두 문자열 집합을 모두 Trie구조로 할 수도 있을 것이다. 그러나 앞서 말했듯 Python은 11초라는 무지막지한 시간 제한이 있기 때문에, 부담없이 복사해버린 다음 딕셔너리에 있는지 확인했다. Trie를 두개 만드는 것보다 이쪽이 간단하다.

Code

링크

# https://www.acmicpc.net/problem/19585
# Trie
from sys import stdin

c, n = map(int, stdin.readline().split())
container = ['']
nxt = [[]]
end = [False]


def add(word):
    pos = i = 0
    while i < len(word):
        for next_pos in nxt[pos]:
            # if next letter matches: move to that node
            if container[next_pos] == word[i]:
                pos = next_pos
                i += 1
                break
        # if nothing matches: insert new node
        else:
            nxt[pos].append(len(container))
            nxt.append([])
            end.append(False)
            pos = len(container)
            container.append(word[i])
            i += 1
    end[pos] = True


# search in trie and yield index whenever available
def find(word):
    pos = i = 0
    while i < len(word) - 1:
        for next_pos in nxt[pos]:
            # if next letter matches: move to that node
            if container[next_pos] == word[i]:
                pos = next_pos
                i += 1
                if end[pos]:
                    yield i
                break
        # if nothing matches: end of iteration
        else:
            return


for _ in range(c):
    color = stdin.readline().strip()
    add(color)

names = {stdin.readline().strip() for _ in range(n)}
for _ in range(int(stdin.readline().strip())):
    query = stdin.readline().strip()
    for i in find(query):
        # print(query[i:])
        if query[i:] in names:
            print('Yes')
            break
    else:
        print('No')

여담

슬슬 코테에 안나오는 알고리즘이라 그런가 솔브닥 class 6 문제임에도 Python으로 푼 사람이 적다. 아마 뒷부분까지 잘 처리해주면 시간을 줄일 수 있을 텐데, 사람이 적어서 제출한 뒤에 1등을 먹어버린터라 귀찮아져버렸다😅😅😅

]]>
Semin Kim<[email protected]>
다 풀었다! ###2022-02-16T10:20:00+00:002022-02-16T10:20:00+00:00https://seminkim.github.io/blog_old/%5BBOJ%5Dproblem_book_finish며칠이 걸렸는지는 모르겠지만 아무튼 하루에 한개씩 꾸준히 풀다보니 어느새 문제집 하나를 다 풀었다. 지금보니까 총 45문제였나 보다.

image
만세!

문제를 풀면서 느끼건데, 삼성 코테 기출이다 보니까 푸는 사람들이 대부분 코테를 준비하려고 푸는 것 같았다. 나는 사실 가까운 시일 내에 코테를 보거나 할 일은 없을 것 같은데, 코포치거나 할때 구현이 가끔 잘 안풀릴 때가 있어서 연습삼아 풀었다고 할 수 있겠다.

좀 구현실력이 늘었냐 하면 아직은 글쎄올씨다…지만 그래도 나름 재미있었다. 옛날에 아이디어 위주의 문제 풀때는 3시간씩 머리싸매던 것이 예사였는데, 바로바로 손가락이 움직이니까 좀 덜 답답한 것 같기도 하다.

또, C++ 연습하는 데에도 도움이 많이 되었다. 지난학기 OS를 들으면서 C를 사용하는데 좀 미숙했던 경험도 있고, Python을 사용하다보면 TLE받을 상황이 꽤 있어서 장기적으로 C++을 연습하려 했는데, 이번에 어느정도 기초를 다질 수 있었던 것 같다.

문제에 대한 생각

알고리즘만 보았을 때에는 BFS, DFS, 백트래킹, 그리고 가끔 Disjoint Set 정도만이 사용되어서 결국은 구현을 얼마나 빠르고 정확하게 하느냐의 싸움이다. 문제의 80퍼센트는 2차원 배열을 움직이고 돌리고 조작하는 문제였던 것 같다.

한가지 좋았던 점은 테케가 풍부했다는 점이다. 돌이켜보면 예제 다 맞추고 맞왜틀 한 기억이 아마 없었던 것 같다. 테케를 거의 10개씩 주니까 한번에 맞았습니다!!를 받을 수 있어서 기분좋게 문제를 풀 수 있다.ㅎㅎㅎ

기억에 남는 문제들

다시한번 문제 제목을 스윽 읽어보니, 인상깊었던 문제들이 몇 보인다.

사다리 조작(링크)

  • 음… 이건 좀 사이즈 큰 edge case 테케를 따로 처리하도록 했더니 4000ms씩 걸리던게 1/10로 줄어서 기억에 남는다. 비슷한 테케중에 그 if문에 안걸리게 만들 수 있을 것도 같은데, 잘 모르겠다.

큐빙(링크)

  • 이건 뭐 말이 필요한가. 이렇게 순도 100% 노가다 문제는 본적이 없다. 뭔가 아름답게 구현할 수 있는 수학적 구조가 없을까 골똘히 생각하다 결국 그런거 없이 공책에 그리면서 차근차근 풀었다.

나무 재테크(링크)

  • 이건 파이썬으로 풀다가 시간초과를 여러번 받아서 기억에 남는다. 솔직히 좋은 문제는 아니라고 생각한다. 그냥 상수시간 차이 정도 나는 부분을 if문 안에 넣어서 처리하면 시간초과 나던게 통과된다. 고작 두줄 차이인데!

어항 정리(링크)

  • 이건 오늘 풀어서 기억에 남는 것 같기는 하지만, 파이썬으로 너무 깔끔하게 구현되어서 기분이 좋았다.

다음에는…

구현은 이 정도면 실컷 한 것 같아서, 예전처럼 새로운 알고리즘을 배울때의 즐거움이 그리워졌다. 앞으로는 FFT, Graph Cut, Maximum Flow 등 이름만 들어보았던 알고리즘을 새로 배워보는 것을 목표로 하려 한다. 이런 구현 문제들은 나중에 내가 혹시 코테를 쳐야한다면 그때가서 다시 복습하는 걸로…

]]>
Semin Kim<[email protected]>
문제2022-02-16T09:45:00+00:002022-02-16T09:45:00+00:00https://seminkim.github.io/blog_old/%5BBOJ%5D23291링크

문제가 조금 복잡한데… 제시된 설명이 어렵거나 헷갈리지는 않다. 간략히 보자면 초기 상태에 일렬로 숫자가 써져있는 $N$개의 어항이 있다. 이 어항의 숫자를 조작하는 어항 정리라는 연산이 정의되어 있고, 이를 몇번 해야 어항의 최대값과 최소값의 차이를 $K$ 이하로 만들 것이냐는 문제이다.

접근

당연하지만 구현만 하면 되는데, 설명을 읽어보면 풀기도 전부터 어떻게 구현할까 머리가 지끈거린다. 어항을 막 쌓았다가 내렸다가 돌렸다가 하는데, 나는 각 층별로 하나의 리스트를 사용하였고 리스트 슬라이싱을 활용해서 조작했다. 즉, fishbowl리스트는 [[1층 숫자들...], [2층 숫자들...], ...]꼴의 2차원 리스트이다. 여기서 2층 이상에서의 숫자 갯수는 동일할 수 밖에 없지만, 2층의 숫자 갯수와 1층 숫자개수는 달라질 수 있다. C++로 짜면 이 부분이 귀찮을 것 같아 Python을 골랐다.

먼저 물고기 수가 최소인 어항에 물고기를 넣는 것은 어렵지 않다. 루프의 시작 부분에서 최소값과 최대값을 찾아 차이가 K이하인지 종료조건을 체크해주고, 최소값을 찾은 김에 어항을 돌면서 최소값인 항목에 1을 더해주자.

이제 공중부양시키고 90도 회전하는 부분이 다소 곤란하다. 가장 처음에는 1층의 맨 앞 숫자를 2층으로 올려준다.

first = fishbowl[0].pop(0)
fishbowl.append([first])

그러면 [[1,2,3,4]]와 같이 1층에만 1,2,3,4가 있었던 경우에 [[2,3,4],[1]]로 변했을 것이다.

다음은 불가능할 때 까지 2칸 이상 쌓인 어항을 90도 회전시켜 뒤쪽에 쌓는 작업이다.

while len(fishbowl) <= len(fishbowl[0]) - len(fishbowl[1]):
    new_bowl = [list(wow) for wow in zip(*fishbowl)]
    new_bowl.reverse()
    fishbowl = [fishbowl[0][len(fishbowl[1]):], *new_bowl]

쌓는 것이 불가능한 경우는, 가장 앞쪽에 있는 쌓인 어항의 층수가 너무 높을 때이다. 정확히는, 뒤쪽에 1층으로만 되어있는 부분의 길이보다 크면 안된다.

다음으로, zip(*fishbowl)을 이용하여 공중부양할 부분을 뽑아낸다. 이것이 가능한 이유는 입력받은 argument의 어느 하나라도 iteration이 끝나면 바로 끝나는 zip의 특성 때문이다. 예를 들어 zip([1,2],[3])(1,3)만을 yield하고 끝난다.

뽑고 나서 잘 보면, 순서가 뒤집혀있다. [[1,2,3,4],[5,6]]이라면 (1,5)(2,6)순으로 뽑아져 나오므로, [[3,4],[2,6],[1,5]]가 되려면 뒤집어주어야 한다(reverse()). 1층에서 공중부양하지 않는 부분을 리스트 슬라이싱하여 앞에다가 포함시켜주면 작업이 끝난다.

여기까지 왔다면 해결한 것이나 다름이 없다. 물고기 수를 조절하고 다시 1층짜리 어항으로 만드는 부분은 두 번 등장하니까 함수로 맨위에 하나 넣어두자. 이 함수를 구현하는 방법은 여러가지가 있겠지만, 나는 어떤 두 숫자사이에서 변화가 일어나는지 수평으로 한번, 수직으로 한번 세어서 변화량을 delta리스트에 기록한 다음 원래 리스트fishbowl에 더해주었다 (move_and_linearize()함수).

마지막 부분은 위에서 만든 함수를 한번 호출하고, 반접고쌓기를 두번 반복하고, 다시한번 호출해준다. 반접고 쌓기 두번은 아래와 같이 간단하게 할 수 있다(파이썬 만세!).

first = fishbowl[0][n // 2:]
second = fishbowl[0][:n // 2][::-1]
fishbowl = [first[n // 4:], second[n // 4:], second[:n // 4][::-1], first[:n // 4][::-1]]

주의할 점

구현하다보면 무엇이 리스트이고 무엇이 수인지 헷갈리므로 잘 구별하자.

Code

링크

# https://www.acmicpc.net/problem/23291

from sys import stdin

n, k = map(int, stdin.readline().split())
fishbowl = [list(map(int, stdin.readline().split()))]  # save at zeroth floor


# move fish and linearize to zeroth floor
def move_and_linearize(fishbowl):
    delta = []
    # horizontal
    for floor in range(len(fishbowl)):
        temp = [0 for _ in range(len(fishbowl[floor]))]
        for i in range(len(fishbowl[floor]) - 1):
            diff = int((fishbowl[floor][i + 1] - fishbowl[floor][i]) / 5)
            temp[i] += diff
            temp[i + 1] -= diff
        delta.append(temp)
    # vertical
    for pos in range(len(fishbowl[1])):
        for floor in range(len(fishbowl) - 1):
            diff = int((fishbowl[floor + 1][pos] - fishbowl[floor][pos]) / 5)
            delta[floor][pos] += diff
            delta[floor + 1][pos] -= diff
    # apply
    for floor in range(len(fishbowl)):
        for i in range(len(fishbowl[floor])):
            fishbowl[floor][i] += delta[floor][i]

    new_bowl = []
    for wow in zip(*fishbowl):
        new_bowl.extend(wow)
    new_bowl.extend(fishbowl[0][len(fishbowl[1]):])
    return [new_bowl]


for t in range(100000):
    # end condition
    minimum = min(fishbowl[0])
    maximum = max(fishbowl[0])
    if maximum - minimum <= k:
        print(t)
        break

    # add fish
    for i in range(n):
        if fishbowl[0][i] == minimum:
            fishbowl[0][i] += 1

    # stacking
    first = fishbowl[0].pop(0)
    fishbowl.append([first])
    while True:
        if len(fishbowl) <= len(fishbowl[0]) - len(fishbowl[1]):
            new_bowl = [list(wow) for wow in zip(*fishbowl)]
            new_bowl.reverse()
            fishbowl = [fishbowl[0][len(fishbowl[1]):], *new_bowl]
        else:
            break

    fishbowl = move_and_linearize(fishbowl)

    # flip half and add to second floor.
    first = fishbowl[0][n // 2:]
    second = fishbowl[0][:n // 2][::-1]
    fishbowl = [first[n // 4:], second[n // 4:], second[:n // 4][::-1], first[:n // 4][::-1]]

    fishbowl = move_and_linearize(fishbowl)

여담

이 문제를 마지막으로 삼성 SW 역량 테스트 기출 문제집(이거)를 다 풀었다. 뿌듯하다!

리스트 슬라이싱과 zip을 활용해 문제를 풀었더니 꽤 깔끔하게 구현할 수 있었다. 게다가 별로 푼 사람이 많은 문제는 아니지만 실행시간 1위도 찍어서 만족스럽다.

]]>
Semin Kim<[email protected]>
문제2022-02-01T17:22:22+00:002022-02-01T17:22:22+00:00https://seminkim.github.io/blog_old/%5BBOJ%5D20058링크

$N\times N$정사각형 격자판이 있다. 각 칸에는 초기 얼음의 양이 주어진다. 이 격자판에서 할 ‘파이어스톰’이라는 연산이 정의되는데, 이 연산을 주어진 입력에 따라 했을 때 마지막에 남아있는 얼음의 양의 합가장 큰 연결된 얼음 덩어리 크기를 찾는 문제이다.

파이어스톰 연산은 다음과 같다. 주어진 변수 $L$에 대해서, 전체 격자를 $2^L \times 2^L$크기의 부분격자로 나눈 후 각 부분격자를 시계방향으로 회전 시킨다. 그 다음, 어떤 칸$(r,c)$에 인접(상하좌우)한 칸들 중 얼음이 있는 칸이 2개 이하라면 $(r,c)$의 얼음의 양을 1 줄인다.

접근

문제 설명이 다소 복잡하지만, 결국 핵심은 (1) 부분격자로 어떻게 나누고, 각 부분격자에서 회전은 어떻게 구현해야 하는지 (2) 가장 큰 연결된 얼음 덩어리 크기는 어떻게 구할지 이다.

(1)의 경우 두 개의 for문을 이용하여 먼저 각 부분격자의 가장 위쪽, 왼쪽 모서리의 좌표를 뽑아냈다. 이 점을 $(row, col)$이라 하면, 시계방향 90도 회전은 다음과 같이 구현할 수 있다. 임시배열 temp, 원래 배열board, 현재 부분격자의 크기 len이라 하면,

for (int i = 0; i < len; i++) {
    for (int j = 0; j < len; j++) {
        temp[row + j][col + len - 1 - i] = board[row + i][col + j];
    }
}

이다. 기준점 $(row,col)$ 기준으로 좌표를 어떻게 표현하는지 그려서 생각해보면 어렵지 않다.

(2)의 경우 간단한 DFS이다. visited배열에 방문 여부를 저장하고, 모든 칸을 이중 for문으로 돌면서 아직 방문하지 않았다면 dfs(row, col)를 호출해주었다. 네 개의 방향을 살펴보아 유효한 좌표면 재귀적으로 호출하도록 구현하였다.

주의할 점

처음에 문제 설명과 테스트 케이스를 읽고 언제 얼음이 줄어드는지 이해가 잘 안되었다. 인접한 칸중 얼음이 있는 칸이 2개 이하면 얼음이 줄어드므로, 최소한 네 귀퉁이에서는 얼음이 줄어드게 된다.

테케중에 덩어리 0인 케이스가 없으므로, 이 경우에도 원하는 대로 출력되는지 유의하자.

Code

링크

#include <algorithm>
#include <iostream>

using namespace std;

int N, Q, board_size;
int board[64][64];
int temp[64][64];
bool visited[64][64] = {false};
int dx[4] = {0, 1, 0, -1};
int dy[4] = {-1, 0, 1, 0};
int sum = 0;

// rotate and check ice-melting condition.
void firestrom(int len) {
    for (int row = 0; row < board_size; row += len) {
        for (int col = 0; col < board_size; col += len) {
            // rotate for each subgrid, given its top-left point (row,col)
            for (int i = 0; i < len; i++) {
                for (int j = 0; j < len; j++) {
                    temp[row + j][col + len - 1 - i] = board[row + i][col + j];
                }
            }
        }
    }
    // check for ice-melting condition
    for (int i = 0; i < board_size; i++) {
        for (int j = 0; j < board_size; j++) {
            int ice_count = 0;
            for (int d = 0; d < 4; d++) {
                int ni = i + dx[d];
                int nj = j + dy[d];
                if (0 <= ni && ni < board_size && 0 <= nj && nj < board_size && temp[ni][nj] > 0) {
                    ice_count++;
                }
            }
            if (ice_count >= 3) {
                board[i][j] = temp[i][j];
            } else {
                board[i][j] = max(0, temp[i][j] - 1);
            }
        }
    }
}

// return the number of connected ices adjacent to (row, col)
int dfs(int row, int col) {
    int ret = 0;
    for (int d = 0; d < 4; d++) {
        int nrow = row + dx[d];
        int ncol = col + dy[d];
        if (0 <= nrow && nrow < board_size && 0 <= ncol && ncol < board_size && !visited[nrow][ncol] && board[nrow][ncol] > 0) {
            visited[nrow][ncol] = true;
            ret += 1 + dfs(nrow, ncol);
        }
    }
    return ret;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);
    // get input
    cin >> N >> Q;
    board_size = 1 << N;
    for (int i = 0; i < board_size; i++) {
        for (int j = 0; j < board_size; j++) {
            cin >> board[i][j];
        }
    }
    // do firestorm
    int L;
    for (int i = 0; i < Q; i++) {
        cin >> L;
        firestrom(1 << L);
    }
    for (int i = 0; i < board_size; i++) {
        for (int j = 0; j < board_size; j++) {
            sum += board[i][j];
        }
    }
    // find maximum mTE
    int maximum = 0;
    for (int i = 0; i < board_size; i++) {
        for (int j = 0; j < board_size; j++) {
            if (!visited[i][j] && board[i][j] > 0) {
                visited[i][j] = true;
                maximum = max(maximum, 1 + dfs(i, j));
            }
        }
    }
    cout << sum << '\n'
         << maximum;
    return 0;
}

여담

이번에는 뜬금없이 컴파일 에러를 한번 당했는데, 처음에 board_len변수를 size라 이름지었더니 그렇게 되었다. 내 설정에서는 문제 없었는데 왜 그러는지는 잘 모르겠지만, 일단은 vector나 deque 등의 stl에서 size가 사용되는 것 같다.

]]>
Semin Kim<[email protected]>
문제2022-01-29T12:20:00+00:002022-01-29T12:20:00+00:00https://seminkim.github.io/blog_old/%5BBOJ%5D19238링크

$N\times N$정사각형 격자판이 있다. 각 칸은 이동가능한 칸이거나 벽이다. 또 $M$명의 택시 승객이 있는데, 각 승객은 현재 위치와 목적지를 갖는다. 택시는 벽이 아닌 상하좌우 칸으로 이동할 수 있으며, 초기 연료의 양도 주어진다.

택시를 운전해서 가장 가까운 승객(여럿이라면 그 중 행/열번호가 가장 작은)을 목적지로 태워주는 것을 반복한다고 하자. 택시는 한 칸 움직일 때마다 연료를 1 소모하며, 어떤 승객을 성공적으로 태워주었다면 승객을 태운 상태로 이동한 거리의 두 배 만큼 연료를 회복한다.

모든 승객을 태워다 줄 수 있는지, 가능하다면 마지막에 남는 연료는 몇인지 구하는 문제이다.

접근

어떤 승객이 존재하는지, 또 존재한다면 목적지는 어디인지 구하기 위하여 map<pair<int,int>,pair<int,int>>을 사용하였다. 그리고 택시가 한 명의 승객을 성공적으로 태우는 부분을 taxi()함수로 구현하고, 이것이 실패할 때까지 무한 루프가 돌도록 하였다.

다음 태울 승객을 구하려면 각 승객까지의 위치를 알아야 한다. 이를 위해 먼저 BFS를 이용하여 현재 택시의 위치를 기준으로 모든 칸까지의 거리를 구하는 void get_all_distance()를 구현하였다. 이 함수를 호출 한 이후에, 승객 정보를 담은 map을 순회하면서 가장 가까운 승객을 찾는다. 태울 승객을 찾았다면 그 승객을 태우고, 목적지로 이동한다. 이렇듯 기본적인 알고리즘은 간단하고, 사이 사이에 어떤 경우에 성공적으로 승객을 운송할 수 없는지 생각하여 처리해주면 된다.

주의할 점

승객을 성공적으로 나를 수 없는 경우를 꼼꼼히 생각해보아야 한다.

  • 벽에 막혀서 다음 승객이 있는 곳까지 갈 수 없는 경우
  • 다음 승객이 있는 곳까지 갈 연료가 없는 경우
  • 비슷하게, 벽에 막혀서 목적지까지 갈 수 없는 경우
  • 연료가 없어서 목적지까지 갈 수 없는 경우

등이 있을 것이다. 나는 80% 언저리에서 틀렸습니다를 한 번 받았는데, 승객이 벽에 막혀서 갈 수 없는 곳을 목적지로 설정한 경우를 제대로 처리해주지 않아서였다.

Code

링크

#include <algorithm>
#include <deque>
#include <iostream>
#include <map>
#include <vector>

using namespace std;

int N, M, F;
int x, y;
int board[20][20] = {0};
int distance_board[20][20];
int MAX_DIST = 1000;
int dx[4] = {-1, 0, 1, 0};
int dy[4] = {0, 1, 0, -1};
map<pair<int, int>, pair<int, int>> customer;

void get_all_distance() {
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            distance_board[i][j] = MAX_DIST;
        }
    }
    deque<pair<int, int>> Q;
    Q.push_back(make_pair(x, y));
    distance_board[x][y] = 0;
    while (Q.size() != 0) {
        int row, col;
        tie(row, col) = Q.front();
        Q.pop_front();
        for (int d = 0; d < 4; d++) {
            int nrow = row + dx[d];
            int ncol = col + dy[d];
            int ndist = distance_board[row][col] + 1;
            if (0 <= nrow && nrow < N && 0 <= ncol && ncol < N && board[nrow][ncol] != 1 && ndist < distance_board[nrow][ncol]) {
                distance_board[nrow][ncol] = ndist;
                Q.push_back(make_pair(nrow, ncol));
            }
        }
    }
}

bool taxi() {
    // first, do bfs to find next target customer.
    get_all_distance();
    pair<int, int> nearest_customer;
    int nearest_distance = MAX_DIST;
    for (auto iter : customer) {
        pair<int, int> key = iter.first;
        if (distance_board[key.first][key.second] < nearest_distance) {
            nearest_distance = distance_board[key.first][key.second];
            nearest_customer = key;
        }
    }
    // stop iteration if customer was not found
    if (nearest_distance == MAX_DIST) {
        if (customer.size() != 0) {
            F = -1;
        }
        return false;
    }
    // stop iteration if fuel is low
    if (F < nearest_distance) {
        F = -1;
        return false;
    }
    // move taxi to customer position
    F -= nearest_distance;
    x = nearest_customer.first;
    y = nearest_customer.second;

    // move taxi to goal.
    get_all_distance();
    pair<int, int> goal = customer[nearest_customer];
    x = goal.first;
    y = goal.second;
    int used = distance_board[x][y];
    customer.erase(nearest_customer);
    if (used == MAX_DIST || F < used) {
        F = -1;
        return false;
    }
    // refill fuel twice.
    F += used;
    return true;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);
    // get input
    cin >> N >> M >> F;
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            cin >> board[i][j];
        }
    }
    cin >> x >> y;
    x--, y--;  // zero-based index
    for (int i = 0; i < M; i++) {
        int a, b, c, d;
        cin >> a >> b >> c >> d;
        customer[make_pair(a - 1, b - 1)] = make_pair(c - 1, d - 1);
    }
    while (taxi()) {
    }
    cout << F;
    return 0;
}

여담

이번에는 map을 사용하는 법을 공부해보게 되었다. 사실 꼭 map을 쓸필요는 없는것 같은데, Python에는 없는 구조라 맛을 보았다. 뭐…그냥 신기한 것 같다.

]]>
Semin Kim<[email protected]>
문제2022-01-28T03:20:00+00:002022-01-28T03:20:00+00:00https://seminkim.github.io/blog_old/%5BBOJ%5D19237%EC%96%B4%EB%A5%B8%20%EC%83%81%EC%96%B4링크

$N \times N$ 격자판에 $M$마리의 상어가. 각 상어는 방향과 번호를 갖는다. 매 초 다음의 일들이 일어난다.

  1. 먼저, 각 상어가 있는 칸에 그 상어의 냄새가 뿌려진다. 이 냄새는 $K$초 뒤에 사라진다.
  2. 그 다음, 상어들이 움직이는데, 모든 상어는 동시에 움직인다. 인접한 냄새가 없는 칸이 있다면 그 칸으로 움직이고, 없다면 자신의 냄새가 있는 칸으로 움직인다. 만약 가능한 칸이 여러개라면 우선순위대로 움직인다.
  3. 모든 상어가 움직인 다음, 만약 둘 이상의 상어가 겹쳐있다면 번호가 가장 작은 상어만 남고 나머지는 쫓겨난다.

이때, 1번 상어만 남게 되는 시간을 구하는 문제이다.

접근

으레 그렇듯이 구현만 잘 하면 된다. 여러 방법으로 구현할 수 있겠지만, 나는 다음과 같이 구현하였다.

  1. 상어의 현재 위치를 저장하는 2차원 shark리스트, 남아 있는 냄새를 [번호, 남은 시간]꼴로 저장할 3차원 board리스트, 각 상어의 방향을 저장할 direction리스트, 각 상어의 방향 우선순위를 저장해 놓는 prefer리스트를 만든다. 또, 상어 수를 저장할 count를 $M$으로 초기화 해 둔다.
  2. $t=1$부터 $t=1000$까지 다음을 반복한다:
    • shark리스트를 돌면서 상어들의 현재 위치를 찾아서, board리스트의 각 위치에 남은시간 $K$짜리 냄새를 만든다.
    • 그 다음 상어를 움직이는데, 먼저 상어가 한번에 움직이도록 하기 위해 모든 칸이 빈 칸, 즉 [-1,-1]으로 저장되어있는 new_board리스트를 만든다.
    • 각 상어에 대해서, 만약 아직 존재한다면(shark[i][0]!=-1)
      1. direction리스트에서 현재 방향을 가져오고, 이를 이용해 prefer리스트의 네 방향을 우선순위 순서대로 살펴본다.
        • 만약 냄새가 없는 칸을 만나면, 그 칸이 가장 우선순위가 높은 칸이므로 즉시 break해준다.
        • 자신의 냄새가 있는 칸을 만나면, 가장 먼저 만나는 하나만 저장해 놓는다.
      2. 냄새가 없는 칸이 없었다면, 자신의 냄새였던 칸을 목표로 한다. 없었다면 냄새가 없는 그 칸이 목표이다.
      3. 목표하는 칸으로 상어를 이동시킨다. direction을 바꿔주고, shark리스트에 저장된 위치를 바꿔준다.
      4. new_board리스트의 새 위치에 상어 번호를 [num, -1]꼴로 적어준다. 만약 이미 그 자리에 상어 번호가 적혀있다면, 두 번호를 비교해서 낮은 번호를 놔두고, 높은 번호는 없애준다. shark리스트에 [-1,-1]을 넣어 쫓겨났음을 표시한다. 이때 count도 1 줄어든다.
    • 마지막으로, board리스트에서 냄새를 1씩 줄여 new_board로 옮겨준다.
    • new_boardboard에 새로 assign한다.
    • 만약 상어 수가 1이라면, 현재 시간을 출력하고 끝낸다.
  3. $t=1000$까지 상어 수가 여러 마리라면, -1을 출력한다.

주의할 점

비어있는 칸, 잡아먹힌 상어, 냄새 등을 어떤 형식으로 저장할 지 잘 생각해보아야 한다.

1번 상어만 남는 경우와 상어 수가 1인 상태는 완벽히 동일하므로, 매 루프마다 각 상어의 상태를 확인할 필요 없이 상어 수만 세어도 충분하다.

Code

링크

# https://www.acmicpc.net/problem/19237
# Implementation

from sys import stdin

dx = [-1, 1, 0, 0]  # up, down, left, right
dy = [0, 0, -1, 1]

n, m, k = map(int, stdin.readline().split())
shark = [[] for _ in range(m)]
for i in range(n):
    line = list(map(int, stdin.readline().split()))
    for j in range(n):
        if line[j] != 0:
            shark[line[j] - 1] = [i, j]

board = [[[-1, -1] for _ in range(n)] for _ in range(n)]  # initially there's no fragrance.
direction = list(map(lambda x: int(x) - 1, stdin.readline().split()))
prefer = [[list(map(lambda x: int(x) - 1, stdin.readline().split())) for _ in range(4)] for _ in range(m)]
count = m

for t in range(1, 1001):
    # first, make smell
    for i in range(m):
        row, col = shark[i]
        if row == -1:
            continue
        board[row][col] = [i, k]  # shark number, duration

    # then, move
    new_board = [[[-1, -1] for _ in range(n)] for _ in range(n)]
    for i in range(m):
        row, col = shark[i]
        if row == -1:
            continue
        d = direction[i]
        target_direction = None
        my_fragrance = None
        # first, find for no-fragrance cell
        for target in prefer[i][d]:
            nrow = row + dx[target]
            ncol = col + dy[target]
            if 0 <= nrow < n and 0 <= ncol < n:
                if board[nrow][ncol][0] == -1:  # no-fragrance
                    target_direction = target
                    break
                if board[nrow][ncol][0] == i and my_fragrance is None:  # my-fragrance
                    my_fragrance = target
        # if there was no empty cell, go to my-fragrance
        if target_direction is None:
            target_direction = my_fragrance
        # actually moving
        nrow = row + dx[target_direction]
        ncol = col + dy[target_direction]
        direction[i] = target_direction
        shark[i] = [nrow, ncol]
        # when two sharks collide
        if new_board[nrow][ncol][0] != -1:
            other = new_board[nrow][ncol][0]
            count -= 1
            # lose
            if other < i:
                shark[i] = [-1, -1]
            else:
                shark[other] = [-1, -1]
                new_board[nrow][ncol] = [i, -1]
        else:
            new_board[nrow][ncol] = [i, -1]

    # finally, decrease smell.
    for i in range(n):
        for j in range(n):
            num, dur = board[i][j]
            if num != -1 and dur > 1:
                new_board[i][j] = [num, dur - 1]

    board = new_board
    if count == 1:
        print(t)
        break

else:
    print(-1)
]]>
Semin Kim<[email protected]>
문제2022-01-27T14:00:00+00:002022-01-27T14:00:00+00:00https://seminkim.github.io/blog_old/%5BBOJ%5D19236%EC%B2%AD%EC%86%8C%EB%85%84%20%EC%83%81%EC%96%B4링크

$4 \times 4$ 격자판에 물고기가 가득 차있다. 각 물고기는 방향과 번호를 갖는다.

맨 처음에는 상어가 가장 왼쪽 위의 칸으로 들어가 그 칸의 물고기를 잡아먹고, 그 물고기의 방향을 가진다.

그다음으로는 모든 물고기가 번호 순으로 움직이는데, 가려는 곳에 물고기가 있으면 두 물고기의 위치가 바뀌는 식으로 움직인다. 만약 그 방향으로 움직일 수 없다면 가능할 때까지 반시계방향으로 45도 회전한 이후에 움직이려고 시도한다.

물고기가 모두 움직인 다음에는 상어가 움직이는데, 현재 방향으로 원하는 칸 수만큼 움직일 수 있다. 지나치는 칸의 물고기는 잡아먹지 않는다. 잡아먹는 물고기 번호 합의 최대값을 구하는 문제이다.

접근

여느 문제와 다름없이 이동 규칙을 잘 구현하기만 하면 어렵지 않게 풀 수 있다. 기본적으로 전체 격자판의 상태를 저장해 놓고, 또 물고기의 이동은 번호 순서대로 일어나므로 각 번호에 해당하는 물고기 위치를 리스트에 저장해 놓아 사용한다. 리스트를 돌면서 물고기를 이동시키는데, 두 물고기 위치를 교환하는 식으로 움직일 때에는 두 리스트를 모두 갱신시켜주면 된다. 현재 방향으로 움직일 수 없을 때에는 방향을 돌려야 하는데, nd=(d+1)%8과 같이 계속 돌 수 있도록 해주면 편리하다.

상어의 움직임은 최대 3개의 선택지가 있으므로, DFS와 백트래킹을 이용해서 정답을 찾아준다. 백트래킹 할때 먹힌 물고기는 되돌리기 어렵지 않은데, 움직인 물고기는 되돌리기 다소 곤란하다. DFS함수 처음에 리스트를 모두 deepcopy해주어서 해결한다. 1초 시간제한이 있지만 문제 크기가 작아 여유롭게 통과할 수 있다.

주의할 점

원 문제 설명에는 물고기가 움직일 수 없으면 움직이지 않는다고 되어있다. 그러나 잘 생각해보면 물고기는 항상 최소한 3칸과 인접해 있으므로 상어가 그 중 한 칸을 차지해도 움직일 곳이 있다. 즉 물고기가 움직일 수 없는 상황은 없다.

테케가 4개나 있기는 하지만, 손으로 직접 풀어보기에 복잡해서 디버깅이 쉽지 않다. 마지막에 디버깅하려고 하지 말고 각 부분을 구현한 다음 예상한 대로 움직이는지 틈틈이 확인해보자.

Code

링크

# https://www.acmicpc.net/problem/19236
# Implementation

from sys import stdin
from copy import deepcopy

direction = [[-1, 0], [-1, -1], [0, -1], [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1]]
board = []
for i in range(4):
    line = list(map(lambda x: int(x) - 1, stdin.readline().split()))
    board.append([[line[0], line[1]], [line[2], line[3]], [line[4], line[5]], [line[6], line[7]]])

fish = [[] for _ in range(16)]
for row in range(4):
    for col in range(4):
        pos, d = board[row][col]
        fish[pos] = [row, col, d]


def dfs(shark_x, shark_y, shark_d, acc):
    # first move fish
    global fish, board
    save_fish = deepcopy(fish)
    save_board = deepcopy(board)
    for i in range(16):
        r, c, d = fish[i]
        if r == -1:  # already eaten.
            continue
        nr = r + direction[d][0]
        nc = c + direction[d][1]
        nd = d
        # maybe fish can always move??
        while not (0 <= nr < 4 and 0 <= nc < 4 and (nr, nc) != (shark_x, shark_y)):
            nd = (nd + 1) % 8
            nr = r + direction[nd][0]
            nc = c + direction[nd][1]
        # swap position
        target_num, target_d = board[nr][nc]

        fish[i] = [nr, nc, nd]
        if target_num != -1:
            fish[target_num] = r, c, target_d
        board[nr][nc] = [i, nd]
        board[r][c] = [target_num, target_d]

    # then, move shark.
    ret = acc
    for mult in range(1, 4):
        nx = shark_x + mult * direction[shark_d][0]
        ny = shark_y + mult * direction[shark_d][1]
        if not (0 <= nx < 4 and 0 <= ny < 4):
            break
        target_num, target_d = board[nx][ny]
        if target_num == -1:
            continue
        fish[target_num] = [-1, -1, -1]
        board[nx][ny] = [-1, -1]
        ret = max(ret, dfs(nx, ny, target_d, acc + target_num + 1))
        board[nx][ny] = [target_num, target_d]
        fish[target_num] = [nx, ny, target_d]
    fish = save_fish
    board = save_board
    return ret


num, d = board[0][0]
fish[num] = [-1, -1, -1]
board[0][0] = [-1, -1]

print(dfs(0, 0, d, num + 1))
]]>
Semin Kim<[email protected]>
문제2022-01-22T07:15:00+00:002022-01-22T07:15:00+00:00https://seminkim.github.io/blog_old/%5BBOJ%5D20061%EB%AA%A8%EB%85%B8%EB%AF%B8%EB%85%B8%EB%8F%84%EB%AF%B8%EB%85%B8%202링크

테트리스 비슷한 게임을 구현하는 문제이다. 대신 한 칸짜리 모노미노와 두 칸짜리 도미노를 사용한다. 또, 초록색 보드와 파란색 보드가 있어서 한 입력에 대해서 초록색 보드는 위에서 아래로 떨어지고, 파란색 보드는 왼쪽에서 오른쪽으로 도형이 떨어진다. 초록색 보드는 행이 꽉 차면 테트리스처럼 그 행이 지워지고, 파란색 보드는 열이 꽉 차면 지워진다. 지워질때는 점수를 1점 얻는다. 테트리스 게임은 맨 위까지 도형이 차면 게임 오버인데, 이 게임은 그 대신 가장 위 두 줄에 도형이 있으면 가장 아래줄(파란색 보드는 가장 오른쪽 줄)을 지우는 것으로 대신한다. 주어진 입력대로 도형을 배치했을 때, 얻는 점수와 남은 도형 개수를 구하는 문제이다.

접근

규칙만 잘 따라간다면 그렇게 어렵지 않은 구현 문제이다. 일단 처음 고민해야 할 것은 파란색 보드와 초록색 보드를 어떻게 구현할 것인가이다. 그냥 각각 함수를 따로 구현해 사용할 수도 있겠지만, 그보다는 초록색 보드만 구현하고 이를 재사용하면 편리하다. 초록색 보드에서 $(t,x,y)$인 경우,

  • $t=1$일때는 파란색 보드의 $(1, y, 3-x)$
  • $t=2$일때는 파란색 보드의 $(3, y, 3-x)$
  • $t=3$일때는 파란색 보드의 $(2, y, 2-x)$

에 대응되게 된다. 따라서 그냥 초록색 보드만 구현해 주고, 한 보드에는 위 규칙에따라 변환한 입력을 먹여주면 된다.

그 다음은 각 $t$에 대해 경우를 나누어 구현해주면 된다. 먼저 미노/도미노가 위에서 떨어진다면 어디에 쌓이는지 찾아주고, 그러고 나면 한 줄이 차면 비우는 것, 비운 다음 한 줄씩 내리는 것, 만약 가장 위 두 줄에 하나라도 점유된 칸이 있으면 아래 줄을 비우도록 하는 것을 차근차근 구현하면 된다. 어떤 줄이 차있는지 확인하는 것과 한 줄을 비우는 것은 자주 나오니 함수로 따로 짜면 간결해진다.

주의할 점

입력의 형태가 $2 \times 1$ 도미노 $(t=3)$일때를 주의하자. 초록색 보드에서 기준점은 세로 도미노의 위칸으로 들어오지만, 이는 파란색 보드에서 가로 도미노의 오른쪽 칸이다. 그래서 $(3, x, y)$에는 $(2, y, 2-x)$가 대응되어야 한다.

세로 도미노의 경우, 동시에 두 줄이 지워질 수도 있으니 주의하자. 아랫 줄만 두번 체크한다면, 윗 줄만 지워지는 경우에 틀리게 된다. 아랫 줄을 체크하고 윗줄을 체크하면, 반대로 두 줄 다 지워지는 경우에 틀리게 된다. 이를 해결하려면 윗 줄 먼저 체크하고, 그다음 아랫 줄 체크하면 된다.

Code

링크

#include <algorithm>
#include <deque>
#include <iostream>
#include <vector>

using namespace std;

int N;
int green[6][4] = {0};
int blue[6][4] = {0};

// delete a row and move all upper rows downward.
void burst_row(int arr[][4], int row) {
    for (int i = 0; row - i - 1 >= 0; i++) {
        for (int j = 0; j < 4; j++) {
            arr[row - i][j] = arr[row - i - 1][j];
        }
    }
    for (int j = 0; j < 4; j++) {
        arr[0][j] = 0;
    }
}

// check if a row is full and delete it.
int check_and_burst(int arr[][4], int row) {
    int ret = 0;
    bool check_burst = true;
    for (int i = 0; i < 4; i++) {
        if (arr[row][i] == 0) {
            check_burst = false;
            break;
        }
    }
    if (check_burst) {
        // add point
        ret++;
        burst_row(arr, row);
    }
    return ret;
}

// debug function
void print_minos() {
    cout << "green" << '\n';
    for (int i = 0; i < 6; i++) {
        for (int j = 0; j < 4; j++) {
            cout << green[i][j] << ' ';
        }
        cout << '\n';
    }
    cout << "blue" << '\n';
    for (int i = 0; i < 6; i++) {
        for (int j = 0; j < 4; j++) {
            cout << blue[i][j] << ' ';
        }
        cout << '\n';
    }
}

int put_mino(int arr[][4], int t, int x, int y) {
    int row = 0;
    int ret = 0;
    if (t == 1) {
        while (arr[row][y] == 0 && row < 6) {
            row++;
        }
        arr[--row][y] = 1;
        ret += check_and_burst(arr, row);
        // check special cells
        for (int j = 0; j < 4; j++) {
            if (arr[1][j] == 1) {
                burst_row(arr, 5);
                break;
            }
        }
    } else if (t == 2) {
        // find available position
        while (arr[row][y] == 0 && arr[row][y + 1] == 0 && row < 6) {
            row++;
        }
        row--;
        arr[row][y] = 1;
        arr[row][y + 1] = 1;
        ret += check_and_burst(arr, row);
        // special cells
        for (int j = 0; j < 4; j++) {
            if (arr[1][j] == 1) {
                burst_row(arr, 5);
                break;
            }
        }
    } else {  // t== 3
        // find available position
        while (arr[row + 1][y] == 0 && row + 1 < 6) {
            row++;
        }
        row--;
        arr[row][y] = 1;
        arr[row + 1][y] = 1;
        // check line burst
        ret += check_and_burst(arr, row);      // upper
        ret += check_and_burst(arr, row + 1);  // lower
        // special cells
        // do it twice.
        for (int foo = 0; foo < 2; foo++) {
            for (int j = 0; j < 4; j++) {
                if (arr[1][j] == 1) {
                    burst_row(arr, 5);
                    break;
                }
            }
        }
    }
    return ret;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);
    // get input
    cin >> N;
    int ans = 0;
    while (N--) {
        int t, x, y;
        cin >> t >> x >> y;
        ans += put_mino(green, t, x, y);
        if (t == 1) {
            ans += put_mino(blue, 1, y, 3 - x);
        } else if (t == 2) {
            ans += put_mino(blue, 3, y, 3 - x);
        } else {
            ans += put_mino(blue, 2, y, 2 - x);
        }
        // print_minos();
    }

    int num = 0;
    for (int i = 2; i < 6; i++) {
        for (int j = 0; j < 4; j++) {
            num += green[i][j];
            num += blue[i][j];
        }
    }

    cout << ans << '\n'
         << num;
    return 0;
}

여담

  • 2차원 배열을 함수의 파라미터로 넘기는 법을 이번 기회에 알아보게 되었다. 여기를 참고했으며, fixed size array일 때는 그냥 int arr[][42]와 같이 넘기면 된다.
]]>
Semin Kim<[email protected]>