聰明不如鈍筆
총명불여둔필
assignment KindeR

R로 유사 '숫자 야구' 퀴즈 풀기(feat. str_split)

'숫자 야구'와 비슷한 숫자 퀴즈

'페이스북'에서 이런 퀴즈를 발견하고 종이와 연필로 열심히 도전해 보았습니다.

 

숫자 야구와 거의 같은 구조인데 결과는 '꽝'.

 

그래서 '차라리 코드를 짜보자'고 마음을 고쳐먹었습니다.

 

R로 '스도쿠(數獨)'도 풀 수 있다는 건 이런 퀴즈 역시 풀 수 있다는 뜻일 테니까요.

 

무한도전 '숫자야구' 특집 이미지

혹시 모르시는 분이 계실까 봐 말씀드리면 숫자 야구에서는 △해당 숫자가 정답 안에 들어 있는데 위치가 다를 때는 '볼' △위치까지 정확하게 맞으면 '스트라이크'라고 규정합니다.

 

예를 들어 12345가 정답인데 54321이라고 했다면 숫자가 모두 있고 3은 자리까지 정확하게 맞았으니 4볼 1스트라이크가 되는 방식입니다.

 

다만 우리가 푸는 문제에서는 이럴 때 5볼 1스트라이크처럼 표현합니다.

 

이 차이는 간단하게 바꿀 수 있지만 일단 문제에 맞게 5볼 1스트라이크 방식으로 가겠습니다.

 

숫자 뭉치를 이해하는 방식

인간은 숫자 야구 게임을 할 때 12345이라는 숫자를 1, 2, 3, 4, 5로 나눠 생각할 수 있지만 컴퓨터는 1만2345라고 생각할 따름입니다.

 

그래서 c(1, 2, 3, 4, 5)처럼 벡터 형태로 풀어 줘야 합니다.

 

이렇게 풀어주고 나서 이 숫자가 저 숫자 안에 들어 있는지 아닌지 확인할 때는 '%in%' 연산자를 쓰면 됩니다.

 

c(1, 2, 3, 4, 5) %in% c(5, 4, 3, 2, 1)
## [1] TRUE TRUE TRUE TRUE TRUE

 

TRUE는 1, FALSE는 0이니까 이 결과를 더하면 볼 개수를 확인할 수 있습니다.

 

sum(c(1, 2, 3, 4, 5) %in% c(5, 4, 3, 2, 1))
## [1] 5

 

같은 원리로 스트라이크는 '==' 연산자를 써서 확인하면 그만입니다.

 

sum(c(1, 2, 3, 4, 5) == c(5, 4, 3, 2, 1)) 
## [1] 1

 

문제는 숫자를 계속해 이렇게 쓰는 건 번거로워도 너무 번거로운 일이라는 점입니다.

 

자동으로 숫자를 분류해 주는 기계 그러니까 '함수'를 써서 이 문제를 해결해 보겠습니다.

 

숫자 분류기를 만들어 보자

시작은 늘 그런 것처럼 tidyverse 패키지 (설치하고) 불러오기.

 

#install.packages('tidyverse')
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.0     ✔ readr     2.1.4
## ✔ forcats   1.0.0     ✔ stringr   1.5.0
## ✔ ggplot2   3.4.1     ✔ tibble    3.1.8
## ✔ lubridate 1.9.2     ✔ tidyr     1.3.0
## ✔ purrr     1.0.1     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors

 

tidyverse 안에는 문자열을 다룰 때 쓰는 stiringr 패키지가 들어 있습니다.

 

그리고 이 패키지 안에 들어 있는 str_split() 함수를 쓰면 문자열을 쪼갤 수 있습니다.

 

이렇게 말입니다.

 

'12345' %>% 
  str_split(., '')
## [[1]]
## [1] "1" "2" "3" "4" "5"

 

리스트 형태로 나왔다고 걱정할 필요는 없습니다. 

 

