파이썬 코딩테스트 강좌, 너비 우선 탐색

너비 우선 탐색(Breath-First Search, BFS)은 그래프나 트리 탐색의 한 방법으로, 시작 정점에서 인접한 정점들을 먼저 방문하고, 그 다음 인접한 정점들 중 아직 방문하지 않은 정점들을 방문하는 방식입니다. 경우에 따라 최단 경로 탐색 알고리즘으로도 사용됩니다. 오늘은 BFS의 기초 개념과 함께 깊이 있는 문제 풀이를 진행해보겠습니다.

문제: 단지번호붙이기

문제 설명

주어진 n x n 크기의 0과 1로 구성된 배열에서 1은 집이 있는 곳을 의미하고 0은 집이 없는 곳을 의미합니다. 모든 집의 단지를 구별하기 위하여 각 단지에 고유한 번호를 붙이는 프로그램을 작성하십시오. 단지의 크기는 각 단지에 속한 집의 수로 정의됩니다.

입력

n (1 ≤ n ≤ 25)
0 또는 1로 구성된 n x n 배열

출력

각 단지의 크기를 오름차순으로 출력

예제 입력

7
0 0 1 0 0 1 1
0 0 1 0 1 1 1
0 1 1 0 0 0 0
0 0 0 0 1 0 0
1 1 1 0 1 1 0
0 0 0 0 0 0 0
0 0 1 0 0 0 0

예제 출력

1
7

문제 접근법

이 문제를 해결하기 위해 BFS 알고리즘을 이용하여 그래프를 탐색할 것입니다. 입력된 2차원 배열을 순회하며 각 집(1)을 만나면 BFS를 통해 연결된 모든 집의 수를 세어 단지의 크기를 계산합니다. 다음 단계에서 BFS의 개념을 자세히 설명하겠습니다.

너비 우선 탐색(BFS) 개념

BFS는 큐를 이용하여 구현됩니다. 다음 프로세스를 따릅니다:

  1. 시작 노드를 큐에 추가하고 방문 표시를 합니다.
  2. 큐에서 노드를 꺼내고 그 노드에 인접한 모든 노드(아직 방문하지 않은)들을 큐에 추가하고 방문 표시를 합니다.
  3. 큐가 비어있지 않으면 2번으로 돌아갑니다.

코딩 구현

BFS 알고리즘을 적용하여 문제를 해결하는 코드를 작성하겠습니다.


from collections import deque

def bfs(x, y, n, graph, visited):
    queue = deque()
    queue.append((x, y))
    visited[x][y] = True

    directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
    size = 1  # 단지의 크기를 저장

    while queue:
        cx, cy = queue.popleft()  # 현재 위치
        for dx, dy in directions:
            nx, ny = cx + dx, cy + dy
            if 0 <= nx < n and 0 <= ny < n and not visited[nx][ny] and graph[nx][ny] == 1:
                visited[nx][ny] = True
                queue.append((nx, ny))
                size += 1  # 연결된 집의 수를 증가

    return size  # 단지의 크기 반환

def find_complexes(n, graph):
    visited = [[False] * n for _ in range(n)]
    complexes = []

    for i in range(n):
        for j in range(n):
            if graph[i][j] == 1 and not visited[i][j]:
                size = bfs(i, j, n, graph, visited)
                complexes.append(size)

    return sorted(complexes)  # 크기를 오름차순으로 반환

# 예제 입력
n = 7
graph = [
    [0, 0, 1, 0, 0, 1, 1],
    [0, 0, 1, 0, 1, 1, 1],
    [0, 1, 1, 0, 0, 0, 0],
    [0, 0, 0, 0, 1, 0, 0],
    [1, 1, 1, 0, 1, 1, 0],
    [0, 0, 0, 0, 0, 0, 0],
    [0, 0, 1, 0, 0, 0, 0]
]

complex_sizes = find_complexes(n, graph)
print(len(complex_sizes))  # 단지의 수 출력
for size in complex_sizes:
    print(size)  # 각 단지의 크기 출력

코드 설명

위 코드는 BFS를 이용하여 단지의 크기를 계산하는 방법을 보여줍니다. 주요 함수는 find_complexesbfs입니다.

  • bfs(x, y, n, graph, visited): 시작 점 (x, y)에서 BFS를 실행하여 해당 단지의 크기를 계산합니다. 큐를 이용하여 방문한 각 집을 기록하며 연결된 집을 모두 집계합니다.
  • find_complexes(n, graph): 주어진 그래프를 순회하며 집(1)을 찾아 BFS를 호출하고, 계산된 단지 크기를 리스트에 저장합니다.

