[BOJ] Python 백준 1806번 부분합 골드 4

728x90

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

 

1806번: 부분합

첫째 줄에 N (10 ≤ N < 100,000)과 S (0 < S ≤ 100,000,000)가 주어진다. 둘째 줄에는 수열이 주어진다. 수열의 각 원소는 공백으로 구분되어져 있으며, 10,000이하의 자연수이다.

www.acmicpc.net

문제 풀이


자연수로 이루어진 수열이므로 연속된 합을 구하는 다양한 알고리즘 중에 투 포인터(Two Pointer) 알고리즘을 사용하는 문제다.

 

만약 투 포인터에 대해 잘 모르겠다면 밑에 있는 더보기를 눌러서 설명을 보고 이해하기 바란다.

 

더보기

 

 

투 포인터 알고리즘이란 두 개의 지점을 이동시키면서 연속된 숫자의 합(구간 합)을 구하는 알고리즘이다.

 

비슷한 알고리즘으로 슬라이딩 윈도우(Sliding Window)가 있는데 이 둘의 차이점은 구간의 길이다.

 

슬라이딩 윈도우는 구간의 길이가 정해져 있고, 투 포인터는 상황에 따라 변한다.

 

또한 투 포인터는 음수가 존재할 경우 구간의 길이가 증가했을 때 구간 합이 증가할지 감소할지 보장할 수 없으므로 양의 정수만 존재할 때 사용 가능하다는 특징이 있다. 

 

그림을 통해 이해하면 쉬울 것이다.

 

리스트

 

다음과 같이 자연수로 이루어진 리스트가 있을 때 합이 5인 구간의 최소 길이를 찾는 예시를 보자.

 

이때 두 가지 변수를 Left, Right로 선언하고 처음부터 Right 변수를 오른쪽으로 움직이며 구간 합에 원소를 합하고 Left 변수를 오른쪽으로 움직이며 구간합에 원소를 빼는 방식이다.

 

리스트

 

먼저 Right 변수가 첫 번째 원소를 가리킬 때다.

 

Sum에 해당하는 원소를 더하고 5와 비교했을 때 더 작으므로 구간을 늘려야 한다.

 

따라서 Right 변수를 옮겨야 한다.

 

리스트

 

Right 변수를 옮기고 나서 구간합이 5가 되므로 이 구간의 길이를 갱신시키면 된다.

 

최소 길이를 구해야 하므로 Left 변수를 옮기면 된다.

 

리스트

 

Left 변수를 옮기고 나서 Sum이 5보다 작으므로 Right 변수를 옮긴다.

 

리스트

 

Right 변수를 옮긴 뒤 Sum이 5보다 크므로 Left 변수를 옮겨 구간 합을 더 작게 만든다.

 

리스트

 

Left 변수를 옮기고 Sum이 5보다 더 크지만 동일한 원소(구간의 길이가 1)를 가리키므로 Right 변수를 옮긴다.

 

 

Right 변수를 옮긴 뒤 Sum이 5보다 크므로 Left 변수를 옮긴다.

 

리스트

 

Left 변수를 옮긴 뒤 해당 원소를 빼주니 Sum이 5이다.

 

해당 구간의 길이는 1이므로 구간 길이를 갱신해주고 Right 변수를 옮긴다.

 

리스트

 

Right 변수를 옮기니 리스트를 벗어나므로 탐색을 종료한다.

 

인덱싱을 어떻게 구현하는지는 본인의 자유이며 인덱싱 과정이 헷갈릴 수 있기에 여러 가지 방법 중에 가장 자신한테 적합한 방식을 적용하는 편이 좋을 것 같다.

 

 

다음은 문제를 푼 전체 코드이다.

 

n,s=map(int,input().split())
left=-1
right=0
min_len=100001
num_array=list(map(int,input().split()))
temp_sum=num_array[0]
while right<n:
    if temp_sum>=s:
        if min_len>right-left:
            min_len=right-left
        left+=1
        temp_sum-=num_array[left]
    else:
        right+=1
        if right<n:
            temp_sum+=num_array[right]
if min_len==100001:
    print(0)
else:
    print(min_len)

 

문제에서 수열의 길이 N은 100000 미만이므로 100000 이상의 숫자 중 아무거나 구간의 최소 길이로 정하고 그걸 갱신하면 최소 길이를 구할 수 있다.

 

만약 처음에 정한 숫자가 그대로 있다면 합이 s보다 큰 구간이 없단 뜻이며 4번째 줄에선 선언하고, 17번째 줄에선 판단하도록 구현했다.

 

7번째 줄의 while문에서는 right 변수가 수열의 길이를 넘어가면 안 되므로 길이(n) 미만일 때까지만 반복을 한다.

 

또한 while문 내부에서는 구간합이 S이상이면 길이를 갱신하고 Left를 이동시켜 원소를 구간합에서 빼고 구간합이 S미만이면 Right를 이동시켜 원소를 구간합에 더하도록 구현했다.

 

결과 화면
결과 화면

 

인덱싱 과정이 다소 헷갈릴 수 있는 알고리즘이라서 소수의 원소를 집어넣고 접근하는 과정을 하나씩 출력하다 보면 실수를 줄일 수 있을 것이다.

728x90

댓글()

[BOJ] Python 백준 1991번 트리 순회 실버 1

728x90

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

 

1991번: 트리 순회

첫째 줄에는 이진 트리의 노드의 개수 N(1 ≤ N ≤ 26)이 주어진다. 둘째 줄부터 N개의 줄에 걸쳐 각 노드와 그의 왼쪽 자식 노드, 오른쪽 자식 노드가 주어진다. 노드의 이름은 A부터 차례대로 알파

www.acmicpc.net

 

문제 설명에서도 잘 나와 있듯이 이진 트리를 순서대로 순회하여 출력하는 문제다.

 

