반응형

너무나도 어렵군... 화이팅... 🔥


 

20168번: 골목 대장 호석 - 기능성

첫 줄에 교차로 개수 N, 골목 개수 M, 시작 교차로 번호 A, 도착 교차로 번호 B, 가진 돈 C 가 공백으로 구분되어 주어진다. 이어서 M 개의 줄에 걸쳐서 각 골목이 잇는 교차로 2개의 번호와, 골목의

www.acmicpc.net

 

20182번: 골목 대장 호석 - 효율성 1

첫 줄에 교차로 개수 N, 골목 개수 M, 시작 교차로 번호 A, 도착 교차로 번호 B, 가진 돈 C 가 공백으로 구분되어 주어진다. 이어서 M 개의 줄에 걸쳐서 각 골목이 잇는 교차로 2개의 번호와, 골목의

www.acmicpc.net

 

20183번: 골목 대장 호석 - 효율성 2

첫 줄에 교차로 개수 N, 골목 개수 M, 시작 교차로 번호 A, 도착 교차로 번호 B, 가진 돈 C 가 공백으로 구분되어 주어진다. 이어서 M 개의 줄에 걸쳐서 각 골목이 잇는 교차로 2개의 번호와, 골목의

www.acmicpc.net

문제

소싯적 호석이는 골목 대장의 삶을 살았다. 호석이가 살던 마을은 N 개의 교차로와 M 개의 골목이 있었다. 교차로의 번호는 1번부터 N 번까지로 표현한다. 골목은 서로 다른 두 교차로를 양방향으로 이어주며 임의의 두 교차로를 잇는 골목은 최대 한 개만 존재한다. 분신술을 쓰는 호석이는 모든 골목에 자신의 분신을 두었고, 골목마다 통과하는 사람에게 수금할 것이다. 수금하는 요금은 골목마다 다를 수 있다.

당신은 A 번 교차로에서 B 번 교차로까지 C 원을 가지고 가려고 한다. 호석이의 횡포를 보며 짜증은 나지만, 분신술을 이겨낼 방법이 없어서 돈을 내고 가려고 한다. 하지만 이왕 지나갈 거면, 최소한의 수치심을 받고 싶다. 당신이 받는 수치심은 경로 상에서 가장 많이 낸 돈에 비례하기 때문에, 결국 갈 수 있는 다양한 방법들 중에서 최소한의 수치심을 받으려고 한다. 즉, 한 골목에서 내야 하는 최대 요금을 최소화하는 것이다.

 

예를 들어, 위의 그림과 같이 5개의 교차로와 5개의 골목이 있으며, 당신이 1번 교차로에서 3번 교차로로 가고 싶은 상황이라고 하자. 만약 10원을 들고 출발한다면 2가지 경로로 갈 수 있다. 1번 -> 2번 -> 3번 교차로로 이동하게 되면 총 10원이 필요하고 이 과정에서 최대 수금액을 5원이었고, 1번 -> 4번 -> 5번 -> 3번 교차로로 이동하게 되면 총 8원이 필요하며 최대 수금액은 6원이 된다. 최소한의 수치심을 얻는 경로는 최대 수금액이 5인 경로이다. 하지만 만약 8원밖에 없다면, 전자의 경로는 갈 수 없기 때문에 최대 수금액이 6원인 경로로 가야 하는 것이 최선이다.

당신은 앞선 예제를 통해서, 수치심을 줄이고 싶을 수록 같거나 더 많은 돈이 필요하고, 수치심을 더 받는 것을 감수하면 같거나 더 적은 돈이 필요하게 된다는 것을 알게 되었다. 마을의 지도와 골목마다 존재하는 호석이가 수금하는 금액을 안다면, 당신이 한 골목에서 내야하는 최대 요금의 최솟값을 계산하자. 만약 지금 가진 돈으로는 절대로 목표 지점을 갈 수 없다면 -1 을 출력하라.

입력

첫 줄에 교차로 개수 N, 골목 개수 M, 시작 교차로 번호 A, 도착 교차로 번호 B, 가진 돈 C 가 공백으로 구분되어 주어진다. 이어서 M 개의 줄에 걸쳐서 각 골목이 잇는 교차로 2개의 번호와, 골목의 수금액이 공백으로 구분되어 주어진다. 같은 교차로를 잇는 골목은 최대 한 번만 주어지며, 골목은 양방향이다.

출력

호석이가 지키고 있는 골목들을 통해서 시작 교차로에서 도착 교차로까지 C 원 이하로 가는 경로들 중에, 지나는 골목의 요금의 최댓값의 최솟값을 출력하라. 만약 갈 수 없다면 -1을 출력한다.


20168과 20182, 20183번은 문제가 같다. 다른점은 주어진 변수의 입력 범위이다.

좌 : 20168번 / 중 : 20182번 / 우 : 20183번

 

20168번은 N의수가 최대 10까지이다. 따라서 모든 교차로를 확인할 경우 최대 O(n^2)의 복잡도를 가지는데 이는 100이므로 시간초과 범위 내에서 해결이 가능하다.

반면 20182번은 N의 수가 최대 100,000까지여서 O(n^2)의 경우 100,000,000,000 이 되므로 5초 제한에서 timeout이 발생한다. 따라서 20182번과 20183번은 탐색의 수를 줄여야 한다.


20168 : DFS 로 풀기

앞선 문제는 연산횟수가 최대 100이므로 DFS로 풀어보았다.

import sys
input = sys.stdin.readline
n, m, a, b, c = map(int, input().split())
road = [[] for _ in range(n+1)] # index가 교차로 (n+1), 이어지는골목과 비용을 append 한다
for _ in range(m):
    from_c, to_c, cost = map(int, input().split())
    # 양방향 골목이므로 두번 삽입
    road[from_c].append((to_c, cost))
    road[to_c].append((from_c, cost))

res_max_min = sys.maxsize
def dfs(from_city, total_cost, max_cost):
    global res_max_min
    # 경로의 총 비용이 소지한 비용을 넘어서면 return
    if total_cost > c:
        return
    # 위의 validation을 통과하고, 도착지점에 도착하면 값 갱신
    if from_city == b:
        res_max_min = min(res_max_min, max_cost)
        return
    # 비용미초과, 도착지점 미도달 상태라면 계속해서 탐색
    for to_city, to_cost in road[from_city]):
    	# 이전에 방문하지 않은 교차로만 탐색
        if not visit[to_city]:
            visit[from_city] = True
            dfs(to_city, total_cost+to_cost, max(max_cost, to_cost))
            visit[from_city] = False
visit = [False] * (n+1)
visit[0] = True
visit[a] = True
dfs(a, 0, 0)
if res_max_min == sys.maxsize:
    res_max_min = -1
print(res_max_min)

해당 풀이로 20168번 풀이가 가능하다.


20182, 20183 : 이분탐색과 다익스트라 알고리즘 활용하기

정답에서 요구하는 것은 "최소값이 정답이 되는 것" 이다. 이러한 유형의 문제에서는 "이분탐색" 으로 정답이 되는 값을 탐색하며 풀 수 있다. 참고로 이분탐색법은 값이 정렬되어 있을때 O(logN)의 시간복잡도를 지닌다.

