https://programmers.co.kr/competitions/2231/2022-sk-challenge

 

2022 SK ICT Family 개발자 채용 챌린지

접수   22년 02월 25일 10:00 ~ 03월 10일 17:00 테스트   22년 03월 12일 10:00 ~ 03월 19일 17:00

programmers.co.kr

 

지난주에 치른 1차 코테 결과 합격 메일을 받고 오늘 2차 코테를 치렀다.

오늘은 4시간 동안 4문제를 푸는 시험이었다.

결론부터 말하자면 1solve로 광탈 확정이다.

 

1번 문제: 브루트 포스

2번 문제: 정렬 및 스위핑, 엔트리 처리

3번 문제: 서로 다른 모양의 트리를 서로 같게 만들기(주어진 조건 하에서 최적의 방법으로) - 처음 보는 유형

4번 문제: 읽어보지도 못함

 

1차 코테 문제들은 큰 틀에서 로직만 떠올릴 수 있다면,

그 로직을 구현함에 있어서는 큰 어려움이 없었는데,

2차 코테 문제들은 그렇지 않은 듯하다.

 

1번 문제 후딱 풀고 2번 문제 로직을 구상할 때까지만 해도

올해 목표인 "입사 시험 2차까지 붙기"를 달성하는 꿈을 꿨다. 아주 잠깐.

꿈에서 깨고 난 뒤에는 조급함, 착잡함과 편두통으로 가득한 3시간 30분을 보냈다.

 

탈탈 털린 오늘 시험이었지만,

아쉬운 실수는 없었다.

그냥 실력이 모자랐다.

처음 치른 2차 코테였고,

아직은 나아갈 길이 많이 남았다.

아직 이 화면을 보고 후련해지기에도 많이 이른 것 같다

후후 불어서 만든 풍선이 터져서 씁쓸한 오늘이지만,

툴툴 털어넘기고,

지금껏 해오던 대로 쭉 이어가야지.

1.1.1 버전에서는 데이터의 저장을 사용자의 수동 처리에 의존하고 있었기 때문에, 사용자가 깜빡하거나 일부러 데이터를 입력하지 않았을 경우, 시계열 데이터에 흠결이 생긴다. 또한 데이터셋의 날짜 컬럼을 다루는 로직을 설계하는 일이 까다로워진다. 따라서 데이터 처리 과정을 매끄럽게 하고, 신뢰성 있는 결과를 도출하기 위해, 데이터가 저장되지 않은 날짜의 데이터를 자동으로 저장하는 기능을 넣었다.

 

1일 1백준에 아주 커다란 동기부여가 되어 준 solved.ac의 [스트릭]을 프로그램에 넣어 봤다. 내가 어느 시기에 소홀했고, 어느 시기에 꾸준했는지를 한 눈에 볼 수 있다는 장점이 있다. 현재 연속 기록을 경신하고 있다면, 계속 이어가기 위해서라도 꼭 하게 되는 효과도 있다.

이 인터페이스를 묘사해 볼 것이다

 

요약

미입력 데이터 자동 업데이트 및 플래그 스트릭 출력 기능 추가

사용 언어

Python

라이브러리

  1. csv
  2. datetime
  3. pytz
  4. numpy
  5. pandas
  6. matplotlib
  7. seaborn

새로운 기능

  1. 미입력 데이터 자동 업데이트
    1. 사용자가 할 일 목록 및 해낸 일 목록만 채우고 데이터 저장을 하지 않았을 경우
      • 해당 날짜에 데이터를 저장하고 초기화함
    2. 사용자가 아무 것도 하지 않았을 경우
      • 해당 날짜에 빈 배열과 숫자 0을 저장함
    3. 데이터 업데이트 시점
      • 프로그램이 실시간으로 돌아갈 수 없으므로, 사용자가 프로그램을 실행시켰을 때, 지난 날짜들에 대해 일괄적으로 저장되지 않은 데이터를 추가함
  2. 플래그 스트릭
    1. 지난 1년간 하루 할 일 관리를 한 날에 깃발을 꽂음
    2. 꽂힌 깃발들을 보여줌
    3. 할 일 관리의 최대 연속 일수, 현재 연속 일수를 표시함

내부 변경 사항

  1. 데이터 시각화 디자인을 위해 pandas와 seaborn 라이브러리 사용
  2. 요일별 분류가 용이하도록, data.csv의 날짜 컬럼에서 요일 정보를 제외하고 요일 컬럼을 추가함
  3. 파일 경로 추적이 용이하도록, 파일 경로를 나타내는 변수 선언
  4. pytz 라이브러리를 사용하여, date 라이브러리로 호출되는 모든 시간이 한국 표준시에 따름

 

메인 프롬프트에 오늘 날짜가 나오도록 했다.

 

 

표준출력으로 구현한 스트릭. 마땅하게 호환이 되는 유니코드 문자로 깃발을 사용했는데, 이 프로그램을 클라이언트와 서버로 나누게 될 때 두 번째로 개선할 기능이다. 유저와 상호작용이 안 되는 스트릭은 역시 밋밋하다. 흑색과 백색밖에 없어서 그런 것 같기도...
비슷한가???

 

그래프가 빽빽하게 늘어서 있을수록 디자인이 좋아지는 그래프다. 막대를 365개 세우는 그날까지 꾸준히!

더보기
# © 2022 starsein <dbtjd1928@gmail.com>
import csv
import datetime as dt
import pytz
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from typing import List, Tuple

# 입출력에 사용되는 파일 목록
DATA_CSV = 'data.csv'
TODO_LIST_CSV = 'todoList.csv'
COMPLETED_TASK_LIST_CSV = 'completedTaskList.csv'


# data.csv 의 컬럼별 항목 분류
DATE = 0
WEEKDAY = 1
UNCOMPLETED_TASK_LIST = 2
UNCOMPLETED_TASK_NUM = 3
COMPLETED_TASK_LIST = 4
COMPLETED_TASK_NUM = 5


