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

 

2504번: 괄호의 값

4개의 기호 ‘(’, ‘)’, ‘[’, ‘]’를 이용해서 만들어지는 괄호열 중에서 올바른 괄호열이란 다음과 같이 정의된다. 한 쌍의 괄호로만 이루어진 ‘()’와 ‘[]’는 올바른 괄호열이다. 만일 X

www.acmicpc.net

 

구현방법에서 낑낑댔다. 괄호는 스택을 사용해서 매칭시킬 수 있다는 것은 스택을 공부해봤다면 알고 가게 되는 기본 지식이지만, 중간값들을 어떻게 처리할 것인지에 대해서 계속 고민이 됐다.

 

어떻게 처리할지 고민하다 분할정복 기법으로 풀어봤다. 다 풀고 다른 사람들 풀이를 봤는데, 음.. 내가 좀 어렵게 푼 편이구나 싶기도 하고. 그래도 문제 푸는 방법이 1가지만 있는 게 아니니까 뭐.

 

def solution(parenthesis, s, e):
	# base case : 바로 답을 구할 수 있는 경우
    if e == s + 1:
        if parenthesis[s] == "(":
            return 2
        else:
            return 3
	
    # recursive case : 문제가 너무 커서 바로 답 못 구하는 경우 -> 그룹별로 쪼갠다
    group, stack = [], []
    little_s, little_e = 0, 0

    for i in range(s + 1, e):
        if parenthesis[i] in ("(", "["):
            if not stack:
                little_s = i
            stack.append(parenthesis[i])
            continue
        if len(stack) == 1:
            little_e = i
            group.append([little_s, little_e])
        stack.pop()
        continue

    result = 0
    for ns, ne in group:
        result += solution(parenthesis, ns, ne)
    return 2 * result if parenthesis[s] == "(" else 3 * result


parenthesis = "(" + input().strip() + ")"
stack = []
for i in range(len(parenthesis)):
    if parenthesis[i] in ("(", "["):
        stack.append(parenthesis[i])
        continue
    if not stack:
        print(0)
        exit()
    if (parenthesis[i] == ")" and stack[-1] == "[") or (parenthesis[i] == "]" and stack[-1] == "("):
        print(0)
        exit()
    stack.pop()

