파이썬 코딩테스트 강좌, ‘좋은 수’ 구하기

작성자: 조광형 | 날짜: 2024년 11월 26일

문제 설명

주어진 숫자 n이 ‘좋은 수’라면, 그 수를 구하는 문제입니다.
‘좋은 수’의 조건은 다음과 같습니다.

  • ‘좋은 수’는 두 자리 이상의 자연수이다.
  • 각 자리의 합이 홀수인 수를 ‘좋은 수’로 정의한다.
  • 주어진 n 이하의 모든 ‘좋은 수’를 찾아내어 리스트로 반환하라.

예를 들어, n이 30일 경우 ‘좋은 수’는 11, 13, 15, 17, 19, 21, 23, 25, 27, 29이다.
이 문제를 해결하기 위한 알고리즘을 설계하고 코드를 구현해보겠습니다.

문제 해결 접근법

문제를 단계별로 접근해보겠습니다.
첫째로, n이 두 자리 이상의 자연수인 경우들만 고려해야 하므로, 10부터 n까지 반복문을 사용하여 각각의 숫자에 대해 자리의 합을 계산하겠습니다.

숫자의 각 자리의 합을 구하기 위해, 주어진 숫자를 문자열로 변환한 후 각 자리의 숫자를 정수로 변환하여 합계에 더하는 방법을 사용할 수 있습니다.
다음으로, 그 합이 홀수인지 판별한 후, 조건을 만족하는 숫자를 리스트에 추가하겠습니다.

코드 구현

이제 위에서 설명한 알고리즘을 파이썬으로 구현해보겠습니다.


def is_good_number(num):
    # 자리의 합을 계산하여 홀수인지 판단
    digit_sum = sum(int(digit) for digit in str(num))
    return digit_sum % 2 == 1

def find_good_numbers(n):
    good_numbers = []
    for i in range(10, n + 1):  # 두 자리 이상의 숫자
        if is_good_number(i):
            good_numbers.append(i)
    return good_numbers

# 테스트
n = 30
print(find_good_numbers(n))
            

위 코드는 is_good_number라는 함수를 사용하여 주어진 수의 각 자리의 합을 계산하고,
해당 합이 홀수인지를 검사합니다.

find_good_numbers 함수는 10부터 n까지의 모든 수를 반복하면서,
‘좋은 수’를 찾고 리스트에 추가하여 반환합니다.

실행 결과

주어진 n = 30에 대해 함수를 실행한 결과는 다음과 같습니다:


[11, 13, 15, 17, 19, 21, 23, 25, 27, 29]
            

위와 같이 11, 13, 15, 17, 19, 21, 23, 25, 27, 29의 ‘좋은 수’가 반환되었습니다.
이 결과는 “각 자리의 합이 홀수인 두 자리 이상의 자연수”라는 조건을 만족합니다.

복잡도 분석

위 알고리즘의 시간 복잡도를 살펴보면,
주어진 n까지의 숫자를 모두 반복해야 하므로 시간 복잡도는 O(n)입니다.
각 숫자에 대해 최대 2자리 수에 대해 자리의 합을 계산해야 하므로 추가적인 상수 시간이 필요하므로
종합적으로 O(n)으로 확인할 수 있습니다.

공간 복잡도는 반환할 ‘좋은 수’를 저장하기 위한 리스트를 사용하므로 O(k)
(k는 ‘좋은 수’의 개수)로 볼 수 있습니다.
실제로는 n이 작을수록 저장될 ‘좋은 수’가 적어지므로 효율적인 공간 사용이 가능합니다.

결론

이번 문제를 통해 반례를 고려하고 자리의 합을 계산하는 과정을 통해
‘좋은 수’를 효과적으로 찾아내는 방법을 학습하였습니다.
문제 해결을 위한 단계별 접근과 파이썬 코드를 통한 구현,
그리고 복잡도 분석을 통해 효율성을 높이기 위한 다양한 방법을 모색할 수 있었습니다.

앞으로 더 많은 알고리즘 문제를 통해 다양한 사고 방식과 해결 방법을 연습해 보세요.
코딩 테스트에서는 문제를 이해하고 접근하는 과정이 매우 중요합니다.

파이썬 코딩테스트 강좌, K번째 최단 경로 찾기