def update_data():
    with open(DATA_CSV, 'r', encoding='utf-8-sig') as rf:
        total_data = rf.readlines()
        if len(total_data) == 0:
            return
        recent_date_str = total_data[-1].split(',')[DATE]
        today_obj = dt.date.fromisoformat(get_today_weekday()[0])
        target_day_obj = dt.date.fromisoformat(recent_date_str) + dt.timedelta(days=1)
        updating_data_list = []
        while target_day_obj < today_obj:
            target_day_str = target_day_obj.strftime("%Y-%m-%d")
            target_weekday_str = weekday_en_to_kr(target_day_obj.isoweekday())
            updating_data_list.append([target_day_str, target_weekday_str, [], 0, [], 0])
            target_day_obj += dt.timedelta(days=1)

    if len(updating_data_list):
        unsaved_todo = get_todo()
        unsaved_completed_task = get_completed_task()
        updating_data_list[0][2] = unsaved_todo
        updating_data_list[0][3] = len(unsaved_todo)
        updating_data_list[0][4] = unsaved_completed_task
        updating_data_list[0][5] = len(unsaved_completed_task)

        with open(TODO_LIST_CSV, 'w', encoding='utf-8-sig', newline=''):
            pass
        with open(COMPLETED_TASK_LIST_CSV, 'w', encoding='utf-8-sig', newline=''):
            pass

    with open(DATA_CSV, 'a', encoding='utf-8-sig', newline='') as af:
        writer = csv.writer(af)
        writer.writerows(updating_data_list)


def add_todo():
    print("[할 일 추가]")
    with open(TODO_LIST_CSV, 'a', encoding='utf-8-sig', newline='') as af:
        writer = csv.writer(af)
        print("오늘 할 일을 입력하세요.",
              "이전 메뉴로 돌아가려면 q 또는 Q를 입력하세요.", sep='\n')
        while True:
            todo_str = input().rstrip()
            if todo_str == 'q' or todo_str == 'Q':
                return 0
            writer.writerow([todo_str])
            print(f"{todo_str}가 할 일 목록에 정상적으로 추가되었습니다!")


def get_todo() -> List[str]:
    with open(TODO_LIST_CSV, 'r', encoding='utf-8-sig') as rf:
        todo_list = []
        reader = csv.reader(rf)
        for todo in reader:
            todo_list.append(*todo)
    return todo_list


def show_todo():
    print("[할 일 확인]")
    todo_list = get_todo()
    print("+------------------------------------+")
    for idx, todo in enumerate(todo_list, start=1):
        print(idx, todo)
    print("+------------------------------------+")
    while True:
        cmd = input("이전 메뉴로 돌아가려면 q 또는 Q를 입력하세요.\n").rstrip()
        if cmd == 'q' or cmd == 'Q':
            return 0


def add_completed_task():
    print("[해낸 일 추가]")
    todo_list = get_todo()
    with open(COMPLETED_TASK_LIST_CSV, 'a', encoding='utf-8-sig', newline='') as af:
        writer = csv.writer(af)
        print("오늘 해낸 일을 입력하세요.",
              "이전 메뉴로 돌아가려면 q 또는 Q를 입력하세요.", sep='\n')
        while True:
            print("[현재 할 일 목록]")
            print("+------------------------------------+")
            for idx, todo in enumerate(todo_list, start=1):
                print(idx, todo)
            print("+------------------------------------+")
            todo_str = input().rstrip()
            if todo_str == 'q' or todo_str == 'Q':
                break

            if todo_str not in todo_list:
                print("오늘 할 일에 없는 입력입니다.")
                continue

            todo_list.remove(todo_str)
            writer.writerow([todo_str])
    with open(TODO_LIST_CSV, 'w', encoding='utf-8-sig', newline='') as wf:
        writer = csv.writer(wf)
        for todo in todo_list:
            writer.writerow([todo])


def get_completed_task() -> List[str]:
    with open(COMPLETED_TASK_LIST_CSV, 'r', encoding='utf-8-sig') as rf:
        ct_list = []
        reader = csv.reader(rf)
        for completed_task in reader:
            ct_list.append(*completed_task)
    return ct_list


def show_completed_task():
    print("[해낸 일 확인]")
    ct_list = get_completed_task()
    print("+------------------------------------+")
    for idx, completed_task in enumerate(ct_list, start=1):
        print(idx, completed_task)
    print("+------------------------------------+")
    while True:
        cmd = input("이전 메뉴로 돌아가려면 q 또는 Q를 입력하세요.\n").rstrip()
        if cmd == 'q' or cmd == 'Q':
            return 0


def check_data(today_str: str) -> Tuple[str, List[List[str]]]:
    with open(DATA_CSV, "r", encoding='utf-8-sig') as rf:
        data_list = []
        reader = csv.reader(rf)
        for day_data in reader:
            data_list.append(day_data)
            date = day_data[0]
            if date == today_str:
                print("현재 날짜에 이미 저장된 데이터가 있습니다.")
                while True:
                    user_cmd = input("새로운 데이터로 덮어쓰기 하시겠습니까? [y/n]").rstrip()
                    if user_cmd == 'y':
                        return "OVERWRITE", data_list
                    elif user_cmd == 'n':
                        return "DON\'T OVERWRITE", data_list
    return "NOT TO OVERWRITE", data_list


def weekday_en_to_kr(weekday_int: int) -> str:
    translate_table = {1: "월",
                       2: "화",
                       3: "수",
                       4: "목",
                       5: "금",
                       6: "토",
                       7: "일"}
    return translate_table.get(weekday_int)


def get_today_weekday() -> Tuple[str, str]:
    KST = pytz.timezone('Asia/Seoul')
    info = dt.datetime.now(KST)
    today_str = info.strftime("%Y-%m-%d")
    weekday_str = weekday_en_to_kr(info.isoweekday())
    return today_str, weekday_str