이분탐색법으로 정답이 되는값 "목표 교차로까지 가는 경로중 최대 비용들 중에서의 최소값" 을 탐색하며, 해당 값으로 도착지점까지 도달할 수 있는지 다익스트라 알고리즘으로 탐색한다.

 

모든 골목의 비용 리스트 = costs
이분탐색으로 탐색한 값 = mid
이분탐색으로 탐색한 값 기준 제한 골목 비용 = costs[mid]
한개 경로의 최대값 one_max
모든 경로의 one_max 들 중에서의 최소값 = road_max_min
교차로 = 노드
골목 = 간선

 

다익스트라 알고리즘은 a에서 b까지 가는 경로의 최단거리를 찾아낸다. 즉 시작지점에서 도착지점까지 최소비용을 찾아내고, 그 값이 costs[mid] 값과 같거나 그 값보다 적다면 해당 그 값이 답이 될수 있다는 뜻이다.

반면, 최단거리 탐색 중 한개 간선의 비용이 mid값 보다 크다면 해당 조건으로 탐색은 불가능한 것이므로 mid 값을 키워주며 탐색한다.

import heapq
import sys
sys.stdin = open("input.txt")
n, m, a, b, c = map(int, input().split())

costs = []
road = [[] for _ in range(n + 2)]
for _ in range(m):
    from_cc, to_cc, ccost = map(int, input().split())
    costs.append(ccost)
    road[from_cc].append((to_cc, ccost))
    road[to_cc].append((from_cc, ccost))

def check_cost(mid):
    distances = [sys.maxsize for _ in range(n + 1)]
    distances[a] = 0
    edge = []
    heapq.heappush(edge, [0, a])  # 비용과 현재 노드
    while edge:
        this_cost, this_node = heapq.heappop(edge)
        # 최단경로 확인한다 : 이미 이번 노드로 가는 비용이 누적 비용보다 적다면 갱신 X
        if distances[this_node] < this_cost:
            continue
        for next_node, next_cost in road[this_node]:
            if distances[next_node] > this_cost + next_cost and next_cost <= mid:
                distances[next_node] = next_cost + this_cost
                heapq.heappush(edge, [this_cost + next_cost, next_node])
    if distances[b] > c:
        return False
    return True
road_max_min = sys.maxsize
costs.sort()
lt = 0
rt = len(costs) -1
while lt <= rt:
    mid = (lt + rt) // 2
    # mid 값을 최대로 갖는 경로가 있는지 확인한다.
    check_res = check_cost(costs[mid])
    if check_res:
        # 있다면 최대 값을 더 줄여본다
        rt = mid -1
        # 답으로 기록한다.
        road_max_min = min(road_max_min, costs[mid])
    else:
        # 없다면 최대값을 더 늘려본다
        lt = mid +1
if road_max_min == sys.maxsize:
    print(-1)
else:
    print(road_max_min)

이때 check_cost 함수 내에서 목표골목 b까지의 최단거리가 완성되었을때 그 값이 c 보다 크다면 결국엔 갈수 없는 경로이므로 mid 값을 줄여준다.

 

위 풀이로 20182과 20183 모두 해결 가능하다.


Python으로 풀었을 경우 백준에서는 Python3 와 Pypy3 두가지로 채점 가능하다.

Python3로 채점했을때 시간초과가 뜬 경우 Pypy3로 채점하면 통과하기도 하는데 위 문제중 20182가 그러한 경우이다. 따라서 Pypy3 로 제출하면 된다!

반응형
반응형

여러가지 조건에서 최단거리를 구하는 알고리즘 문제가 나오기 때문에 관련 공부는 필수인 듯 하다.

기존에는 다익스트라 알고리즘만 활용가능한 수준이었는데 이번에 11403번 문제를 보면서 플로이드 워셜 알고리즘을 처음 사용해 보았다.

 

처음 풀이에서는 플로이드 워셜 알고리즘을 사용하지 않고, deque를 활용하여 문제를 풀었는데 백준 문제 기준으로 deque를 사용한 풀이의 시간이 덜 소요되는것으로 보였다.

두가지 풀이에서 어떤 점이 소요시간의 차이를 일으켰는지 알아보고자 한다.


시간 제한메모리 제한제출정답맞힌 사람정답 비율
1 초 256 MB 47580 29309 21659 61.491%

문제

가중치 없는 방향 그래프 G가 주어졌을 때, 모든 정점 (i, j)에 대해서, i에서 j로 가는 길이가 양수인 경로가 있는지 없는지 구하는 프로그램을 작성하시오.

입력

첫째 줄에 정점의 개수 N (1 ≤ N ≤ 100)이 주어진다. 둘째 줄부터 N개 줄에는 그래프의 인접 행렬이 주어진다. i번째 줄의 j번째 숫자가 1인 경우에는 i에서 j로 가는 간선이 존재한다는 뜻이고, 0인 경우는 없다는 뜻이다. i번째 줄의 i번째 숫자는 항상 0이다.

출력

총 N개의 줄에 걸쳐서 문제의 정답을 인접행렬 형식으로 출력한다. 정점 i에서 j로 가는 길이가 양수인 경로가 있으면 i번째 줄의 j번째 숫자를 1로, 없으면 0으로 출력해야 한다.


1. deque 활용

deque를 활용한 풀이는 다음과 같다.

import sys
import time

sys.stdin = open("input.txt")
from collections import deque
n = int(input())
city = [list(map(int, input().split())) for _ in range(n)]

# deque 버전
def solution_deque():
    visit = [[0]*n for _ in range(n)]
    dq = deque()
    for from_first, m in enumerate(city):
        for to_city, moving in enumerate(m):
            if moving == 1 and visit[from_first][to_city] == 0:
                visit[from_first][to_city] = 1
                dq.append(to_city)
                while dq:
                    to_city = dq.popleft()
                    for next_to, next_move in enumerate(city[to_city]):
                        if next_move == 1 and visit[from_first][next_to] == 0:
                            visit[from_first][next_to] = 1
                            dq.append(next_to)

    for i in visit:
        print(*i)

여기서 visit은 방문여부를 체크하기도 하지만, 최종 답을 적기 위해 그 초기값을 0으로 하였다.

 

city 이중배역에 문제로 주어진 간선 정보를 담았다.

이후 첫번째 for loop 에서 현재 확인하고자 하는 노드를 "from_fist" 값으로 하고, from_first와 연결된 노드를 "to_city" 라고 하였다.

from_first 에서 to_city 로 가는 간선이 존재한다면 visit정보를 1로 업데이트 하고, 간선 정보를 deque에 담는다.

이후 deque값이 존재할 동안 (while 조건) 이어지는 간선 정보를 확인한다.

to_city 를 기준으로 for loop로 찾는 도시 "next_to" 의 1인 경우 (next_move가 1) 방문여부를 확인하고 from_first에서 next_to가 연결되었음을 체크한다.

또한, to_city 에서 next_to의 연결정보를 다시 deque에 담아 next_to의 도시에서 시작하는 연결정보를 계속해서 탐색한다.