print(solution(parenthesis, 0, len(parenthesis) - 1) // 2)

 

기본적인 컨셉은 괄호 문자열을 그룹별로 나눌 수 있다는 점을 이용한 거다. ([])[[]]라는 문자열은 ([])과 [[]]라는 두 개의 그룹으로 구성되는 것을 이용하는 것. 그룹별로 계속 쪼개주다가 ()나 []이라는 base case를 만나게 되는데, 그 때는 2나 3을 리턴하도록 만들었다. 각 그룹에 대해 solution메서드가 동작하며, 이 놈이 파라미터로 받은 그룹을 쪼개서 쪼개진 그룹의 결과들을 더해가도록 만들면 됐다.

 

분할정복은 다음 영상을 참고하면 된다(본인 유튜브 홍보..ㅋㅋㅋㅋ)

https://www.youtube.com/watch?v=qDEKiNzAH1U&t=144s 

 

3월 8일 오늘 14시, 저번 주 토요일에 본 sw마에스트로 2차 코테의 결과가 나왔다.

일단 결과는 합격이어서 다음 주 심층면접을 준비해야 하는 상황.

마음도 진정시키고, 어차피 심층면접 때 2차 코테 내용을 물을 수도 있다 하니 간단히 복기할 겸 짧은 글을 써보기로 한다.


1차 코딩테스트 

  유형 난이도(백준 기준) 예상
1번 단순 구현 실버 4
2번 라인스위핑 or 완전탐색 골드 5 ~ 골드 4
3번 조합 + BFS 실버 1 ~ 골드 5
4번 DFS(백트래킹) 골드 5 ~ 골드 4
5번(sql) 문자열 및 정규표현식  

※ 유형은 각자가 푼 방법이 다르기 때문에 꼭 저 유형이 나왔다고 할 수 없습니다. 난이도는 제가 체감한 난이도입니다. 시험 응시 당시 제 티어는 골드 1이었습니다.

 

우선 나는 sql을 공부한 적이 없기에 1차 코테 전에 1주일 정도 프로그래머스 고득점 kit을 풀며 sql을 준비했었다. 그래서 코테 응시할 때 염두한 전략이 sql을 먼저 푸는 거였는데, 고득점 kit에서는 보지 못한 유형이 나와 못 풀었다..

 

1, 3번 풀고 2번을 풀다가 끝이 났다. 즉 2솔로 제출..이 때 서버가 터져서 45분의 추가시간이 주어졌는데, 나는 스터디룸을 예약하여 응시하던 상태라 추가시간을 활용하지 못 했다. 그러나 마에스트로 측에서도 이런 불합리성 등을 감안했는지 1차 코테를 응시한 전 인원을 합격처리해줬다.

 

2차 코딩테스트

  유형 난이도(백준 기준)
1번 단순 구현 실버 3 ~ 실버 2
2번 그리디..? 골드 3 이상
3번 자료구조 + 빡구현 골드 4
4번 플로이드 워셜..? 골드 3 이상
5번(sql) LEFT JOIN  

※ 유형은 각자가 푼 방법이 다르기 때문에 꼭 저 유형이 나왔다고 할 수 없습니다. 난이도는 제가 체감한 난이도입니다. 시험 응시 당시 제 티어는 골드 1이었습니다.

 

1, 3, 5번 풀어서 냈는데, 5번 풀 때는 그냥 JOIN으로 했다가 다 끝나고 나서 LEFT JOIN으로 했어야 한다는 걸 알아버렸다. 1, 3번은 구현유형이었던 만큼 내 풀이에 대한 확신을 가진 상태. sql문제가 부분점수가 있는지는 모르겠지만 2솔 ~ 2.5솔 정도 된다고 생각하고 기다렸다. 아 그리고 2번 문제는 풀다가 끝났었다.

 

 

암튼, 결과는 합격. 기준은 모른다. 1.5솔 합격이신 분도 계시고, 2솔 탈락인 분도 계시고..

 

 

이제부턴 정말 주저리.

 

개인적으로 소마 자체에 합격하는 것을 바라지만, 코테 합격이라도 했으면 좋겠다는 생각을 했다.

 

 작년 12월에 우아한테크코스 프리코스에서 떨어진 적이 있다. 최종테스트에서 떨어진 것도 아니고, 최종테스트 인원 선발 과정에서 떨어졌었다. 적잖은 충격을 받았었다. 학교 과제까지 제껴두고 프리코스 과정에 올인했었고, 제시받은 모든 요구사항들을 잘 지켜나갔으며 매 주차마다 회고글을 작성하고 다른 사람들과 피어리뷰도 진행하는 등 내가 할 수 있는 모든 최선을 다해 프리코스에 임했던 만큼, 최소한 최종테스트를 응시할 기회는 받을 수 있으리라 예상했기 때문이다.

 어찌보면 내 자만이었을 것 같기도 하다. 왜냐하면 난 지금까지 내 스스로가 최선을 다했다고 느꼈던 부분들, 내가 봐도 나 정말 열심히 했다고 느꼈던 부분들에서는 비교적 좋은 결과를 얻어왔기 때문인 것 같다. 그러나 최종테스트 응시를 할 기회조차 주어지지 않았다. 답답한 것은, 어떤 부분이 부족해서 떨어졌는지를 모른다는 거였다.

 그래서였을까. 스스로에게 회의감이 들었다. 내가 해온 공부방식이 사실은 잘못된 방식이 아닐까라는 느낌. 매번 달리지는 않았어도 꾸준히 조금씩이라도 걸어가고 있다고 생각한 길이 사실 돌아보면 이미 한참 전에 잘못 들어선 길이 아닐까. 나는 제대로, 잘 해오고 있던 걸까. 내가 공부해왔던 것들이 사실 무의미했던 것들이 아닐까.

 

 사실 이번 마에스트로에서 응시한 코테가 내 인생 첫 코딩테스트다. 1월 말부터 시작해 약 1달 정도 강도있게 준비하기 시작했지만, 사실 이전에도 알고리즘 공부를 했던 적이 있다. 빡세게 하던 건 아니었지만 20년도 7월부터 21년도 2월 정도까지 기초적인 문제들을 풀었었고, 21년도 9월부터 22년도 4월정도까지는 멋사 동아리에서 만난 사람들과 스터디 형태로 일주일에 4문제씩은 풀었었다. 1월 말에 다시 알고리즘 공부를 시작한 시점에 이전에 공부했던 것들을 까먹기도 했고 감도 잃은 상태였지만, 어찌됐든 난 알고리즘 공부를 옛날부터 해왔던 사람인 거다.

 그래서 조금은 무서웠다. 만약 이번에 마에스트로 과정을 코테 단계에서 떨어진다면, 우아한테크코스 프리코스 과정에서 탈락했을 때와 마찬가지로 뭔가 스스로가 부정당할 것 같은 느낌이었다. "나 그래도 옛날부터 조금씩은 알고리즘 공부해온 사람인데, 사실 내가 옛날부터 해왔던 알고리즘 공부 역시 잘못된 게 아닐까" 라는 두려움.

 

 물론 내가 이번 코테를 잘 본 사람은 아니다. 난 딱 합격컷 위에 서있는 사람일거다. 그래도 다행인 건, 내가 틀리지 않았다는 느낌이 든다는 거다. 거의 다 까먹은 상태에서 1달동안 빡세게 준비하긴 했지만, 그 전에 알고리즘 공부를 했던 적이 없다면 아마 코테에서 떨어졌을 거다. 그 이전에 알고리즘 공부를 한 경험들이 있기 때문에 1달동안 준비가 나름대로 돼서 붙은 것 같다. 내가 공부해온 것들이 의미가 있었다 라는 걸 느껴서 스스로에게 정말 다행인 것 같다.

 

 암튼, 다음 주가 심층면접인 만큼 다시 한 번 최선을 다해 준비해야한다. 마지막 한 걸음 남았다고 생각한다. 열심히 준비해서 최선을 다해보자. 화이팅..!

 

 

 

 

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

 

17142번: 연구소 3

인체에 치명적인 바이러스를 연구하던 연구소에 승원이가 침입했고, 바이러스를 유출하려고 한다. 바이러스는 활성 상태와 비활성 상태가 있다. 가장 처음에 모든 바이러스는 비활성 상태이고

www.acmicpc.net


조합과 BFS가 짬뽕된 문제. 바이러스를 놓을 수 있는 위치들에 대한 m개의 조합들을 하나하나 살펴보며 BFS를 돌려 판별하면 된다. 

이 문제에선 고려해야 할 사항이 좀 더 있다. 바로 BFS의 종료조건.

 

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

 

7576번: 토마토

첫 줄에는 상자의 크기를 나타내는 두 정수 M,N이 주어진다. M은 상자의 가로 칸의 수, N은 상자의 세로 칸의 수를 나타낸다. 단, 2 ≤ M,N ≤ 1,000 이다. 둘째 줄부터는 하나의 상자에 저장된 토마토

www.acmicpc.net

 

위 문제 "토마토"는 본 문제인 "연구소 3"과 꽤나 유사하다. 익은 토마토가 매 페이즈마다 주변 토마토를 익게 하듯이, 본 문제에서의 바이러스도 매 페이즈마다 주변 칸들에 퍼져 나간다. 또한 모든 토마토가 익을 때까지 최소 며칠이 걸리는지 계산하는 것과 빈 칸이 모두 바이러스가 퍼질 때까지 최소 얼마나 걸리는지를 요구하는 것도 비슷하다.

 

그러나 디테일에 차이가 있다. 토마토 문제는 모든 토마토가 익을 때까지 BFS를 돌려야 한다면, 본 문제는 빈 칸에 바이러스가 채워질 때까지만 BFS를 돌려야 한다. 즉 벽을 제외한 모든 칸이 활성 바이러스로 채워질 때까지 돌리는 게 아니다. 비활성된 바이러스가 있어도 다른 빈 칸들이 남아있지 않다면 더 이상 BFS를 돌릴 필요가 없다.

 

따라서 안 익는 토마토가 있든 없든 일단 Queue가 빌 때까지 BFS를 돌려 가능한 모든 구역의 토마토가 익게 한 다음, Queue가 비고 나면 그제서야 모든 토마토가 익어도 되는지 판단해도 되는 토마토 문제와는 달리, 본 문제는 BFS를 돌릴 때마다 연구소 상황을 체크해야 한다. (무작정 큐가 빌 때까지 돌리면 빈 칸들이 다 찰 때까지가 아니라 벽을 제외한 모든 칸에 활성 바이러스가 퍼질 때까지를 보게 되는 문제로 바뀌니까)

 

즉 빈칸들을 채울 때마다 카운트해주고, 매 BFS마다 현재까지 채운 빈칸의 수와 처음에 셌던 빈칸의 수가 일치하는지를 판단해야 한다. 즉 큐에서 꺼낸 노드마다 상하좌우 네 방향에 대해 방문여부를 확인하고 방문여부를 안 했고 그 칸이 빈칸이면 채운 빈칸의 수를 하나 늘리면 된다.

 

내가 실수한 부분은, 빈칸을 채운 뒤 채운 빈칸의 수를 늘리는 작업과 빈 칸을 다 채웠는지 판단하는 작업을 동시에 안 했다는 거다. 나는 BFS를 할 때 보통 종료조건을 판단하는 작업을 큐에서 노드를 꺼내고 난 바로 다음에 한다. 얘를 들어 지금 꺼낸 노드가 도착지점이라면 지금까지 카운트한 수가 정답이다! 처럼 판단하는 것. 즉 이 문제는 이런 습관에 따라 처음에 다음과 같이 BFS코드를 짰다.

 

while q:
    1. 만약 지금까지 채운 빈칸의 수가 처음에 센 빈칸의 수와 같다면 종료하는 코드
    2. 인접한 칸들이 빈 칸이고 방문하지 않았다면 그 칸을 방문처리하고 큐에 추가한 다음
    지금까지 채운 빈칸 수를 += 1

 

빈칸을 채우고 채운 빈칸 수를 늘리는 작업(2)과 빈칸을 다 채웠는지 판단하는 작업(1)을 동시에 하고 있지 않는 모습이다. 이 때문에 오답이 날 수 있다. 큐에서 노드를 하나 꺼냈는데 x초에 방문한 노드였고, 이 노드의 인접한 빈칸을 채우고 채운 빈칸 수를 늘렸다. 큐에서 다음 노드를 빼냈고 똑같이 x초에 방문한 노드인데, (1)에 의해 지금껏 채운 빈칸 수가 처음에 센 빈칸의 수와 같아 x초에 모든 빈칸을 채웠다고 오답을 뱉게 된다. 사실 x + 1초에 모든 빈 칸이 채워지는 건데 말이다. 만약 임의의 시간 x초에 방문하는 모든 노드들이 방문하지 않은 인접한 빈 칸을 하나 이상씩 가지고 있는게 보장된다면 이런 일은 안 생기겠지만, x초에 방문하는 모든 노드가 방문하지 않은 인접한 빈칸을 가지고 있다는 보장이 없으니 이런 상황이 생긴다.

 

난 그래서 처음에 틀렸다..ㅋㅋㅋㅋㅋ

 

암튼..! 빈칸을 채우고 채운 빈칸 수를 늘리는 작업과 빈칸을 다 채웠는지 판단하는 작업을 동시에 해야 한다. 다음과 같이 바꿀 수 있을 것이다.

 

while q:
    1. 지금 꺼낸 노드가 빈 칸이면 지금껏 채운 빈 칸의 수 += 1
    2. 만약 지금까지 채운 빈칸의 수가 처음에 센 빈칸의 수와 같다면 종료
    3. 인접한 칸들이 빈 칸이고 방문하지 않았다면 그 칸을 방문처리하고 큐에 추가

 

또는 다음과 같이도 가능하다. 중요한 건 동시에 두 작업을 순서대로 해야 한다는 거.

 

while q:
    1. 꺼낸 노드에 인접한 칸들이 빈 칸이고 방문하지 않았다면 그 칸을 방문처리하고 큐에 추가한 다음
    지금까지 채운 빈칸 수를 += 1
    2. 지금까지 채운 빈칸 수가 처음에 센 빈칸 수와 같다면 종료. 단 이 작업은 1이 이루어지고 난 이후에만 진행

 

전체 코드는 다음과 같다.

 

from itertools import combinations
from collections import deque
import sys
input = sys.stdin.readline


def solution(n, m, board):
    directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]

    empty_count = 0
    virus = []
    for r in range(n):
        for c in range(n):
            if board[r][c] == 2:
                virus.append((r, c))
            elif board[r][c] == 0:
                empty_count += 1

    answers = []

    for activate_viruses in combinations(virus, m):
        q = deque([])
        visited = set()
        for r, c in activate_viruses:
            q.append((r, c, 0))
            visited.add((r, c))

        empty_to_virus_count = 0

        while q:
            r, c, h = q.popleft()
            if board[r][c] == 0:
                empty_to_virus_count += 1
            if empty_to_virus_count == empty_count:
                answers.append(h)
                break
            for dr, dc in directions:
                nr, nc = r + dr, c + dc
                if nr < 0 or nc < 0 or nr >= n or nc >= n:
                    continue
                if board[nr][nc] != 1 and (nr, nc) not in visited:
                    visited.add((nr, nc))
                    q.append((nr, nc, h + 1))

    if answers:
        return min(answers)
    return -1