def store_data():
    today_str, weekday_str = get_today_weekday()
    todo_list = get_todo()
    ct_list = get_completed_task()

    res, data_list = check_data(today_str)

    if res == "OVERWRITE":
        data_list.pop()
    elif res == "DON\'T OVERWRITE":
        return 0
    else:
        pass
    data_list.append([today_str, weekday_str, todo_list, len(todo_list), ct_list, len(ct_list)])

    with open(DATA_CSV, 'w', encoding='utf-8-sig', newline='') as wf:
        writer = csv.writer(wf)
        writer.writerows(data_list)

    with open(TODO_LIST_CSV, 'w', encoding='utf-8-sig', newline=''):
        pass
    with open(COMPLETED_TASK_LIST_CSV, 'w', encoding='utf-8-sig', newline=''):
        pass
    print("오늘의 데이터 집계 및 초기화가 완료되었습니다!")


def visualize_data():
    date_arr = np.array([])
    num_t_arr = np.array([])
    num_ct_arr = np.array([])
    cr_arr = np.array([])
    with open(DATA_CSV, 'r') as rf:
        reader = csv.reader(rf)
        for data in reader:
            stored_date, stored_weekday, ut, num_ut, ct, num_ct = data
            num_ut = int(num_ut)
            num_ct = int(num_ct)
            date_arr = np.append(date_arr, stored_date)
            num_t_arr = np.append(num_t_arr, num_ut + num_ct)
            num_ct_arr = np.append(num_ct_arr, num_ct)
            cr = round(num_ct / (num_ct + num_ut) * 100) if num_ct | num_ut != 0 else 0
            cr_arr = np.append(cr_arr, cr)

    print("[현재까지 집계된 데이터 시각화]")
    print(f"총 {len(date_arr)}개 날짜의 데이터가 저장되어 있습니다.")
    user_cmd = int(input("최근에 저장된 데이터를 몇 개까지 표시할까요?\n"))
    date_arr = date_arr[-user_cmd:]
    num_ct_arr = num_ct_arr[-user_cmd:]
    num_t_arr = num_t_arr[-user_cmd:]
    cr_arr = cr_arr[-user_cmd:]

    data_dict = {'date': date_arr,
                 'num_t': num_t_arr,
                 'num_ct': num_ct_arr,
                 'cr': cr_arr}
    data_df = pd.DataFrame(data=data_dict)

    plt.figure(figsize=(16, 8))
    plt.title("TodoTracker", fontsize=25)
    sns.set_style('darkgrid')
    sns.set_context('notebook')
    sns.barplot(x='date', y='num_t', data=data_df, palette='OrRd')
    sns.barplot(x='date', y='num_ct', data=data_df, palette='GnBu')
    sns.lineplot(x='date', y='cr', data=data_df, color='limegreen', marker='o', linestyle='-.')
    for i, v in enumerate(date_arr):
        plt.text(v, cr_arr[i], f"{int(cr_arr[i])}%", fontsize=13, fontfamily='monospace', horizontalalignment='center',
                 verticalalignment='bottom', color='limegreen')
    plt.xlabel("Date", fontsize=15)
    plt.ylabel(" ")
    plt.xticks(rotation=45)
    plt.show()


def show_date_streak():
    presence_date_set = set()
    with open(DATA_CSV, 'r') as rf:
        reader = csv.reader(rf)

        curr_cnt = 0
        max_cnt = 0
        for day_data in reader:
            _date = day_data[0]
            presence_date_set.add(_date)
            if day_data[COMPLETED_TASK_NUM]:
                curr_cnt += 1
                max_cnt = max(max_cnt, curr_cnt)
            else:
                curr_cnt = 0

    NUM_WEEK = 53
    NUM_WEEKDAY = 7
    matrix = [["  " for row in range(NUM_WEEK + 2)] for col in range(NUM_WEEKDAY + 1)]
    matrix[0][0], matrix[1][0], matrix[2][0], matrix[3][0], matrix[4][0], matrix[5][0], matrix[6][0]\
        = "S ", "M ", "T ", "W ", "T ", "F ", "S "

    today_obj = dt.date.fromisoformat(get_today_weekday()[0])
    a_year_ago_obj = today_obj - dt.timedelta(days=364)
    a_year_ago_weekday = a_year_ago_obj.isoweekday() % 7
    col = a_year_ago_weekday
    row = 2
    curr_day_obj = a_year_ago_obj
    while curr_day_obj <= today_obj:
        curr_day_str = curr_day_obj.strftime("%Y-%m-%d")
        BLACK_FLAG = "\u2691 "
        WHITE_FLAG = "\u2690 "
        if curr_day_str in presence_date_set:
            matrix[col][row] = BLACK_FLAG
        else:
            matrix[col][row] = WHITE_FLAG

        if curr_day_obj.day == 1:
            if curr_day_obj.month == 1:
                matrix[7][row] = f"{curr_day_obj.year}"
                try:
                    matrix[7][row+1] = ""
                except IndexError:
                    pass
            else:
                matrix[7][row] = f"{curr_day_obj.month}월"
                try:
                    matrix[7][row+1] = " "
                except IndexError:
                    pass

        curr_day_obj += dt.timedelta(days=1)
        col += 1
        if col == 7:
            col = 0
            row += 1

    print(f"최대 {max_cnt}일 연속 하루 관리, 현재 {curr_cnt}일", end=' ')
    if max_cnt == curr_cnt:
        print("!!!\n")
    else:
        print()
    for c in range(NUM_WEEKDAY + 1):
        for r in range(NUM_WEEK + 2):
            print(matrix[c][r], end='')
        print()