결론

너비 우선 탐색(BFS)은 유용한 탐색 방법으로, 다양한 분야에서 활용될 수 있습니다. 이번 강좌를 통해 BFS의 활용으로 문제를 해결하는 방법을 배웠기를 바랍니다. 문제 풀이과정에서의 코드 이해와 활용이 여러분의 코딩테스트에서도 큰 도움이 되기를 바랍니다.

추가 연습 문제

아래의 문제를 풀어보세요:

  • 주어진 그래프에서 특정 노드 간의 최단 경로를 구하는 문제
  • 미로 탐색 문제를 BFS를 이용하여 해결하기

참고 자료

파이썬 코딩테스트 강좌, 내림차순으로 자릿수 정렬하기

1. 문제 정의

이번 문제의 목표는 주어진 숫자를 내림차순으로 정렬하여, 최대한 큰 수를 만드는 것입니다. 입력으로 주어진 숫자는 정수이며, 우리가 해야 할 일은 이 숫자의 자릿수를 이용하여 제일 큰 수를 반환하는 것입니다.

예제 입력/출력

  • Input: 2183
  • Output: 8321

2. 문제 접근 방법

이 문제는 각 자릿수의 개별 숫자를 추출한 후, 이를 내림차순으로 정렬하는 과정을 포함합니다. Python에서는 리스트의 sort() 메서드를 활용하여 손쉽게 이러한 정렬 작업을 수행할 수 있습니다. 구체적인 절차는 다음과 같습니다:

  1. 정수를 문자열로 변환한다.
  2. 문자열을 각 숫자(문자)로 분리하여 리스트를 만든다.
  3. 리스트를 내림차순으로 정렬한다.
  4. 정렬된 리스트를 다시 문자열로 합쳐 정수로 변환하여 반환한다.

3. 알고리즘 구현

이제 위의 접근 방법을 바탕으로 Python 코드로 문제를 해결해 보겠습니다.

def sort_digits_descending(n):
    # 1단계: 정수를 문자열로 변환
    str_n = str(n)
    
    # 2단계: 문자열을 리스트로 변환
    digits = list(str_n)
    
    # 3단계: 리스트를 내림차순으로 정렬
    digits.sort(reverse=True)
    
    # 4단계: 정렬된 리스트를 문자열로 합치고 정수로 변환
    sorted_n = int(''.join(digits))
    
    return sorted_n

4. 코드 설명

위 함수 sort_digits_descending는 매개변수 n을 받아서 전체 과정을 수행합니다. 각 단계는 다음과 같이 작동합니다:

  • 문자열 변환: str(n)을 통해 정수를 문자열로 변환합니다.
  • 리스트 변환: list(str_n)를 통해 문자열의 각 문자(숫자)를 리스트로 변환합니다.
  • 정렬: sort(reverse=True)를 사용하여 리스트를 내림차순으로 정렬합니다.
  • 합치기: "".join(digits)를 사용하여 리스트를 다시 문자열로 합쳐서, int()로 정수로 변환하여 반환합니다.

5. 테스트 케이스

우리의 기능이 제대로 작동하는지 확인하기 위해 여러 가지 테스트 케이스를 사용해 봅시다.

print(sort_digits_descending(2183)) # 8321
print(sort_digits_descending(1450)) # 5410
print(sort_digits_descending(9876543210)) # 9876543210
print(sort_digits_descending(0)) # 0
print(sort_digits_descending(1001)) # 1100

6. 결과 분석

각 테스트 케이스의 결과를 확인한 결과, 모든 경우에서 기대한 출력이 반환되었습니다. 함수는 매우 간단하게 구현되었으며, Python의 기본 기능을 사용하여 효율적으로 해결되었습니다.

7. 최적화 및 고려 사항

위에서 작성한 코드는 각 자릿수를 정렬하는 데 O(m log m) 시간 복잡도가 소요됩니다. 여기서 m은 입력 정수의 자릿수 수입니다. 정수의 최대 자릿수가 10으로 한정되기 때문에 성능상의 문제는 없지만, 효율성을 고려하여 더 큰 수에서도 유효하게 만들거나 복잡도를 줄일 필요가 있을 수 있습니다.

8. 정리

