Python에서는 변수와 객체가 메모리에 어떻게 저장되는지에 대해 처음부터 깊이 배우는 경우가 많지 않다.

나도 예전에 풀스택 쪽을 잠깐 본 적이 있었는데, 그 때 JavaScript(자바스크립트)를 배우면서 메모리 할당과 데이터 저장 방식에 대해 배우게 되었다. JavaScript는 원시 데이터 타입과 참조 타입을 구분하고, 얕은 복사와 깊은 복사의 차이도 눈에 띄게 다르게 작용하기 때문에 메모리 참조에 대해 더 많은 주의가 필요하다.

그러나 Python은 메모리 관리를 자동으로 처리해 주고, 변수와 객체는 직관적으로 사용할 수 있도록 설계되었기 때문에, 복잡한 메모리 관리를 사용자가 직접 다룰 일이 거의 없고, 따라서 초보자가 쉽게 접근할 수 있는 편이다.

이러한 차이는 언어의 철학과 설계 방식에서도 기인한다고 한다. JavaScript는 클라이언트 측에서 웹 성능과 자원을 관리해야 하므로 메모리 참조와 관련된 개념을 초반에 배워야 하는 경우가 많고, 파이썬에서는 성능 최적화보다는 코드의 가독성과 간결함을 중요시하는 경향이 있기 때문에, 메모리 관리에 대한 부담이 상대적으로 적게 느껴질 수 있다.

본 포스팅에서는 참조복사/얕은복사/깊은복사의 차이점을 다루고, 파이썬의 copy 라이브러리를 통해 각 복사의 차이점을 구현해 보도록 하겠다.


파이썬 기본 참조 복사

# 원본 리스트
a = ["떡볶이", "튀김", "순대"]

# a를 b에 복사
b = a

# b의 첫 번째 항목을 변경
b[0] = "라면"

# 결과 확인
print("a 리스트:", a)  # ['라면', '튀김', '순대']
print("b 리스트:", b)  # ['라면', '튀김', '순대']

b=a 구절에서 b가 a와 같은 메모리 위치를 참조하기 때문에 a, b는 실제로는 같은 객체가 된다.


  •  

copy 라이브러리

Python의 copy 라이브러리는 객체의 얕은 복사(shallow copy) 깊은 복사(deep copy)를 지원하는 유용한 라이브러리로, 특히 다차원 리스트나 중첩된 데이터 구조를 복사해야 할 때 유용하게 사용할 수 있다.

copy와 deepcopy의 차이점

copy 라이브러리에는 copy()와 deepcopy() 두 가지 주요 기능이 있다.

  • copy.copy(): 얕은 복사로, 최상위 레벨의 객체만 복사함. 즉, 리스트 안의 리스트와 같은 중첩 객체의 경우, 내부 객체는 같은 메모리 참조를 가지게 된다.
    • a를 복사해서 b를 만들었는데, b를 수정하면 a도 같이 수정됨
  • copy.deepcopy(): 깊은 복사로, 모든 객체의 중첩 레벨을 따라가며 재귀적으로 새 객체를 생성하여 완전히 독립적인 복사본을 만든다. 따라서 복사된 객체가 원본 객체와 독립적으로 수정될 수 있다.
    • a를 복사해서 b를 만들었는데, b를 수정해도 a가 변하지 않고 원본을 유지함

얕은 복사(shallow copy)

얕은 복사(shallow copy)는 새로운 객체를 생성하지만, 그 객체 안에 들어 있는 요소들은 원본 객체와 같은 참조를 가리킨다. 즉, 리스트의 최상위 레벨은 복사되지만, 내부의 중첩된 객체들은 원본을 참조하기 때문에.

깊은 복사(deepcopy)의 필요성

 

  • 리스트, 딕셔너리 등 다차원 데이터를 복사할 때 원본 데이터의 변경이 복사본에 영향을 주지 않도록 해야 할 때 - 즉, 원본 객체를 보호하고자 할 때 사용(deepcopy)
    • 작업 중간에 데이터를 보존하여 나중에 초기 상태로 복원해야 할 때 deepcopy를 사용하면 데이터 일관성을 유지할 수 있음
  • 특히 다수의 객체가 동일한 데이터를 참조하는 상황에서 유용

예제 코드

import copy

# 원본 리스트
a = [[["떡볶이"], "튀김"], "순대"]

b = a                 # 1. 참조 복사
c = copy.copy(a)      # 2. 얕은 복사
d = copy.deepcopy(a)  # 3. 깊은 복사

# 복사 방식의 차이 확인
b[0][0][0] = "라면"   # 참조 복사한 b의 첫 번째 요소의 첫 번째 리스트를 수정
c[0][1] = "김밥"      # 얕은 복사한 c의 첫 번째 리스트의 두 번째 요소를 수정
d[1] = "만두"         # 깊은 복사한 d의 두 번째 요소를 수정

# 결과 출력
print("원본 리스트 a:", a)  ### [[['라면'], '김밥'], '순대']

원본 리스트 [[['떡볶이'], '튀김'], '순대'] 가 [[['라면'], '김밥'], '순대']가 되었다.

  1. b[0][0][0]="라면"을 통해 참조 복사한 객체의 "떡볶이"를 "라면"으로 바꾸었다. 참조 복사로 인해 원본 객체의 떡볶이가 라면이 되었다.
  2. c[0][1]="김밥"을 통해 얕은 복사한 객체의 "튀김"을 "김밥"으로 바꾸었다. 얕은 복사로 인해 원본 객체의 튀김도 김밥이 되었다.
  3. d[1]="만두"를 통해 깊은 복사한 객체의 "순대"를 "만두"로 바꾸었다. 깊은 복사로 인해 원본 객체는 "순대"를 유지하게 되었다.

마무리

  • 참조 복사와 얕은 복사, 깊은 복사의 차이를 이해하고 
  • 필요에 따라 copy 라이브러리를 활용하여
  • 데이터의 독립성을 보장하고
  • 코드의 안정성을 높이는
  • 멋진 엔지니어가 되도록 하겠다 :)

 

 

 

사이킷런의 프레임워크와 연동할 수 있는 전용 XGBoost 래퍼 클래스에는 분류용 XGBoostClassifier, 회귀용 XGBoostRegressor이 있습니다. 래퍼 클래스는 다음과 같은 장점을 가지고 있습니다.

  • 사이킷런의 기본 estimator를 그대로 상속해 만들었기 때문에 fit()과 predict()만으로 학습과 예측이 가능합니다.
  • GridSearchCV, Pipeline 등 다른 사이킷런의 다른 유틸리티를 그대로 함께 사용할 수 있습니다.
  • 기존의 다른 프로그램의 알고리즘으로 XGBoost 래퍼 클래스를 사용할 수도 있습니다.

 

https://smartest-suri.tistory.com/40

 

ML | 파이썬 XGBoost API 사용하여 위스콘신 유방암 예측하기

XGBoost란?트리 기반의 앙상블 학습에서 가장 각광받고 있는 알고리즘 중 하나로, 캐글(kaggle) 등 경연 대회에서 입상한 많은 데이터 사이언티스트들이 XGboost를 사용하면서 널리 알려지게 되었습니

smartest-suri.tistory.com

 

이전 글에서 학습했던 기본 XGBoost API 대신 사이킷런 연동 XGBoost 래퍼 클래스 XGBoostClassifier를 사용해 모델을 학습시키고 예측을 수행해 보겠습니다.

 


 

 

호출 및 hyperparameter

from xgboost import XGBClassifier
xgb_wrapper = XGBClassifier(n_estimators = 400, # num_boost_round -> n_estimators
                            learning_rate = 0.05, # eta -> learning_rate
                            max_depth = 3, 
                            eval_metric = 'logloss')

 

learning_rate와 같이 기존 사이킷런 하이퍼 파라미터와의 호환성 유지를 위해 변경된 하이퍼 파라미터들이 있으므로 유의합니다.

 

 

 

학습 및 예측

fit()과 predict() 메소드를 이용해서 모델을 학습시키고 예측을 수행해 보겠습니다.

# 학습
xgb_wrapper.fit(X_train, y_train, verbose = True)

# 예측
w_preds = xgb_wrapper.predict(X_test)
w_preds[:10]
# array([1, 0, 1, 0, 1, 1, 1, 1, 1, 0])

w_pred_proba = xgb_wrapper.predict_proba(X_test)[:, 1]
w_pred_proba[:10]
# array([8.8368094e-01, 2.7957589e-03, 8.9875424e-01, 1.8748048e-01,
#        9.9204481e-01, 9.9990714e-01, 9.9954444e-01, 9.9904817e-01,
#        9.9527210e-01, 1.9664205e-04], dtype=float32)

 

아주 빠르고 간단하게 학습과 예측을 수행했습니다.

 

 

평가

이전 포스팅에서 작성해 둔 get_clf_eval() 함수를 이용해서 사이킷런 래퍼 XGBoost로 만들어진 모델의 예측 성능 평가를 해 보겠습니다.

get_clf_eval(y_test, w_preds, w_pred_proba)

지표 이전 이후
Accuracy 약 0.96 약 0.97
Precision 약 0.97 약 0.97
Recall 약 0.97 약 0.98
F1-Score 약 0.97 약 0.98
ROC-AUC 약 0.99 약 0.99

 