def main():
    update_data()
    func_str_dict = {1: "할 일 추가",
                     2: "할 일 확인",
                     3: "해낸 일 추가",
                     4: "해낸 일 확인",
                     5: "오늘의 데이터 집계 및 초기화",
                     6: "현재까지 집계된 데이터 시각화",
                     7: "플래그 스트릭 조회"}
    func_exec_dict = {1: "add_todo()",
                      2: "show_todo()",
                      3: "add_completed_task()",
                      4: "show_completed_task()",
                      5: "store_data()",
                      6: "visualize_data()",
                      7: "show_date_streak()"}
    while True:
        current_date, cw = get_today_weekday()
        cy, cm, cd = current_date.split('-')
        print("+--------------------+",
              "| TodoTracker v1.1.2 |",
              "+--------------------+",
              f"# 오늘은 {cy}년 {cm}월 {cd}일 {cw}요일",
              "사용하고자 하는 기능의 번호를 입력하세요!",
              "프로그램을 종료하려면 기능의 번호 이외의 숫자나 문자를 입력하세요.", sep='\n')
        print("+--+---------------------------+")
        for func_key, func_str in func_str_dict.items():
            print(f"|{func_key:>2d}|{func_str}")
        print("+--+---------------------------+")
        try:
            user_cmd = int(input())
        except ValueError:
            break
        try:
            exec(func_exec_dict[user_cmd])
        except KeyError:
            break
    print("프로그램을 종료합니다.",
          "이용해주셔서 감사합니다!", sep='\n')
    return 0


if __name__ == '__main__':
    main()

스트릭을 구현할 때는 뭔가 알고리즘 구현 문제를 푸는 것 같은 재미와 골치아픔이 있었다. 즐거운 코딩이었다. 과제가 아니니까

지난 학기 조료구조 수업에서는 마지막 '우아한' 코드 선정에서 처음으로 선정되었는데, 이번 학기 조고리즘 수업에서는 첫 과제의 모범 코드로 선정되었다. 이번 선정 대상은 '5개의 우아한 코드'는 아니고 '5개의 대표적인 풀이'였다.

 

유사회문(Quasi-palindrome)을 찾는 과제였다. 간단히 생각하면 불일치 발생 지점에서 포인터를 넘겨서 재귀적으로 풀 수 있는, 그리디한 문제였다. 그러나 그 간단한 실마리를 구체화한 해법을 떠올리지 못해서, 브루트 포스 탐색 과정에서 나타나는 규칙성을 찾아내고 메모이제이션의 원리를 적용해서 최적화한 풀이를 제출했다. 정석 풀이가 아니었기에, "이런 풀이도 있으니 한 번 보세요" 느낌으로 내 코드가 선정된 것일 테지만, 64명의 만점자 중 선정된 5명에 내가 들어가 있다는 사실에 기분 좋았다.

 

백준 플래티넘을 찍고 나서 내가 어느 정도 이 분야의 진입 장벽을 막 넘어섰다는 느낌을 받았는데, 요즘 돌아오는 좋은 결과들도 이를 뒷받침 해주는 듯했는데, 그 장벽 너머에 역시 넓은 세계가 있고 고수가 많다는 걸 새삼 깨닫는다. 심지어는 같은 수업을 듣는 학우들 중에도 실력자가 엄청 많이 있는 것 같다.

 

 

위 사진에서 나 다음으로 글을 작성한 사람은, 위 사진에 등장하는 과제 채점 사이트의 프론트 엔드 개발자라서 눈여겨 보고 있었는데, 지난 학기 조료구조에서도 '우아한' 코드에 많이 선정되었던 사람이다. 깔끔한 풀이가 항상 인상적이었는데, 이번에는 간결하기까지 해서 더 놀라웠다. 아직 내가 컴퓨터 알고리즘의 깊이가 낮음을, 아직 내가 못 가본 곳이 많음을 상기시켜주는 코드였다.

 

누군가의 코드를 보고 감명받는 내가 감명이 고픈 건지, 누군가를 감명받게 하는 코드를 작성한 저 사람이 대단한 건지. 아무튼 열심히 하고 싶게 만드는 코드 리뷰였다.

https://programmers.co.kr/competitions/2231/2022-sk-challenge

 

2022 SK ICT Family 개발자 채용 챌린지

접수   22년 02월 25일 10:00 ~ 03월 10일 17:00 테스트   22년 03월 12일 10:00 ~ 03월 19일 17:00

programmers.co.kr

 

코딩 테스트와 면접만으로 신입 개발자를 뽑는 전형이라 신기했다.

 

오늘 1차 코딩 테스트를 치렀다.

총 3시간 동안 4문제를 풀어야 했다.

 

1번: 그리디, 시간 복잡도 O(N) (N은 동전의 종류)

2번: 구현, 시간 복잡도 O(N^2) (N은 최대 1000)

3번: 수학, 시간 복잡도 O(N) (N은 대각선의 개수)

4번: 트리 DP, 시간 복잡도 O(N) (N은 노드의 개수)

 

2022 카카오 코테에 비해서 평이했다.

다만 2022 카카오 코테에서는 실시간 채점이 가능했는데,

SK ICT Family Challenge에서는 실시간 채점 기능을 제공하지 않는다는 점이 변수였다.

 

각 잡고 하는 경진 프로그래밍 실전에서 처음으로 문제를 다 풀어서 뿌듯했다.

그렇게 느낀 게 종료 30분 전이었다.

 

작성한 코드를 다시 보고, 엣지 케이스를 떠올려 보던 중

4번 문제에서 엣지 케이스가 생각났다.

로직 에러는 아니지만 시간 초과를 야기하는 케이스였다.

그래서 부랴부랴 시간을 제일 많이 잡아먹는 로직을 찾았다.

그게 종료 10분 전이라 많이 떨렸다.

결국 종료 3분 전까지 비효율적인 로직을 개선해서 적절한 시간 복잡도를 달성할 수 있었다.

 

처음으로 내 풀이들이 정답임에 확신이 있어서 뿌듯하다. 

 

 

 

오늘의 할 일 목록은 '오늘 하루'를 관리하기 위한 목적으로, 대부분의 어플이 최대한 편리하고 가벼운 프로그램, 마치 내 손에 딱 맞는 아담한 다이어리 하나와 볼펜 하나 같은 도구를 지향하는 듯하다. 그 때문인지 '지난 하루들'을 돌아보는 일에는 별로 초점을 두지 않는다. 과거의 데이터를 집계하고 보여주는 기능의 부재는 늘 아쉬웠기 때문에, 이번에 직접 만들어서 사용해 보고자 한다.