우리는 주어진 정수를 내림차순으로 정렬하여 최대한 큰 값을 만드는 알고리즘을 구현했습니다. 이 과정에서 Python의 리스트 메서드와 문자열 조작 기능을 사용하여 간단하게 해결할 수 있었습니다. 향후 추가적인 최적화와 다른 접근 방식을 고려하기 바랍니다. 이를 통해 코딩 테스트에서 더 뛰어난 문제 해결 능력을 기를 수 있습니다.

파이썬 코딩테스트 강좌, 깊이 우선 탐색

1. 깊이 우선 탐색(DFS) 소개

깊이 우선 탐색(Depth-First Search, DFS)은 그래프의 탐색 알고리즘 중 하나로,
주어진 노드에서 가능한 깊이까지 탐색한 후, 더 이상 탐색할 노드가 없으면 마지막으로
방문했던 노드로 되돌아가서 다른 경로를 탐색하는 방식입니다. 이 방법은 그래프의 형태에 따라
다양한 용도로 사용되며, 트리의 탐색과 연결 요소의 탐색, 경로 탐색 문제 등에 활용됩니다.

DFS의 작동 방식은 다음과 같습니다:

  • 시작 노드를 스택에 추가한다.
  • 스택에서 노드를 하나 꺼내 탐색한다.
  • 탐색한 노드를 방문한 것으로 표시한다.
  • 아직 방문하지 않은 인접 노드를 모두 스택에 추가한다.
  • 스택이 비어있지 않은 동안 위 과정을 반복한다.

2. 문제 설명

문제: 섬의 개수 세기

주어진 2차원 배열 grid에서 ‘1’은 육지를, ‘0’은 바다를 나타냅니다.
한 섬은 상하좌우로 연결된 육지의 집합입니다.
섬의 개수를 세는 함수를 작성하세요.

            입력: 
            grid = [
                ["1","1","0","0","0"],
                ["1","1","0","0","0"],
                ["0","0","1","0","0"],
                ["0","0","0","1","1"]
            ]
            출력: 3
            

3. 문제 해결 과정

3.1. 문제 이해하기

문제를 해결하기 위해서는 먼저 육지가 연결된 부분(즉, 섬)을 찾아야 합니다.
각각의 섬은 DFS를 통해 찾을 수 있으며, 육지를 탐색하면서 인접한 육지도 함께 탐색하여
섬의 모든 부분을 확인할 수 있습니다.

3.2. 데이터 구조 설계하기

DFS를 구현하기 위해 스택을 사용하겠습니다.
자원을 효율적으로 관리하기 위해 2차원 배열의 모든 요소를 방문 여부를 체크할 수 있는
boolean 배열을 사용할 것입니다.

3.3. DFS 구현하기

            def dfs(grid, visited, i, j):
                # 범위를 벗어나거나 이미 방문한 경우 종료
                if i < 0 or i >= len(grid) or j < 0 or j >= len(grid[0]) or visited[i][j] or grid[i][j] == '0':
                    return
                # 현재 위치 방문 처리
                visited[i][j] = True
                
                # 상하좌우 탐색
                dfs(grid, visited, i - 1, j)  # 위
                dfs(grid, visited, i + 1, j)  # 아래
                dfs(grid, visited, i, j - 1)  # 왼쪽
                dfs(grid, visited, i, j + 1)  # 오른쪽
            

3.4. 최종 함수 구현하기

            def numIslands(grid):
                if not grid:
                    return 0
                
                visited = [[False for _ in range(len(grid[0]))] for _ in range(len(grid))]
                island_count = 0
                
                # 그리드의 모든 노드 탐색
                for i in range(len(grid)):
                    for j in range(len(grid[0])):
                        if grid[i][j] == '1' and not visited[i][j]:
                            dfs(grid, visited, i, j)
                            island_count += 1  # 새로운 섬 찾음
                            
                return island_count
            

3.5. 코드 테스트

            grid = [
                ["1","1","0","0","0"],
                ["1","1","0","0","0"],
                ["0","0","1","0","0"],
                ["0","0","0","1","1"]
            ]

            print(numIslands(grid))  # 3이 출력되어야 합니다.
            

4. 문제 해결 리뷰

DFS를 활용하여 문제를 해결하였고, 시간 복잡도는 O(N * M)이며,
N은 행의 수, M은 열의 수를 나타냅니다.
모든 노드를 최대 한 번씩 방문하기 때문에 효율적인 방법이라고 할 수 있습니다.
또한, 공간 복잡도 또한 O(N * M)이 들어간다고 할 수 있습니다.
이는 visiting 리스트를 위해 추가적인 공간을 사용했기 때문입니다.