알고리즘 문제 해결에 대한 이해와 연습은 소프트웨어 개발자, 특히 취업을 목표로 하는 학생들에게 필수적입니다. 다양한 유형의 문제를 풀어보면서 문제 해결 능력을 키우고 알고리즘과 자료 구조를 실질적으로 사용하는 법을 배워야 합니다. 이번 강좌에서는 “K번째 최단 경로 찾기”라는 주제를 다루고, 해당 문제를 파이썬으로 해결하는 과정을 상세히 설명하겠습니다.

문제 설명

그래프가 주어졌을 때, 특정 두 노드 사이의 K번째 최단 경로를 찾는 문제입니다. K번째 최단 경로란, 두 노드 사이의 최단 경로 중에서 K번째로 짧은 경로를 의미합니다. 이 문제는 Dijkstra 알고리즘과 같은 최단 경로 탐색 알고리즘의 변형을 통해 접근할 수 있습니다.

문제 요약

  • 그래프는 비가중치 및 가중치를 가진 방향성 그래프입니다.
  • Graph가 주어짐 (노드 수 N, 간선 수 M).
  • 시작 노드 S와 끝 노드 E가 주어진다.
  • K번째 최단 경로를 구해야 한다.

입력 형식

N M K
a1 b1 c1
a2 b2 c2
...

위의 입력 형식에서, N은 노드의 수, M은 간선의 수, K는 찾고자 하는 K번째 최단 경로를 의미합니다. 각 간선은 세 개의 숫자로 주어지며, a는 시작 노드, b는 종료 노드, c는 가중치를 뜻합니다.

출력 형식

K번째 최단 경로의 길이 또는 K번째 최단 경로가 존재하지 않으면 -1

예제

입력 예제

4 5 2
1 2 4
1 3 2
2 3 5
2 4 1
3 4 8

출력 예제

6

풀이 과정

이 문제를 해결하기 위해서 K번째 최단 경로를 찾는 알고리즘을 구현해야 합니다. 일반적인 최단 경로 알고리즘인 Dijkstra 알고리즘을 변형하여 사용할 예정이며, 각 경로의 비용을 관리하는 방법을 고안해야 합니다. 여기서는 우선순위 큐(Priority Queue)를 활용하여 경로를 탐색하는 방식으로 해결하겠습니다.

단계별 접근

1단계: 그래프 표현하기

그래프를 인접 리스트 형태로 표현합니다. 각 노드와 간선 정보를 담기 위해 딕셔너리나 리스트를 사용합니다.

from collections import defaultdict
import heapq

def build_graph(edges):
    graph = defaultdict(list)
    for (u, v, w) in edges:
        graph[u].append((v, w))
    return graph

2단계: 우선순위 큐를 사용한 경로 탐색

우선순위 큐를 사용하여 최단 경로를 탐색합니다. 각 경로의 비용과 노드를 튜플 형태로 저장하여 관리할 수 있습니다. 이때, K개의 경로를 저장하기 위한 리스트를 준비해야 합니다.

def kth_shortest_path(graph, start, end, k):
    # 초기화
    pq = []
    heapq.heappush(pq, (0, start))  # (비용, 노드)
    paths = defaultdict(list)

    while pq:
        cost, node = heapq.heappop(pq)
        
        # K번째 경로를 찾았을 경우
        if node == end:
            paths[end].append(cost)
            if len(paths[end]) >= k:
                break
        
        for neighbor, weight in graph[node]:
            new_cost = cost + weight
            heapq.heappush(pq, (new_cost, neighbor))
    
    return paths[end][k - 1] if len(paths[end]) >= k else -1

3단계: 전체 알고리즘 통합하기

위의 두 기능을 결합하여 전체 알고리즘을 완성합니다. 입력을 받고 그래프를 만들고 K번째 최단 경로를 찾는 로직을 구현하겠습니다.

def main():
    N, M, K = map(int, input().split())
    edges = [tuple(map(int, input().split())) for _ in range(M)]
    graph = build_graph(edges)
    result = kth_shortest_path(graph, 1, N, K)
    print(result)

if __name__ == "__main__":
    main()

결론 및 마무리