이전 실습보다 평가 지표가 조금 상승했습니다. 이번 실습에서는 early stopping을 따로 설정하지 않은 관계로 train 데이터를 train과 valid 데이터로 나누는 과정을 생략하였고, 그래서 트레인 데이터셋의 수가 늘어난 영향이 있을 것으로 파악됩니다. (애초에 트레이닝 데이터가 풍부한 데이터셋은 아닌 관계로)

 

 

여기까지 XGBoost 관련 두 번째 실습을 마쳐봅니다. 감사합니다 :-)

 

XGBoost란?

트리 기반의 앙상블 학습에서 가장 각광받고 있는 알고리즘 중 하나로, 캐글(kaggle) 등 경연 대회에서 입상한 많은 데이터 사이언티스트들이 XGboost를 사용하면서 널리 알려지게 되었습니다. 대체로 분류에 있어서 뛰어난 예측 성능을 보이는 특징을 가지고 있습니다.

 

XGboost는 GBM에 기반하고 있는데요. GBM보다 빠르게 학습이 가능하고 오버핏팅 규제 부재 문제 등을 해결한다는 장점이 있다고 합니다. 그 밖에도 Tree pruning이 가능하여 더 이상 긍정 이득이 없는 분할을 가지치기 해서 분할 수를 더 줄이는 추가적인 장점, 자체 내장된 교차 검증 기능, 결손값을 자체 처리할 수 있는 기능 등의 장점도 가지고 있습니다.

 

XGBoost API 학습을 위해 위스콘신 유방암 데이터로 실습을 진행해 본 결과를 정리하여 공유하고자 합니다 :-)

 

 

 

sklearn dataset 위스콘신 유방암 데이터 불러오기

sklearn의 자체 내장 데이터인 load_breast_cancer을 불러오겠습니다.

import pandas as pd
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
import warnings
warnings.filterwarnings('ignore')

dataset = load_breast_cancer()
features = dataset.data
labels = dataset.target

cancer_df = pd.DataFrame(data = features, columns = dataset.feature_names)
cancer_df.head(3)

cancer_df['target'] = labels
cancer_df.head(3)

 

데이터프레임 마지막에 target 컬럼을 추가하여 각 row의 label을 0 또는 1로 나타냈습니다.

dataset.target_names
# array(['malignant', 'benign'], dtype='<U9')

cancer_df['target'].value_counts()
# target
# 1    357
# 0    212
# Name: count, dtype: int64

 

label이 무엇이 있는지 확인해 보니, 0은 malignanat, 1은 benign임을 확인할 수 있었고, 각각 212개와 357개가 있는 것을 확인할 수 있었습니다.

 

 

train, valid, test 데이터셋 나누기

X_features = cancer_df.iloc[:, :-1]
y_label = cancer_df.iloc[:, -1]

 

먼저 target 컬럼을 제거한 X_feature 데이터프레임, target 컬럼만 떼어낸 y_label 데이터프레임을 생성합니다.

# 8:2 비율로 train:test 나누기
X_train, X_test, y_train, y_test = train_test_split(X_features, y_label, 
                                                    test_size = 0.2, 
                                                    random_state = 156)
                                                    
# 9:1 비율로 다시 train:valid 나누기
X_tr, X_val, y_tr, y_val = train_test_split(X_train, y_train, 
                                            test_size = 0.1, 
                                            random_state = 156)

 

train_test_split() 메소드를 이용하여 8:2 비율로 train:test 데이터셋을 나누고,

나누어진 train은 다시 한 번 9:1 비율로 train:valid으로 나누었습니다.

len(X_tr), len(X_val), len(y_tr), len(y_val), len(X_test), len(y_test)
# (409, 46, 409, 46, 114, 114)

X_tr.shape, y_tr.shape
# ((409, 30), (409,))

 

train, valid, test 데이터의 개수와 shape 등을 확인해 보았습니다.

 

 

 

DMatrix

파이썬 래퍼 XGBoost는 전용 데이터 객체인 DMatrix를 사용합니다. DMatrix의 주요 입력 파라미터는 data와 label입니다.

  • data : 피처 데이터 세트
  • label : (분류) 레이블 데이터 세트 / (회귀) 숫자형인 종속값 데이터 세트
dtr = xgb.DMatrix(data = X_tr, label = y_tr)
dval = xgb.DMatrix(data = X_val, label = y_val)
dtest = xgb.DMatrix(data = X_test, label = y_test)

 

 

Hyperparameter

딕셔너리 형태로 하이퍼 파라미터를 설정합니다.

params = {
    'max_depth' : 3,  # 트리의 최대 깊이는 3
    'eta' : 0.05,     # 학습률 0.05
    'objective' : 'binary:logistic',
    'eval_metric' : 'logloss'
}

num_rounds = 400       # 부스팅 반복 횟수는 400회

 

 

XGBoost 모델 학습

  • early_stopping_rounds : 더이상 지표 개선이 없을 경우에 횟수를 모두 채우지 않고 중간에 반복을 빠져 나올 수 있도록 하는 탈출 도구 (조기 중단, 얼리스탑핑)
  • evals : 평가용 데이터 세트, [(학습튜플), (평가튜플)] 형태 - 반복마다 지정된 평가용 데이터 세트에서 eval_metric의 지정된 평가 지표로 예측 오류를 측정 
xgb_model = xgb.train(params = params,
                      dtrain = dtr,
                      num_boost_round = num_rounds,
                      early_stopping_rounds = 50, # 50번째부터 얼리스탑핑 가능
                      evals = [(dtr,'train'), (dval,'eval')])

400회를 다 채우지 못하고 250회에서 중단되었습니다

 

예측 수행

predict() 메소드를 이용해서 test 데이터의 결과를 예측해 보겠습니다.

pred_probs = xgb_model.predict(dtest)

 

예측한 결과를 다양하게 살펴봅시다. (초반 10개값만 봅시다.)

pred_probs[:10]
# array([9.3829882e-01, 3.6695320e-03, 7.5020140e-01, 4.9393266e-02,
#        9.8045909e-01, 9.9958366e-01, 9.9899417e-01, 9.9919862e-01,
#        9.9767953e-01, 5.2314228e-04], dtype=float32)

 

어떤 값인지 한 눈에 보이지 않으므로 np.round()를 이용해서 살펴보겠습니다.

np.round(pred_probs[:10])
# array([1., 0., 1., 0., 1., 1., 1., 1., 1., 0.], dtype=float32)
# 일의 자리까지 반올림

np.round(pred_probs[:10], 3)
# array([0.938, 0.004, 0.75 , 0.049, 0.98 , 1.   , 0.999, 0.999, 0.998,
#       0.001], dtype=float32)
# 소수 셋째 자리까지 반올림

 

반올림을 해서 보니까 예측한 값이 0과 1 사이의 값으로 도출되었음을 확인할 수 있었습니다. 

이제 예측 확률이 0.5보다 크면 1, 그렇지 않으면 0으로 최종 예측값을 결정하여 preds 리스트에 저장하겠습니다.

preds = [1 if x > 0.5 else 0 for x in pred_probs]
preds[:10]
# [1, 0, 1, 0, 1, 1, 1, 1, 1, 0]

 

 

get_clf_eval() 함수

모델을 평가할 수 있는 함수를 작성해 보겠습니다.

from sklearn.metrics import accuracy_score, precision_score, recall_score, confusion_matrix, f1_score, roc_auc_score

def get_clf_eval(y_test, pred = None, pred_proba = None):
    confusion = confusion_matrix(y_test, pred)
    accuracy = accuracy_score(y_test, pred)
    precision = precision_score(y_test, pred)
    recall = recall_score(y_test, pred)
    f1 = f1_score(y_test, pred)
    roc_auc = roc_auc_score(y_test, pred_proba)
    print('Confusion Matrix')
    print(confusion)
    print(f'accuracy : {accuracy}, precision : {precision:.4f}, recall : {recall:.4f}')
    print(f'f1 : {f1}, roc_auc : {roc_auc}')
get_clf_eval(y_test, preds, pred_probs)

  • 정확도 accuracy 약 0.96
  • 정밀도 precision 약 0.97
  • 재현율 recall 약 0.97
  • F1-스코어 0.97
  • ROC-AUC 약 0.99

평가 지표가 굉장히 좋네요 :-)

 

 

XGBoost 내장 시각화 기능 수행하기

XGBoost의 plot_importance() API피처의 중요도를 막대그래프 형식으로 나타냅니다.

  • 기본 평가 지표로 f스코어를 기반으로 해당 피처의 중요도를 나타냅니다.
  • f스코어는 해당 피처가 트리 분할 시 얼마나 자주 사용되었는지를 지표로 나타낸 값입니다.
  • xgboost 패키지는 plot_importance()를 이용해 바로 피처 중요도를 시각화할 수 있습니다.
  • plot_importance() 호출 시 파라미터로 앞에서 학습이 완료된 모델 객체 및 matplotlib의 ax 객체를 입력하면 됩니다.
import matplotlib.pyplot as plt
%matplotlib inline

fix, ax = plt.subplots(figsize = (10, 12))
plot_importance(xgb_model, ax = ax)
plt.tight_layout()
plt.savefig('plot.png')

 

 

 

여기까지 XGBoost API 실습 포스팅이었습니다 :-)

감사합니다.

 

 

한동안 코테를 잠시 안풀었더니

효율성 테스트를 통과하는데 조금 헤맸던 문제입니다..!

 