대략 이런 식으로 지난 기간의 '오늘의 할 일 달성률'을 확인하고자 이 프로그램을 만들었다. 성취도를 평가하는 지표는 대개 결과중심적이다. 나아지지 않는 결과에 낙담하기도 한다. 그럴 때 완벽하지는 않더라도 꾸준하게 하루를 채우며 걸어온 지난 날들을 한 눈에 볼 수 있다면 기분이 괜찮지 않을까?


요약

데이터 저장 및 시각화 위주의 기능만으로 구성하며, 모든 입력은 표준입력으로 받는다

사용 언어

Python

라이브러리

  1. csv
  2. datetime
  3. numpy
  4. matplotlib

기능

  • 사용자에게 어떤 기능을 이용할 것인지 묻는 프롬프트 출력
  • 기능 1: "오늘 할 일" 목록에 입력받은 "할 일"을 삽입함
    • csv 파일: todoList.csv
  • 기능 2: "오늘 할 일" 목록을 출력함
    • csv 파일: todoList.csv
  • 기능 3: "오늘 할 일" 목록에서 입력받은 "해낸 일"을 삭제하고, "오늘 해낸 일" 목록에 추가함
    • csv 파일: todoList.csv, completeList.csv
    • 예외) 입력받은 "해낸 일"이 "오늘 할 일" 목록에 없다면, 오류 메시지를 출력하고 재입력을 요구함
  • 기능 4: "오늘 해낸 일" 목록을 출력함
    • csv 파일: completeList.csv
  • 기능 5: "오늘 할 일", "오늘 해낸 일" 목록에 있는 요소들을 "전체 데이터" 테이블에 추가하고, 두 개의 목록을 각각 비어있는 파일로 초기화함
    • csv 파일: todoList.csv, completeList.csv, data.csv
    • data.csv: [날짜, 오늘 할 일 목록, 오늘 할 일 개수, 오늘 해낸 일 목록, 오늘 해낸 일 개수]
    • 예외) 현재 날짜에 저장된 데이터가 있다면, "덮어쓰기"를 할 것인지 묻는 프롬프트 출력 후 처리함
  • 기능 6: 조회하려는 기간을 묻는 프롬프트를 출력하고, 입력받은 기간 동안의 데이터를 그래프로 나타냄
    • csv 파일: data.csv
    • 누적 막대 그래프
      • top : 오늘 할 일
      • bottom : 오늘 해낸 일
    • 꺾은선 그래프
      • 달성률 = C / (C + U) * 100 (%) [C: 오늘 해낸 일, U: 오늘 할 일]
      • 꺾은선 그래프의 점 위에 달성률 값을 표시함

프로그램의 메인 메뉴

 

오늘 할 일을 추가하는 기능

 

오늘 추가해 놓은 할 일 목록을 조회하는 기능
오늘 할 일 중 완료한 일을 완료 처리하는 기능
'백준 1문제 풀기'가 현재 할 일에서 사라진 모습이다
'백준 1문제 풀기'가 오늘 해낸 일 목록에 추가된 모습이다
오늘자 데이터를 저장하려고 할 때, 이미 오늘 저장된 데이터가 있으면 덮어쓸지 여부를 묻는다. 제일 먼저 개선해야 할 부분.

 

이 프로그램의 핵심 기능
역시 긴 것보다는 짧은 게 보기 좋다.
별 것 아닌 듯했던 기능들도, 서로 마찰을 빚지 않고 매끄럽게 동작하도록 하기 위해 꽤 신경을 많이 써야 하는구나 싶었던 오늘이다.

더보기
# © 2022 starsein <dbtjd1928@gmail.com>
import csv
import datetime as dt
import numpy as np
from matplotlib import pyplot as plt
from typing import List, Tuple


def add_todo():
    print("[할 일 추가]")
    with open('todoList.csv', 'a', newline='') as af:
        writer = csv.writer(af)
        print("오늘 할 일을 입력하세요.",
              "이전 메뉴로 돌아가려면 q 또는 Q를 입력하세요.", sep='\n')
        while True:
            todo_str = input().rstrip()
            if todo_str == 'q' or todo_str == 'Q':
                return 0
            writer.writerow([todo_str])
            print(f"{todo_str}가 할 일 목록에 정상적으로 추가되었습니다!")


def get_todo() -> List[str]:
    with open('todoList.csv', 'r') as rf:
        todo_list = []
        reader = csv.reader(rf)
        for todo in reader:
            todo_list.append(*todo)
    return todo_list


def show_todo():
    print("[할 일 확인]")
    todo_list = get_todo()
    print("+------------------------------------+")
    for idx, todo in enumerate(todo_list, start=1):
        print(idx, todo)
    print("+------------------------------------+")
    while True:
        cmd = input("이전 메뉴로 돌아가려면 q 또는 Q를 입력하세요.\n").rstrip()
        if cmd == 'q' or cmd == 'Q':
            return 0


def add_completed_task():
    print("[해낸 일 추가]")
    todo_list = get_todo()
    with open('completedTaskList.csv', 'a', newline='') as af:
        writer = csv.writer(af)
        print("오늘 해낸 일을 입력하세요.",
              "이전 메뉴로 돌아가려면 q 또는 Q를 입력하세요.", sep='\n')
        while True:
            print("[현재 할 일 목록]")
            print("+------------------------------------+")
            for idx, todo in enumerate(todo_list, start=1):
                print(idx, todo)
            print("+------------------------------------+")
            todo_str = input().rstrip()
            if todo_str == 'q' or todo_str == 'Q':
                break

            if todo_str not in todo_list:
                print("오늘 할 일에 없는 입력입니다.")
                continue

            todo_list.remove(todo_str)
            writer.writerow([todo_str])
    with open('todoList.csv', 'w') as wf:
        writer = csv.writer(wf)
        for todo in todo_list:
            writer.writerow([todo])


def get_completed_task() -> List[str]:
    with open('completedTaskList.csv', 'r') as rf:
        ct_list = []
        reader = csv.reader(rf)
        for completed_task in reader:
            ct_list.append(*completed_task)
    return ct_list