이번 강좌에서는 K번째 최단 경로 찾기 문제를 다루어 보았습니다. 그래프의 구조와 Dijkstra 알고리즘의 변형 방법을 이해하고, 우선순위 큐를 통해 경로를 탐색하는 방법을 익혔습니다. 알고리즘 문제는 다양하게 변형되어 출제될 수 있으므로, 여러 문제를 많이 풀어보는 것이 중요합니다.

이제 여러분은 주어진 문제를 해결할 수 있는 기반을 다졌습니다. K번째 최단 경로 문제뿐만 아니라 다양한 그래프 관련 문제에서도 활용할 수 있는 방법들이므로, 응용력을 키우는 것이 중요합니다. 앞으로도 더 많은 알고리즘과 문제 해결 방법을 연습하시길 바랍니다.

Happy Coding!

파이썬 코딩테스트 강좌, K번째 수 구하기

이번 강좌에서는 ‘K번째 수 구하기’라는 알고리즘 문제를 통해 파이썬을 이용한 코딩 테스트 준비 방법에 대해 알아보겠습니다. 이 문제는 기본적인 정렬과 리스트 조작을 요구하므로, 관련된 기본 문법과 알고리즘 기법을 연습하는 데 유용합니다.

문제 설명

문제는 다음과 같습니다:

n: 정수의 개수
k: 찾고자 하는 K번째 수
arr: n개의 정수가 저장된 리스트

1. 리스트에서 k번째 수를 찾으시오.
2. 단, 숫자들은 양의 정수이며, 1 ≤ n ≤ 1000, 1 ≤ k ≤ n이라는 조건이 있습니다.
3. k번째 수는 오름차순으로 정렬된 상태에서 k의 위치에 해당하는 수를 의미합니다.

예제

다음과 같은 입력 값이 주어진다고 가정합니다:

n = 5
k = 2
arr = [5, 2, 3, 1, 4]

위의 입력에 대해서는 다음과 같은 출력이 나와야 합니다:

2번째 수는 2입니다.

문제 해결 전략

이 문제를 해결하기 위해 다음과 같은 단계로 접근할 수 있습니다:

  1. 입력 받기: 사용자로부터 n, k, arr의 값을 입력받습니다.
  2. 정렬하기: 리스트 arr을 오름차순으로 정렬합니다.
  3. K번째 수 찾기: 리스트의 인덱스는 0부터 시작하므로, k-1 위치의 값을 추출하여 출력합니다.

코드 구현

이제 위의 해결 전략을 바탕으로 실제 코드를 구현해 보겠습니다.

def find_kth_number(n, k, arr):
    # 1. 리스트 정렬
    arr.sort()
    
    # 2. K번째 수 찾기
    return arr[k - 1]

# 입력 받기
n = int(input("정수의 개수를 입력하세요: "))
k = int(input("K번째 수를 입력하세요: "))
arr = list(map(int, input("정수 리스트를 입력하세요 (공백으로 구분): ").split()))

# K번째 수 찾기
kth_number = find_kth_number(n, k, arr)
print(f"{k}번째 수는 {kth_number}입니다.")

코드 설명

위의 코드는 세 부분으로 나누어 설명할 수 있습니다:

  1. 함수 정의: find_kth_number 함수를 정의하여 n, k, arr을 파라미터로 받습니다. 이 함수는 k번째 수를 반환합니다.
  2. 정렬: arr.sort()를 사용하여 리스트를 오름차순으로 정렬합니다.
  3. 결과 반환: return arr[k - 1]를 통해 k번째 수를 반환합니다. k는 사용자 입력으로 받기 때문에 k-1을 사용하여 0부터 시작하는 인덱스에 맞춥니다.

설계 고려사항

문제를 해결할 때 컵을 고려해야 할 몇 가지 사항이 있습니다. 코드를 작성하기 전에 이러한 점들을 고려하는 것이 좋습니다:

  • 입력 값의 유효성 검사: 주어진 범위 내에서 n과 k의 값이 주어졌는지 확인할 필요가 있습니다.
  • 리스트의 중복 처리: 중복된 값이 있을 경우, 여러 개의 k번째 수 중 어떤 수를 선택해야 할지를 명확히 하는 것이 좋습니다.
  • 시간 복잡도: arr을 정렬하는 작업은 O(n log n) 시간이 소요되므로, 효율적인 알고리즘을 선택해야 합니다.

추가 연습 문제