만약 이진 트리에 대한 구현이나 순회가 잘 이해되지 않는다면 이전 포스팅을 보고 이해하길 바란다.

 

https://khsung0.tistory.com/24

 

[자료구조] 트리(Tree) 이진 트리(Binary Tree) 트리 순회 (Tree Traversal) (Python)

설명 자료구조는 크게 선형 구조, 비선형 구조로 나눠진다. 선형 구조는 Data를 차례대로 나열시킨 형태를 뜻하고 비선형 구조는 선형 구조가 아닌 모든 형태를 말한다. 즉, 노드와 간선으로 이루

khsung0.tistory.com

 

문제 풀이


이진 트리와 순회에 대해 구현한 경험이 있다면 아주 쉽게 풀 수 있는 문제지만 경험이 없다면 아마 상당히 막막하게 느껴질 수 있는 문제라고 생각한다.

 

다행히 파이썬은 딕셔너리란 자료구조를 지원하므로 딕셔너리를 이용해서 아주 쉽게 구현해보겠다.

 

def preorder(trees,root):
    print(root,end="")
    if trees[root][0]!=".":
        preorder(trees,trees[root][0])
    if trees[root][1]!=".":
        preorder(trees,trees[root][1])

def inorder(trees,root):
    if trees[root][0]!=".":
        inorder(trees,trees[root][0])
    print(root,end="")
    if trees[root][1]!=".":
        inorder(trees,trees[root][1])

def postorder(trees,root):
    if trees[root][0]!=".":
        postorder(trees,trees[root][0])
    if trees[root][1]!=".":
        postorder(trees,trees[root][1])
    print(root,end="")

n=int(input())
trees={}
root="A"
for i in range(n):
    temp=input().split()
    trees[temp[0]]=[temp[1],temp[2]]

preorder(trees,root)
print()
inorder(trees,root)
print()
postorder(trees,root)

 

뭔가 코드는 많아 보이지만 하나씩 보면 별 거 없는 문제이다.

 

23번째 줄의 trees는 딕셔너리로 선언했고 문제에서 A가 루트라 했으므로 24번째 줄에서 루트를 A로 지정해준다.

 

그리고 25번째 줄에서 반복문을 돌면서 딕셔너리에 키와 값을 저장하는데 길이가 2인 리스트 형태의 값에서 0번째 인덱스는 왼쪽 자식 노드를 뜻하고 1번째 인덱스는 오른쪽 자식 노드를 뜻한다.

 

이때 자식 노드가 없다면 . 으로 쓴다는거만 주의하면 된다.

 

선언한 3개의 함수는 각 각 전위 순회, 중위 순회, 후위 순회하는 함수인데 이것도 상당히 간단하다.

 

현재 노드의 키를 출력하는 코드의 위치만 변경하면 되기 때문이다.

 

전위 순회를 기준으로 현재 노드 -> 왼쪽 자식 노드 -> 오른쪽 자식 노드 순이기 때문에 현재 노드의 키 출력, 왼쪽 자식 노드가 있다면 왼쪽 자식 노드로 재귀 호출, 오른쪽 자식 노드가 있다면 오른쪽 자식 노드로 재귀 호출 했다.

 

순서만 바꾸면 되기 때문에 이진 트리만 구현할 수 있다면 상당히 쉬운 문제였다.

 

결과 화면
결과 화면

728x90

댓글()

[BOJ] Python 백준 1012번 유기농 배추 실버 2

728x90

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

 

1012번: 유기농 배추

차세대 영농인 한나는 강원도 고랭지에서 유기농 배추를 재배하기로 하였다. 농약을 쓰지 않고 배추를 재배하려면 배추를 해충으로부터 보호하는 것이 중요하기 때문에, 한나는 해충 방지에 

www.acmicpc.net

문제 풀이


문제에서도 한눈에 알 수 있듯이 그래프 탐색을 하는 문제다.

 

문제를 간략하게 다시 설명하자면 0은 비어있는 땅, 1은 배추가 있는 땅이고 각 케이스마다 배추가 이어져있는 구역을 세서 출력하면 된다.

 

그래프를 탐색하는 방법은 스택을 쓰는지, 큐를 쓰는지에 따라 깊이 우선 탐색(DFS), 너비 우선 탐색(BFS)으로 나뉘는데 둘 다 시간 복잡도는 비슷하므로 편한 방식으로 구현하면 된다.

 

필자는 최단 거리를 찾을 때 BFS가 유리하므로 보통 BFS로 구현한다.

 

바로 전체 코드를 확인해 보자.

 

testcase=int(input())
dir=[[-1,0],[0,1],[1,0],[0,-1]]
for i in range(testcase):
    queue=[]
    res=0
    M,N,K=map(int,input().split())
    graph=[[0 for j in range(M)]for k in range(N)]
    for j in range(K):
        tempm,tempn=map(int,input().split())
        graph[tempn][tempm]=1
    for j in range(N):
        for k in range(M):
            if graph[j][k]==1:
                res+=1
                queue.append([j,k])
                graph[j][k]=0
                while len(queue)>0:
                    tempn,tempm=queue.pop(0)
                    for a in range(len(dir)):
                        dx=tempn+dir[a][0]
                        dy=tempm+dir[a][1]
                        if 0<=dx<N and 0<=dy<M and graph[dx][dy]==1:
                            graph[dx][dy]=0
                            queue.append([dx,dy])
    print(res)

 

1번째 줄은 테스트 케이스만큼 반복하기 위한 변수이고 2번째 줄은 각 노드에서 탐색 방향(위, 오른쪽, 아래, 왼쪽)을 묶어놓은 리스트이다.

 

이렇게 방향을 리스트로 묶으면 4가지 방향을 탐색할 때 반복문으로 해결할 수 있다는 장점이 있기에 꼭 잊지 말길 바란다.

 