Stack 알고리즘을 활용하지 않고 문제를 풀었을 때 테스트 케이스 정확성은 통과하는 데 문제가 없으나, 효율성 테스트를 통과하기 어려우실 수 있습니다. 효율성 테스트까지 통과할 수 있는 문제 풀이 방법을 알려드릴게요 :)

 

 

https://school.programmers.co.kr/learn/courses/30/lessons/12909

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

문제 상황 : 괄호가 바르게 짝지어졌다는 것은 '(' 문자로 열렸으면 반드시 짝지어서 ')' 문자로 닫혀야 한다는 뜻입니다. 예를 들어

"()()" 또는 "(())()" 는 올바른 괄호입니다.
")()(" 또는 "(()(" 는 올바르지 않은 괄호입니다.


'(' 또는 ')' 로만 이루어진 문자열 s가 주어졌을 때, 문자열 s가 올바른 괄호이면 true를 return 하고, 올바르지 않은 괄호이면 false를 return 하는 solution 함수를 완성해 주세요.


 

1.

먼저 빠른 탈출을 위해 다음 4가지 경우에는 함수를 False로 얼리 리턴하도록 설정해주겠습니다.

  • 주어진 문자열이 ")" 로 시작하는 경우
  • 주어진 문자열이 "(" 로 끝나는 경우
  • 주어진 문자열의 길이가 홀수인 경우
  • 주어진 문자열이 ")" 또는 "(" 한 종류로만 이루어진 경우
def solution(s):
    if s[0] == ")" or s[-1] == "(" or len(s) % 2 == 1 or len(set(s)) == 1:
        return False

 

 

 

2.

다음으로 stack 리스트를 선언해주도록 하겠습니다.

    stack = []

 

우리는 stack 리스트에 "(" 열기 괄호만 넣어줄 것입니다.

왜 열기 괄호만 넣어줄까요? 예시를 통해 한번 확인해 봅시다.

 

문자열 s = "(((())"가 있다고 합시다. 딱 봐도 열기 괄호는 4개인데 닫기 괄호는 2개여서 짝이 맞지 않는다는 것을 알 수 있습니다. 우리는 for루프를 통해 0번째 자릿값부터 차례대로 돌면서 해당 자리의 값이 "("이면 stack에 넣어주고, ")"이면 stack에 있는 열기괄호를 하나 빼 줄 것입니다.

    for char in s:
        if char == '(':
            stack.append(char)
        elif char == ')':
            if not stack:
                return False
            stack.pop()

s = "(((())"

  1. 첫 번째 자리 "(" -> stack = "(" 
  2. 두 번째 자리 "(" -> stack = "(", "("
  3. 세 번째 자리 "(" -> stack = "(", "(", "("
  4. 네 번째 자리 "(" -> stack = "(", "(", "(", "("
  5. 다섯 번째 자리 ")" -> stack이 비어있지 않으므로 stack.pop()
    -> stack = "(", "(", "("
  6. 여섯 번째 자리 ")" -> stack이 비어있지 않으므로 stack.pop()
    -> stack = "(", "("
    if stack:
        return False
    else:
        return True

 

이터레이션을 다 돌고 났을 때

stack이 비어 있으면 짝이 맞으므로 True

stack에 남은 값이 있다면 짝이 맞지 않는 것이므로 False를 리턴합니다.

 

결국 "("의 갯수와 ")"의 갯수가 동일한지 확인하는 것과 다름 없습니다. 다른 방법으로도 정확성 문제는 통과하는 데 어려움이 없으나, 효율성에 문제가 생기는 관계로 stack 알고리즘을 통해 효율성을 높여준 것이라고 보시면 되겠습니다.

 

 

3.

마지막으로 한 번에 코드를 보겠습니다.

def solution(s):
    if s[0] == ")" or s[-1] == "(" or len(s) % 2 == 1 or len(set(s)) == 1:
        return False

    stack = []
    for char in s:
        if char == '(':
            stack.append(char)
        elif char == ')':
            if not stack:
                return False
            stack.pop()
    
    if stack:
        return False
    else:
        return True

 

 

감사합니다!

 

 

지난 글에서 서울특별시 공중화장실 공중데이터를 판다스 데이터프레임으로 만들고 간단히 정제작업을 해 보았는데요. 정제한 데이터프레임을 가지고 태블로를 이용해서 아주 간단히만 시각화 작업을 진행해 보았습니다.

 

 

 

대시보드 구성 방법

  1. 서울시의 25개 구별 공중화장실 수 합계를 계산하여 그 수를 비교할 수 있도록 시각화했습니다.
  2. 대시보드의 왼쪽에는 지도를 배치하여 화장실의 수를 원의 크기와 색깔로 직관적으로 파악할 수 있도록 구성했습니다
    • 지도를 확대하면 보이지 않는 레이블을 모두 확인할 수 있어요.
    • 화장실 수가 많을수록 원의 크기가 큽니다.
    • 화장실 수가 많을수록 원의 색깔이 진합니다.
  3. 대시보드의 오른쪽에는 가로막대그래프를 배치하여 수치별로 좀더 직관적인 비교가 가능하도록 구성했습니다.
    • 오른쪽의 비교 파라미터를 이용해서 비교선을 100단위로 조절하면서 이동시킬 수 있어요.
    • 비교선을 움직이면서 비교선을 기준으로 색깔이 바뀌는 것을 확인할 수 있어요.

 

태블로 퍼블릭으로 보러가기 (클릭)

 

지하철

지하철

public.tableau.com

클릭하시면 태블로 퍼블릭 웹사이트에서 인터렉티브하게 직접 결과를 조절해 보실 수 있습니다.

 


 

 

<시각화 이후 생각해볼만한 것들>

 

1. 구별 화장실 수와 구별 면적의 관계는 어떻게 되는가?
2. 구별 화장실 수와 지하철 역의 개수의 상관관계가 있는가?
3. 구별 화장실 수와 구별 인구 수의 관계는 어떻게 되는가? 인구와 구별 화장실 총 수는 비례하는가?
4. 서울특별시 구별 장애인 화장실 데이터를 따로 구할 수 있는가? 구할 수 있다면, 전체 화장실과 장애인 화장실의 비율을 비교해 보자.
5. 상업 단지와 화장실 수의 상관관계가 있는가?

6. 관광 구역과 화장실 수의 상관관계가 있는가?

7. 공공화장실 중에서 지하철 역 화장실의 개수를 특정할 수 있는가? 있다면 그 비율은 어떻게 되는가?

8. 지하철 역 화장실 개수를 구할 수 있다면, 지하철 노선별 유동인구 데이터와 병합하여 화장실의 갯수가 적절하게 비치되어 있는지 비교해보자.

 

 

 

여기까지입니다 :-) 좀더 생각해볼 수 있을것 같지만 본격적인 개인 EDA 프로젝트 준비를 위해서 이번엔 이정도로 간단히만 포스팅을 마치려고 합니다. 쉽고 짧은 작업이었지만 맘에드는 공공데이터를 구하고, 정제하고, 시각화 후 생각해볼거리 도출까지 은근 시간이 걸렸네요. 다음엔 더 능숙하고 멋진 프로젝트를 가져와서 공유해보겠습니다. 감사합니다. :)

 

 


 

 

(+) 판다스 데이터프레임 CSV파일로 내보내기 한 후 태블로에 불러온 과정

 

t.to_csv("toilet_df.csv")

 

먼저 to_csv() 메소드를 통해 간단하게 csv파일로 내보내기를 해주었습니다. 저는 구글 코랩에서 실습을 진행했습니다.

 

 

 

 

 

구글 코랩의 왼쪽 파일메뉴에서 간단하게 바로 다운로드를 해서 다운로드 폴더에 넣어 주었는데요. 로컬 환경에 따로 다운로드하지않고 태블로 퍼블릭 환경에서 구글 드라이브를 연동해서 바로 오픈해도 됩니다.

 

 

 

 

 

태블로 퍼블릭 프로그램을 실행하고 로컬 환경에 다운로드한 toilet_df.csv 파일을 오픈했습니다.

 

 

 

따로 사용하지 않을 예정인 산지, 부지번 컬럼은 숨기기(Hide) 해주었습니다. 이후 시트 2개에서 작업을 하고 대시보드 1개에서 두개를 합쳐주는 방식으로 간단히 끝을 내주었습니다.

 

 

 

 
서울시 공중화장실 공공데이터를 가지고 아주 간단한 데이터 시각화, 분석 실습을 해 보려고 합니다.
먼저 이번 포스팅에는 파이썬 pandas 라이브러리를 이용해서 데이터 전처리 작업한 것을 간단히 정리해 보았습니다. 
데이터 시각화, 분석은 태블로 프로그램을 이용하여 마친 뒤 다음 포스팅에 이어서 올리도록 하겠습니다.
 
 
https://data.seoul.go.kr/dataList/OA-1370/S/1/datasetView.do

열린데이터광장 메인

데이터분류,데이터검색,데이터활용

data.seoul.go.kr

 
사용한 데이터 링크입니다.
 


 

1. pandas : 필요없는 컬럼 삭제, 인덱스 지정

import pandas as pd

t = pd.read_csv("toilet.csv", engine='python', encoding = "cp949")
t

 
pd.read_csv를 이용해서 데이터를 불러오는데 한글 깨짐이 좀 있어서 encoding = "cp949"를 이용해주니 깔끔하게 불러오기가 잘 되었습니다.

 
먼저 value_counst()를 이용해서 대략적으로 확인해보니 별 다른 정보가 담겨있지 않은 관계로 새주소명, 생성일 컬럼은 삭제하기로 결정했습니다. 또, 고유번호에 중복값이 없는것을 확인한 관계로 고유번호를 인덱스로 지정해주겠습니다.