N, M = map(int, input().split())
_board = [list(map(int, input().split())) for _ in range(N)]

print(solution(N, M, _board))

 

이 문제를 통해 얻은 것.

"다음 노드들에 대한 작업과 종료여부를 판단하는 작업은 동시에 할 것"

 

 

저번에 등록(create) 기능까지 만들었고, 이번엔 조회(Read), 수정(Update), 삭제(Delete) 기능을 만들어보려 한다. 우선 조회부터 시작쓰.

 

게시글의 상세내용을 보기 위해서 클라쪽에서 서버 쪽으로 게시글id를 넘기도록 할 건데, 이 때 url변수로 넘기든가(ex : /board/1) 아님 쿼리스트링 형태로 넘기든가(ex : /board?id=1) 해야 한다. (body에 적는 방법 등도 있지만..패쓰) 아무래도 대중적인 방법은 url변수로 하는 방법이라 어떻게 하는지 알아봤다.

 

방법은 간단했다. @PathVariable이라는 어노테이션을 사용하면 됐다! 방법은

 

  • GetMapping(Post든 뭐든 상관 X)의 url입력하는 부분에 {받을 변수명}
  • 메서드의 파라미터로 @PathVariable("받을 변수명")

 

이다. 다음과 같이 활용해줬다.

 