from collections import deque
# from_first = 0
# to_city = 1
city = [[0, 1, 0], [0, 0, 1], [1, 0, 0]]
"""
from_first에서 to_city의 연결정보가 1이므로 방문여부 확인 후 체크한다.
그리고 이어서 탐색하기 위해 해당 연결 정보를 deque에 담는다
"""
dq = deque()
if city[from_first][to_city] == 1 and visit[from_first][to_city] == 0:
	visit[from_first][to_city] = 1
    dq.append(to_city)
    # dq에 담긴 정보에서 이어서 탐색 한다. (from_first에서 시작한 연결정보가 끝날때까지)
    while dq:
    	from_city, to_city = dq.popleft()
        for next_to, next_move in enumerate(city[to_city]):
        	if next_move == 1 and visit[first_from][next_to] == 0:
            	visit[first_from][next_to] = 1
                """
                to_city에서 next_to가 연결되어있다면 from_first에서 next_to도 연결된 것이다.
                방문여부를 체크하고, 이어서 연결정보를 탐색하게 한다.
                """
                dq.append(next_to)

한가지 상황을 예로 들어 과정을 풀이하면 위와 같다. 총 시간복잡도는 N^3으로 확인된다. 단, 자기자신의 노드로 가는 경우는 0이기 때문에 해당 경우의 수는 확인하지 않는다.

 


2. 플로이드-워셜 활용

플로이드-워셜 알고리즘이란 그래프에서 가능한 모든 노드 쌍에 대해 최단 거리를 구하는 알고리즘이다.

위 알고리즘을 더욱 잘 활용할수 있는 예제는 "가중치가 있는 간선" 이 주어졌을때 인데, 위 문제의 경우 가중치가 없어 오로지 "연결 가능한 노드인지" 를 확인만 하면 된다.

 

플로이드-워셜 알고리즘으로 i 에서 j 노드로 갈때 k 노드를 거쳐서 갈수있는지 확인하여 i 에서 j 노드의 연결을 확인한다. i에서 j 노드의 모든 수를 확인하는 N^2 시간복잡도에서 k 노드 확인 경우를 추가하여 최종적으로 N^3의 시간복잡도를 보이고, 모든 노드를 확인하여 제외되는 경우는 없다.

def solution_floid():
    """
    i에서 j의 경로를 탐색하면서 거쳐가는 k 노드의 값을 확인, k 값으로 연결된 i, j는 연결가능하다
    """
    for k in range(n):
        for i in range(n):
            for j in range(n):
                if city[i][k] == 1 and city[k][j] == 1:
                    city[i][j] = 1
    for row in city:
        print(*row)

3. 두 풀이의 풀이시간 차이

2) 동일한 노드수에서 간선 비율의 차이에 따른 시간차이

i. 노드의 수가 14개

당연히 알고리즘으로 풀면 더 빠르겠지! 싶었던 생각이 예제로 소요시간을 확인하며 깨졌다.

노드의 수가 14개이면서, 간선의 비율이 약 25%, 약 52% 약 92% 일때 시간을 측정해 보았다.

# 노드의 수 14 / 간선비율 약 25%
# deque 소요시간 : 0.0006220340728759766
# floyd 소요시간 : 0.0008339881896972656

14
0 0 0 1 0 0 0 1 0 0 0 1 0 0
1 0 0 0 1 0 0 0 1 0 0 0 1 0
0 0 0 1 0 0 0 1 0 0 0 1 0 0
1 0 0 0 0 0 1 0 0 0 1 0 0 0
0 1 0 0 0 1 0 0 0 1 0 0 0 1
0 0 1 0 0 0 1 0 0 0 1 0 0 0
0 1 0 0 0 0 0 1 0 0 0 1 0 0
1 0 0 0 1 0 0 0 1 0 0 0 1 0
0 0 0 1 0 0 0 1 0 0 0 1 0 0
1 0 0 0 1 0 0 0 1 0 0 0 1 0
0 0 0 1 0 0 0 1 0 0 0 1 0 0
1 0 1 0 1 0 1 0 1 0 1 0 0 0
0 1 0 0 0 1 0 0 0 1 0 0 0 1
0 0 1 0 0 0 1 0 0 0 1 0 0 0
# 간선 비율 약 52%
# deque 소요시간 : 0.0006856918334960938
# floyd 소요시간 : 0.0009648799896240234

14
0 1 0 1 0 1 0 1 0 1 0 1 0 1
1 0 1 0 1 0 1 0 1 0 1 0 1 0
0 1 0 1 0 1 0 1 0 1 0 1 0 1
1 0 1 0 1 0 1 0 1 0 1 0 1 0
0 1 0 1 0 1 0 1 0 1 0 1 0 1
1 0 1 0 1 0 1 0 1 0 1 0 1 0
0 1 0 1 0 1 0 1 0 1 0 1 0 1
1 0 1 0 1 0 1 0 1 0 1 0 1 0
0 1 0 1 0 1 0 1 0 1 0 1 0 1
1 0 1 0 1 0 1 0 1 0 1 0 1 0
0 1 0 1 0 1 0 1 0 1 0 1 0 1
1 1 1 1 1 1 1 1 1 1 1 0 1 0
0 1 0 1 0 1 0 1 0 1 0 1 0 1
1 0 1 0 1 0 1 0 1 0 1 0 1 0
# 간선비율 약 92%
# deque 소요시간 : 0.0006418228149414062
# floyd 소요시간 : 0.0008568763732910156

14
0 1 1 1 1 1 1 1 1 1 1 1 1 1
1 0 1 1 1 1 1 1 1 1 1 1 1 1
1 1 0 1 1 1 1 1 1 1 1 1 1 1
1 1 1 0 1 1 1 1 1 1 1 1 1 1
1 1 1 1 0 1 1 1 1 1 1 1 1 1
1 1 1 1 1 0 1 1 1 1 1 1 1 1
1 1 1 1 1 1 0 1 1 1 1 1 1 1
1 1 1 1 1 1 1 0 1 1 1 1 1 1
1 1 1 1 1 1 1 1 0 1 1 1 1 1
1 1 1 1 1 1 1 1 1 0 1 1 1 1
1 1 1 1 1 1 1 1 1 1 0 1 1 1
1 1 1 1 1 1 1 1 1 1 1 0 1 1
1 1 1 1 1 1 1 1 1 1 1 1 0 1
1 1 1 1 1 1 1 1 1 1 1 1 1 0

 

노드수가 14개일 경우 간선의 비율차로는 플로이드-워셜의 소요시간이 항상 더 길게 나온다.

ii) 노드 수가 5개

# 간선비율 약 80%
# deque 소요시간 : 9.012222290039062e-05
# floyd 소요시간 : 8.797645568847656e-05

5
0 1 1 1 1
1 0 1 1 1
1 1 0 1 1
1 1 1 0 1
1 1 1 1 0
# 간선비율 약 20%
# deque 소요시간 : 9.918212890625e-05
# floyd 소요시간 : 9.894371032714844e-05

5
0 1 0 0 0
0 0 1 0 0
0 0 0 1 0
0 0 0 0 1
1 0 0 0 0

 

노드의 수가 5개이거나, 이의 약 3배인 14개이거나 플로이드-워셜 풀이로 풀었을 경우 소요시간이 더 크게 나오는것을 확인 할 수 있다.

극단적으로 노드의 수가 3개일 경우에는 플로이드-워셜의 소요시간이 더 적게 나오긴 하다