# 새주소명, 생성일 컬럼 드랍(삭제)
t = t.drop('새주소명', axis=1)
t.drop('생성일', axis = 1, inplace = True)

# 고유번호 컬럼 중복값 없는지 확인
len(t['고유번호'].unique()) == len(t['고유번호'])  # True

# 고유번호 인덱스화
t.set_index('고유번호', inplace = True)

 
필요없는 컬럼을 삭제하고 인덱스를 고유번호로 바꾸어 주니 어느정도 보는 게 깔끔해졌습니다.
이제 구명과 법정동명을 확인해보려고 하는데요.
 
 

2. 구명

t['구명'].value_counts()

 
value_counts()를 이용해서 확인해 보니

  1. 끝에 '구'가 붙어 있지 않은 구
  2. 오타 작렬한 구
  3. 빌딩이 왜 여기서 나와? 갈암구는 또 어디야? 갈현송방차풀소는 뭐야?

이것들을 해결해줘야 할 것 같습니다.
먼저 구가 안붙어있는 것들에 구를 붙여줘 보기로 했습니다. (예:노원 > 노원구)

t['구명'] = t['구명'].apply(lambda x: x + '구' 
                          if x in ['동작', '금천', '강서', '양천', '노원', '관악', '영등포', '서대문'] 
                          else x)

 
'구명'이 동작/금천/강서/양천/노원/관악/영등포/서대문 중 하나인 경우
컬럼에 apply와 lambda 함수를 이용해서 끝에 '구'를 붙여 줬습니다. 해당사항이 없는 경우는 그냥 놔두도록 처리했습니다.

t['구명'].value_counts()


그 외 갈현송방차풀소~남서울빌딩에 해당하는 row들은 그냥 제거하겠습니다.

# 이것들에 해당하는 '구명'을 가진 row들을 제외한(~) 줄만 남겨서 t에 재할당
t = t[~t['구명'].isin(['갈현송방차풀소', '송북구', '송파ㅜ', '영등로구', 
                      '영등표구', '송파두성빌딩', '갈암구', '구로수', '남서울빌딩'])]
len(t['구명'].value_counts().index)
# 25

 
혹시 몰라 확인해 보니 총 25개의 구가 있는것이 잘 확인되었습니다. 검색해보니 서울에는 25개 자치구와 426개 행정동이 있다고 하네요! 서울 살면서도 계속 까먹어요... 상식으로 외워둬야지.
 
법정동은 426개를 일일이 확인하기 불가능 + 의미가 없는 것 같아서 일단 놔두도록 하겠습니다. 다음 포스팅에는 태블로를 이용하여 간단한 시각화를 해서 가져오도록 하겠습니다!
 

 

 


 
파이썬 requests, BeautifulSoup 라이브러리를 이용한 웹크롤링 후 데이터분석 실습을 해보았습니다 :-) 
 
연금복권720+은 제가 한달에 2-3회정도 꾸준하게 구매하는 최애 복권인데요. 슬프게도 지금까지 제대로 당첨된 적은 단 한번도 없지만, 앞으로도 저는 꾸준히 구매를 할 예정인 아주아주 매력적인 복권입니다. 1등에 당첨이 되면 (세전) 700만원을 매월 20년동안 수령할 수 있어요. 동행복권 온라인 사이트에서 간단히 온라인 구매를 할 수도 있구요. 1등 번호는 온라인 1명, 오프라인 1명 총 2명이 당첨될 수 있습니다. 자세한 복권 구조는 동행복권 홈페이지를 참고해 보시구요.
 
복권의 경우 통계를 공부해보신 분들께는 아주 친숙한 소재이실텐데요. (저는 아닙니다.ㅋㅋㅋ) 동행복권 사이트에서는 복권 당첨번호를 엑셀파일로도 제공하고 통계 자료를 따로 분석해서 메뉴도로 제공하고 있습니다. 다만 저는 철저히 requests 라이브러리를 이용한 웹크롤링에 익숙해지기 위해서 엑셀 파일이나 통계자료를 건드리지 않고 처음부터 끝까지 혼자 힘으로 본 실습을 했습니다! 
 

 
[참고] 본 포스팅은 수리링 본인의 공부 기록을 목적으로 작성하였습니다. 해당 라이브러리에 대해 전혀 모르시는 분께서 보면서 따라하시기엔 많이 불친절하게 느껴질 수 있습니다. 참고하시고 봐 주시면 감사드리겠습니다 :-)
 
[참고] 본 포스팅은 책, 강의, 다른 사람의 포스팅을 참고하지 않은 스스로의 창작물입니다! 참고하여 포스팅 하시는 경우 출처 밝혀주심 감사드리겠습니다!
 


 
[실습 목차]

  1. 206회차로 모의 실습
  2. 원하는 회차 구간을 입력하면 모든 정보를 담아 데이터프레임으로 리턴하는 함수 작성
  3. 데이터프레임으로 간단한 데이터분석 (은근 재밌으니 귀찮으시면 이것만 보고 가세요...^^)

1-1. 숨은 URL 찾아내기

 
동행복권 사이트의 회차별 당첨번호 페이지(클릭)에 가 봅니다.
 

 
 
회차 바로가기 메뉴를 통해 원하는 회차를 선택해서 당첨 결과를 볼 수 있었습니다.
 

 
 
그런데 기본 URL에 회차 정보가 드러나지 않고 숨어 있어요. 206회를 조회해도, 200회를 조회해도 계속 같은 URL이 유지됩니다. 따라서 회차를 특정하여 정보를 뽑아낼 수가 없는 상황입니다. 우리는 회차를 조회할 수 있는 상세URL을 알아내야 해요.
 
문제상황을 해결하기 위해 크롬 웹브라우저의 inspection(개발자 도구) 메뉴의 Network 탭을 확인해 봅시다.
 

 
 
위와 같이 네트워크 탭을 켜둔 상태로 조회 버튼을 눌러봅니다. Name 탭의 맨 첫 번째 gameResult 어쩌구를 클릭한 다음 Payload를 확인합니다. (누가 봐도 수상한) Round: 206 이라는 정보를 확인했습니다. 기존 url 뒤에 &Round=206을 붙여 주면 될 것 같다는 합리적 의심을 해봅니다.
 

https://dhlottery.co.kr/gameResult.do?method=win720&amp;amp;Round=205

 
 
주소 뒤에 &Round=205 를 붙여넣고 엔터를 치니 205회 당첨결과 페이지로 잘 이동합니다 ㅎㅎ 찾았다 요놈! 이제 상세 url주소를 찾았으니 코드를 작성하면서 원하는 데이터를 뽑아내 보겠습니다.
 


 

1-2. requests, BeautifulSoup 라이브러리

* 본 실습에서 해당 라이브러리에 대한 상세 설명은 생략합니다

import requests
from bs4 import BeautifulSoup as BS

 
먼저 requests와 BeautifulSoup 라이브러리를 임포트해줍니다.

url = "https://dhlottery.co.kr/gameResult.do?method=win720&Round=206"
res = requests.get(url)
soup = BS(res.text, "html.parser")

 
우리가 찾아낸 url을 선언해 준 다음 차례대로 라이브러리에 넣어서 html 자료를 뽑아냅니다.

 
soup을 실행해 보니 html 정보가 잘 들어왔습니다 :)
저는 html 코드를 하나하나 뜯어보면서 원하는 정보를 뽑아내 봤어요.

nums = rows[0].find_all("span", {"class":"num"})

#조, 당첨번호
group = int(nums[0].find("span").text)
n_1 = int(nums[1].find("span").text)
n_2 = int(nums[2].find("span").text)
n_3 = int(nums[3].find("span").text)
n_4 = int(nums[4].find("span").text)
n_5 = int(nums[5].find("span").text)
n_6 = int(nums[6].find("span").text)

print(f"{group}조 {n_1}, {n_2}, {n_3}, {n_4}, {n_5}, {n_6}")
# '3조 4, 8, 9, 0, 7, 5'

 
먼저 제일 중요한 1등 조, 6개의 당첨번호를 뽑아봤습니다. 

# 등위(등수명) 
# rows[0]이므로 첫번째 1등을 구함 -> 나중에 인덱스를 바꾸어 다른 등수의 이름도 구할 수 있음
rank = rows[0].find_all("td")[0].text
rank
# '1등'

 
등수명도 뽑아봤어요. 이정도는 그냥 작성해도 되지만 연습삼아서 뽑아봤습니다 :)

# 당첨결과(매)
rank_counts = int(rows[0].find_all("td", {"class":"ta_right"})[-1].text.strip())
rank_counts
# 2

 
1등의 당첨 매수를 뽑아봤습니다. 206회차는 1등이 2명입니다. 연금복권 1등은 온라인/오프라인 각1명씩 최대 2명이 나올 수 있습니다. 가끔 1등이 1명밖에 없을 때도 많아요. 아주 드물게 0명일 때도 있는 거 같아요.

# 보너스 당첨번호 6자리
bonus_nums = []
for i in range(6):
    bonus_num = rows[7].find_all("span", {"class" : "num"})[i].find("span").text
    bonus_num = int(bonus_num)
    bonus_nums.append(bonus_num)