@GetMapping("/{id}")
    public BoardResponseDTO getBoard(@PathVariable("id") Long id) {
        Board board = boardService.findBoard(id).get();
        // ... 생략
    }

 

그리고 다음과 같이 BoardResponseDTO를 만들어줬다.

 

@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class BoardResponseDTO {
    private Long boardId;
    private String writerNickname;
    private String title;
    private String content;
    private Long hits;
    private Long like;
    private LocalDateTime createdDate;
    private LocalDateTime updatedDate;
    private int categoryId;

    public static BoardResponseDTO from(Member writer, Board board) {
        return new BoardResponseDTO(
                board.getBoardId(),
                writer.getNickname(),
                board.getTitle(),
                board.getContent(),
                board.getHits(),
                board.getLike(),
                board.getCreatedDate(),
                board.getUpdatedDate(),
                board.getCategoryId()
        );
    }
}

 

요즘 이 토이플젝을 하면서 참고하는 분이 있는데, 그 분은 이런 식으로 from메서드를 만들어 활용하는 식으로 하는 것 같아 참고해봤다. AccessLevel지정해주는 건 Lombok으로 만들어주는 생성자의 접근지정자를 설정하는거다. private로 한 것은 외부에서 요 놈의 인스턴스를 만드는 것을 막아주므로, BoardResponseDTO를 만들려면 from메서드를 사용하는 수밖에 없다!

 