대부분은 4가지 방향이지만 가끔 대각선도 고려하는 8가지 방향 문제도 있는데 단순하게 방향을 고려해서 8개의 리스트를 넣으면 된다.

 

4번째 줄의 queue는 큐를 사용함으로써 BFS 방식을 쓰기 위해 선언한 리스트이다.

 

가끔 리스트를 큐 방식으로 쓸 때 시간 초과가 나는 경우가 있는데 이는 pop() 함수에서 마지막 원소부터 탐색하기 때문에 첫 번째 원소를 제거하기 위해서는 O(n)의 시간 복잡도가 걸리기 때문이다.

 

그럴 경우엔 deque 모듈을 사용하면 O(1)로 첫 번째 원소에 삽입하고 삭제할 수 있다.

 

8번째 줄은 배추의 위치를 갱신하는 반복문이고 11번째 줄부터는 그래프를 전체 탐색하면서 배추 구역을 카운트하는 반복문이다.

 

13번째 줄은 배추가 있을 경우 카운트를 하고 인접한 배추 구역을 0으로 바꾸는 작업이다.

 

보통 원본 그래프를 변경하지 않기 위해 visited로 그래프와 똑같은 크기의 리스트를 선언하고 방문한 노드인지 아닌지 체크하여 중복된 방문을 방지하는데 해당 문제에서는 단지 배추 구역만 세면 되므로 원본 리스트를 변경해도 상관없어서 아예 0으로 바꿨다.

 

또한 20, 21번째 줄에서 탐색할 노드의 위치를 계산하여 따로 변수에 저장하는 이유는 미리 값을 저장해놔야 그래프 크기 안인지 배추가 있는 노드인지 체크할 때 불필요한 연산 횟수를 줄일 수 있기 때문에 값을 저장하는 것을 추천한다.

 

결과 화면
결과 화면

 

탐색 방향을 고려할 필요도 없고 단지 구역만 세면 되기 때문에 그래프 문제 중에서도 꽤 쉬운 편에 속한다고 생각한다.

 

 

 

728x90

댓글()

[BOJ] Python 백준 23841번 데칼코마니 브론즈 2

728x90

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

 

23841번: 데칼코마니

첫 줄에 그림의 세로 길이 정수 N과 가로 길이 정수 M이 주어진다. (1 ≤ N, M ≤ 50, M은 짝수) N개 줄에 M개씩 그림에 대한 정보가 주어진다. 물감은 26가지가 있고, 각각 알파벳 대문자 하나로 나타

www.acmicpc.net

문제 풀이


2차원 그래프에서도 상당히 쉬운 편에 속하는 문제였다.

 

그림의 가로길이는 짝수이며 접었을 때 물감이 겹치는 부분도 없다고 하니 그냥 구간을 절반으로 나눠 물감이 있으면 반대편에도 추가하는 방식으로 풀면 되는 브론즈다운 문제였다.

 

상당히 쉬우니 바로 전체 코드로 넘어가겠다.

 