print(bonus_nums)
# [5, 8, 7, 6, 9, 5]

 
보너스 당첨번호 6자리도 뽑아봤습니다.

# 보너스 당첨결과(매)
bonus_counts = int(rows[7].find_all("td", {"class":"ta_right"})[-1].text.strip())
bonus_counts
# 10

 
10명이나 당첨됐네요.
 


 

2-1. 회차를 입력할 수 있는 함수로 작성해보기

206회차를 가지고 적당히 연습을 해 봤으니, 원하는 회차를 입력하면 하나씩 모두 조회해서 딕셔너리로 담아 리턴하는 함수를 작성해 보았습니다.

def win720(round):
	# 입력받은 회차 번호로 url을 만들고 정보를 받아냅니다.
    url = f"https://dhlottery.co.kr/gameResult.do?method=win720&Round={round}"
    res = requests.get(url)
    soup = BS(res.text, "html.parser")
    rows = soup.find("tbody").find_all("tr")
	
    # data_dict에 앞으로 하나씩 정보를 추가할 겁니다. 먼저 라운드 값을 첫 번째로 넣어줬습니다.
    data_dict = {"round":round}
    nums = rows[0].find_all("span", {"class":"num"})

    # 1등 조, 당첨번호
    group = int(nums[0].find("span").text)
    n_1 = int(nums[1].find("span").text)
    n_2 = int(nums[2].find("span").text)
    n_3 = int(nums[3].find("span").text)
    n_4 = int(nums[4].find("span").text)
    n_5 = int(nums[5].find("span").text)
    n_6 = int(nums[6].find("span").text)

    data_dict["group"] = group
    data_dict["n_1"] = n_1
    data_dict["n_2"] = n_2
    data_dict["n_3"] = n_3
    data_dict["n_4"] = n_4
    data_dict["n_5"] = n_5
    data_dict["n_6"] = n_6

    # 1-7등 당첨자수
    for i in range(7):
        rank_counts = rows[i].find_all("td", {"class":"ta_right"})[-1].text.strip()
        rank_counts = re.sub(",","", rank_counts)
        rank_counts = int(rank_counts)
        column_name = f"rank{i+1}"
        data_dict[column_name] = rank_counts

    # 보너스 당첨번호 6개
    for i in range(6):
        bonus_num = rows[7].find_all("span", {"class" : "num"})[i].find("span").text
        column_name = f"bonus_{int(i)+1}"
        data_dict[column_name] = bonus_num

    # 보너스 당첨자수
    bonus_counts = int(rows[7].find_all("td", {"class":"ta_right"})[-1].text.strip())
    data_dict["bonus"] = bonus_counts

    return data_dict

 
더럽게 길지만 그래도 잘 작동했습다^_^;;;; 
너무 길어져서 쓰면서 불길했는데 그래도 오류 수정 2-3번만에 원하는 대로 값이 나와서 다행이였어요

 
205회차로 테스트를 해 봤는데요. 조, 1등 넘버 6자리, 등수별 당첨매수, 보너스 번호 6자리, 보너스 당첨매수가 딕셔너리로 제대로 들어온 것을 확인했습니다 :) 이게 뭐라고 너무 재밌었어요 (ㅋㅋㅋㅋ)
 


 

2-2. 회차 구간을 설정하고 데이터프레임을 리턴하는 함수 작성하기

 
위에서 작성한 win720()함수를 가지고 원하는 회차 구간의 모든 정보를 담은 데이터프레임을 반환하는 함수를 작성해 주었습니다.

def lucky_chart(start, end):
    lucky_results = []

    for i in range(start, end+1):
        win = win720(i)
        values = list(win.values())
        print(values)
        print(len(values))
        lucky_results.append(values)

    columns = list(win.keys())
    print(columns)
    print(len(columns))
    
    import pandas as pd
    df = pd.DataFrame(lucky_results, columns = columns)
    
    return df.set_index("round")

 
중간 중간에 있는 print 함수들은 제가 함수를 작성하면서 중간 과정을 시각화하기 위해 굳이 넣어줬구요, 깔끔하게 없애줘도 됩니다.

history = lucky_chart(190, 206)

 
190회부터 206회차까지 럭키차트 함수를 돌려보았습니다.

 
요런식으로 진행상황을 시각화 하기 위해 print 함수를 넣어줬습니다. (중간에 오류가 있었어서 저런식으로 시각화 하면서 수정해줬어요!)
 

history

 
알흠다운 판다스 데이터프레임이 완성되었어요 ❤️
 


 

3. 간단 데이터분석

마지막으로 데이터분석은 1회차부터 206회차까지로 구간을 늘려서 진행했습니다! (1등은 제껍니다.)
 

  • 1등 조 비율이 어떻게 될까?
# 조별 value_counts() 구하기
group_counts = history['group'].value_counts()

# matplotlib 임포트
import matplotlib.pyplot as plt

# 차트 작성
plt.pie(group_counts,
        labels = group_counts.index,
        shadow = True,
        autopct = '%1.2f%%')
plt.title("Rank 1 groups ratio")
plt.legend()
plt.show()

 
아주.. 흥미로운.. 결과입니댜... 연금복권720+ 1회차부터 206회차까지 모든 데이터들을 살펴본 결과... 지금까지 가장 많은 1등을 배출한 조는 4조였습니다. 4조 > 1조 > 3조 > 5조 > 2조 순이네요. 
 
연금복권을 아는 분들께서는 이해를 하실텐데, 저는 혹시라도 번호 6개를 다 맞췄지만 조가 다를 때 2등이라도 당첨되도록 + 혹시라도 1등이 되면 2등도 동시 당첨되도록(ㅋㅋㅋ) 번호 6개를 고르고 나면 모든 조(1~5조)로 총 5줄(5,000원)을 구매해버립니다. 솔직히 이게 더 확률이 낮을 것 같기는 한데.......... 만약 특정 조를 골라서 구매해야 한다면 앞으로 저는 1조 또는 4조를 고르겠습니댜.
 

  • 지금까지 역대 2등은 몇명이 나왔을까?
import seaborn as sns

sns.set_style("darkgrid")
sns.set_palette("bright")

sns.barplot(history["rank2"].value_counts())
history['rank2'].agg(['min', 'max', 'mean'])
# min     0.0
# max     8.0
# mean    4.5

 
1등에 가려 2등의 당첨자 수는 사실 잘 확인해 본 적이 없는데요. 2등에 당첨되면 매달 100만원씩 10년간 수령할 수 있거든요. 2등이라도 당첨시켜 주신면 제가 굉장히 감사할텐데요. 몇 명이나 당첨되나 보니, 역대 2등 당첨자 수는 최대 8명, 최소 0명(ㅋㅋㅋㅋㅋㅋ), 평균 4.5명이 나왔다고 합니다. 그래프로 확인해 보니 2등이 한 명도 나오지 않은 회차가 20번이 넘네요? 실화냐?
 

  • 그럼 1등이 한 번도 안 나온 회차도 있을까?
history['rank1'].value_counts()
# 1    101
# 2     63
# 0     42
sns.countplot(data = history,
              x = 'rank1',
              legend = 'full')

아니 미친... 1등이 0명인 회차가 40회가 넘는다고? 1등이 2명 다 나온 적보다 1명밖에 안 나온 적이 더 많다고? 여러분 빨리 연금복권 사세요! 저거 다 우리돈이라고(흥분)
 

  • 1번부터 6번까지 각 자리마다 번호가 몇번씩 나왔을까
df = pd.DataFrame()

for i in range(1, 7):
    col = f"n_{i}"
    df[col] = history[col].value_counts().sort_index()

df
for i in range(1, 7):
    col = f"n_{i}"
    print(f"{i}번째 자리에서 가장 많이 나온 숫자는 {df[col].idxmax()}")

 
각 자리에서 가장 많이 나온 숫자는 순서대로 4 - 4 - 9 - 0 - 5 - 6 이었습니다. 이게 엄청 큰 의미가 있을지는 모르겠지만, 해당 자리에 어떤 숫자를 고를지 고민되신다면 이 정보도 참고해 봐도 좋을 것 같습니다.

for i in range(1, 7):
    col = f"n_{i}"
    print(f"{i}번째 자리에서 가장 조금 나온 숫자는 {df[col].idxmin()}")

 
반대로 각 자리별로 가장 조금 나온 숫자도 구해봤어요. 두 정보를 종합하면, 숫자 0 4 7 9가 자주 보이네요. 반대로 0 4 7 9를 제외한 1 2 3 5 6 8 을 고르는 것도 안전하게 갈 수 있는(?) 방법일 수도 있을 것 같고.. 복권의 세계는 정말 어렵네요.
 
tmi지만 저는 항상 첫자리를 6으로 구매를 하는데, 첫자리 6은 꼴찌 0에 이어서 두 번째로 나온 횟수가 적네요. 전략을 바꿔야 하나.... 고민이 되지만 (ㅋㅋㅋㅋㅋㅋㅋㅋㅋ) 복권 통계는 재미일 뿐이라고 생각합니다.
 
 
 
 
여기까지 간단 분석을 마쳐 보겠습니다! :-)
감사합니다.
 

 
 

 

https://school.programmers.co.kr/learn/courses/30/lessons/12945

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

 