이 문제를 통해 K번째 수를 찾는 기초적인 알고리즘을 익혔다면, 유사한 문제를 풀어보면서 더 나아가 올바른 설계 능력을 기를 수 있습니다. 다음은 추가 연습 문제입니다:

  1. 주어진 리스트에서 1~n까지의 숫자 중 k번째로 작은 수를 찾으시오.
  2. 정렬된 두 개의 리스트가 주어질 때, 두 리스트의 합쳐진 후 K번째 수를 찾으시오.
  3. 2D 배열에서 k번째 작은 숫자를 검색하는 문제를 해결해 보십시오.

결론

이번 강좌에서는 K번째 수 구하기 문제를 통해 파이썬을 활용한 알고리즘 문제 해결 과정을 살펴보았습니다. 알고리즘 문제를 해결할 때는 문제를 명확히 이해하고, 해결 전략을 세운 후, 이를 코드로 구현하는 과정을 거쳐야 합니다. 연습을 통해 다양한 유형의 문제를 접하고 이를 해결하면서 실력을 향상시켜 나가기를 바랍니다.

감사합니다.

파이썬 코딩테스트 강좌, DNA 비밀번호

코딩테스트 준비를 위해 많은 사람들이 알고리즘 문제를 풀고 있습니다. 이번 강좌에서는 DNA 비밀번호 문제에 대해 알아보고, 문제를 어떻게 해결할 수 있는지 단계별로 설명하겠습니다.

문제 설명

DNA의 비밀번호는 특정한 패턴의 조합으로 이루어져 있습니다. 주어진 DNA 문자열에서 비밀번호가 되는 서브 문자열이 몇 개 있는지를 찾는 문제가 주어집니다. 다음은 문제의 세부 내용입니다.

문제 정의

주어진 DNA 문자열에서, “””ACGT””” 문자만 포함하고, 길이가 K 이상인 모든 서브 문자열 중에서 서로 다른 서브 문자열의 개수를 세어라.

입력

  • 첫 번째 줄에는 DNA 문자열이 주어진다. (1 ≤ 문자열 길이 ≤ 1000)
  • 두 번째 줄에는 K의 값이 주어진다. (1 ≤ K ≤ 100)

출력

서로 다른 서브 문자열의 총 개수를 출력하라.

문제 해결 과정

이 문제를 효율적으로 해결하기 위해 다음과 같은 단계로 문제를 풀겠습니니다:

1단계: 서브 문자열 생성

주어진 DNA 문자열에서 K 이상의 길이를 가진 모든 서브 문자열을 생성합니다. 이를 위해 문자열의 시작 인덱스를 순회하면서, 각 시작 인덱스에서 K 이상의 길이를 가지는 서브 문자열을 추출합니다.

2단계: 고유한 서브 문자열 저장

생성된 서브 문자열을 집합(Set)에 저장합니다. 집합은 중복을 허용하지 않기 때문에, 서로 다른 서브 문자열만 저장됩니다. 이 과정에서 set 자료형을 활용할 수 있습니다.

3단계: 결과 출력

최종적으로 집합에 저장된 서브 문자열의 개수를 센 후 출력하면 됩니다. 이제 이러한 과정을 코드로 변환해 보겠습니다.

파이썬 코드


def count_unique_dna_substrings(dna: str, k: int) -> int:
    unique_substrings = set()  # 서브 문자열을 저장할 집합
    n = len(dna)

    # 문자열의 모든 시작 위치에 대해 서브 문자열을 추출
    for start in range(n):
        for end in range(start + k, n + 1):  # K 이상인 길이만 고려
            substring = dna[start:end]
            unique_substrings.add(substring)

    return len(unique_substrings)  # 고유한 서브 문자열의 개수 반환

# 입력 받기
if __name__ == "__main__":
    dna_string = input("DNA 문자열을 입력하세요: ")
    k_value = int(input("길이 K를 입력하세요: "))
    
    # 고유 서브 문자열의 수 계산 및 출력
    result = count_unique_dna_substrings(dna_string, k_value)
    print(f"서로 다른 서브 문자열의 개수는: {result}")
    

코드 설명