def show_completed_task():
    print("[해낸 일 확인]")
    ct_list = get_completed_task()
    print("+------------------------------------+")
    for idx, completed_task in enumerate(ct_list, start=1):
        print(idx, completed_task)
    print("+------------------------------------+")
    while True:
        cmd = input("이전 메뉴로 돌아가려면 q 또는 Q를 입력하세요.\n").rstrip()
        if cmd == 'q' or cmd == 'Q':
            return 0


def check_data(current_time_str: str) -> Tuple[str, List[List[str]]]:
    with open("data.csv", "r") as rf:
        data_list = []
        reader = csv.reader(rf)
        for day_data in reader:
            data_list.append(day_data)
            date = day_data[0]
            if date == current_time_str:
                print("현재 날짜에 이미 저장된 데이터가 있습니다.")
                while True:
                    user_cmd = input("새로운 데이터로 덮어쓰기 하시겠습니까? [y/n]").rstrip()
                    if user_cmd == 'y':
                        return "OVERWRITE", data_list
                    elif user_cmd == 'n':
                        return "DON\'T OVERWRITE", data_list
    return "NOT TO OVERWRITE", data_list


def store_data():
    info = dt.datetime.now()
    current_time_list = list(info.strftime("%Y %m %d %A").split())
    translate_table = {"Monday": "월",
                       "Tuesday": "화",
                       "Wednesday": "수",
                       "Thursday": "목",
                       "Friday": "금",
                       "Saturday": "토",
                       "Sunday": "일"}
    current_time_list[3] = translate_table.get(current_time_list[3])
    current_time_str = ' '.join(current_time_list)
    todo_list = get_todo()
    ct_list = get_completed_task()

    res, data_list = check_data(current_time_str)

    if res == "OVERWRITE":
        data_list.pop()
    elif res == "DON\'T OVERWRITE":
        return 0
    else:
        pass
    data_list.append([current_time_str, todo_list, len(todo_list), ct_list, len(ct_list)])

    with open("data.csv", 'w', newline='') as wf:
        writer = csv.writer(wf)
        writer.writerows(data_list)

    with open("todoList.csv", 'w'):
        pass
    with open("completedTaskList.csv", 'w'):
        pass
    print("오늘의 데이터 집계 및 초기화가 완료되었습니다!")


def visualize_data():
    date_arr = np.array([])
    num_ut_arr = np.array([])
    num_ct_arr = np.array([])
    cr_arr = np.array([])
    with open("data.csv", 'r') as rf:
        reader = csv.reader(rf)
        for data in reader:
            stored_date, ut, num_ut, ct, num_ct = data
            num_ut = int(num_ut)
            num_ct = int(num_ct)
            date_arr = np.append(date_arr, stored_date)
            num_ut_arr = np.append(num_ut_arr, num_ut)
            num_ct_arr = np.append(num_ct_arr, num_ct)
            cr = round(num_ct / (num_ct + num_ut) * 100) if num_ct | num_ut != 0 else 0
            cr_arr = np.append(cr_arr, cr)
    print("[현재까지 집계된 데이터 시각화]")
    print(f"총 {len(date_arr)}개 날짜의 데이터가 저장되어 있습니다.")
    user_cmd = int(input("최근에 저장된 데이터를 몇 개까지 표시할까요?\n"))
    date_arr = date_arr[-user_cmd:]
    num_ct_arr = num_ct_arr[-user_cmd:]
    num_ut_arr = num_ut_arr[-user_cmd:]
    cr_arr = cr_arr[-user_cmd:]

    plt.figure(figsize=(16, 8))
    plt.title("TodoTracker", fontsize=17)
    plt.bar(date_arr, num_ct_arr, color='aqua')
    plt.bar(date_arr, num_ut_arr, bottom=num_ct_arr, color='lightcoral')
    plt.legend(["완료한 일", "미완료한 일"])
    plt.plot(date_arr, cr_arr, color='springgreen')
    for i, v in enumerate(date_arr):
        plt.text(v, cr_arr[i], f"{int(cr_arr[i])}%", fontsize=9, horizontalalignment='center',
                 verticalalignment='bottom', color='springgreen')
    plt.xlabel("일자", fontsize=15)
    plt.xticks(rotation=45)
    plt.show()


def main():
    func_str_dict = {1: "할 일 추가",
                     2: "할 일 확인",
                     3: "해낸 일 추가",
                     4: "해낸 일 확인",
                     5: "오늘의 데이터 집계 및 초기화",
                     6: "현재까지 집계된 데이터 시각화"}
    func_exec_dict = {1: "add_todo()",
                      2: "show_todo()",
                      3: "add_completed_task()",
                      4: "show_completed_task()",
                      5: "store_data()",
                      6: "visualize_data()"}
    while True:
        print("+--------------------+",
              "| TodoTracker v1.1.1 |",
              "+--------------------+",
              "사용하고자 하는 기능의 번호를 입력하세요!",
              "프로그램을 종료하려면 기능의 번호 이외의 숫자나 문자를 입력하세요.", sep='\n')
        print("+--+---------------------------+")
        for func_key, func_str in func_str_dict.items():
            print(f"|{func_key:>2d}|{func_str}")
        print("+--+---------------------------+")
        try:
            user_cmd = int(input())
        except ValueError:
            break
        try:
            exec(func_exec_dict[user_cmd])
        except KeyError:
            break
    print("프로그램을 종료합니다.",
          "이용해주셔서 감사합니다!", sep='\n')
    return 0


if __name__ == '__main__':
    main()

 

이 프로그램을 일주일 사용해 보고, 핵심 기능이 정말로 내게 의미가 있다면 사용성을 개선하고 세부 기능을 확장할 예정이다.

이게 결국 찍어지긴 하는구나.

 

신중하되 주저하지 않으며

하루에 한 문제씩 풀어서 다이아까지 가자!