# 간선비율 : 약 33%
# deque 소요시간 : 3.910064697265625e-05
# floyd 소요시간 : 2.9087066650390625e-05

3
0 1 0
0 0 1
1 0 0

 

하지만 보통의 문제에서는 n의 범위가 3까지일리가 없으니... 시간복잡도를 생각한다면 플로이드-워셜이 아닌 deque를 사용한 풀이를 선택해도 될것같다.

 

반응형
반응형

문제

아래 <그림 1>과 같이 여러개의 정사각형칸들로 이루어진 정사각형 모양의 종이가 주어져 있고, 각 정사각형들은 하얀색으로 칠해져 있거나 파란색으로 칠해져 있다. 주어진 종이를 일정한 규칙에 따라 잘라서 다양한 크기를 가진 정사각형 모양의 하얀색 또는 파란색 색종이를 만들려고 한다.

전체 종이의 크기가 N×N(N=2k, k는 1 이상 7 이하의 자연수) 이라면 종이를 자르는 규칙은 다음과 같다.

전체 종이가 모두 같은 색으로 칠해져 있지 않으면 가로와 세로로 중간 부분을 잘라서 <그림 2>의 I, II, III, IV와 같이 똑같은 크기의 네 개의 N/2 × N/2색종이로 나눈다. 나누어진 종이 I, II, III, IV 각각에 대해서도 앞에서와 마찬가지로 모두 같은 색으로 칠해져 있지 않으면 같은 방법으로 똑같은 크기의 네 개의 색종이로 나눈다. 이와 같은 과정을 잘라진 종이가 모두 하얀색 또는 모두 파란색으로 칠해져 있거나, 하나의 정사각형 칸이 되어 더 이상 자를 수 없을 때까지 반복한다.

위와 같은 규칙에 따라 잘랐을 때 <그림 3>은 <그림 1>의 종이를 처음 나눈 후의 상태를, <그림 4>는 두 번째 나눈 후의 상태를, <그림 5>는 최종적으로 만들어진 다양한 크기의 9장의 하얀색 색종이와 7장의 파란색 색종이를 보여주고 있다.

입력으로 주어진 종이의 한 변의 길이 N과 각 정사각형칸의 색(하얀색 또는 파란색)이 주어질 때 잘라진 하얀색 색종이와 파란색 색종이의 개수를 구하는 프로그램을 작성하시오.

입력

첫째 줄에는 전체 종이의 한 변의 길이 N이 주어져 있다. N은 2, 4, 8, 16, 32, 64, 128 중 하나이다. 색종이의 각 가로줄의 정사각형칸들의 색이 윗줄부터 차례로 둘째 줄부터 마지막 줄까지 주어진다. 하얀색으로 칠해진 칸은 0, 파란색으로 칠해진 칸은 1로 주어지며, 각 숫자 사이에는 빈칸이 하나씩 있다.

출력

첫째 줄에는 잘라진 햐얀색 색종이의 개수를 출력하고, 둘째 줄에는 파란색 색종이의 개수를 출력한다.


주어진 조건에 맞을때 기존 역역을 4분할 하는 문제이며 이런 방식을 쿼드 트리 라고 한다.

경우의 수가 두개인 상황이라 분할정복 - 쿼드트리 문제의 입문이라고 볼 수 있겠다.

분할정복 알고리즘 ?
기본적으로는 엄청나게 크고 방대한 문제를 조금씩 조금씩 나눠가면서 용이하게 풀 수 있는 문제 단위로 나눈 다음 그것들을 다시 합쳐서 해결하자는 개념에서 출발하였다. 대표적으로는 퀵소트나  병합정렬이 있다.
출처 : 나무위키 - 분할 정복 알고리즘

 

사실 처음엔 큰 문제를 작게 쪼개서 계산?? 이 문제에 적용이 된다고? 싶었지만 처음 접해보는 분할정복 문제였어서 감이 안왔던듯 하다.

직접 문제를 손으로 써내려가면서 이해해 보았다.

 


문제에서 주어진 조건은 크게 두가지가 있다.

1. 주어진 영역 내 서로 다른 색이 존재 할 경우 해당 영역을 4분할 한다.

2. 정사각형 내 동일 색상 단위로 총 몇개 정사각형이 존재하는지 출력한다.

 

따라서 1번 조건에 맞을 경우 문제를 쪼개면 되는것이고, 쪼갠 상황에서 2번 조건에 맞는다면 답으로 기록하면 된다.

시작점은 [0, 0] 으로 한다. 시작점의 색상을 기억해 두고, 일정한 인접 행렬을 탐색하다가 다른 값이 나온다면 1번 조건에 부합하므로 즉시 4분할을 해준다.

 

분할된 영역의 구간은 규칙적으로 변경된다.

python의 // 는 값을 나눈 몫을 반환하므로 n값이 8일때 n//2를 하면 그 값은 4가 된다. 또한 python의 for loop에서 range의 end parameter는 해당 값 "미만"으로 동작하기 때문에 n//2 값을 넣어주면 된다.

 

처음에 탐색의 시작을 [0,0] 에서 한다고 하였으므로 첫번째 이중 for loop의 탐색 범위는 탐색x는 (이하 xx) x ~ x+n(미만) 이고, 탐색y는 (이하 yy) y ~ y+n 이 된다.

 

첫번째 cut 에서 또다시 서로다른 색이 발견되어 4분할을 진행해 줄때, x값과 y값의 범위 변화는 규칙적으로 변경된다.

위 규칙을 토대로 풀이를 작성하면 아래와 같다.

import sys
sys.stdin = open("input.txt")
n = int(input())
board = [list(map(int, input().split())) for _ in range(n)]