위 코드에서 사용한 함수 count_unique_dna_substrings는 두 개의 매개변수를 받습니다: dna 문자열과 k 값입니다. 이 함수는 먼저 빈 집합을 만들어 서브 문자열을 저장할 준비를 합니다. 이후 아랫 단계에서 설명할 반복문을 통해 서브 문자열을 추출합니다.

반복문 해설

첫 번째 반복문은 DNA 문자열의 시작 인덱스(start)를 순회합니다. 두 번째 반복문은 시작 인덱스부터 k 이상의 길이를 가진 서브 문자열을 추출하며, 이를 통해 end 인덱스를 결정합니다. 각 서브 문자열은 집합에 추가되어 서로 다른 경우의 수만 저장됩니다.

이해를 돕는 예시

예를 들어, 입력으로 주어진 DNA 문자열이 ACGTACGT이고 K의 값이 2라면, 생성될 수 있는 서브 문자열은 다음과 같습니다:

  • AC, ACG, ACGT, ACGTA, ACGTAC, ACGTACGT
  • CG, CGT, CGTA, CGTAC, CGTACG
  • GT, GTA, GTAC, GTACG
  • TA, TAC, TACG
  • AC, ACG, ACGT

이 처럼 총 14개의 서로 다른 서브 문자열이 존재하게 됩니다.

성능 고려사항

위 코드는 모든 시작 인덱스에 대해 모든 길이의 서브 문자열을 체크하기 때문에, 최악의 경우 시간 복잡도는 O(n^3)이 됩니다. 문자열 길이가 1000에 이르는 경우 성능 문제가 발생할 수 있습니다. 극한의 경우 성능을 개선하기 위하여 Sliding Window 기법을 고려할 수 있습니다.

Sliding Window 기법 적용

Sliding Window 기법을 사용하면 한 번의 순회를 통해 길이 >= K를 가진 서브 문자열의 개수를 쉽게 계산할 수 있습니다. 즉, 한 번의 순회로 문자열의 길이에 대한 정보를 저장하여 탐색하는 방식입니다. 이 기법은 시간 복잡도를 O(n^2) 또는 O(n)으로 줄일 수 있습니다.

마무리

이번 강좌에서는 DNA 비밀번호 문제를 통해 문자열의 서브 문자열을 처리하고, 집합을 활용하여 중복 문제를 해결하는 방법에 대해 알아보았습니다. 이와 같은 문제는 코딩테스트에 자주 등장하므로, 기본적인 문자열 처리 방법을 잘 익혀두는 것이 좋습니다. 다음 강좌에서는 또 다른 유용한 알고리즘 문제를 다루어 보겠습니다.

다음 강좌를 기대해 주세요! 관심과 사랑을 주셔서 감사합니다.

파이썬 코딩테스트 강좌, DFS와 BFS 프로그램

1. 서론

최근 많은 기업들이 알고리즘을 활용한 코딩 테스트를 진행하고 있습니다. 이러한 코딩 테스트에서는
DFS(깊이 우선 탐색)와 BFS(너비 우선 탐색)와 같은 그래프 탐색 알고리즘이 빈번하게 출제됩니다.
본 글에서는 DFS와 BFS의 기초 개념을 정리하고, 이를 바탕으로 한 알고리즘 문제 하나를 제시하여
문제 풀이 과정을 자세히 설명하겠습니다.

2. DFS와 BFS 개요

2.1 DFS (Depth-First Search)

DFS는 그래프의 한 정점에서 시작하여 가능한 멀리 있는 정점까지 탐색한 후, 더 이상 탐색할 정점이 없으면
마지막으로 방문한 정점으로 돌아와서 다시 탐색을 진행하는 방식입니다. 이때 스택 자료구조를 사용하거나,
재귀 함수를 이용하여 구현할 수 있습니다. DFS는 다음과 같은 과정을 거칩니다:

  • 시작 정점을 스택에 추가하고 방문처리합니다.
  • 스택의 꼭대기에서 정점을 하나 꺼내고, 해당 정점의 인접 정점 중 아직 방문하지 않은 정점을
    스택에 추가합니다.
  • 모든 정점을 탐색할 때까지 위의 과정을 반복합니다.

2.2 BFS (Breadth-First Search)