DFS는 메모리 사용량이 높을 수 있지만, 문제의 성격이나 크기에 따라 BFS보다 효과적인 경우가 많습니다.
특히, 경로 문제나 연결 구성 요소를 구별하는 문제에서 DFS의 장점을 누릴 수 있습니다.

5. 결론

이번 강좌를 통해 DFS의 기본 개념과 문제 해결 방식에 대해 알아보았습니다.
다양한 문제에서 DFS를 활용할 수 있으니, 본 강좌를 바탕으로 여러 문제를 풀어보시기 바랍니다.
감사합니다.

파이썬 코딩테스트 강좌, 나머지 합 구하기

안녕하세요, 여러분! 이번 강좌에서는 파이썬을 이용한 코딩 테스트에서 자주 출제되는 알고리즘 문제 중 하나인 ‘나머지 합 구하기’에 대해 알아보겠습니다. 이 문제는 대량의 데이터 처리 및 모듈러 연산을 이해하고 사용하는 데 도움을 주는 좋은 예시입니다.

문제 설명

여러분은 N개의 정수로 이루어진 배열 A가 주어질 때, 배열의 부분 배열의 합을 K로 나눈 나머지를 구하는 문제를 해결해야 합니다. 부분 배열의 정의는 배열의 시작 인덱스와 끝 인덱스에 의해 정의된 연속된 배열의 집합입니다.

예를 들어 배열 A가 [3, 1, 4, 1, 5]이고 K가 2라고 가정하면, 모든 부분 배열의 합을 K로 나눈 나머지를 구해야 합니다. 이 문제는 기본적인 수학과 프로그래밍 스킬을 요구하며, 다양한 접근 방법으로 해결할 수 있습니다.

입력

  • 첫 번째 줄에 두 개의 정수 N (1 ≤ N ≤ 100,000)과 K (1 ≤ K ≤ 10,000)가 주어진다.
  • 두 번째 줄에 N개의 정수 A[1], A[2], …, A[N] (0 ≤ A[i] ≤ 109)가 주어진다.

출력

부분 배열의 합을 K로 나눈 나머지가 0인 부분 배열의 개수를 출력한다.

예제

    입력
    5 2
    3 1 4 1 5

    출력
    4
    

접근 방법

이 문제를 해결하기 위해서는 다음과 같은 접근 방법을 사용할 수 있습니다.

1. 부분 배열의 정의 이해

부분 배열은 원 배열에서 연속된 원소들의 집합이므로, 주어진 배열에서 모든 가능한 부분 배열을 생성한 다음, 각 부분 배열의 합을 계산하고 K로 나눈 나머지를 확인해야 합니다.

2. 효율적인 계산 방법

부분 배열을 직접적으로 구하는 것은 최악의 경우 O(N2)의 시간 복잡도를 가지므로 비효율적입니다. 따라서, 누적합(Cumulative Sum)과 해시 맵(Hash Map)을 활용하여 이 문제를 O(N)으로 해결할 수 있습니다.

3. 누적합과 모듈러 연산 사용

누적합을 구하여 현재까지의 합을 K로 나눈 나머지를 저장합니다. 동일한 나머지 값을 가진 경우, 해당 인덱스 사이의 부분 배열의 합이 K로 나눠질 수 있다는 사실을 이용합니다.

코드 예시

    
    def count_subarrays_with_zero_modulo(n, k, arr):
        count = 0
        mod_count = [0] * k
        current_sum = 0
        
        for num in arr:
            current_sum += num
            mod = current_sum % k
            
            if mod < 0:  # Python's mod can be negative, adjust it
                mod += k
            
            # If current modulo is zero, we can count it
            if mod == 0:
                count += 1
            
            # Add the number of times this modulo has appeared before
            count += mod_count[mod]
            mod_count[mod] += 1
            
        return count

    # Example usage
    n = 5
    k = 2
    arr = [3, 1, 4, 1, 5]
    result = count_subarrays_with_zero_modulo(n, k, arr)
    print(result)  # Output: 4
    
    

결과 분석

위 코드에서 count_subarrays_with_zero_modulo 함수는 배열에 있는 부분 배열의 합이 K로 나눠진 결과가 0인 경우의 수를 세는 함수입니다. 이 과정에서 누적합을 통해 각 인덱스의 합을 계산하고, 해시 맵을 통해 같은 나머지를 가진 경우를 카운트합니다. 이렇게 함으로써, 우리는 O(N)의 시간 복잡도로 문제를 해결할 수 있습니다.

마무리

