새소식

PS/프로그래머스

[프로그래머스] 숫자 타자 대회 (Python)

  • -

Problem : 코딩테스트 연습 - 숫자 타자 대회 | 프로그래머스 스쿨 (programmers.co.kr)

 

프로그래머스

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

programmers.co.kr

Status : Solved

Time : 01:16:52

 


 

 

문제 설명

 

더보기

위와 같은 모양으로 배열된 숫자 자판이 있습니다. 숫자 타자 대회는 이 동일한 자판을 사용하여 숫자로만 이루어진 긴 문자열을 누가 가장 빠르게 타이핑하는지 겨루는 대회입니다.

대회에 참가하려는 민희는 두 엄지 손가락을 이용하여 타이핑을 합니다. 민희는 항상 왼손 엄지를 4 위에, 오른손 엄지를 6 위에 두고 타이핑을 시작합니다. 엄지 손가락을 움직여 다음 숫자를 누르는 데에는 일정 시간이 듭니다. 민희는 어떤 두 숫자를 연속으로 입력하는 시간 비용을 몇몇 가중치로 분류하였습니다.

  • 이동하지 않고 제자리에서 다시 누르는 것은 가중치가 1입니다.
  • 상하좌우로 인접한 숫자로 이동하여 누르는 것은 가중치가 2입니다.
  • 대각선으로 인접한 숫자로 이동하여 누르는 것은 가중치가 3입니다.
  • 같지 않고 인접하지 않은 숫자를 누를 때는 위 규칙에 따라 가중치 합이 최소가 되는 경로를 따릅니다.

예를 들어 1 위에 있던 손가락을 0 으로 이동하여 누르는 것은 2 + 2 + 3 = 7 만큼의 가중치를 갖습니다.
단, 숫자 자판은 버튼의 크기가 작기 때문에 같은 숫자 버튼 위에 동시에 두 엄지 손가락을 올려놓을 수 없습니다. 즉, 어떤 숫자를 눌러야 할 차례에 그 숫자 위에 올려져 있는 손가락이 있다면 반드시 그 손가락으로 눌러야 합니다.

숫자로 이루어진 문자열 numbers가 주어졌을 때 최소한의 시간으로 타이핑을 하는 경우의 가중치 합을 return 하도록 solution 함수를 완성해주세요.

 

 

입력 및 출력

 

더보기

제한사항

  • 1 ≤ numbers의 길이 ≤ 100,000
    • numbers는 아라비아 숫자로만 이루어진 문자열입니다.

 

입출력

numbers result
"1756" 10
"5123" 8

 

 


 

 

풀이

수많은 맞왜틀 끝에 다다른 또다른 문제... 이건 DP 경험이 부족해서 생긴 일이 아닐까 싶다. 맨 처음엔 브루트포스로 풀려는 뻘짓을 했으니 더더욱.

 

왼손, 오른손으로 하나의 number를 처리하는 경우의 수는 최대 2가지이므로, 브루트 포스로 풀게 되면 최대 2^100000의 경우의 수를 모두 테스트해봐야 한다. 즉 시간 초과에 걸린다. 따라서 우리는 dp를 적용해 봐야 한다.

 

잘 생각해 보면, 왼손과 오른손은 처음 시작 위치만 다를 뿐 구분의 의미가 없다. 또한 한 number를 처리할 때 가질 수 있는 최대 경우의 수는 10*9 / 2 = 45가지이다. 문제의 제약 조건 상 같은 숫자를 가리킬 수 없으므로 경우의 수는 더 줄어들게 된다. 즉 이 최대 경우의 수에 대해, 각 경우의 수가 지금의 number를 처리할 때 어떤 가중치로 변화하는지를 체크한다면 O(N)의 시간 내에 처리 가능하다. 이러한 접근법을 가지고 문제를 풀어보자.

 

  • weight_cal : 숫자 타자에서, 한 숫자에서 다른 숫자로 이동할 때 가중치가 부여되며 이 가중치를 계산하는 함수이다. 쉽게 생각해 보면 대각선 상에 위치, 직선 상에 위치, 이외의 경우로 생각해 볼 수 있으며 각각의 경우를 쉽게 계산 가능하다.
  • find_move_weight : 위 weight_cal 함수를 통해 모든 숫자에 대한 경우의 수를 나타내는 이차원 리스트를 완성하는 함수이다.
  • solution : 메인함수. 앞서 밝힌 알고리즘대로 각 number에 대해 다음 경우의 수를 구하게 되는 DP를 구현하였다. 또한, 각 경우의 수에 대해 최소 가중치를 다음 iteration으로 넘겨주게 된다. 또한 최대 경우의 수를 일일히 테스트해보는 것보다는, 시작 위치에서부터 생성 가능한 경우의 수만을 고려하게 되면 더 연산량이 줄어들게 되므로 이를 고려하였다.

 

 

풀이 코드

from collections import defaultdict

MAX = float('inf')
move_weight = [[-1]*10 for _ in range(10)]

num_coord = lambda x : ( (x-1) % 3, (x-1) // 3 ) if x != 0 else (1, 3)

def weight_cal(ix, iy, jx, jy) :
    if abs(ix - jx) == abs(iy - jy) :
        return 3 * abs(ix - jx) # 대각선방향
    if abs(ix - jx) == 0 :
        return 2 * abs(iy - jy) # 직선방향
    if abs(iy - jy) == 0 :
        return 2 * abs(ix - jx)
    # 혼합
    d_mv = max(abs(ix - jx), abs(iy - jy))
    return 3 + 2 * (d_mv - 1)

def find_move_weight():
    for i in range(10) :
        move_weight[i][i] = 1
        ix, iy = num_coord(i) 
        for j in range(i+1, 10) :
            jx, jy = num_coord(j)
            weight = weight_cal(ix, iy, jx, jy)
            move_weight[i][j] = move_weight[j][i] = weight   
            
def solution(numbers):
    find_move_weight()   
    length = len(numbers)

    num_dict = {(4, 6) : 0}
    
    for num in numbers :
        num = int(num)
        next_dict = defaultdict(lambda : MAX)
        for (lnum, rnum), weight in num_dict.items() :
            if lnum == num or rnum == num :
                next_dict[(lnum, rnum)] = min(next_dict[(lnum, rnum)], weight+1)
                continue
            lweight, rweight = move_weight[lnum][num], move_weight[rnum][num]
            for _weight, _num in [(lweight, rnum), (rweight, lnum)] :
                anum, bnum = (_num, num) if _num < num else (num, _num)
                next_dict[(anum, bnum)] = min(next_dict[(anum, bnum)], weight+_weight)

        num_dict = next_dict

    return min(num_dict.values())

풀이 완료!

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.