white = 0
blue = 0
def cut(x, y, n):
    global white
    global blue
    check_color = board[x][y]
    for xx in range(x, x+n):
        for yy in range(y, y+n):
            if board[xx][yy] != check_color: # 조건 1 부합 : 4분할 진행
                cut(x=x, y=y, n=n//2) # 1영역 : x = x ~ x+(n//2) | y = y ~ y+(n//2) | n = n//2
                cut(x=x, y=y+n//2, n=n//2) # 2영역 : x = x ~ x+(n//2) | y = y+n//2 ~ y+n//2+n | n = n//2
                cut(x=x+n//2, y=y, n=n//2) # 3영역 : x = x+n//2 ~ x+n//2+n | y = y ~ y+(n//2) | n = n//2
                cut(x=x+n//2, y=y+n//2, n=n//2) # 4영역 : x = x+n//2 ~ x+n//2+n | y = y+n//2 ~ y+n//2+n | n = n//2
                return # 분할은 분할대로 호출해놓고, 값에 적용되는걸 막기 위해 분할 뒤 return 한다.
    # 조건 2 부합 : 분할되지 않으면 모두 색이 같다는 것으로 정답으로 기록한다.
    if check_color == 1:
        blue += 1
    else:
        white += 1

cut(0, 0, n)  # 시작 조건 [0,0]
print(f"{white}\n{blue}")

 

큰 문제를 쪼갠다는 것과, 4분할 처리 하는 것에 대해 알 수 있었다.!

반응형
반응형

이분탐색법을 이용해 최소/최대 값으로 답이 결정되는 알고리즘을 풀어보자


2792 번

문제

보석 공장에서 보석 상자를 유치원에 기증했다. 각각의 보석은 M가지 서로 다른 색상 중 한 색상이다. 원장 선생님은 모든 보석을 N명의 학생들에게 나누어 주려고 한다. 이때, 보석을 받지 못하는 학생이 있어도 된다. 하지만, 학생은 항상 같은 색상의 보석만 가져간다.

한 아이가 너무 많은 보석을 가져가게 되면, 다른 아이들이 질투를 한다. 원장 선생님은 이런 질투심을 수치화하는데 성공했는데, 질투심은 가장 많은 보석을 가져간 학생이 가지고 있는 보석의 개수이다. 원장 선생님은 질투심이 최소가 되게 보석을 나누어 주려고 한다.

상자에 빨간 보석이 4개 (RRRR), 파란 보석이 7개 (BBBBBBB) 있었고, 이 보석을 5명의 아이들에게 나누어 주는 경우를 생각해보자. RR, RR, BB, BB, BBB로 보석을 나누어주면 질투심은 3이 되고, 이 값보다 작게 나누어 줄 수 없다.

상자 안의 보석 정보와 학생의 수가 주어졌을 때, 질투심이 최소가 되게 보석을 나누어주는 방법을 알아내는 프로그램을 작성하시오.

입력

첫째 줄에 아이들의 수 N과 색상의 수 M이 주어진다. (1 ≤ N ≤ 109, 1 ≤ M ≤ 300,000, M ≤ N)

다음 M개 줄에는 구간 [1, 109]에 포함되는 양의 정수가 하나씩 주어진다. K번째 줄에 주어지는 숫자는 K번 색상 보석의 개수이다.

출력

첫째 줄에 질투심의 최솟값을 출력한다.

 

예제1

[입력]     [출력]
5 2       3
7
4

 

예제2

[입력]    [출력]
7 5       4
7
1
7
4
4

이분탐색법은 주어진 정답 구간의 최소값과 최대값을 각각 left point, right point로 잡고, 그의 중간값을 확인하여 답에 맞는지 확인하며 찾아가는 방법이다. 기준이 되는 lt, rt의 중간값으로 왼쪽구간, 오른쪽 구간을 나누기 때문이 이분탐색이라고 한다.

 

위 문제에서 정답으로 요구하는것은 "질투수치" 이다. 질투수치는 보석을 나눠받은 학생들 중 최고로 많은 보석을 가져간 학생의 보석갯수를 의미한다. 이 값의 최소치를 구한다는 것은 곧, 학생들에게 보석을 최대한 적게 골고루 나누준다는 뜻이다.

 

위 문제에서 주어진 조건은

1. 학생은 한가지 색의 보석만 받을 수 있다.

2. 보석은 모두 나눠주어야 한다. (보석이 남으면 안됨) 그러나 보석을 아예 못받는 학생이 있을 수 있다. (5명중 3명만 보석받아도 괜찮음)

 

위 두가지 조건에 유의하며 코드를 작성해 보았다.

import sys
sys.stdin = open("input.txt")

students, color = map(int, input().split())
jewels = [int(input()) for _ in range(color)]

lt = 1  # 보석을 나눠받는 가장 작은 수 1
rt = max(jewels) # 보석을 나눠받는 가장 큰 수
while lt <= rt:
    mid = (lt + rt) // 2  # 중간값 탐색
    get_s = 0  # mid 갯수로 보석을 나눠받은 총 학생 수
    for j in jewels:
        if j % mid == 0:
            get_s += (j//mid) # 똑 나눠 떨어지면 몫이 곧 그 수만큼 나눠받은 학생 수
        else:
            get_s += (j//mid) + 1 # 안나눠떨어지면 몫에다가 나머지만큼 가져간 학생 수
            
    if get_s > students:
    	# 나눠받은 학생 수 가 주어진 학생수보다 많다면 더 많이씩 나눠줘야 한다.
        lt = mid + 1
    else:
        # 나눠받은 학생 수가 주어진 학생수보다 같거나 적다면 더 적게 나눠줘도 된다.
        rt = mid - 1
print(lt)  #

 

[변수] mid, get_s

- 이분탐색을 진행하면서 몇개의 보석으로 학생들이 나눠받을 것인지 mid 값을 정한다. 이 값은 항상 구간의 최소값과 최대값의 중간값이다. (2로 나눈 몫)

- mid 갯수만큼 보석을 나눠받을때, 한개 색상의 보석이 mid의 배수이면 그 몫 만큼의 학생수가 보석을 나눠갖는다.

- mid 갯수만큼 보석을 나눠받을때, 한개 색상 보석의 수가 mid로 나누었을때 나머지가 발생한다면 몫의 수 만큼의 학생이 mid값 만큼의 보석수를 나눠갖고, 한 학생은 나머지 수 만큼 보석을 나눠받게 되므로 몫 + 1 만큼의 학생들이 나눠받게 된다.

 

[계산하기] lt, rt

> lt = mid + 1

mid 갯수만큼 학생들이 보석을 나눠가질때, 나눠갖게된 학생의 수가 문제로 주어진 students의 수보다 크다면 이는 보석을 좀더 많이씩 나눠가져서 나눠받는 학생의 수를 줄여하 한다는 것을 의미한다.

따라서 mid 갯수만큼은 정답이 되지 않는것을 인지하고, 탐색 범위를 높이기 위해 최소값이었던 lt를 mid+1만큼으로 변경한다.

 

> rt = mid - 1

mid 갯수만큼 학생들이 보석을 나눠가질때, 나눠받은 학생의 수가 주어진 students의 수보다 같거나 적다면 일단 mid값은 정답이 된다는 것이다. 하지만 답에서 구하는것은 그 값의 최소값이기 때문에 더 작은 값도 정답이 될수 있는지 탐색해야 한다.

따라서 mid 값보다 1 작은수로 rt값을 변경하고 그 사이값들을 비교한다.

 

[정답 도출하기] lt

while 문을 빠져나오는 조건은 두가지가 있다.

1. lt와 rt의 값이 동일할때, lt 값이 mid+1 된 경우

2. lt와 rt의 값이 동일할때, rt 값이 mid-1 된 경우

 

먼저 1번의 경우 새로운 lt값이 된 mid+1값은 언젠가 그 값이 정답이었던 mid 값이다. 따라서 while 조건에서 벗어나게 되어 더이상 탐색이 불가능하게 되더라도 lt값은 이전에 정답으로 체크되었던 mid 값이었으므로 정답이 된다.

 

2번의 경우 이미 2번의 조건이 정답에 부합하는 조건이므로 그 당시의 lt값 (이때는 lt와 rt가 같지만 한번 더 탐색을 위해 rt값이 줄어들어버린것) 을 정답으로 확정한다.

 


주어진 예제 1번을 확인해보면 [정답 도출하기]의 2번 상태에서 while 문이 종료된다. 예제2번의 경우 1번 상태에서 while 문이 종료되는데 풀이를 확인해보면 아래와 같다.

 

 


15810번

위와 동일한 개념으로 문제를 풀면 된다.

import sys
sys.stdin = open("input.txt")

staff, ba = map(int, input().split())
times = list(map(int, input().split()))

lt = 1
rt = max(times) * ba

while lt <= rt:
    mid = (lt + rt) // 2
    # 총 소요시간이 mid일때 몇개까지 풍선을 불 수 있는가
    total_balloon = 0
    for one in times:
        # 불고나서 몇분 남았다 해도 그 시간안에 부는것 완성 못하므로 나머지는 버린다.
        total_balloon += mid//one
    if total_balloon >= ba:
        # 목표갯수와 같거나 보다 많다면 총 소요시간을 줄여본다
        rt = mid - 1
    else:
        # 목표갯수보다 모자르다면 총 소요시간을 늘려준다
        lt = mid + 1
print(lt)

 

최소 / 최대 값이 답으로 결정되는 알고리즘 풀이법에는 이렇게 이분탐색법을 활용할수 있으므로 이 개념을 꼭 익혀두면 좋다!

반응형
반응형

알고리즘 문제를 풀다보면 2중배열을 조작하여 풀어야 하는 유형이 종종 보인다.

자주 나오는 유형은 그 풀이공식을 어느정도 외워두는것이 좋은 듯 하다.


문제

크기가 N×M인 배열이 있을 때, 배열을 돌려보려고 한다. 배열은 다음과 같이 반시계 방향으로 돌려야 한다.

A[1][1] ← A[1][2] ← A[1][3] ← A[1][4] ← A[1][5]
   ↓                                       ↑
A[2][1]   A[2][2] ← A[2][3] ← A[2][4]   A[2][5]
   ↓         ↓                   ↑         ↑
A[3][1]   A[3][2] → A[3][3] → A[3][4]   A[3][5]
   ↓                                       ↑
A[4][1] → A[4][2] → A[4][3] → A[4][4] → A[4][5]

예를 들어, 아래와 같은 배열을 2번 회전시키면 다음과 같이 변하게 된다.

1 2 3 4       2 3 4 8       3 4 8 6
5 6 7 8       1 7 7 6       2 7 8 2
9 8 7 65 6 8 21 7 6 3
5 4 3 2       9 5 4 3       5 9 5 4
 <시작>         <회전1>        <회전2>

배열과 정수 R이 주어졌을 때, 배열을 R번 회전시킨 결과를 구해보자.

입력

첫째 줄에 배열의 크기 N, M과 수행해야 하는 회전의 수 R이 주어진다.

둘째 줄부터 N개의 줄에 배열 A의 원소 Aij가 주어진다.

출력

입력으로 주어진 배열을 R번 회전시킨 결과를 출력한다.

제한

  • 2 ≤ N, M ≤ 300
  • 1 ≤ R ≤ 1,000 (16926번) / 1 ≤ R ≤ 10**9 (16927번)
  • min(N, M) mod 2 = 0
  • 1 ≤ Aij ≤ 108

 먼저 "반시계 방향" 으로 회전을 어떻게 할 것인가? 를 두고 한참 고민했다. 모두가 같은 방향으로 회전하는 것이 아니라 총 4가지 방향으로 회전해야 하기 때문에
  1. 어느 포인트에서 회전방향을 바꾸어야 하는지
  2. 얼마만큼 길이를 해당 방향으로 회전해야 하는지
를 구하는게 관건이었다.
 
이 문제를 풀기 위해서는 "이중배열을 1차원 배열로 풀기" 가 포인트이다.


1. 이중배열을 1차원 배열로 풀기

1차원 배열로 풀기 위해서는 회전하는 기준이 되는 묶음 (이하 루프) 의 갯수를 파악해야 한다. 제한조건에서 n과 m중 최솟값은 2의 배수임을 명시했기 때문에 루프가 생기지 않는 가운데 부분은 고려하지 않는다.
 

가로, 세로의 최솟값을 2로 나눈 몫 만큼 루프가 형성된다.

# 1. 가로 4, 세로 5
standard = min(4, 5) // 2 # => 2

# 2. 가로 5, 세로 4
standard = min(5, 4) // 2 # => 2

# 3. 가로 2, 세로 4
standard = min(2, 4) // 2 # => 1

# 4. 가로 6, 세로 2
standard = min(6, 2) // 2 # => 1

 

따라서 새로 만들어질 1차원 배열도 루프의 갯수만큼 생성될 것이다.

 

위, 오른쪽, 아래, 왼쪽 순서대로 배열을 새로 정의하는데 이때 "모서리에 있는 숫자" 의 중복 처리를 주의해야 한다.

위와 아래에는 모든 모서리를 포함하도록 하고, 오른쪽과 왼쪽은 모서리를 제외한 변의 숫자만을 처리하도록 한다.

n, m, r = map(int, input().split()) # n = 세로 / m = 가로 / r = 회전 수
board = [list(map(int, input().split())) for _ in range(n)]
new_board = [[0]*m for _ in range(n)]
q = deque()
# 2차원 배열을 1차원 배열로 펼치기
loop_count = min(n, m) // 2

for i in range(loop_count):
    q.clear()
    # 위쪽
    q.extend(board[i][i:m-i])
    # 오른쪽 (모서리 제외)
    q.extend([row[m-i-1] for row in board[i+1:n-i-1]])
    # 아래쪽
    q.extend(board[n-i-1][i:m-i][::-1])
    # 왼쪽 (모서리 제외)
    q.extend([row[i] for row in board[i+1:n-i-1][::-1]])
    q.rotate(-r)

 

또한 위와 아래는 숫자 처리 방향이 반대이고, 오른쪽과 왼쪽도 서로 반대방향이므로 아래쪽과 왼쪽의 수를 배열로 생성할때에는 [::-1] 을 하여 순서를 반대로 되게 해준다.

 

2. r만큼 회전시키기

루프를 1차원 배열로 만든 뒤 r의 횟수만큼 회전을 시켜줌에 따라 pop, popleft, append, appendleft 가 발생한다.

문제는 "반시계 방향" 이므로 popleft와 append가 일어나게 된다.

 

하지만 python의 deque 에서는 회전을 동작하는 rotate 함수가 있으므로 이를 사용해 준다.

왼쪽으로 회전할때는 음의값을, 오른쪽으로 회전할때는 양의 값을 인자로 받는다.

from collections import deque

dq = deque()
dq.extend([1, 2, 3, 4])

# 좌로 회전
dq.rotate(-2) # => [3, 4, 1, 2]

# 우로 회전
dq.rotate(2) # => [3, 4, 1, 2]

# r 만큼 좌로 회전 (반시계 방향)
q.rotate(-r)

 

3. 1차원 배열을 2차원 형태로 가공하기

회전이 완료된 1차원 배열을 다시 원래 자리의 2차원 배열 형태로 만들어 주어야 한다.

별다른 공식은 없고 위치를 주의하여 넣어주면 된다.

    # 위
    for j in range(i, m-i):
        new_board[i][j] = q.popleft()

    # 오른쪽
    for j in range(i+1, n-i-1):
        new_board[j][m-i-1] = q.popleft()
    # 아래쪽
    for j in range(m-i-1, i-1, -1):
        new_board[n-i-1][j] = q.popleft()

    for j in range(n-i-2, i, -1):  # 왼쪽
        new_board[j][i] = q.popleft()

q 에는 회전이 완료된 수들이 순서대로 들어있다. 1차원 배열에 담았던 순서대로 위 -> 오른쪽 -> 아래쪽 -> 왼쪽 으로 처리해 준다.

이때 for-loop 에서 range(a, b, c) 형태는 a 부터 b 까지 c의 값만큼 더하여 처리한다는 뜻이다.

 


전체 풀이

import sys
from collections import deque
sys.stdin = open("input.txt", "r")
n, m, r = map(int, input().split())
board = [list(map(int, input().split())) for _ in range(n)]
new_board = [[0]*m for _ in range(n)]
q = deque()
# 2차원 배열을 1차원 배열로 펼치기
loop_count = min(n, m) // 2

for i in range(loop_count):
    q.clear()
    # 위쪽
    q.extend(board[i][i:m-i])
    # 오른쪽 (모서리 제외)
    q.extend([row[m-i-1] for row in board[i+1:n-i-1]])
    # 아래쪽
    q.extend(board[n-i-1][i:m-i][::-1])
    # 왼쪽 (모서리 제외)
    q.extend([row[i] for row in board[i+1:n-i-1][::-1]])
    q.rotate(-r)

    # 다시 board 정렬하기
    # 위
    for j in range(i, m-i):
        new_board[i][j] = q.popleft()

    # 오른쪽
    for j in range(i+1, n-i-1):
        new_board[j][m-i-1] = q.popleft()
    # 아래쪽
    for j in range(m-i-1, i-1, -1):
        new_board[n-i-1][j] = q.popleft()

    for j in range(n-i-2, i, -1):  # 왼쪽
        new_board[j][i] = q.popleft()

for row in new_board:
    print(*row)

"""
[입력]
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
"""

"""
[출력]
3 4 8 12
2 11 10 16
1 7 6 15
5 9 13 14
"""

 

16926번과 16927번의 문제가 동일하여 같은 풀이로 제출하면 되는데

앞의것은 실버레벨이고 뒤의것은 골드레벨인것을 보아, 16926번을 풀때 deque를 활용하지 않고 순수 배열 처리로만 했을때 풀리더라도 같은 풀이로 16927번을 풀었을때 타임아웃이 나지 않을까 싶다.

제한 조건을 보면 16926은 R의 값이 최대 1000 이지만 16927은 최대 10**9 값을 가질수 있다.

 

위 풀이로는 두 문제 전부 python3으로 체점하여 통과 가능하다.

반응형
반응형

입력값 처리

python에서 sys 가 있다면 java에서는 import java.util.Scanner 가 있다.
이때 Scanncer는 InputStream을 생성자로 입력받는데, 입력하는 곳이 어디냐에 따라 그 값을 넣어주면 된다.

[1] 입력 방식의 차이

1-1) 키보드에서 입력받기 : System.in

키보드에서의 입력을 받으려면 System.in의 InputStream 을 넘겨주면 된다.

import java.util.Scanner

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
    }
}

1-2) 변수에서 입력받기 : 변수명

키보드에서의 입력을 받으려면 System.in의 InputStream 을 넘겨주면 된다.

import java.util.Scanner

public class Main {
    public static void main(String[] args) {
        String ex = "2 apple";
        Scanner sc = new Scanner(ex);
    }
}

1-3) 외부 파일에서 입력받기 : new File()

import java.util.Scanner

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(new File({파일명}));
    }
}