암튼! 그래서 결과는..

 

 

성공~! 저번에 인터셉터 통해서 응답값들을 통일시켜준 방식 그대로 뱉어지는 모습도 곁들여 볼 수 있었다.

 

그 다음으론 삭제기능을 만들어보기로 했다. 삭제 기능에서의 고민은 바로 "삭제 권한". 글의 작성자만이 본인의 글을 삭제할 수 있도록 해야 할 것이다. 저번 학기에 프론트를 했을 때는 따로 프론트 쪽에서 현재 로그인한 유저의 정보를 가지고 있었기 때문에, 내가 지금 보고 있는 글의 작성자와 내가 가진 유저의 정보가 같으면 그 글에 삭제버튼을 보이게 하는 식으로 이를 만들었다. 그러나 지금 드는 생각은 어차피 contextHolder?거기에 현재 로그인한 유저의 정보를 가진다 하니 걔를 통해서 비교해줄 수 있을 것 같다!

 

그래서 다음과 같이 일단 delete에 해당하는 메서드를 만들어줬다.

 

@DeleteMapping("/{id}")
public String deleteBoard(@AuthenticationPrincipal Member member, @PathVariable("id") Long id) {
    Board board = boardService.findBoard(id).get();
    Member writer = board.getWriter();
    if (writer.getEmail().equals(member.getEmail())) {
        return "same";
    }
    return "diff";
}

 