요즘 PS 스터디에 참여하고 있다. 함께 선택한 문제를 주중에 하루 1문제씩 풀고, 주말 특정 시간에 zoom에서 모여 코드 리뷰를 하는 방식이다. 코딩 테스트라는 저 너머의 목적이 있지만, 꾸준히 조금씩이나마 경진형 프로그래밍을 통해 심오하게 짱구를 굴려서 로직을 설계하고 코드로 구현하는 연습 내지는 습관을 들이려는 커다란 목적도 있다. 또한 정답 처리 받고 넘어가는 게 아니라, 해당 문제와 제출 코드를 둘러싼 온갖 '왜?'를 정리해 보고 이해하기 쉽게 표현해서 학습 효과를 높이려는 목적도 있다. 아직까지는 그 목적이 다 달성되고 있어서 참 좋다. 효율적인 스킬이나 바람직한 코드 스타일에 대한 의견을 공유하면서 배워가는 것 자체가 많기도 하다.

 

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

 

18222번: 투에-모스 문자열

0과 1로 이루어진 길이가 무한한 문자열 X가 있다. 이 문자열은 다음과 같은 과정으로 만들어진다. X는 맨 처음에 "0"으로 시작한다.  X에서 0을 1로, 1을 0으로 뒤바꾼 문자열 X'을 만든다. X의 뒤에

www.acmicpc.net

오늘은 이 문제가 내 몫이었다. 분할 정복의 냄새가 물씬 풍기는 문자열 생성의 로직. 그 점을 착안해 시간복잡도 O(logN)으로 푸는 풀이를 발표했다. 오늘은 지금까지의 스터디 활동 중 가장 인상적인 날이다. 달리 말하면 가장 기분이 들떴던 날이다. "와~" 하는 다른 사람의 감탄 어린 표정을 zoom이라는 공간에서 처음 본 날이기도 하다. 코딩과 관련해서 몸 둘 바를 모를 정도로 칭찬을 받아본 첫 경험이기도 하다. PS를 누군가와 같이 진행한 첫 경험은 '버스 타기'였다. 작년 5월, 학교 수업 시간에 페어 프로그래밍을 할 때 내 짝은 백준 플래티넘 티어의 실력자였고, 아직 실버 티어였던 나는 표준입력을 받는 코드만 달랑 몇 줄 완성해놓고, 그 사람이 DP를 이용해 코딩하는 것을 보며 "와~"로 일관했었다. 그런 기억이 떠올라서 그런지, 오늘 스터디원들의 반응이 더욱 달가웠다.

 

확실히 오늘의 나는 다른 날과 달랐다. 보통 때 같았으면 '저 사람들에게 내 메시지가 잘 전달되고 있으려나?'하는 염려가 오히려 메시지 전달력을 약화시켰다면, 오늘은 나름대로 시각화도 하고, 내가 해당 알고리즘과 로직을 잘 알고 있다는 확신도 있어서, 꽤 매끄럽게 발표할 수 있었다. 살면서 거의 경험해 본 적 없는 '발표할 때의 침착함'은 충분하다 싶은 준비와 충분함에 대한 확신에서 나오는 것 같다.


오늘 처음으로 코드포스 온라인 대회에 참가해봤다. 영어로 된 지문이 장벽까지는 아니었지만, 정답을 향해 나아가는 길에 꽤 긴 시간을 허우적대게 만드는 늪지대였다. 2시간 동안 7문제 중 첫 2문제를 풀고, 3번째 문제의 '시간 초과'에서 5번 연속으로 막혔다.

https://codeforces.com/contest/1629/problem/C

아래는 마지막 제출 코드다. 내가 구현한 로직 안에서 최적화를 이뤄내기 위해, 자료구조를 바꿔보고 하다가 대회 종료 시간이 임박해서, 다시 훑어보지도 못하고 후다닥 제출했다. 대회가 끝나고 생각해보니, 최악의 경우 시간복잡도가 O(N^2)이었다. 어떻게 접근해야 개선할 수 있는지는 아직도 잘 모르겠다. 

import sys
from collections import deque
from typing import List

input = sys.stdin.readline
MAX_N = 2 * 100_000


def solution(a_list: List[int]) -> List[str]:
    b_list = []
    mex_limit = max(a_list) + 1
    a_list.reverse()
    while len(a_list):
        current_mex = 0
        contained_set = set()
        best_k, best_mex = 0, -1
        for k in range(1, len(a_list) + 1):
            contained_set.add(a_list[len(a_list) - k])
            while current_mex in contained_set:
                current_mex += 1
            if current_mex > best_mex:
                best_mex = current_mex
                best_k = k
                if best_mex == mex_limit:
                    break
        b_list.append(str(best_mex))
        for i in range(best_k):
           a_list.pop(-1)

    return b_list


if __name__ == '__main__':
    t = int(input())
    for tc in range(t):
        n = int(input())
        a_list = list(map(int, input().split()))
        sol = solution(a_list)
        print(len(sol))
        print(' '.join(sol))

시작하기 전에 대회 중에 마실 물을 챙겨놓았는데, 그랬다는 사실마저 잊고 있었다. 전 세계의 사람들과 함께 경쟁하는 대회라 그런지, 백준의 온라인 대회보다 한층 더 긴장감이 있었다. 문제가 한국어로 제공되었다고 해도, 오늘과 같은 결과였을 것 같다. 내게 익숙한 '직관'의 범위를 넓혀야겠다는 생각이 한층 더 깊어진다.

나만의 언어로 설명할 수 없으면 완전히 이해한 것이 아니다. 라는 원칙이 위태로운 지금이다.

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

 

2512번: 예산

첫째 줄에는 지방의 수를 의미하는 정수 N이 주어진다. N은 3 이상 10,000 이하이다. 다음 줄에는 각 지방의 예산요청을 표현하는 N개의 정수가 빈칸을 사이에 두고 주어진다. 이 값들은 모두 1 이상

www.acmicpc.net

이 문제를 다 풀고 나서 내 코드를 해설하는 과정에서 맞닥뜨린 어려움이다.

내가 짠 코드의 완결성을 어떻게 증명하나?

아래의 코멘트들은 고민과 삽질을 문제 풀이 시간의 2배만큼 더 들여서 완성한 최종 결과물이다.


import sys