2) 타입에 따라 입력받기

파이썬 처럼 입력받을때 타입을 지정할 수 있다.

import java.util.Scanner

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        // int 형으로 입력받기 : Short(), Long(), Double(), Float() 도 가능하다.
        int input_int = sc.nextInt();

        // 문자로 입력받기
        String input_str = sc.next();
        // 문자를 '줄' 단위로 입력받기
        String input_str = sc.nextLine();
    }
}

이때 next()nextInt() 는 모두 기본적으로 공백을 구분자 로 하여 한개 단어씩만 입력받는다.


public class Main {
    public static void main(String[] args) {
        String ex = "2 apple";
        Scanner sc = new Scanner(ex);

        int input_int = sc.nextInt(); // 2   
        String input_str = sc.next();  // apple

        System.out.println(input_int+input_srt);
    }
}
>>> 2apple

3) 구분자로 입력받기

3-1) 한줄로 입력

한줄에 구분자를 두고 여러값을 입력받는 경우도 있다.

3
1 6 7

이런식의 입력예제는 알고리즘 문제에서 많이 볼수 있었다. nextLine()을 통해 개행문자로 구분되기 전 한줄의 값을 모두 읽어올 수 있다.

public class Main {
    public static void main(String[] args) {
        String ex = "2 apple";
        Scanner sc = new Scanner(ex);

        String input_line = sc.nextLine();        
        System.out.println(input_line);
    }
}
>>> 2 apple