현재 유저와 게시글의 작성자 이메일이 같다면 same을 뱉고, 다르면 diff를 뱉을 거다. 우선 작성자로 로그인하고 request를 보내봤다! 결과는..

 

 

같다고 잘 나왔다. 이번엔 다른 사용자로 로그인하고 (즉 포스트맨 상에서는 헤더에 담는 토큰을 다른 걸로 설정) 보내봤다. 결과는..

 

 

다르다고 잘 나온다! 삭제코드 자체는 BoardRepository에 deleteById메서드를 만들어 다음과 같이 구현해줬다.

 

@Override
public void deleteById(Long id) {
    Board board = em.find(Board.class, id);
    em.remove(board);
}

 

암튼 그렇게 해서 만든 삭제기능은..

 

 

27번 게시물이 잘 삭제된 것을 볼 수 있었다.

 

마지막으로 수정 기능을 만들 차례. 제목이나 본문의 내용이 잘 바뀌는지도 중요하겠지만, 아무래도 관건은 

 

  1. 수정일이 알아서 업데이트되는지, 생성일자는 변함없는지
  2. 게시물의 id가 그대로 유지되는지

 

정도가 될 것 같다. 

컨트롤러에서 다음과 같이 코드를 작성했다.

 

@PutMapping("/{id}")
public BoardResponseDTO updateBoard(
        @AuthenticationPrincipal Member member,
        @PathVariable Long id,
        @RequestBody BoardDTO boardDTO) {
    Board oldBoard = boardService.findBoard(id).get();
    Member writer = oldBoard.getWriter();

    if (writer.getEmail().equals(member.getEmail())) {
        Board updatedBoard = boardService.updateBoard(id, boardDTO);
        return BoardResponseDTO.from(writer, updatedBoard);
    }
    
    return null;
}

 