이번 강좌를 통해 나머지 합 문제에 대한 접근 방법을 이해하고, 효율적인 코딩 기법을 배웠습니다. 이러한 기법은 대규모 데이터 처리가 필요한 다양한 상황에서 활용할 수 있으며, 여러분의 알고리즘 문제 해결 능력을 크게 향상시킬 것입니다.

추가적으로 이와 유사한 문제에 대한 풀이를 시도해보며 경험을 쌓아보시기 바랍니다. 다음 강좌에서는 또 다른 흥미로운 주제를 가지고 찾아뵙겠습니다. 감사합니다!

파이썬 코딩테스트 강좌, 기하 알아보기

안녕하세요! 오늘은 파이썬 코딩테스트에서 자주 출제되는 기하 문제를 해결하는 방법에 대해 알아보겠습니다. 기하 문제는 주로 도형의 넓이, 둘레, 교차 여부, 거리 계산 등을 포함하며, 알고리즘 문제 풀이 시 중요한 부분입니다. 본 강좌에서는 기본적인 기하학 개념을 바탕으로 자주 출제되는 문제를 설명하고, 이를 성공적으로 풀기 위한 접근 방법을 단계별로 살펴보겠습니다.

문제: 두 선분의 교차 여부 판단하기

두 선분이 주어졌을 때, 이 두 선분이 교차하는지 여부를 판단하는 문제입니다.

문제 정의

주어진 두 선분 ABCD의 교차 여부를 판단하시오. 각 선분은 다음과 같이 두 점으로 정의됩니다:

  • 선분 AB: 점 A(x1, y1), 점 B(x2, y2)
  • 선분 CD: 점 C(x3, y3), 점 D(x4, y4)

입력 형식

4개의 정수 x1, y1, x2, y2, x3, y3, x4, y4가 주어집니다.

출력 형식

선분이 교차하면 “YES”, 그렇지 않으면 “NO”를 출력합니다.

문제 접근 방법

두 선분이 교차하는지 판단하기 위해서는 기하학적인 개념인 ‘선분의 방향’을 사용해야 합니다. 두 선분의 각 점에 대하여 방향을 구하고, 이를 통해 교차 여부를 확인할 수 있습니다.

1. 방향 계산

선분 ABCD에 대해 방향을 계산하기 위해서는 다음의 공식을 사용합니다:

def direction(px, py, qx, qy, rx, ry):
    return (qx - px) * (ry - py) - (qy - py) * (rx - px)

이 함수는 점 P, Q, R에 대해 방향을 계산하여 양수, 음수, 0을 반환합니다.

2. 선분 교차 판단

선분 ABCD의 끝점이 서로 다른 방향을 가질 때 교차한다고 판단할 수 있습니다. 즉, 다음의 조건을 만족해야 합니다:

  • 방향(A, B, C)과 방향(A, B, D)의 곱은 0보다 크고, 방향(C, D, A)과 방향(C, D, B)의 곱도 0보다 큽니다.

이러한 조건을 연결하여 전체 로직을 하나의 함수로 통합할 수 있습니다.

3. 코드 구현

이제까지 설명한 내용을 바탕으로 전체 코드를 구현해 보겠습니다.

def ccw(px, py, qx, qy, rx, ry):
    return (qx - px) * (ry - py) - (qy - py) * (rx - px) > 0

def is_intersect(x1, y1, x2, y2, x3, y3, x4, y4):
    d1 = ccw(x1, y1, x2, y2, x3, y3)
    d2 = ccw(x1, y1, x2, y2, x4, y4)
    d3 = ccw(x3, y3, x4, y4, x1, y1)
    d4 = ccw(x3, y3, x4, y4, x2, y2)

    if d1 != d2 and d3 != d4:
        return "YES"

    return "NO"

# 예제 입력
x1, y1, x2, y2 = map(int, input().split())
x3, y3, x4, y4 = map(int, input().split())

print(is_intersect(x1, y1, x2, y2, x3, y3, x4, y4))

결론

이번 강좌에서는 두 선분이 교차하는지 여부를 판단하는 알고리즘을 구현해 보았습니다. 기하 문제는 기초적인 수학 이론을 기반으로 하며, 알고리즘에 대한 이해가 필요합니다. 다양한 예제를 통해 연습하시면 기하 문제에 대한 자신감을 가질 수 있을 것입니다. 앞으로 더 많은 알고리즘 문제를 접해보고, 이를 통해 코딩 실력을 더욱 향상시키시길 바랍니다.

감사합니다!