그냥 .[[1]]로 선택해 주면 그만입니다.

 

'12345' %>% 
  str_split(., '') %>% 
  .[[1]]
## [1] "1" "2" "3" "4" "5"

 

문자열로 나온 것도 걱정할 필요가 없습니다.

 

as.numeric() 함수를 쓰면 다시 숫자로 바꿀 수 있습니다.

 

'12345' %>% 
  str_split(., '') %>% 
  .[[1]] %>% 
  as.numeric()
## [1] 1 2 3 4 5

 

지금까지 알아본 내용을 바탕으로 count_check() 함수를 만들어 보겠습니다.

 

숫자 두 개를 입력 받아 볼과 카운트를 알려주는 방식입니다.

 

지금까지 잘 따라 오신 분이라면 아래 함수를 별로 어렵지 않게 이해하실 수 있을 겁니다.

 

count_check <- function(answer, guess){
  str_split(answer, '') %>% .[[1]] %>% as.numeric() -> answer
  str_split(guess, '') %>% .[[1]] %>% as.numeric() -> guess
  sum(guess %in% answer) -> balls
  sum(guess == answer) -> strikes 
  str_c(balls, '볼 ', strikes, '스트라이크') -> result
  return(result)
}

 

실제로 잘 작동하는지 봐야겠죠?

 

12345와 54321을 넣으면 숫자는 다 같고 3이 겹치니까 5볼 1스트라이크가 나와야 합니다.

 

count_check(12345, 54321)
## [1] "5볼 1스트라이크"

 

네, 예상대로 결과가 잘 나왔습니다.

 

우리는 힌트가 이미 있을 때 정답을 맞추는 거니까 코드를 조금 더해야 합니다.

 

그래서 볼과 스트라이크를 알려주고 이 결과가 맞는지 확인하는 과정이 필요합니다.

 

이 기능을 하는 code_cracker() 함수는 이렇게 쓸 수 있습니다.

 

code_cracker <- function(code, guess, balls, strikes){
  str_split(code, '') %>% .[[1]] %>% as.numeric() -> code
  str_split(guess, '') %>% .[[1]] %>% as.numeric() -> guess
  sum(guess %in% code) -> catch
  sum(guess == code) -> command 
  catch == balls & command == strikes -> result
  return(result)
}

 

이 함수는 TRUE, FALSE로 결과를 출력합니다.

 

12345가 정답일 때 61785를 넣으면 1과 5가 겹치니까 2볼, 5는 위치도 맞으니까 1스트라이크입니다.

 

이 결과가 맞는지 확인해 보면 이런 결과가 나옵니다.

 

code_cracker(12345, 61785, 2, 1)
## [1] TRUE

 

숫자 목록이 주루룩 있을 때 위에 나온 힌트 다섯 가지를 모두 충족시키는 숫자가 정답입니다.

 

여기서 숫자 야구 규칙 하나를 더 적용하겠습니다.

 

숫자 야구에는 똑같은 숫자가 두 번 나올 수 없습니다.

 

이번에는 n_distinct() 함수를 쓰면 이를 확인할 수 있습니다.

 

'12345' %>% 
  str_split(., '') %>% 
  .[[1]] %>% 
  as.numeric() %>% 
  n_distinct()
## [1] 5

 

이제 목록을 만들어 보겠습니다.

 

숫자 야구에 쓸 수 있는 숫자는 01234부터 99999까지입니다.

 

그런데 일반적으로는 0으로 시작하는 숫자를 만들 수 없습니다.

 

str_pad() 함수를 쓰면 이 문제를 피해갈 수 있습니다.

 

tibble(code = str_pad(0:99999, width = 5, side = 'left', pad = '0'))
## # A tibble: 100,000 × 1
##    code 
##    <chr>
##  1 00000
##  2 00001
##  3 00002
##  4 00003
##  5 00004
##  6 00005
##  7 00006
##  8 00007
##  9 00008
## 10 00009
## # ℹ 99,990 more rows

 