오랜만에 피보나치 수열 문제 풀이를 했습니다. 처음에 간단한 재귀함수를 적용해서 풀이를 했더니 테스트는 모두 통과하는데 효율성 테스트에서 시간 초과로 계속 실패를 하는 문제가 발생했습니다. 그래서 효율성을 올리기 위해 메모화를 적용해서 풀이를 했더니 효율성 테스트도 모두 통과할 수가 있었습니다 :-)

 

간단하게 풀이방법을 설명해 보겠습니다!

 

 

피보나치 수열

 


1. 메모화 없이 기본 피보나치 수열 함수 작성하기

def basic_fibonacci(n):
    if n in [1, 2]:
        return 1
    if n == 3:
        return 2

    return basic_fibonacci(n-2) + basic_fibonacci(n-1)

 

만약 주어진 정수가 1 또는 2라면 바로 1을 리턴,

주어진 정수가 3이라면 바로 2를 리턴하도록 초기 설정을 해 줍니다.

4 이상의 정수 n이 주어졌을 때 재귀함수 형식으로 n-2와 n-1에 basic_fibonacci 함수를 적용한 값을 구해 더한 값을 리턴합니다.

basic_fibonacci(8), basic_fibonacci(9), basic_fibonacci(10), basic_fibonacci(11)
# (21, 34, 55, 89)

 

위 함수는 제대로 작동하지만, n의 값이 커질수록 효율성이 급격하게 떨어진다는 단점이 있어요. 메모화를 이용해서 함수의 효율을 높여보겠습니다.


2. 메모화를 적용한 피보나치 수열 함수 작성하기

def fibonacci(n, memo = {1:1, 2:1, 3:2, 4:3, 5:5, 6:8}):
    if n in [2, 3, 4, 5, 6]:
        return memo[n]
    
    if n > 6:
        for i in range(7, n+1):
            if i in memo: pass
            else:
                memo[i] = memo[i-2] + memo[i-1]
    
    return memo[n]

 

하나씩 뜯어보겠습니다 :)

def fibonacci(n, memo = {1:1, 2:1, 3:2, 4:3, 5:5, 6:8}):

 

가장 먼저 함수를 선언할 때 파라미터 값으로 memo라는 이름으로 딕셔너리를 포함해 주었습니다. 저는 간단하게 n이 1부터 6번째일때 피보나치 수열 값을 딕셔너리에 초기값으로 미리 메모를 해 두었습니다.

def fibonacci(n, memo = {1:1, 2:1, 3:2, 4:3, 5:5, 6:8}):
    if n in [2, 3, 4, 5, 6]:
        return memo[n]

 

내가 구하고자 하는 것은 피보나치 수열의 n번째 값입니다. 만약 n이 2, 3, 4, 5, 6인 경우 (문제 조건에서 n은 2 이상의 정수라고 했으므로 1은 제외합니다) memo 딕셔너리의 해당하는 value 값을 바로 리턴하고 함수를 종료하게 됩니다.

    if n > 6:
        for i in range(7, n+1):
            if i in memo: pass
            else:
                memo[i] = memo[i-2] + memo[i-1]
    
    return memo[n]

 

만약 n이 6보다 크면 어떻게 해야 할까요?

 

주어진 n의 값이 50일 때를 예시로 생각해 봅시다. 우리는 50번째 값을 구하기 위해 48, 49번째 값이 필요합니다.

49번째 값을 구하기 위해서는 47, 48번째가 필요하고

48번째 값을 구하기 위해서는 46, 47번째가 필요합니다.

결국 50번째 값을 구하기 위해서는 1부터 49번째까지 피보나치 수열 값을 모두 알아야 합니다.

 

그럼 메모화는 어떻게 작동할까요?

제가 만약 50번째 피보나치 수열 값을 구하기 전에,

40번째 피보나치 수열 값을 먼저 구했다고 생각해 보겠습니다.

fibonacci(40)
# 102334155

 

우리는 40번째 피보나치 수열 값을 구하기 위해 지금 1번째~39번째 피보나치 수열의 값을 열심히 구했습니다.

이제 다음으로 50번째 피보나치 수열 값을 구하려고 합니다.

 

 

먼저 메모화를 해놓지 않은 경우를 생각해 봅시다.

우리는 이미 1번부터 40번째까지의 값을 이전에 미리 구한 전적이 있지만

따로 기록을 해 두지 않았기 때문에 같은 계산을 또 반복해야 합니다.

1번째~49번째 피보나치 수열 값을 열심히 또 구하는 거죠.

 

하지만 제가 기록을(메모화를) 해 두었다면 얘기가 달라집니다.

즉 처음에 fibonacci(40)을 계산했을 때

memo 딕셔너리에 1번부터 40번까지 피보나치 수열 값을

key(n) : value(값) 형식으로 미리 저장을 해 두었다면,

우리는 fibonacci(50)을 구할 때 

1. 1번부터 40번까지는 간단히 딕셔너리에서 값을 찾아 가져올 수 있습니다.

2. 41번부터 49번째 값은 새로 구하고 딕셔너리에 값을 추가해주는 방식으로 계속해서 메모를 해나갈 수 있습니다.

 

+ 참고로 함수를 여러번 작동하면서 메모장에 추가하는 key와 value 값은 매번 초기화되지 않고 계속해서 누적 기록을 보관합니다.

 

def fibonacci(n, memo = {1:1, 2:1, 3:2, 4:3, 5:5, 6:8}):
    # 주어진 정수가 2-6중 하나일 경우 바로 메모장에서 수열값을 찾아 리턴, 함수종료
    if n in [2, 3, 4, 5, 6]:
        return memo[n]
    
    # 주어진 정수가 6보다 큰 경우(7 이상)
    if n > 6:
    	# 7부터 n까지의 정수 i에 대해서
        for i in range(7, n+1):
            # 만약 메모장에 i값이 이미 메모되어 있다면 그냥 넘어가고
            if i in memo: 
                pass
            # 만약 메모장에 i값이 없다면 피보나치 수를 구해서 추가해주세요
            else:
                memo[i] = memo[i-2] + memo[i-1]
    
    # 메모장에서 n번째 피보나치 수열 값을 찾아 리턴해주세요
    return memo[n]

 

최종 피보나치 함수입니다 :)

내가 구하고자 하는 n의 피보나치 값을 찾기 위해 7부터 n-1번째의 수열값을 계속해서 차례대로 메모장에 누적 메모해나가는 방식입니다. 재귀함수랑 약간 비슷한듯 다른 느낌이네요!


3. 최종 문제풀이

마지막으로 문제 해결을 위해 피보나치 수열을 활용해서 soluton()함수를 작성했습니다. 저는 코딩테스트 문제를 풀 때 이렇게 함수를 나누어 작성하는것을 좋아합니다. 함수 안에 함수 작성하는거 시러요.....(개취)

def fibonacci(n, memo = {1:1, 2:1, 3:2, 4:3, 5:5, 6:8}):
    if n in [2, 3, 4, 5, 6]:
        return memo[n]
    
    if n > 6:
        for i in range(7, n+1):
            if i in memo: 
                pass
            else:
                memo[i] = memo[i-2] + memo[i-1]
    
    return memo[n]

def solution(n):
    fibo_num = fibonacci(n)
    
    # fibonacci(30)< 1234567 < fibonacci(31)
    # 만약 n이 30 이하라면 바로 fibo_num을 리턴하고(몫이 0이므로 나머지와 동일)
    if n <= 30 : 
        return fibo_num
    # 만약 n이 30보다 크다면 fibo_num을 1234567으로 나눈 나머지를 리턴
    else :
        return fibo_num % 1234567

 

정확성: 100.0
합계: 100.0 / 100.0

 

 

 

 

풀이나 설명에 오류가 있다면 댓글로 알려주세요!

감사합니다! :-)

 

 

 

코딩테스트 문제를 해결할 때면 2개 이상의 정수의 최대공약수 또는 최소공배수를 구해야 하는 경우가 종종 있습니다. 파이썬에서 최대공약수와 최소공배수는 넘파이 라이브러리를 이용해서 아주 간단하게 구해낼 수 있는데요. 

 

먼저 함수를 소개해 드리기 전에, 최대공약수와 최소공배수를 영어로 뭐라고 부르는지 알고 넘어갈게요.

  • gcd : greatest common division (최대공약수)
  • lcm : lowest common multiple (최소공배수)

이런 간단한 영어 정도는 숙지해 두시면 좀더 직관적으로 쉽게 프로그래밍하실 수 있어요 :-)

 

[1] 최대공약수 구하기

import numpy as np

 

먼저 넘파이 라이브러리를 임포트해줍니다.

np.gcd(12, 20)
# 4

np.gcd(30, 45)
# 15

 

np.gcd() 함수 안에 두 정수를 넣으면 두 정수의 최대공약수를 리턴합니다. gcd인 이유는 위에서 설명한 대로 greatest common divison의 약자 gcd가 최대공약수를 의미하기 때문입니다.

 

만약 3개 이상의 정수의 최대공약수를 구하고 싶다면 어떻게 할 수 있을까요?

np.gcd.reduce([15, 25, 35])
# 5

np.gcd.reduce([15, 27, 18])
# 3

before = np.arange(0, 20, 5)
after = np.gcd.reduce(before)
after
# 5

 

np.gcd.reduce() 함수 안에 리스트 또는 넘파이 어레이를 넣어주면 리스트 또는 어레이 안의 모든 정수의 최대 공약수를 리턴합니다.

 

 

[2] 최소공배수 구하기

np.lcm(10, 14)
# 70

np.lcm.reduce([10, 15, 20])
# 60