역시나 요청을 보낸 사람의 정보와 게시글 작성자가 같을 때에만 수정이 이뤄지게 했다. 그리고 게시글 작성 때 사용했던 BoardDTO를 수정할 때도 그대로 재탕해줬다. 어차피 게시글 작성할 때 작성하는 부분이 수정할 때 작성하는 부분이랑 같으니까..

BoardRepository에서 다음과 같이 코드를 짜 수정이 이루어지게끔 했다.

 

@Override
public Board updateBoard(Long id, BoardDTO boardDTO) {
    Board board = em.find(Board.class, id);
    board.setTitle(boardDTO.getTitle());
    board.setContent(boardDTO.getContent());
    board.setCategoryId(boardDTO.getCategoryId());
    em.persist(board);
    return board;
}

 

세터 메서드들을 사용해 Board엔티티의 내용물들을 바꿔치기해주고 저장해주는 식. entitymanager의 save라는 메서드가 인자로 받는 entity의 id값이 있냐없냐에 따라 insert를 할지 update를 할지 결정해준다고 한다. 이 경우에선 이미 board는 id가 있는 상태이니 update가 될 것이다.

 

이제 한 번 시험해볼 차례! 우선 수정 전 db상황은 다음과 같았다. 여기서 28번 게시물을 바꿀 거다.

 

 

포스트맨을 사용해 수정 테스트하기. 결과는..!

 

 

우선 응답값 자체는 문제가 없어보인다! 과연 db에서는..?

 

 

수정한 부분들이 잘 바뀌었음을 확인했다. title, content, category_id 컬럼값이 바뀌었으며 board_id나 writer는 변함없는 걸 볼 수 있다. 또한 created_date는 변화가 없고 updated_date에만 변화가 생겼음을 잘 확인할 수 있었다.

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

 

14890번: 경사로

첫째 줄에 N (2 ≤ N ≤ 100)과 L (1 ≤ L ≤ N)이 주어진다. 둘째 줄부터 N개의 줄에 지도가 주어진다. 각 칸의 높이는 10보다 작거나 같은 자연수이다.

www.acmicpc.net

 

행 하나하나, 열 하나하나에 대해 지나갈 수 있는지 판단해주면 되는 문제. 단순히 구현만 해주면 된다.

내가 처음에 작성한 코드는 다음과 같다.

 

import sys
input = sys.stdin.readline


def is_all_same_height(heights):
    for i in range(len(heights) - 1):
        if heights[i] != heights[i + 1]:
            return False
    return True


def is_all_same_column(board, c):
    for r in range(len(board) - 1):
        if board[r][c] != board[r + 1][c]:
            return False
    return True