이 숫자 역시 자리를 쪼개야 하니까 str_split() 함수를 다시 써야 합니다.

 

행마다 다른 결과가 나와야 하니까 rowwise() 함수를 쓰고 열에 넣어야 하니 list() 함수도 같이 쓰겠습니다.

 

편의상 12344와 12345 행만 가지고 작업을 이어갑니다.

 

tibble(code = str_pad(12344:12345, width = 5, side = 'left', pad = '0')) %>% 
  rowwise() %>% 
  mutate(digits = str_split(code, '') %>% .[[1]] %>% as.numeric() %>% list())
## # A tibble: 2 × 2
## # Rowwise: 
##   code  digits   
##   <chr> <list>   
## 1 12344 <dbl [5]>
## 2 12345 <dbl [5]>

 

열에 리스트가 들어 있을 때는 어떤 내용이 들어있는지 보지 못합니다.

 

내용이 정 궁금할 때는 unnest() 함수로 열어볼 수 있습니다.

 

tibble(code = str_pad(12344:12345, width = 5, side = 'left', pad = '0')) %>% 
  rowwise() %>% 
  mutate(digits = str_split(code, '') %>% .[[1]] %>% as.numeric() %>% list()) %>% 
  unnest(digits)
## # A tibble: 10 × 2
##    code  digits
##    <chr>  <dbl>
##  1 12344      1
##  2 12344      2
##  3 12344      3
##  4 12344      4
##  5 12344      4
##  6 12345      1
##  7 12345      2
##  8 12345      3
##  9 12345      4
## 10 12345      5

 

계속해 서로 다른 숫자가 몇 개 들어 있는지 확인해 보겠습니다.

 

tibble(code = str_pad(12344:12345, width = 5, side = 'left', pad = '0')) %>% 
  rowwise() %>% 
  mutate(digits = str_split(code, '') %>% .[[1]] %>% as.numeric() %>% list(),
         unique = n_distinct(digits))
## # A tibble: 2 × 3
## # Rowwise: 
##   code  digits    unique
##   <chr> <list>     <int>
## 1 12344 <dbl [5]>      4
## 2 12345 <dbl [5]>      5

 

이중에서 우리에게 필요한 건 서로 다른 숫자가 5개 들어 있을 때니까 잘라냅니다.

 

tibble(code = str_pad(12344:12345, width = 5, side = 'left', pad = '0')) %>% 
  rowwise() %>% 
  mutate(digits = str_split(code, '') %>% .[[1]] %>% as.numeric() %>% list(),
         unique = n_distinct(digits)) %>% 
  filter(unique == 5)
## # A tibble: 1 × 3
## # Rowwise: 
##   code  digits    unique
##   <chr> <list>     <int>
## 1 12345 <dbl [5]>      5

 

지금까지 작업한 내용을 바탕으로 실전용으로 00000부터 99999까지 같은 작업을 처리한 뒤 codes라는 객체에 넣어놓겠습니다.

 

tibble(code = str_pad(00000:99999, width = 5, side = 'left', pad = '0')) %>% 
  rowwise() %>% 
  mutate(digits = str_split(code, '') %>% .[[1]] %>% as.numeric() %>% list(),
         unique = n_distinct(digits)) %>% 
  filter(unique == 5) -> codes

 

이제 마지막 단계만 남았습니다.

 

처음에 나온 힌트를 가지고 각 숫자에 적용해 보면 이런 결과가 나옵니다.

 

codes %>%
  rowwise() %>% 
  mutate(
    hint1 = code_cracker(code, 82619, 2, 0),
    hint2 = code_cracker(code, 47530, 3, 0),
    hint3 = code_cracker(code, 86302, 2, 1),
    hint4 = code_cracker(code, 91704, 2, 2),
    hint5 = code_cracker(code, 70629, 1, 1),
  )