before = np.array([3, 7, 10])
after = np.lcm.reduce(before)
after
# 210

 

np.lcm() 함수 안에 두 정수를 넣으면 두 정수의 최소공배수를 리턴합니다. lcm인 이유는 위에서 설명한 대로 lowest common multiple의 약자 lcm이 최소공배수를 의미하기 때문입니다.

 

마찬가지로 np.lcm.reduce()  함수 안에 리스트 또는 넘파이 어레이를 넣어서 3개 이상의 정수의 최소공배수도 구해낼 수 있습니다.

 

 


 

 

알고 있으면 아주아주 유용한 파이썬 넘파이 모듈 이용해서 최소공배수/최대공약수 구하는 방법이었습니다.

 

감사합니다 :-)

 

 

문자열을 분리하고 변형해야 하는 코딩 테스트 문제에서 공백 문자가 연속으로 나오는 경우가 많이 있습니다. 공백을 기준으로 문자열을 분리할 때 자연스럽게 split() 함수를 사용하게 되는데, 이 경우 연속 공백을 상실하게 되는 문제점이 발생합니다.

 

연속 공백을 유지하면서 문자열을 분리하여 리스트로 만들기 위해 정규식(regular expression)을 사용할 수 있습니다.

 

 

https://school.programmers.co.kr/learn/courses/30/lessons/12951#

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

프로그래머스 JadenCase 문자열 만들기 문제를 가지고 방법을 설명해 보겠습니다. (다른 다양한 문제에서도 활용할 수 있습니다!)


 

문제 : JadenCase란 모든 단어의 첫 문자가 대문자이고, 그 외의 알파벳은 소문자인 문자열입니다. 단, 첫 문자가 알파벳이 아닐 때에는 이어지는 알파벳은 소문자로 쓰면 됩니다. 문자열 s가 주어졌을 때, s를 JadenCase로 바꾼 문자열을 리턴하는 함수, solution을 완성해주세요.

 

이렇게 공백문자가 연속해서 나올 수 있다는 조건이 있는 경우 split() 함수를 사용하면 공백 여러개가 모두 없어져버리므로 조심해야 합니다.

 

 

저는 먼저 단어 1개를 JadenCase로 바꾸어 리턴하는 함수 jaden을 작성했습니다.

def jaden(w):
    try:
    	# 첫 번째 글자가 정수일 때
        int(w[0]) 
        return w[0] + w[1:].lower() 
    except:
    	# 첫 번째 글자가 정수가 아닐 때
        return w[0].upper() + w[1:].lower()

 

이렇게 작성한 jaden 함수를 가지고 최종 solution 함수를 작성해 볼게요.

 

import re

 

먼저 정규식 패키지를 불러옵니다.

 

word = "Hello       World   Bye"

 

위의 예시를 단어와 공백문자로 구분한 리스트를 어떻게 만들 수 있을까요?

 

word = "Hello       World   Bye"
re.findall("\S+|\s+",word)
# ['Hello', '       ', 'World', '   ', 'Bye']
  • re.findall() : RE가 일치하는 모든 부분 문자열을 찾아 리스트로 반환합니다. 활용해서 원하는 구성요소를 찾아 리스트로 만들어줍니다.
  • \s+ : 일치하는 모든 공백 문자를 찾아줍니다. (여러 개의 공백도 하나로 평탄화하지 않고 유지한 채로 찾아줍니다.)
  • \S+ : 일치하는 모든 비공백 문자를 찾아줍니다.

결과를 보면 여러 개의 공백 문자가 잘 보존되어 있는 것을 볼 수 있습니다. 이제 최종 솔루션 함수를 작성해 볼게요.

 

def jaden(w):
    try:
    	# 첫 번째 글자가 정수일 때
        int(w[0]) 
        return w[0] + w[1:].lower() 
    except:
    	# 첫 번째 글자가 정수가 아닐 때
        return w[0].upper() + w[1:].lower()

import re

def solution(s):
	# 입력 받은 s를 whitespace와 nonwhitespace로 구분합니다
    words = re.findall("\S+|\s+", s)
    answer = ""
    for word in words:
    	# 만약 공백문자가 아니라면 jaden 함수를 적용해서 answer에 추가합니다.
        if word.isalnum(): 
        	answer += jaden(word)
        # 만약 공백문자라면 그냥 바로 answer에 추가합니다.
        else: 
        	answer += word
    return answer

 

 
정확성: 100.0
합계: 100.0 / 100.0
 

 

끝입니다! 정규식이 기억에 잘 남지는 않지만,
잘만 숙지해 두면 연속 공백문자 처리를 해야하는 문제에서 요긴하게 잘 쓸 수 있답니다 :-) 
읽어주셔서 감사합니다!

 

 

프로그래머스 코딩테스트 1단계에 수록되어있는 카카오 인턴십 '키패드 누르기' 문제를 풀었습니다. 풀고 나서 코드가 좀 길다고 생각했는데 다른 사람들의 풀이를 보니 다들 비슷한 길이라서 안심이 되었습니다. (한심...ㅎ) 길다고 무조건 나쁜 코드, 짧다고 무조건 좋은 코드는 아니긴 합니다. 그래도 최대한 니트하고 간결하게 작성하고싶은 욕심이 드는 건 어쩔 수가 없네요 ㅎ_ㅎ (그럼에도 불구하고 제 코드는 항상 긴 편인거 같아요.)

 

저는 키패드에 좌표 개념을 도입하여 간단하게 문제를 풀어보았습니다. 문제 풀이 시간은 5분-10분정도로 그래도 최근에 연습했던 알고리즘 문제들에 비교하면 비교적 수월했던 문제였던 것 같습니다. 그럼 풀어보겠습니다!

 

 

문제 상황 : 이 전화 키패드에서 왼손과 오른손의 엄지손가락만을 이용해서 숫자만을 입력하려고 합니다. 맨 처음 왼손 엄지손가락은 * 키패드에 오른손 엄지손가락은 # 키패드 위치에서 시작하며, 엄지손가락을 사용하는 규칙은 다음과 같습니다.

1. 엄지손가락은 상하좌우 4가지 방향... (중략....)


자세한 문제 상황과 조건, 입출력, 테스트 케이스 등은
꼭 프로그래머스에서 확인해 보세요!

https://school.programmers.co.kr/learn/courses/30/lessons/67256

 

 

 

1. 문제의 이해

먼저 정수로 이루어진 리스트 numbers가 인풋으로 주어집니다. 문제 조건을 보면, 정수가 1, 4, 7일 때에는 바로 왼손을, 3, 6, 9일때는 바로 오른손을 쓰면 되기 때문에 아무런 제약이 없어서 굉장히 쉽습니다. 우리가 고민해야 할 부분은 정수가 2, 5, 8, 0일 때입니다.

 

2, 5, 8, 0이 주어졌을 때 조건에 맞게 손가락을 움직이기 위해서는

 1)  그 전의 손가락 위치를 알고 있어야 합니다.

 2)  그 전의 손가락 위치와 2/5/8/0 사이의 거리를 숫자로 나타낼 수 있어야 합니다.

 3)  왼손, 오른손과 2/5/8/0 사이의 거리를 비교할 수 있어야 합니다.

 

그럼 위 세 가지를 고민하면서 문제를 풀어보겠습니다.

 

 

2. 풀이

[1] 변수와 좌표 설정

def solution(numbers, hand):
    answer = ""
    current_left_thumb = (4, 1)
    current_right_thumb = (4, 3)

 

저는 키패드 1이 있는 곳을 좌표 (1, 1)으로, 

키패드 3이 있는 곳을 좌표 (1, 3)으로,

키패드 *이 있는 곳을 좌표 (4, 1)으로,

키패드 #이 있는 곳을 좌표 (4, 3)으로 놓았습니다.

 

첫줄을 파이썬 인덱싱과 같이 0부터 시작할까 하다가 보다 직관적으로 하기 위해 1부터 시작해줬는데, 0부터 시작해도 큰 상관은 없습니다.

 

current_left_thumb, current_right_thumb라는 변수명을 사용하여 왼손/오른손을 구분하여 현재 손가락이 어디 있는지를 좌표값으로 표현했습니다. 변수명이 조금 길고 번잡하긴 하지만 직관적으로 이해할 수 있어서 스스로 코드 이해하기가 편했습니다. 이 두 가지 변수는 앞으로 numbers에 주어진 정수값에 따라 손가락이 움직일 때 함께 업데이트가 되어 갈 예정입니다!

 

 

[2] 1/4/7, 3/6/9 처리

    for number in numbers:
        if number == 1:
            answer += "L"
            current_left_thumb = (1, 1)
        elif number == 4:
            answer += "L"
            current_left_thumb = (2, 1)
        elif number == 7:
            answer += "L"
            current_left_thumb = (3, 1)
        elif number == 3:
            answer += "R"
            current_right_thumb = (1, 3)
        elif number == 6:
            answer += "R"
            current_right_thumb = (2, 3)
        elif number == 9:
            answer += "R"
            current_right_thumb = (3, 3)

 

다음으로 numbers 리스트의 값(value)를 가지고 하나씩 살펴보면서 이터레이션을 돌려줍니다. 리스트 이터레이션은 습관적으로 인덱스로 하게 되는데, 이번 문제에서는 굳이 인덱스로 조회할 필요가 없어서 편했습니다.

 