def solution(n, k, board):
    answer = 0

    for row in board:
        checked = [False for _ in range(n)]
        if is_all_same_height(row):
            answer += 1
        else:
            for c in range(n - 1):
                if row[c] == row[c + 1]:
                    continue
                if row[c] + 1 == row[c + 1]:
                    for nc in range(c, c - k, -1):
                        if nc < 0 or checked[nc] or row[nc] != row[c]:
                            break
                    else:
                        nc = c - k + 1
                        for i in range(nc, c + 1):
                            checked[i] = True
                        continue
                    break
                elif row[c] == row[c + 1] + 1:
                    for nc in range(c + 1, c + 1 + k):
                        if nc >= n or checked[nc] or row[nc] != row[c + 1]:
                            break
                    else:
                        nc = c + 1 + k
                        for i in range(c + 1, nc):
                            checked[i] = True
                        continue
                    break
                else:
                    break
            else:
                answer += 1

    for c in range(n):
        checked = [False for _ in range(n)]
        if is_all_same_column(board, c):
            answer += 1
        else:
            for r in range(n - 1):
                if board[r][c] == board[r + 1][c]:
                    continue
                if board[r][c] + 1 == board[r + 1][c]:
                    for nr in range(r, r - k, -1):
                        if nr < 0 or checked[nr] or board[nr][c] != board[r][c]:
                            break
                    else:
                        nr = r - k + 1
                        for i in range(nr, r + 1):
                            checked[i] = True
                        continue
                    break
                elif board[r][c] == board[r + 1][c] + 1:
                    for nr in range(r + 1, r + 1 + k):
                        if nr >= n or checked[nr] or board[nr][c] != board[r + 1][c]:
                            break
                    else:
                        nr = r + 1 + k
                        for i in range(r + 1, nr):
                            checked[i] = True
                        continue
                    break
                else:
                    break
            else:
                answer += 1
    return answer


N, L = map(int, input().split())
_board = []
for _ in range(N):
    _board.append(list(map(int, input().split())))

print(solution(N, L, _board))

 

사실 나조차도 돌이켜보면 어떤 일을 하는지 참 알기 어려운 코드..우테코 프리코스 이후로 이런 더러운 코드 작성을 내면에서 혐오하곤 있긴 한데, 코테의 특성상 일단 구현하는게 먼저라고 생각해 더러운 걸 알면서 일단 저렇게 풀어봤다.

 

대충 저 코드의 핵심은 행과 열에 대해 지나갈 수 있는지를 판단하는 코드가 다르다는 것이다. 행에 접근할 때와 열에 접근할 때는 인덱싱 방법이 달라지기 때문에 구분해서 만들었다.

 

그러나 다른 사람들의 풀이를 보니, 좀 더 간단한 방법이 있었다. 바로 행과 열을 인덱싱 방법 이런 거 상관없이 똑같은 하나의 1차원 배열로 보는 것이다. 즉 열도 행처럼 보는 것! 파이썬의 for문을 이용하면

 

board = [
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12],
    [13, 14, 15, 16]
]

column = [board[i][0] for i in range(4)]

 

이런 식으로 하나의 열을 1차원 리스트, 즉 하나의 행처럼 가져올 수 있다. 이렇게 하면 행과 열에 대한 판단을 할 때 각기 다른 코드를 만들 필요 없이 하나의 코드로 행과 열에 대한 판단을 내릴 수 있다.

 

import sys
input = sys.stdin.readline


def check_path(path, k):
    length = len(path)
    check = [0 for _ in range(length)]

    for i in range(length - 1):
        if abs(path[i] - path[i + 1]) > 1:
            return False
        if path[i] + 1 == path[i + 1]:
            for j in range(i, i - k, -1):
                if j < 0 or check[j] or path[j] != path[i]:
                    return False
                check[j] = 1
            continue
        if path[i] == path[i + 1] + 1:
            for j in range(i + 1, i + 1 + k):
                if j >= length or check[j] or path[j] != path[i + 1]:
                    return False
                check[j] = 1
    return True


def solution(n, k, board):
    answer = 0
	# 행에 대한 판단
    for row in board:
        if check_path(row, k):
            answer += 1
	# 열에 대한 판단
    for c in range(n):
        if check_path([board[i][c] for i in range(n)], k):
            answer += 1

    return answer


N, L = map(int, input().split())
_board = []
for _ in range(N):
    _board.append(list(map(int, input().split())))

print(solution(N, L, _board))

 

구현..이라는 종목을 풀 때는, 이런 식으로 단순화해서 생각하는게 중요한 것 같다. 안 그러면 시간이 너무 오래 걸리는 듯..

+ Recent posts