n,m=map(int,input().split())
graph=[]
for i in range(n):
    temp=list(input())
    for j in range(m//2):
        if temp[j]!='.':
            temp[m-j-1]=temp[j]
        elif temp[m-j-1]!='.':
            temp[j]=temp[m-j-1]
    graph.append(temp)
    
for i in range(len(graph)):
    for j in range(len(graph[i])):
        print(graph[i][j],end="")
    print()

 

각 줄을 입력받으면서 바로 물감을 갱신한 뒤 그래프에 추가하여 출력하는 방식으로 구현했다.

 

5번째 줄이 물감을 갱신하는 반복문이며 구간을 절반으로 나눠 6번째 줄은 첫 문자부터 절반 부분까지, 8번째 줄은 뒷 문자부터 절반 부분까지 물감이 있으면 반대편에 추가하고 완료되면 그래프에 추가했다.

 

그리고 완전 탐색을 통해 그래프 출력만 하면 되는 쉬운 문제였다.

 

만약에 인덱싱 부분이 헷갈린다면 작은 숫자를 몇 개 대입해보면 쉽게 해결할 수 있을 것이다.

 

결과 화면
결과 화면

728x90

댓글()

[알고리즘] 이진 탐색 트리(Binary Search Tree) (Python)

728x90

설명


이진 탐색 트리(Binary Search Tree)란 왼쪽 서브 트리는 자신보다 작은 수들만 존재하고 오른쪽 서브 트리는 자신보다 큰 수들만 존재하는 이진트리를 뜻한다.

 

단순한 이진트리는 원하는 값의 존재 여부를 확인하려면 트리 전체를 탐색해야 한다.

 

따라서 자료구조로써의 효율이 떨어진다.

 

그래서 나온 것이 이진 탐색 트리이며 왼쪽은 작은 수, 오른쪽은 큰 수를 유지함으로써 탐색하는데 O(logn)의 효율적인 시간 복잡도를 가질 수 있다.

 

또한 모든 원소들은 서로 다른 값을 가진다는 조건이 붙어있다.

 

삽입과 삭제 과정을 그림을 통해 보면 이해가 쉬울 것이다.

 

이진 탐색 트리의 삽입 연산
이진 탐색 트리의 삽입 연산
이진 탐색 트리의 삽입 연산

 

먼저 트리는 항상 루트부터 시작한다.

 

기존의 트리에서 4를 추가하기 위해 루트 원소를 살펴보니 6으로 4가 더 작기 때문에 왼쪽 자식 노드로 내려간다.

 

다시 비교해보니 추가하려는 원소가 더 크기 때문에 오른쪽으로 내려가야 되는데 더 이상 자식 노드가 없으므로 오른쪽 자식 노드로 추가한다.

 

그다음 8을 추가하기 위해 다시 루트 노드부터 탐색을 시작한다.

 

6보다 크니 오른쪽 자식 노드로 내려가고 9보다 작으니 왼쪽 자식 노드, 7보다 크니 오른쪽 자식 노드로 내려가는데 이때 오른쪽 자식 노드가 없으므로 오른쪽에 노드를 추가하면 된다.

 

루트에서 시작해서 매번 범위를 절반씩 줄여 나가므로 삽입 시 O(logn)의 시간 복잡도가 걸린다.

 

삽입 연산이 크기 비교만을 통해 탐색 후 추가하는 거라 상당히 단순하기 때문에 삽입에 비해서 삭제 연산은 다소 복잡하게 느껴질 수 있다.

 

하지만 케이스를 나눠 순서를 따라가면 이해할 수 있을 것이다.

 

케이스는 크게 2가지로 삭제할 노드가 단말 노드인 경우, 자식 노드가 존재하는 경우로 나눠서 생각하면 된다.

 

단말 노드일 때 삭제 연산
단말 노드일 때 삭제 연산

 

삭제 노드를 찾기 위한 탐색은 삽입 연산과 동일하다.

 

먼저 가장 간단한 경우인 삭제할 노드가 단말 노드인 경우는 그냥 해당 노드와 부모 노드의 관계를 끊어주고 삭제하면 된다.

 

자식 노드가 존재할 경우 삭제 연산
자식 노드가 존재할 경우 삭제 연산
자식 노드가 존재할 경우 삭제 연산

두 번째는 자식 노드가 존재할 경우이다.

 

통상 이진 탐색 트리의 삭제는 단말 노드인 경우, 자식 노드가 한 개인 경우, 자식 노드가 두 개인 경우 이렇게 나누는데 자식 노드가 하나나 둘이어도 원리는 같은 방식이니 굳이 구분 짓진 않겠다.

 

기억해야 할 중요한 점은 삭제할 노드와 가장 차이가 적게 차이나는 노드로 교체한다는 것이다.

 

이것과 이진 탐색 트리의 특성을 기억한다면 방식을 까먹을 일은 없다.

 

즉, 삭제할 노드의 왼쪽 자식(왼쪽 서브 트리)으로 내려갔다면 가장 차이가 적게 나는 값을 찾기 위해 최대한 오른쪽 자식 노드(오른쪽 서브 트리)로 이동해야 값을 찾을 수 있을 것이고, 반대로 오른쪽 자식 노드로 내려갔다면 가장 차이가 적게 나는 값을 찾기 위해 최대한 왼쪽 자식 노드로 이동해야 값을 찾을 수 있을 것이다.

 

가장 차이가 적게 나는 노드를 찾았다면 원래 삭제할 노드와 값을 교환하고 단말 노드의 삭제를 하면 된다.

 

그림을 보면 삭제할 9를 찾기 위해 루트 노드의 오른쪽 자식으로 이동하여 삭제할 노드를 찾았고, 왼쪽 자식이 있으므로 왼쪽으로 이동 후 오른쪽 자식 노드가 존재하지 않을 때까지 오른쪽 자식 노드로 이동하였다.

 

결국 8을 찾았고 이를 9와 교체한 뒤 삭제하면 된다.

 

전체적으로 보면 알겠지만 삽입과 삭제 과정에서도 꾸준히 이진 탐색 트리의 특성을 유지하는 것을 알 수 있다.

 

구현


이진 트리를 구현하는 3가지 방법을 소개하겠다.

 

  1. 리스트(배열)로 구현
  2. 딕셔너리로 구현
  3. 구조체로 구현

보통 리스트로 구현하는 방법은 낭비되는 공간이 많으므로(공간 복잡도가 비효율적이므로) 구조체로 구현하는 방법을 많이 쓴다.

 

또한 파이썬은 키, 값을 갖는 딕셔너리로도 구현이 가능하기 때문에 상황에 맞게 자신이 편한 방식대로 구현하면 된다. (필자는 딕셔너리가 가장 편하고 쉬우며 효율적이기에 자주 쓴다.)

 

코딩 테스트를 기준으로 보통 이진 탐색 트리가 나온다면 삽입이나 순회가 나온다.

 

삭제를 실제로 구현하기엔 다소 복잡하기 때문이다.

 

따라서 이진 탐색 트리를 다양하게 구현해보고 노드 삽입까지 해보겠다.

 

def list_insert(curr,num):
    if binary_tree[curr]=='*':
        binary_tree[curr]=num
    elif num<binary_tree[curr]:
        list_insert(2*curr,num)
    else:
        list_insert(2*curr+1,num)
        
print("리스트로 구현한 이진 탐색 트리\n")
binary_tree=['*' for i in range(16)]
root=1
list_insert(root,6)
list_insert(root,3)
print(binary_tree)
list_insert(root,1)
list_insert(root,9)
print(binary_tree)
list_insert(root,7)
list_insert(root,4)
list_insert(root,8)
print(binary_tree)

 

리스트로 구현할 때는 연산의 편의성 때문에 인덱스 1을 루트 노드로 가정한다.

 

인덱스 1을 루트로 하기 때문에 인덱스*2는 왼쪽 자식 노드, 인덱스*2+1은 오른쪽 자식 노드로 생각할 수 있다.

 

트리의 형태는 본문의 노드 삽입 결과와 똑같기 때문에 결과 화면과 그림을 비교해보면서 인덱스를 추적하면 이해가 될 것이다.

 

리스트 결과 화면
리스트 결과 화면

 

def dic_insert(curr,num):
    global root
    if len(binary_tree)==0:
        root=num
        binary_tree[root]=['*','*']
    else:
        if num<curr:
            if binary_tree[curr][0]=='*':
                binary_tree[curr][0]=num
                binary_tree[num]=['*','*']
            else:
                dic_insert(binary_tree[curr][0],num)
        else:
            if binary_tree[curr][1]=='*':
                binary_tree[curr][1]=num
                binary_tree[num]=['*','*']
            else:
                dic_insert(binary_tree[curr][1],num)
                
print("딕셔너리로 구현한 이진 탐색 트리\n")
binary_tree={}
root=None
dic_insert(root,6)
dic_insert(root,3)
print(binary_tree)
dic_insert(root,1)
dic_insert(root,9)
print(binary_tree)
dic_insert(root,7)
dic_insert(root,4)
dic_insert(root,8)
print(binary_tree)

 

딕셔너리는 키를 노드의 원소, 값을 길이가 2인 리스트로 선언하여 왼쪽 자식과 오른쪽 자식으로 구분 짓는다.

 

따라서 딕셔너리의 길이가 0일 땐 루트의 원소로 갱신하고 새로운 노드를 삽입할 땐 부모 노드에서 자식 노드를 갱신하고 초기화된 노드를 추가하는 것을 잊지 말아야 한다. 

 

딕셔너리 결과 화면
딕셔너리 결과 화면

 

class Node:
    def __init__(self,num):
        self.data=num
        self.left=self.right=None

class Binary_tree:
    def __init__(self):
        self.root=None

    def Node_insert(self,num):
        if self.root==None:
            self.root=Node(num)
        else:
            self.curr=self.root
            while True:
                if num<self.curr.data:
                    if self.curr.left==None:
                        self.curr.left=Node(num)
                        break
                    else:
                        self.curr=self.curr.left
                else:
                    if self.curr.right==None:
                        self.curr.right=Node(num)
                        break
                    else:
                        self.curr=self.curr.right
                        
print("구조체로 구현한 이진 탐색 트리\n")
Binary=Binary_tree()
Binary.Node_insert(6)
Binary.Node_insert(3)
Binary.Node_insert(1)
Binary.Node_insert(9)
Binary.Node_insert(7)
Binary.Node_insert(4)
Binary.Node_insert(8)

 

구조체로 구현하는 방식은 노드에 대한 구조체를 선언하고 해당 노드들을 이진 트리의 구조체로 묶는 형식이다.

 

파이썬의 특성상 Class의 인스턴스를 생성한 뒤 첫 번째 인자로 넘기기 때문에 self로 받는다.

 

이런 부분까지 다루게 되면 상당히 복잡하고 어려워지므로 이진 탐색 트리는 여기까지 하도록 하겠다. 

728x90

댓글()

[BOJ] Python 백준 5639번 이진 검색 트리 실버 1

728x90

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

 

5639번: 이진 검색 트리

트리를 전위 순회한 결과가 주어진다. 노드에 들어있는 키의 값은 106보다 작은 양의 정수이다. 모든 값은 한 줄에 하나씩 주어지며, 노드의 수는 10,000개 이하이다. 같은 키를 가지는 노드는 없다

www.acmicpc.net

 

문제 풀이


전형적으로 이진 트리(Binary Tree)와 트리 순회(Tree Traversal)에 대한 문제다.

 

만약 이진 트리에 대해 처음 듣거나 익숙하지 않은 사람은 이전 포스팅을 참고하여 이해하고 오는 것이 좋을 것 같다.

 

https://khsung0.tistory.com/24

 

[자료구조] 트리(Tree) 이진 트리(Binary Tree) 트리 순회 (Tree Traversal) (Python)

설명 자료구조는 크게 선형 구조, 비선형 구조로 나눠진다. 선형 구조는 Data를 차례대로 나열시킨 형태를 뜻하고 비선형 구조는 선형 구조가 아닌 모든 형태를 말한다. 즉, 노드와 간선으로 이루

khsung0.tistory.com

 

문제는 전위 순회한 결과가 주어지고 이를 후위 순회한 결과를 출력하면 된다.

 

알고리즘 외적으로도 고려해야 할 부분이 많아서 상당히 골치 아팠던 문제기도 하다.

 

처음 생각한 풀이법은 노드의 수는 10,000개 이하고 전위 순회의 결과가 입력으로 주어지니 전위 순회 순서대로 트리에 삽입하여 완성된 트리를 후위 순회하여 출력하는 방식을 생각했었다.

 

하지만 생각치 못하게 시간 초과와 메모리 초과가 떠서 틀렸었다.

 

불합격 이미지
불합격 이미지

 

다음은 불합격한 코드이다.

 

import sys
sys.setrecursionlimit(10**6)

binary_tree={}        #이진 트리를 위한 딕셔너리 선언
while True:
    try:
        num=int(input())
        if len(binary_tree)==0:        #빈 트리일 때
            root=num                   #루트로 설정
            binary_tree[root]=['*','*']
        else:
            curr=root
            while True:
                #왼쪽 서브 트리일 때
                if num<curr:
                    #왼쪽 노드가 없을 때
                    if binary_tree[curr][0]=='*':
                        binary_tree[curr][0]=num
                        binary_tree[num]=['*','*']
                        break
                    #왼쪽 자식 노드로 이동
                    else:
                        curr=binary_tree[curr][0]
                #오른쪽 서브 트리일 때
                else:
                    #오른쪽 노드가 없을 때
                    if binary_tree[curr][1]=='*':
                        binary_tree[curr][1]=num
                        binary_tree[num]=['*','*']
                        break
                    #오른쪽 자식 노드로 이동
                    else:
                        curr=binary_tree[curr][1]
    except:
        break

def postorder(curr):
    #왼쪽 자식 노드로 이동
    if binary_tree[curr][0]!='*':
        postorder(binary_tree[curr][0])
    #오른쪽 자식 노드로 이동
    if binary_tree[curr][1]!='*':
        postorder(binary_tree[curr][1])
    #현재 노드 출력
    print(curr)

postorder(root)

 

2번째 줄은 파이썬에서 기본 재귀 깊이 제한이 1000이기 때문에 1000000으로 세팅하는 코드이다.

 

이 때문에 런타임 에러(Recursion Error)가 뜬 것이다.

 

또한 입력받는 부분에서 어디까지 받는다는 제한이 없으므로 try except 구문을 사용하여 예외가 발생하기 전까지 입력을 받고 트리에 추가하는 방식으로 하였다.

 

예외가 발생하면 그대로 트리를 후위 순회하여 출력했는데 불합격한 것을 보니 다른 방식을 생각해야 했다.

 

고민하던 중에 전위 순회는 현재 노드 -> 왼쪽 자식 노드 -> 오른쪽 자식 노드 순으로 탐색하고 후위 순회는 왼쪽 자식 노드 -> 오른쪽 자식 노드 -> 현재 노드 순으로 탐색하기 때문에 현재 노드를 출력하는 타이밍을 조절해야겠다는 생각이 들었고 현재 노드보다 작은 숫자는 왼쪽 서브 트리, 큰 숫자는 오른쪽 서브 트리로 구분 지을 수 있겠다는 생각이 들었다.

 

따라서 리스트를 기준으로 첫 번째 원소는 현재 노드라 마지막에 출력하고 현재 노드보다 작은 원소들의 리스트는 왼쪽 서브 트리로 여겨 재귀 호출, 큰 원소들의 리스트는 오른쪽 서브 트리로 여겨 재귀 호출하면 차례대로 왼쪽 서브 트리 -> 오른쪽 서브 트리 -> 현재 노드 순으로 출력될 것이라 생각했고 역시 통과했다.

 

다음은 통과한 전체 코드이다.

 

import sys
sys.setrecursionlimit(10**6)

#입력받을 원소 리스트
num_list=[]
while True:
    try:
        num=int(input())
        num_list.append(num)
    except:
        break

def postorder(left,right):
    #순서 역전시 종료
    if left>right:
        return
    else:
        mid=right+1        #분할 기준
        for i in range(left+1,right+1):
            #해당 원소가 현재 노드보다 크다면 그 전까지는 왼쪽 서브 트리,
            #해당 원소 이후는 오른쪽 서브 트리이다.
            if num_list[left]<num_list[i]:
                mid=i
                break
        postorder(left+1,mid-1)
        postorder(mid,right)
        print(num_list[left])

postorder(0,len(num_list)-1)

 

트리 생성이 아닌 단순히 리스트에 원소를 입력 받으므로 입력 받는 부분은 전보다 더 간단해졌다.

 

시작은 전체 리스트의 길이부터 시작하고 가장 첫 번째 원소는 현재 노드, 그 이후부터 반복문을 돌면서 현재 노드보다 큰 원소가 있다면 그 전까지는 왼쪽 서브 트리, 큰 원소부터 이후는 오른쪽 서브 트리로 구분지어 재귀호출한다.

 

만약 큰 원소가 없을 경우를 대비해 mid=right+1을 함으로써 없으면 현재 노드를 제외한 나머지를 왼쪽 서브 트리로 보내고 두 번째 호출하는 재귀 함수는 순서가 역전되어 종료되도록 구현하였다.

 

결과 화면
결과 화면

 

인덱스를 접근하고 관리하는 방법에서 다소 헷갈리는 부분이 있고 알고리즘 외적으로도 재귀 깊이를 변경한다든지 입력받는데 처리해줘야 하는 부분이 있는 문제였다.

 

또한 해당 코드를 PyPy3로 제출하면 메모리 초과가 뜬다.

 

PyPy3가 Python3보다 연산이 빠르기에 안전하게 하기 위해 PyPy3로 먼저 제출했었는데 자료를 찾아보니 PyPy3는 최대 재귀 깊이는 제한이 없으나 10만 단위 이상으로 들어가면 Stack Overflow가 발생하여 재귀에 약하다는 정보를 얻을 수 있었다.

 

따라서 시간 초과가 뜬다면 PyPy3로, 재귀를 사용하여 메모리 초과가 뜬다면 Python3으로 제출해야한다는 지식을 알게 되었다.

728x90

댓글()

[BOJ] Python 백준 23827번 수열 (Easy) 실버 4

728x90

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

 

23827번: 수열 (Easy)

모든 원소가 양의 정수이고, 길이가 $N$인 수열 $A_1, A_2, ..., A_N$이 주어진다. $1 \le i < j \le N$을 만족하는 모든 정수쌍 $(i, j)$에 대해 $A_i \times A_j$의 합을 $1\, 000 \, 000 \, 007$로 나눈 나머지를 구하시

www.acmicpc.net

 

문제 풀이


문제를 좀 더 쉽게 설명하자면 길이가 N개인 수열이 있는데 여기서 두 개를 뽑아 서로 곱한 값을 모든 경우의 수에서 다 더한 뒤 1000000007로 나눈 값을 출력하라는 뜻이다.

 

즉, 수학의 조합(Combination)을 떠올리면 된다.

 

하지만 제한 조건을 잘 봐야한다.

 

수열의 길이는 최대 500000(50만)이 될 수 있으며 평소 조합을 구하는 방식대로 첫 번째 원소부터 완전 탐색, 두 번째 원소부터 완전 탐색 ... 이렇게 반복하면 O(n^2)의 시간 복잡도가 걸리므로 시간 초과가 날 수밖에 없다.

 

따라서 어떻게 해야되나 고민을 해본 결과 예제에서 힌트를 얻을 수 있었다.

 

예제는 '1 x 2 + 2 x 3 + 1 x 3 = 11' 이렇게 있었다.

 

이걸 원래 조합 구하는 방식으로 순서를 조금 바꿔보면 '1 x 2 + 1 x 3 + 2 x 3 = 11' 이렇게 나타낼 수 있고 조금만 더 이해하기 쉽게 바꾸면 '1 x (2 + 3) + 2 x 3 = 11' 이렇게 나타낼 수 있다.

 

즉 문제가 원하는 답은 각 원소와 해당 원소의 뒷 원소들의 합을 곱하고 이를 다 더한 값에 1000000007로 나눈 결과를 원하는 것이었다.

 

이렇게 하면 처음 수열의 합을 구할때 O(n), 수열 탐색할 때 O(n), 총 O(n)의 시간 복잡도가 걸리므로 시간 초과가 나지 않을 것이다.

 

다음은 전체 코드다.

 

n=int(input())
num_list=list(map(int,input().split()))
sum_list=sum(num_list)
res=0
for i in num_list:
    sum_list-=i
    res=(res+i*sum_list)%1000000007
print(res)

 

원리만 떠올린다면 구현 자체는 아주 쉬운 문제였다.

 

3번째 줄에서 sum() 함수는 O(n)의 시간 복잡도가 걸린다.

 

5번째 줄의 반복문에서는 해당 원소를 리스트의 합에서 빼주고 해당 원소와 리스트의 합을 곱한 값을 결과값(res)에 더하면서 1000000007의 나머지로 갱신하면 끝이다.

 

결과 화면
결과 화면

 

테스트 케이스를 이용하여 원리를 떠올리는 것이 중요한 문제인 것 같다.

728x90

댓글()

[SSAFY] 싸피 7기 인터뷰 합격 후기

취업 과정|2021. 12. 14. 20:58
728x90

오늘 (2021.12.10) 싸피 Interview를 보고 왔다.

4기, 6기 때는 강남에 있는 삼성 멀티캠퍼스에서 인터뷰를 했는데 이번 7기 때는 경기도 용인에 있는 삼성 전자 인재 개발원에서 진행했다.

유튜브를 보니 5기 때도 인재 개발원에서 했다는 것을 보아 짝수기 때는 멀티캠퍼스, 홀수기 때는 인재 개발원에서 진행하는 것 같다.

 

 

인재 개발원 전경
인재 개발원 전경

 

8시 55분까지라 비교적 여유롭게 출발했으나..... 분당선 노선에서 딴 거 신경 쓰다가 그냥 지하철 오길래 탔는데 영통역까지 설정해놓은 지하철 어플 알람이 종료된다는 메시지를 보고 역 상황을 보니 급행을 타버려서 결국 기흥역에서 내리고 부랴부랴 택시를 탔다......

생각보다 엄청 막혀서 안절부절못하고 있는데 8시 53분에 오고 있냐는 전화가 왔다.

시간상 58분쯤 도착할 거 같아서 불쌍한 척하며 길이 막혀 58분쯤 도착할 거 같다고 말씀드리니 입구에서 절차 밟는 시간도 있기에 9시 5분까지 도착한다고 적어놓겠다고 해주셨다.

다행히 안에서 대기하고 이동하는 시간도 조금 있기에 편의를 봐주신 것 같았다. (감사합니다!!!!!)

부랴부랴 도착해서 진행 Staff분들의 도움을 받아 아슬아슬하게 도착하고 바로 이동 대열에 합류했다.

인재 개발원이 역과의 거리가 좀 있어서 Interview 가시는 분들은 최대한 자세하게 거리와 동선을 계산해서 미리 도착할 수 있도록 계획 세우는 걸 추천한다.

프로그램은 약 2시간 정도 진행하며 메일에 나와있는 것처럼 2차 SW 적성 진단(CT 영역)과 Interview를 진행한다.

진행하기 앞서 서약서를 작성하는데 인터뷰에 대한 내용들은 전부 대외비이므로 SNS나 블로그 같은 곳에 게재하면 안 된다는 내용이다.

따라서 인터뷰가 어떻게 구성되었는지, 어떤 내용이 나왔는지, 어떤 질문이 있었는지에 대한 내용은 쓸 수가 없다.

다만 관련 유튜브들과 IT 트렌드들을 관심 있게 찾아본다면 충분히 대처가 가능할 것 같다는 생각이 든다.

또한 4기, 6기 인터뷰 때는 전공자로서 경험한 프로젝트에서 잘한 점을 주로 어필하려고 했다면 이번에는 부족한 점들과 구현은 했으나 아쉬웠던 점들을 싸피를 통해 배우고 역량 성장을 하고 싶단 의지를 어필하려고 노력했다.

이제 막 인터뷰를 본 직후라 결과가 어떻게 나올진 모르겠으나 만약 이번에 합격한다면 아무래도 싸피가 직원을 뽑는 게 아닌 역량 성장을 위한 개발자 양성 프로그램이다 보니 어느 정도는 부족하지만 충분히 성장 가능성이 있는 사람들을 뽑는다는 게 확실시될 것이다.

6기 때는 웹 프로젝트에 Python의 자동화 도구를 접목하였고 앞으로도 역량을 성장하여 업무에 가진 능력을 접목하고 싶다고 어필했더니 면접관님께 "상당히 잘 아시는 것 같네요"란 말씀을 들었지만 떨어졌기 때문이다.

합격 결과가 나오면 바로 포스팅에 추가하도록 하겠다.

이제 장소와 일정 진행에 대한 느낀 점을 말하자면 장소는 상당히 훌륭했고 진행과정은 깔끔했으며 Staff분들도 엄청 친절하셨다.

인재 개발원은 역시 삼성인가 하는 생각이 들 정도로 건물도 크고 디자인이나 가구들도 딱딱한 느낌이 들지 않았으며 엄청 깨끗하고 쾌적한 느낌이 들었다.

만약 여기서 코딩한다면 일주일 내내도 가능할 것 같았다.

또한 발열 검사를 해주시는 Staff 분들도 하얀색 방역 옷을 착용하고 계시고 응시자 전원 라텍스 장갑을 착용했다.

인터뷰는 비대면으로 진행하면서 각자의 방에서 혼자 보게끔 하여 전달이 잘되도록 마스크를 벗어 편했으며 매번 방역을 실시한다고 한다.

이런 식으로 진행하다 보니 코로나 시국에 위생적으로 청결하고 깔끔하단 느낌을 받았다.

(심지어 화장실도 엄청 크고 깨끗하더라)

진행 Staff 분들은 질문도 친절히 받아주셨으며 편하게 동선 안내를 해주셨다.

약 2시간 동안 편하고 재밌는 경험이었으며 이번엔 꼭 붙어서 좋은 곳으로 취직할 수 있도록 역량 성장의 발판이 되면 좋겠다.

 

합격 후기


붙을 거라 생각해서 편하게 게임도 하고 블로그 글도 쓰고 유튜브 보면서 쉬고 있던 와중 오늘 (2021.12.20) 싸피에서 문자가 왔다.

 

인터뷰 결과를 확인하라는 문자 내용이었다.

 

인터뷰 보는 기간(2021.12.8 ~ 2021.12.14, 평일 5일)이 끝나고 6일 만에 결과 발표가 난 거라 생각보다 엄청 빨리 났었다.

 

붙을 거라고 생각은 했지만 막상 결과를 확인하려고 하니 두근두근 심장이 엄청 뛰는데 다행히 합격했다는 화면과 교육생증 제작을 위한 사진 첨부를 하라는 내용이 있었다.

 

합격 화면
합격 화면

 

아무리 비대면이라 해도 이왕이면 가까운 서울에 가고 싶었는데 1 지망으로 붙어서 참 다행이었다.

 

단톡을 보니 아무래도 서울이 가장 경쟁률이 높았던지 2 지망, 3 지망이 된 사람도 꽤 많았었다.

 

3수 만에 합격하게 되니 어느 정도 감을 잡은 것 같다.

 

아마 내가 말하는 내용이 이후에 싸피에 입과하고 싶은 사람들에게 상당히 도움 될 것 같다.

 

먼저 CT는 지원자가 학습할 역량이 어느 정도 되는지 판단하는 테스트 같다.

 

물론 정확한 합격 기준도 모르겠고 합격에 중요한 요소가 맞는지도 모르겠지만 이번 7기 CT는 개인적으로 상당히 쉬웠고 다 풀었다.

 

합격한 사람 중에 절반을 풀고도 합격했다는 사람도 있었고 어디선가 본 내용에 의하면 2차 CT는 1차에서 지원자 본인이 직접 푼 게 맞는지 체크하는 용도라는 정보도 있다.

 

그래도 다소 불안한 지원자들은 알고리즘 문제를 여러 번 접하면 쉽게 풀 수 있는 수준이고 이 알고리즘조차 경험이 없어서 어렵게 느껴진다면 학습을 위해 직접 작성한 PDF 형태의 전자책을 추천한다.

 

https://kmong.com/gig/290286

 

누구나 쉽게 독학 가능한 자료구조 알고리즘 전자책을 드립니다. | 20000원부터 시작 가능한 총 평

2개 총 작업 개수 완료한 총 평점 5점인 코딩독학의 투잡∙노하우, 직무스킬 전자책 서비스를 2개의 리뷰와 함께 확인해 보세요. 투잡∙노하우, 직무스킬 전자책 제공 등 20000원부터 시작 가능한

kmong.com

 

C와 파이썬으로 작성했고 주석과 함께 자세하게 설명하기 때문에 이해가 쉬울 것이다.

 

물론 언어를 모르더라도 파이썬은 문법 자체가 쉽고 무료로 나와있는 자료가 많아서 언어를 학습하는 것 문제가 되지 않을 것이다.

 

또한 CT만을 준비한다면 굳이 지금 코드로 구현하지 않아도 다양한 알고리즘의 이론을 그림과 함께 이해하고 나중에 코딩 테스트 준비 겸 구현해보는 것도 괜찮을 것 같다.

 

그리고 평소에 IT 트렌드에 관심 있게 보는 것을 추천한다.

 

구체적으로 설명 가능한 수준 까진 아니더라도 단순히 "이런 게 있으면 이렇게 적용해보면 어떨까?" 하며 상상의 나래를 펼쳐보는 것이 중요할 것 같다.

 

당연한 말이지만 이런 상상에 현실적이고 논리적인 사고방식은 필수이다.

 

예를 들어 사람이 하늘을 난다고 상상했을 때 "팔을 새처럼 움직이면서 자유롭게 난다"는 몸무게와 공기를 아래로 밀어 얻는 반작용을 고려하면 상당히 비현실적인 뜬구름 잡는 소리지만, "열기구나 비행기를 이용하여 하늘을 난다" 같이 현실적으로 어떤 원리를 적용하여 상상해보면 충분히 가능성 있고 자신의 논리적인 사고력을 어필할 수 있기 때문이다.

 

마지막으로 싸피는 직장이 아닌 교육기관이라는 점을 명심해야 한다.

 

앞서 말했지만 4기, 6기 때는 내가 프로젝트 시 뭘 잘했는지 어필하여 둘 다 인터뷰에서 떨어졌다.

 

하지만 이번에는 부족한 점과 이를 싸피를 통해 채워나가 개발자로서 성장하고 싶다는 내용을 어필했다.

 

이게 핵심적인 합, 불합의 요소 같다.

 

싸피 입장에서는 그만큼 능력이 있는 지원자라면 충분히 기업에 합격할 수 있을 것이고 교육에 대한 메리트가 떨어지기 때문에 어느 정도 교육을 받아들일 수 있으며 성장 가능성을 기대해 볼만한 지원자를 중점적으로 생각하는 게 아닌가 싶다.

 

여기까지가 합격 꿀팁이다.

 

본인도 이제 막 입과 합격 통지를 받은 지원자이지만 나 또한 인터넷에서 많은 유용한 정보를 얻었기에 좋은 정보를 공유하기 위해서 포스팅을 작성했다.

 

앞으로도 싸피를 진행하면서 공유할 만한 정보가 있으면 포스팅하고 공부하는 내용에 관해서도 꾸준히 포스팅하도록 하겠다.

728x90

댓글()