만약 number가 1/4/7중에 하나라면 answer 스트링에 L을 추가하고 해당 좌표값으로 왼손을 움직입니다(current_left_thumb 변수의 좌표를 업데이트합니다.). 만약 number 3/6/9중에 하나라면 answer 스트링에 R을 추가하고 해당 좌표값으로 오른손을 움직입니다(current_right_thumb 변수의 좌표를 업데이트합니다.). 

 

이 부분의 문법이 어쩔수없이 위처럼 길어지게 되었는데, 어떻게 하면 더 짧게 쓸 수 있을까 잠깐 고민했어요. 하지만 지금 이대로도 아주 직관적이고 이해하기 편하기 때문에 굳이 줄이지 않아도 되겠다고 판단하고 다음 단계로 넘어갔습니다.

 

 

[3] 2/5/8/0 처리

        elif number in [2, 5, 8, 0]:
            if number == 2: x = 1
            if number == 5: x = 2
            if number == 8: x = 3
            if number == 0: x = 4

 

다음으로 가장 중요한 2, 5, 8, 0입니다. 저는 2, 5, 8, 0이 모두 같은 세로줄에 있다는 점에 착안하여 아이디어를 얻어서 2/5/8/0의 열(y)을 2로 고정하고 행(x)만 변수로 각각 1/2/3/4를 할당해 주었습니다. 더 자세히 설명해보겠습니다.

 

숫자 2의 좌표값은 (1, 2)입니다.

숫자 5의 좌표값은 (2, 2)입니다.

숫자 8의 좌표값은 (3, 2)입니다.

숫자 0의 좌표값은 (4, 2)입니다.

 

따라서 숫자 2, 5, 8, 0의 좌표를 (x, 2)와 같이 표현하고 x에만 각각 1/2/3/4를 할당해 주어 반복을 줄이겠다는 의미입니다.

 

distance_r = abs(current_right_thumb[0] - x) + abs(current_right_thumb[1] - 2)
distance_l = abs(current_left_thumb[0] - x) + abs(current_left_thumb[1] - 2)

# 가독성을 위해 인덴트를 없앴습니다

 

이제 거리 구하기로 넘어갑니다. 우리는 current_left_thumb와 current_right_thumb 라는 변수에 각각 왼손과 오른손 엄지의 직전 좌표값을 저장해 두고 있어요. 우리가 가고자 하는 2/5/8/0의 좌표값과 왼손, 오른손 사이의 거리를 각각 구해보도록 하겠습니다.

 

일반화된 식을 쓰기 전에 예시를 가지고 생각을 열어 볼게요. 키패드 2와 6 사이의 거리는 2입니다. 좌표로 어떻게 구할 수 있을까요? 키패드 2의 좌표는 (1, 2), 키패드 6의 좌표는 (2, 3)입니다. x값의 차이 1과 y값의 차이 1을 더해 주면 간단히 2가 나오는 것을 알 수 있어요. 키패드 8과 키패드 1의 경우는 어떨까요? 우선 거리는 3입니다. 좌표로 확인해 봅니다. 키패드 8의 좌표는 (3, 2)입니다. 키패드 1의 좌표는 (1, 1)입니다. x값의 차이 2와 y값의 차이는 1이며 둘을 더해 주면 3이 됩니다. 이제 일반화가 되었습니다.

 

abs(current_right_thumb[0] - x) : 오른손 엄지손가락의 행좌표와 2/5/8/0의 행좌표(x)의 차(절댓값)

abs(current_right_thumb[1] - 2) : 오른손 엄지손가락의 행좌표와 2/5/8/0의 열좌표(2)의 차(절댓값)

 

이 두개를 더해 주면 오른손이 움직이는 거리(distance_r)가 되고,

같은 방법으로 왼손이 움직이는 거리(distance_l)도 구해준 거예요.

 

            if distance_r < distance_l:
            # 오른손이 가야 하는 거리가 더 짧을 때
                answer += "R"
                current_right_thumb = (x, 2)
            elif distance_r > distance_l:
            # 왼손이 가야 하는 거리가 더 짧을 때
                answer += "L"
                current_left_thumb = (x, 2)
            else:
            # 왼손 = 오른손 거리가 같을 때
                if hand == 'right':
                # 오른손잡이라면
                    answer += "R"
                    current_right_thumb = (x, 2)
                else:
                # 왼손잡이라면
                    answer += "L"
                    current_left_thumb = (x, 2)

 

이제 거리를 비교합니다. 오른손이 가야 하는 거리가 더 짧을 때, 왼손이 가야 하는 거리가 짧을 때, 왼손과 오른손이 가야할 거리가 같을 때로 크게 셋으로 나누어 생각합니다. 특히 왼손, 오른손 거리가 같다면 왼손잡이인지 오른손잡이인지를 판단하는 것이 중요합니다.

 

 

이제 제가 작성한 함수를 한 번에 보겠습니다.

def solution(numbers, hand):
    answer = ""
    current_left_thumb = (4, 1)
    current_right_thumb = (4, 3)

    for number in numbers:
        if number == 1:
            answer += "L"
            current_left_thumb = (1, 1)
        elif number == 4:
            answer += "L"
            current_left_thumb = (2, 1)
        elif number == 7:
            answer += "L"
            current_left_thumb = (3, 1)
        elif number == 3:
            answer += "R"
            current_right_thumb = (1, 3)
        elif number == 6:
            answer += "R"
            current_right_thumb = (2, 3)
        elif number == 9:
            answer += "R"
            current_right_thumb = (3, 3)
        elif number in [2, 5, 8, 0]:
            if number == 2: x = 1
            if number == 5: x = 2
            if number == 8: x = 3
            if number == 0: x = 4

            distance_r = abs(current_right_thumb[0] - x) + abs(current_right_thumb[1] - 2)
            distance_l = abs(current_left_thumb[0] - x) + abs(current_left_thumb[1] - 2)
            if distance_r < distance_l:
                answer += "R"
                current_right_thumb = (x, 2)
            elif distance_r > distance_l:
                answer += "L"
                current_left_thumb = (x, 2)
            else:
                if hand == 'right':
                    answer += "R"
                    current_right_thumb = (x, 2)
                else:
                    answer += "L"
                    current_left_thumb = (x, 2)
    return answer
정확성: 100.0 합계: 100.0 / 100.0
 
 

숫자가 2, 5, 8, 0일 때 열 좌표가 2로 모두 동일하다는 점에 착안하여 행 좌표만 x로 변수 할당을 해줌으로써 반복을 줄일 수 있었습니다. 비록 엄청 짧은 코드는 아니지만 깔끔하고 직관적으로 풀이할 수 있어서 좋았습니다!

 

부족한 풀이었지만 도움이 되었으면 좋겠네요~ 감사합니다.

 

 

 

백준 2178번 미로탐색

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

 

2178번: 미로 탐색

첫째 줄에 두 정수 N, M(2 ≤ N, M ≤ 100)이 주어진다. 다음 N개의 줄에는 M개의 정수로 미로가 주어진다. 각각의 수들은 붙어서 입력으로 주어진다.

www.acmicpc.net

 

 

n = 4
m = 6

graph = [
	[1, 0, 1, 1, 1, 1],
 	[1, 0, 1, 0, 1, 0],
 	[1, 0, 1, 0, 1, 1],
 	[1, 1, 1, 0, 1, 1]
 ]

 

0과 1로 이루어진 n * m 크기의 미로 그래프가 있다. 미로에서 1은 이동할 수 있는 칸을 나타내고, 0은 이동할 수 없는 칸을 나타낸다. 이러한 미로가 주어졌을 때, (1, 1)에서 출발하여 (N, M)의 위치로 이동할지나야 하는 최소의 칸 수를 구하는 프로그램을 작성하시오. 한 칸에서 다른 칸으로 이동할 때, 서로 인접한 칸으로만 이동할 수 있다. 위의 예에서는 15칸을 지나야 (N, M)의 위치로 이동할 수 있다. 칸을 셀 때에는 시작 위치와 도착 위치도 포함한다.

 

 

dx = [-1, 1, 0, 0]
dy = [0, 0, 1, -1]

 

먼저 상하좌우로 움직일수 있는 x, y 델타값을 리스트로 작성한다.

def dfs_escape(start = (0, 0)):
	# x, y의 현재 포지션을 정해 준다. 
    x_pos = start[0]
    y_pos = start[1]
    
    # 할 일을 기록할 디큐 리스트 q를 작성한다.
    q = deque()
    
    # 가장 처음 초기값을 세팅해준다. x, y의 좌표값 위치를 튜플로 집어넣는다.
    q.append((x_pos, y_pos))
    
    # 할 일이 없을 때까지 반복!
    while q:
    	now_x, now_y = q.popleft()
        
        # 상하좌우 4번 반복
        for i in range(4):
        	next_x = now_x + dx[i]
        	next_y = now_y + dy[i]
            
            # 만약 다음으로 갈 좌표값이 미로를 벗어나지 않는다면
            if 0 <= next_x < n and 0 <= next_y < m:
                if graph[next_x][next_y] == 1:
                    graph[next_x][next_y] = graph[now_x][now_y] + 1
                    q.append((next_x, next_y))

 

bfs_miro()


graph
# [[3, 0, 9, 10, 11, 12],
#  [2, 0, 8, 0, 12, 0],
#  [3, 0, 7, 0, 13, 14],
#  [4, 5, 6, 0, 14, 15]]


graph[n-1][m-1]
# 15

 

 

 

+ Recent posts