'페이스북'에서 이런 퀴즈를 발견하고 종이와 연필로 열심히 도전해 보았습니다.
숫자 야구와 거의 같은 구조인데 결과는 '꽝'.
그래서 '차라리 코드를 짜보자'고 마음을 고쳐먹었습니다.
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)
댓글,