input = sys.stdin.readline
MAX_BUDGET = 100_000
MIN_BUDGET = 1


def solution() -> int:
    if sum(budget_list) <= total_budget:
        return max(budget_list)

    sol = binary_search(MIN_BUDGET, MAX_BUDGET)
    return sol


def binary_search(start: int, end: int) -> int:
    if start == end - 1:
        return binary_search(end, end)

    mid = (start + end) // 2
    budget_sum = 0
    for idx, val in enumerate(budget_list):
        budget_sum += min(val, mid)

    if budget_sum > total_budget:
        if start == end:
            return mid - 1

        return binary_search(start, mid)
    elif budget_sum < total_budget:
        if start == end:
            return mid

        return binary_search(mid, end)
    else:
        return mid


if __name__ == '__main__':
    n = int(input())
    budget_list = list(map(int, input().split()))
    total_budget = int(input())
    sol = solution()
    print(sol)
  1. 1~100,000 중에서 최대의 예산 상한선(mid)을 이진 탐색으로 찾는 방식의 풀이입니다.
  2. 전체 국가예산(total_budget)은 고정된 값으로, 탐색값이라고 합시다.
  3. 예산의 총합(budget_sum)은 mid에 따라 변하는 값으로, 계산값이라고 합시다.
  4. ‘탐색값 == 계산값’이 보장되어 있었다면 이진 탐색의 구현이 간단했겠지만
  5. 그런 보장이 없이 탐색값을 넘지 않는 최대의 계산값을 찾고, 그것에 대응되는 예산 상한선(mid)을 반환해야 하므로 더욱 복잡했습니다.
  6. 이진 탐색의 간단함과 복잡함을 나누는 기준은 ‘적당한 값에서 멈추기 위한 중단 조건의 설정 여부’에 있는 듯하며, 저는 중단 조건을 구상하느라 문제 풀이 시간의 90%는 쓴 것 같네요.
# 기존의 코드 (공백 제외 17줄)
def binary_search(start: int, end: int) -> int:
    if start == end - 1:
        return binary_search(end, end)

    mid = (start + end) // 2
    budget_sum = 0
    for idx, val in enumerate(budget_list):
        budget_sum += min(val, mid)

    if budget_sum > total_budget:
        if start == end:
            return mid - 1

        return binary_search(start, mid)
    elif budget_sum < total_budget:
        if start == end:
            return mid

        return binary_search(mid, end)
    else:
        return mid


# 새로운 코드 (공백 제외 13줄)
def binary_search(start: int, end: int) -> int:
    if start > end:
        return end

    mid = (start + end) // 2
    budget_sum = 0
    for idx, val in enumerate(budget_list):
        budget_sum += min(val, mid)

    if budget_sum > total_budget:
        return binary_search(start, mid-1)
    elif budget_sum < total_budget:
        return binary_search(mid+1, end)
    else:
        return mid
  1. 기존의 코드처럼, 이진 탐색 함수를 재귀 호출할 때 인자로 mid를 그대로 넣지 않는 경우 start가 end보다 1만큼 작을 때 mid값은 계속 start가 됩니다.
  2. 따라서 무한 루프를 방지하기 위한 중단 조건을 start == end인 경우와 start == end - 1인 경우를 따로 짜야 하는 불편함이 있습니다.
  3. 새로운 코드처럼, mid+1 또는 mid-1을 재귀 호출의 인자로 넣으면 이미 탐색한 mid값을 앞으로의 탐색에서 제외한다는 점에서 효율적입니다. 또한 [1]의 상황을 피할 수 있다는 점에서도 편리합니다.

'이진 탐색의 중단 조건이 완결성을 지니는 이유'에 대한 나만 이해 가능할 듯한 해설


이번 삽질 덕분에 이진 탐색이라는 땅을 파헤쳐 보고 양질의 이해를 쌓을 수 있었다.

탐색하고자 하는 값에 가장 근사한 값을 탐색하는 알고리즘을 이제는 아주 손쉽게 구현할 수 있을 것이다.

하지만 무근본 삽질의 결과물을 보면 현타가 온다.

이걸 보고 내가 전달하고자 하는 바를 어느 누가 이해할 수 있을까?

동네 할머니는 커녕, 내로라 하는 컴퓨터 알고리즘 전문가 분들도 고개를 젓고 가시지 않을까.

 

수식은 풀이 과정이라는 맥락이 없어도 독자가 그 자체만 보고 직관적인 이해를 할 수 있지만,

코드는 전체 코드의 맥락, 그리고 그 배경에 깔린 프로그램의 제어 흐름을 독자가 알고 있어야 직관으로 받아들일 수 있다.

 

수식은 머릿속의 생각을 표현한 것이지만,

코드는 프로그램이 작동하는 절차를 표현한 것이어서,

코드가 수식에 비해 거리감이 크다.

 

수식으로 푸는 수학 문제 풀이 해설하기보다, 코드로 푸는 PS 문제 풀이 해설이 훨씬 더 어렵게 느껴지는 이유를 확실히 체감한 오늘이다.

공부 블로그에 '알고리즘' 카테고리를 넣어서 이런 삽질을 계속한다면, 분명 조금씩 설명 능력이 향상되겠지만, 너무 과투자가 아닐까 싶은 오늘이다.

알고리즘 문제 풀이를 아주 세세하고 와 닿게 설명해 놓는 몇몇 PS 블로거들이 새삼 대단해 보이는 오늘이다.

 

머리로는 그려지는데, 입과 손으로는 그렇지 못한 상황이 너무 답답하다.

알고리즘 교재를 읽을 때, 그 표현까지도 곱씹어 보며 정독해야겠다.

'내가 가는 여정을 담는 그릇' 카테고리의 다른 글

백준 플래티넘 달성  (0) 2022.02.09
PS 스터디와 첫 코드포스  (0) 2022.01.23
자바의 정석  (0) 2022.01.03
첫 코드 리뷰 작성  (0) 2021.12.29
크리스마스 선물을 풀어보기  (0) 2021.12.28

+ Recent posts