## # A tibble: 30,240 × 8
## # Rowwise: 
##    code  digits    unique hint1 hint2 hint3 hint4 hint5
##    <chr> <list>     <int> <lgl> <lgl> <lgl> <lgl> <lgl>
##  1 01234 <dbl [5]>      5 TRUE  FALSE FALSE FALSE FALSE
##  2 01235 <dbl [5]>      5 TRUE  FALSE FALSE FALSE FALSE
##  3 01236 <dbl [5]>      5 FALSE FALSE FALSE FALSE FALSE
##  4 01237 <dbl [5]>      5 TRUE  FALSE FALSE FALSE FALSE
##  5 01238 <dbl [5]>      5 FALSE FALSE FALSE FALSE FALSE
##  6 01239 <dbl [5]>      5 FALSE FALSE FALSE FALSE FALSE
##  7 01243 <dbl [5]>      5 TRUE  TRUE  FALSE FALSE FALSE
##  8 01245 <dbl [5]>      5 TRUE  TRUE  FALSE FALSE FALSE
##  9 01246 <dbl [5]>      5 FALSE FALSE FALSE FALSE FALSE
## 10 01247 <dbl [5]>      5 TRUE  TRUE  FALSE FALSE FALSE

 

이 다섯 가지 힌트 모두 TRUE인 숫자가 정답이겠죠?

 

codes %>%
  rowwise() %>% 
  mutate(
    hint1 = code_cracker(code, 82619, 2, 0),
    hint2 = code_cracker(code, 47530, 3, 0),
    hint3 = code_cracker(code, 86302, 2, 1),
    hint4 = code_cracker(code, 91704, 2, 2),
    hint5 = code_cracker(code, 70629, 1, 1),
  ) %>% 
  filter(hint1, hint2, hint3, hint4, hint5)
## # A tibble: 1 × 8
## # Rowwise: 
##   code  digits    unique hint1 hint2 hint3 hint4 hint5
##   <chr> <list>     <int> <lgl> <lgl> <lgl> <lgl> <lgl>
## 1 51324 <dbl [5]>      5 TRUE  TRUE  TRUE  TRUE  TRUE

 

네, 정답은 51324였습니다.

 

늘 말씀드리듯 정답이 이것만 있는 건 아닙니다.

 

그럼 Happy Tidyversing -_-)/

 

참고로 같은 내용을 파이썬으로는 이렇게 쓸 수 있습니다.

 

import pandas as pd
import numpy as np

def str_to_digits(s):
    return [int(char) for char in s]

def code_cracker(code, guess, balls, strikes):
    code_digits = str_to_digits(code)
    guess_digits = str_to_digits(str(guess).zfill(5))
    catch = sum(np.isin(guess_digits, code_digits))
    command = sum(np.array(guess_digits) == np.array(code_digits))
    return (catch == balls) and (command == strikes)

codes = pd.DataFrame({
    'code': [str(i).zfill(5) for i in range(100000)]
})

codes['digits'] = codes['code'].apply(str_to_digits)
codes['unique'] = codes['digits'].apply(lambda x: len(set(x)))

codes_filtered = codes[codes['unique'] == 5].copy()

codes_filtered['hint1'] = codes_filtered['code'].apply(lambda x: code_cracker(x, 82619, 2, 0))
codes_filtered['hint2'] = codes_filtered['code'].apply(lambda x: code_cracker(x, 47530, 3, 0))
codes_filtered['hint3'] = codes_filtered['code'].apply(lambda x: code_cracker(x, 86302, 2, 1))
codes_filtered['hint4'] = codes_filtered['code'].apply(lambda x: code_cracker(x, 91704, 2, 2))
codes_filtered['hint5'] = codes_filtered['code'].apply(lambda x: code_cracker(x, 70629, 1, 1))

answer = codes_filtered[
    codes_filtered[['hint1', 'hint2', 'hint3', 'hint4', 'hint5']].all(axis=1)
]

print(answer)

 

댓글,

KindeR | 카테고리 다른 글 더 보기