BFS는 그래프의 한 정점에서 시작하여 인접한 모든 정점을 먼저 탐색한 후, 그 인접 정점에서 다시 인접한
정점을 탐색하는 방식입니다. 이때 큐 자료구조를 사용하여 구현됩니다. BFS의 과정은 다음과 같습니다:

  • 시작 정점을 큐에 추가하고 방문 처리를 합니다.
  • 큐에서 정점을 하나 꺼내고, 해당 정점의 인접 정점 중에서 아직 방문하지 않은 정점을 큐에
    추가합니다.
  • 모든 정점을 탐색할 때까지 위의 과정을 반복합니다.

3. 알고리즘 문제 소개

문제: 경로 탐색하기

다음은 DFS와 BFS를 이용하여 경로를 탐색하는 문제입니다.
문제 설명: 주어진 그래프와 두 정점 A, B가 있을 때,
A에서 B로 가는 경로가 존재하는지 확인하시오.
그래프는 인접 리스트 형태로 주어집니다.

        input:
        graph = {
            'A': ['B', 'C'],
            'B': ['D'],
            'C': ['D', 'E'],
            'D': ['F'],
            'E': [],
            'F': []
        }
        start = 'A'
        end = 'F'
        output: True (A에서 F로 가는 경로가 존재)
    

4. 문제 풀이 과정

4.1 DFS로 경로 탐색하기

DFS를 사용하여 경로를 탐색하는 방법은 다음과 같습니다.
재귀 함수를 이용하여 구현할 수 있으며, 방문한 정점을 저장하기 위해 set 자료구조를 활용할 수 있습니다.

        def dfs(graph, start, end, visited):
            if start == end:
                return True
            
            visited.add(start)
            
            for neighbor in graph[start]:
                if neighbor not in visited:
                    if dfs(graph, neighbor, end, visited):
                        return True
            
            return False
        
        graph = {
            'A': ['B', 'C'],
            'B': ['D'],
            'C': ['D', 'E'],
            'D': ['F'],
            'E': [],
            'F': []
        }
        start = 'A'
        end = 'F'
        visited = set()
        result = dfs(graph, start, end, visited)
    

위의 코드를 통해 DFS를 사용하여 경로 탐색을 할 수 있습니다.
처음에 시작 정점 A가 주어지면, 해당 정점의 방문 상태를 체크한 후 인접 정점으로
재귀 호출을 통해 탐색을 진행합니다.
정점 F에 도달했을 때 True를 반환하면 경로가 존재하는 것입니다.

4.2 BFS로 경로 탐색하기

BFS를 사용하여 경로를 탐색하는 방법은 큐를 사용하여 정점 방문 기록을 유지하는 것입니다.

        from collections import deque
        
        def bfs(graph, start, end):
            queue = deque([start])
            visited = set([start])
            
            while queue:
                vertex = queue.popleft()
                
                if vertex == end:
                    return True
                
                for neighbor in graph[vertex]:
                    if neighbor not in visited:
                        visited.add(neighbor)
                        queue.append(neighbor)
                        
            return False
        
        graph = {
            'A': ['B', 'C'],
            'B': ['D'],
            'C': ['D', 'E'],
            'D': ['F'],
            'E': [],
            'F': []
        }
        start = 'A'
        end = 'F'
        result = bfs(graph, start, end)
    

BFS를 구현한 위의 코드와 같이 큐를 이용해 각 정점의 인접 정점을 하나씩
탐색해 나가면서 확인하면 A에서 F로 가는 경로가 있는지를 알 수 있습니다.
큐는 탐색하는 과정에서 사용되며, 방문한 정점도 기록합니다.

5. 결론

DFS와 BFS는 서로 다른 탐색 방법으로, 각각의 특성에 따라 다양한 문제에 적합합니다.
구조의 깊이를 우선적으로 탐색하고 싶은 경우에는 DFS가, 최단 거리 탐색을 원할 경우에는 BFS가
더 유리합니다. 본 글에서는 간단한 경로 탐색 문제를 통해 두 알고리즘의 구현과
그 과정에서의 차이를 보여드렸습니다. 이러한 기초를 바탕으로 더 복잡한 알고리즘 문제에
도전해 볼 수 있을 것입니다.

6. 참고 자료

  • Data Structures and Algorithms in Python by Michael T. Goodrich
  • Introduction to Algorithms by Thomas H. Cormen
  • Python Documentation: https://docs.python.org/3/