3-2) 입력받은 문자열을 공백을 기준으로 리스트로 담기

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.println("공백으로 구분된 문자열을 입력하세요:");
        String input = scanner.nextLine();

        // 입력된 문자열을 공백으로 분리하여 배열로 저장
        String[] tokens = input.split("\\s+");
    }
}

이때 배열상태인 tokens를 바로 출력하면 배열이 저장된 데이터의 주소값 이 출력된다. 이를 문자로 출력하기 위해선 Arrays.toString() 메소드를 이용해 배열을 문자열로 변환한 후에 출력해야 한다.

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.println("공백으로 구분된 문자열을 입력하세요:");
        String input = scanner.nextLine();

        // 입력된 문자열을 공백으로 분리하여 배열로 저장
        String[] tokens = input.split("\\s+");

        // 배열을 리스트로 변환
        List<String> list = Arrays.asList(tokens);

        // 리스트 출력 (테스트용)
        System.out.println("리스트: " + list);
    }
}

3-3) 입력받은 한줄을 숫자형 리스트로 만들기

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.println("공백으로 구분된 숫자를 입력하세요:");
        List<Integer> numbers = new ArrayList<>();

        while (scanner.hasNextInt()) {
            numbers.add(scanner.nextInt());
        }

        // 숫자 리스트 출력
        System.out.println("입력된 숫자: " + numbers);
    }
}

이때 nextLine() 처럼 한줄을 한번에 읽고, 그 후에 처리하는 방법은 아래와 같다.

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.println("공백으로 구분된 숫자를 한 줄에 입력하세요:");
        String input = scanner.nextLine();

        // 입력된 문자열을 공백으로 분리하여 숫자로 변환하여 리스트에 저장
        List<Integer> numbers = new ArrayList<>();
        String[] tokens = input.split("\\s+");
        for (String token : tokens) {
            numbers.add(Integer.parseInt(token));
        }

        // 숫자 리스트 출력
        System.out.println("입력된 숫자: " + numbers);
    }
}
반응형
반응형

처음에 해메었던 부분들이 있었다.

이유는 구간의 총 합이 100m 로 고정되어있다는 걸 간과한채 미지의 수가 들어오면.. 하면서 복잡하게 생각하느라 로직이 꼬였던듯 하다.

일반 tc는 통과했지만 히든tc에서 오답처리되어 0점...!!

다시 문제를 확인했고 접근방식을 변경했다.

[1] 내 풀이 : 모든 구간의 속도 차이 비교 (단, 과속일때만 기록)

비교해야 할 구간이 100m 까지이고, 단위가 1m 이므로 모든 구간을 비교해도 괜찮겠다 싶었다.

- step1

이것이 가능한 이유는 구간이 100m 로 비교적 짧게 고정되어있기 때문이다. 이 방식은 구간이 미지수에 매우 범위가 크다면 시간제한이슈로 사용하지 못할 듯 하다.

- step2

0부터 100까지 index 를 사용해 stand_alltest_all 의 각 값을 비교한다. 이때 test_all-stand_all 값이 0보다 커야 테스트 중에 과속했다는 것을 의미하므로 그 값만 diff_list 에 담아준다.

- step3

만약 diff_list 에 아무갑솓 안담겨있다면 이는 저속 주행 혹은 정속 주행만 했음 을 의미한다. 따라서 조건에 맞게 0 을 반환한다.
이와 달리 diff_list에 유효한 값이 있다면 이중 가장 큰 값을 출력한다. 이것이 가장 큰 차이가 나는 과속 범위를 뜻한다.


내 풀이가 사용 가능했던 이유는 앞서 말했든 비교범위가 작게, 고정되어있기 때문이다. 실제로 이 방식으로 문제를 풀었을때 평균 0.1 초의 풀이시간이 나온걸 확인할 수 있었다.

만약 범위가 아주아주 크다면 모든 구간을 비교하는것은 사용하지 못할 듯 싶다. 최대한 비교횟수를 줄이기 위해 속도 차이가 나는 구간 마다의 기록 방식을 생각했지만 구현이 잘 안되어 다른 사람들의 풀이를 참고하였다.

[2] 추가 풀이 : 속도가 변하는 구간 값만 확인하기 (deque 활용 가능)

- step 1

구간별 속도를 기록한다. 이중배열속 0번 index는 구간을, 1번 index는 해당 구간의 속도를 뜻한다. 모든 0번 index값을 더하면 주어진 구간인 100이 나온다.

- step 2

약간 queue같은.. 모양인데 정말로 Python의 deque 를 사용해도 될것 같다. 배열보다 pop 하는 효율이 더 좋다고 하니...

기준테스트 의 가장 첫 구간을 비교하여 구간길이의 차는 항상 테스트구간을 기준 으로 한다.

두 구간의 속도 차를 비교하여 해당 값이 0 보다 크다면 모든 속도차이를 비교한 배열에 append 한다. 0이면 정속 주행이고, 음수값이면 저속 주행이므로 배열에 담을 필요가 없다.

  • 2-1 : 구간 차 가 양수" 라면 테스트 구간이 더 길다는 뜻이다. 때문에 남은 구간을 "테스트 list"의 0번째 인덱스에 다시 넣어준다. 속도비교도 하므로 속도값은 검사한 테스트 구간 값 그대로 넣어준다.
  • 2-2 : 구간 차 가 "음수"라면 기준 구간이 더 길다는 뜻이다. 때문에 남은 구간을 "기준 list"의 0번째 인덱스에 다시 넣어주어야 하는데 이때 값 diff_len 은 음수이므로 절대값 처리 하여 넣어준다. 속도비교도 하므로 속도값은 검사한 기준 구간 값 그대로 넣어준다.

- step 3

모든 과속 데이터가 담긴 배열을 확인하여 조건에 맞게 답을 출력한다. 데이터가 없다면 0을, 데이터가 있다면 그중 가장 많이 과속한 max값을 출력한다.

반응형
반응형

알고리즘 문제풀이를 이어가다가 DFS (깊이우선탐색) 주제를 공부하게 되었다.

예제로 나온 문제에서 이항계수 개념을 활용해야 하는 문제가 나왔는데, 졸업한지 오래되어 개념을 까먹었다..

다시 개념을 익힐 겸! 기록으로 남겨본다.

1. 이항계수

이항계수?
이항식을 이항 정리로 전개했을 때 각 항의 계수이며, 주어진 크기의 (순서 없는) 조합의 가짓수

 

이항계수의 표현식은 nCk 로 표현된다.

nCk = n! / (k!(n-k)!)

아래는 n이 5이고 k가 3일때 5C3을 구하는 과정을 트리형태로 그려보았다.

이항계수 계산법인 팩토리얼로 계산했을때 답이 10 이 나오는데 직접 flow를 그려가보니 정말 10개의 가짓수가 나왔다.

2. 이항계수 계산하기

2-1) DFS (깊이우선탐색) - 재귀함수 활용하기

재귀함수를 활용해 중복을 허용하지 않는 n개 중 k 개를 구하는 방법은 아래와 같다.

# L = level
# s = start point
def DFS(L, s):
    global cnt
    if L == m:
        for row in res:
            print(row, end=" ")
        print()
    else:
        for i in range(s, n+1):
            res[L] = i
            DFS(L+1, i+1)


if __name__ == "__main__":
    n,m = map(int, input().split())
    res = [0]*m
    DFS(0, 1)
-------------------------------------------------
1 2 
1 3 
2 3

여기서 중복을 허용하지 않기 위해 DFS 함수 내의 s값은 1직전 s값보다 1만큼씩 증가하여 다음 level로 넘어가게 한다.
중복을 허용한 가짓수를 확인하려면 아래와 같이 코드를 수정하면 된다.

# L = level
# s = start point
def DFS(L, s):
    global cnt
    if L == m:
        for row in res:
            print(row, end=" ")
        print()
    else:
        for i in range(1, n+1):  # s부터 시작하는것 대신 항상 1부터 시작하도록 변경
            res[L] = i
            DFS(L+1, i+1)


if __name__ == "__main__":
    n,m = map(int, input().split())
    res = [0]*m
    DFS(0, 1)
-------------------------------------------------
1 1 
1 2 
1 3 
2 1 
2 2 
2 3 
3 1 
3 2 
3 3

2-2) python 라이브러리 itertools 활용하기

python nCk 계산에 활용할수 있는 라이브러리가 있다. (아주.. 굿...) 바로 itertools 이다.

재귀함수를 통해 문제를 풀면서 이해가 되지 않아 머리를 쥐어뜯어가면서.. 노트에 받아적으면서 겨우 이해가 됐을 때 이 라이브러리를 알고 신세계를 맛봤다

그러나 일부 코테에서는 해당 내장함수 사용을 금지하는 경우도 있으니 반드시 재귀함수 방식으로의 풀이도 익혀두어야 한다.

지난 년도 삼성 코테에서 itertools 라이브러리 사용을 막았다는....

 

또한 "특정 조건" 을 충족해야하는 조합을 구할땐 이런 함수 사용이 어려울 수 있으므로 주의!!

 

a) itertools 의 permutations

중복을 허용하는 리스트 n 중에서 k개를 선별하는 가짓수 반환

n = [1, 2, 3]
k = 3
for tmp in it.permutations(n, k):
    print(tmp)
-------------------------------------------------
(1, 2)
(1, 3)
(2, 1)
(2, 3)
(3, 1)
(3, 2)

 

b) itertools 의 combinations

중복을 허용하지 않는 n개 중 k개를 선별하는 가짓수 반환

n = [1, 2, 3]
k = 3
for tmp in it.combinations(n, k):
    print(tmp)
-------------------------------------------------
(1, 2)
(1, 3)
(2, 3)

이어서..!!! 어흐 어려워

반응형

+ Recent posts