LCD 패널 방식으로 숫자를 표시해보기 (파이썬)

LCD 처럼 숫자를 표시하는 코드를 만들어 보자

크기와 출력할 숫자를 입력받는다. 크기는 LCD 표시 요소 하나의 크기를 가리킨다. (크기는 1~10,  숫자는 0~99,999,999)  가로선은 -, 세로선은 | 문자를 통해서 표현하며, 사이즈만큼 길이가 길어진다. 따라서 하나의 문자를 표시하기 위해서는 가로는 size + 2, 세로는 size * 2 + 3 만큼의 공간이 필요하다.

예시 – 출력: 0123456789. size: 3

 ---         ---   ---         ---   ---   ---   ---   --- 
|   |     |     |     | |   | |     |         | |   | |   |
|   |     |     |     | |   | |     |         | |   | |   |
|   |     |     |     | |   | |     |         | |   | |   |
             ---   ---   ---   ---   ---         ---   --- 
|   |     | |         |     |     | |   |     | |   |     |
|   |     | |         |     |     | |   |     | |   |     |
|   |     | |         |     |     | |   |     | |   |     |
 ---         ---   ---         ---   ---         ---   ---

접근

왼쪽과 같이 LCD 숫자 하나를 표현하기 위해서는 총 7개의 패널이 필요하다. 각 패널의 번호를 왼쪽과 같이 지정한다고 하자. 어떤 숫자를 표현하고자 한다면 각 패널이 어떤 숫자에서는 켜질 것인지, 켜지지 않을 것인지를 구분해야 한다. 예를 들어 위쪽 가로 방향 패널인 0의 경우에는 1을 그려야 하는 시점에는 켤 필요가 없다. 하지만 0이나 2, 3 등을 그려야 하는 시점에서는 켜져야 한다.

즉 0번 패널이 켜져야 하는 숫자는 아래 그림에서 알 수 있듯이, 0, 2, 3, 4, 6, 7, 8, 9 가 된다.  같은 방식으로 각 패널마다 어떤 숫자에 켜져야 할 것인지를 미리 정해두도록 하자.

이렇게 각 패널마다 켜져야 하는 숫자들을 정리하는 것은 생각보다 간단하다. 위 그림을 이용해서 쉽게 리스트를 완성할 수 있다.

masks = ("02356789", "045689", "01234789", "2345689", "02689", "013456789", "0235689")

이제 숫자를 출력할 차례이다. 우선 한개의 숫자를 출력하는 과정을 생각해보자. 숫자를 출력하는 과정은 가로선을 표현하는 구간 3군데와 세로선을 표현하는 구간 2군데로 나뉜다.

가로선 표현

패널 1개의 크기가 S인 숫자의 폭은 S + 2 (양쪽의 1씩 세로선이 표시되어야 하므로)이다. 따라서 가로선의 경우 표시하려는 숫자의 해당 가로 패널이 켜지는 경우에는 (공백) + (-) * S + (공백)을 출력하게 된다. 만약 패널이 꺼져 있는 경우에는 (-) 대신에 공백이 출력될 것이다. 따라서 맨 위에 가로선을 표시하는 것은 출력하려는 숫자 d 에 따라서 다음과 같은 문자열을 만들게 된다.

' ' + ('-' if d in masks[0] else ' ') * s + ' '
## "#---#" 또는 "#####" 

세로선 표현

세로선을 표현할 때는 한 줄에 1번 2번 혹은 4번 5번 패널을 같이 판단해야 한다. 표시되는 문자의 상황은 가로선과 반대로 양끝에 패널이 있고 가운데는 크기 값 만큼의 공백이 들어온다. 따라서 사이즈 3일 때의 둘째줄은 다음과 같이 만들어진다.

('|' if d in masks[1] else ' ') + ' ' * s + ('|' if d in masks[2] else ' ')

그리고 이렇게 만들어지는 세로선은 사이즈 값만큼 반복하여 라인을 만들어야 한다. 사이즈 3인 경우에는 2,3,4 번 줄이 같은 내용을 반복 출력한다.

이후 가운데 가로선과, 다시 아래쪽 세로선, 마지막으로 바닥자리 가로선을 같은 식으로 표시한다.

정리

한 번에 한 글자에 대해서 한줄씩 문자열을 만드는 방법을 알아보았으니, 이번에는 여러 글자를 출력할 때 한줄은 어떻게 만드는지에 대해서 알아보자. 이미 모든 글자는 같은 규격으로 생성되며, 매번 줄을 만드는 방법을 알았다. 따라서 여러 글자의 경우에는 각 글자에서 해당줄의 문자열을 생성하고 공백으로 구분하여 하나로 합치면 된다. 이는 str.join() 을 이용하면 된다. 출력해야하는 숫자 문자열을 digits 라고 하면, 해당 코드는 리스트 축약을 통해서 손쉽게 표현할 수 있다.

## 가로줄
' '.join(
    [(' ' + ('-' if d in mask[0] else ' ') * s + ' ') for d in digits]
}

## 세로줄
' '.join(
    [('|' if d in mask[1] else ' ') + ' ' * s + ('|' if d in mask[2] else ' ')\
     for d in digits]
)

이렇게 각 줄을 생성하여 리스트에 담고 개행문자를 끼워서 하나로 합치면 출력해야하는 최종 문자열이 완성된다. 이제 코드를 정리해보자.

def make_presentation(digits, s=1):
  masks = ("02356789", "045689", "01234789", 
           "2345689", "02689", "013456789", "0235689")
  result = []  
  ## 윗줄
  result.append(' '.join([' '+('-' if d in masks[0] else '-') * s + ' '\
                          for d in digits]))
  ## 위쪽 세로줄
  for _ in range(s):
    result.append(' '.join([ ('|' if d in masks[1] else ' ') + ' ' * s +\
                             ('|' if d in masks[2] else ' ') + ' '\
                            for d in digits]))
  
  ## 가운데 가로줄
  result.append(' '.join([' '+('-' if d in masks[3] else '-') * s + ' '\
                          for d in digits]))

  ## 위쪽 세로줄
  for _ in range(s):
    result.append(' '.join([ ('|' if d in masks[4] else ' ') + ' ' * s +\
                             ('|' if d in masks[5] else ' ') + ' '\
                            for d in digits]))

  ## 바닥줄
  result.append(' '.join([' '+('-' if d in masks[6] else '-') * s + ' '\
                          for d in digits]))

  return '\n'.join(result)

def main():
  digits, size = input().split()[:2]
  size = int(size)
  print(make_presentation(digits, size))

정리

중복되는 복잡한 코드가 너무 많아서 이를 별도의 람다식으로 빼내었다.

(연습문제) 리사의 워크북 문제

Lisa의 워크북 문제

https://www.hackerrank.com/challenges/bear-and-workbook

n개의 챕터가 있는 워크북이 있고, 이 워크북의 매 페이지는 k 개 문제를 담을 수 있다.
이후에 n 개의 정수를 받는데 이는 각 챕터의 문제 수이다. 각 챕터의 문제는 1번 부터 시작하며,
새로운 챕터는 새 페이지에서 시작한다.

이 때, 문제번호와 페이지번호(1번부터 시작)가 같은 문제를 특별한 문제라고 할 때, 주어진 데이터 내에서 특별한 문제의 개수를 구하라는 내용이다.

풀이

이 문제는 하스켈에서 의외로 쉽게 풀린다. 다음과 같은 순서로 풀어보자.

  1. 숫자 x 를 받았을 때, [[1,2,3], [4,5,6], [7]] 과 같이 해당 챕터의 페이지 구성을 리턴하는 함수를 하나 작성한다.
  2. 입력받은 숫자들에 대해서 1의 함수를 맵핑하고, 이를 concat으로 폴드한다.
  3. 2의 결과를 [1..] 무한집합과 zipping 한다. 그러면 (페이지번호, [문제번호])로 구성된 리스트가 나오는데
  4. 3에서 페이지번호가 문제번호에 포함된 (elem 함수를 쓴다) 것의 개수를 센다.

코드는 아래와 같다. 가장 기본이 되는 핵심 아이디어는 processChapter를 구현하는 것인데, drop, take를 잘 써서 쉽게 구현할 수 있다.

import Control.Applicative
import Data.List

processChapter :: Int -> Int -> [[Int]]
processChapter k n = helper k [] [1..n]
  where
    helper :: Int -> [[Int]] -> [Int] -> [[Int]]
    helper _ ys [] = reverse ys
    helper k ys xs = 
      let ps = take k xs in helper k (ps:ys) (drop k xs)

solve :: [Int] -> Int
solve k xs = let ys = zip [1..] . concat . map (processChapter k) $ xs
              in length [x | x <- ys, test x]
                   where test (a, bs) = elem a bs

main = do
  [n,k] <- (map read . words) <$> getLine
  s <- (solve k . map read . take n . words) <$> getLine
  print s

그리드 검색 문제

h, w 를 입력받고, h개만큼의 줄을 입력받아 wxh 의 그리드를 구성한다.
이렇게 2개의 그리드를 입력받아서 (단, 이 때 두 번째 그리드가 더 작다)
큰 그리드 내에 작은 그리드가 있으면 YES, 없으면 NO를 출력한다.

풀이

a 타입의 요소로 이루어진 그리드는 [[a]]로 나타낼 수 있다. 주어진 리스트의 특정 범위를 슬라이스할 수 있는 함수로부터 출발하면 특정 위치에서의 매칭 여부를 검사할 수 있고, 다시 이를 전체에 반복하여 결과를 얻을 수 있다.

역시나 T회만큼 반복하면서 그리드의 크기와 그리드 데이터를 입력받는 과정을 처리하기가 매우 불편하다.

import Control.Applicative
import Control.Monad

range (i, j) = take (j - i) . drop i

myLookupHelper :: (Eq a) => (Int, Int) -> [[a]] -> [[a]] -> Bool
myLookupHelper (x, y) ms ts =
  let w = length . head $ ts
      h = length ts
      rs = range (y, y+h) . map (range (x, x+w)) $ ms
   in rs == ts

myLookup :: (Eq a) => [[a]] -> [[a]] -> String
myLookup ms ts =
  let mw = length . head $ ms
      mh = length ms
      tw = length . head $ ts
      th = length ts
      pos = [(x, y) | x <- [0..(mw-tw)], y <- [0..(mh-th)]]
   in if any (\p -> myLookupHelper p ms ts) pos then "YES" else "NO"

solve :: IO String
solve = do
  [mw, mh] <- (map (read :: String -> Int) . words) <$> getLine
  ms <- forM [1..mw] (const getLine)
  [tw, th] <- (map (read :: String -> Int) . words) <$> getLine
  ts <- forM [1..tw] (const getLine)
  return $ myLookup ms ts

main = do
  n <- readLn :: IO Int
  rs <- forM [1..n] $ (const solve)
  putStrLn . unlines $ rs

(연습문제) 행렬의 대각선의 차

대각선의 차이 구하기

N*N 크기의 행렬이 주어졌을 때, 이 행렬의 대각선상에 위치하는 요소들의 합끼리의 차를 구하는 프로그램을 작성하는 문제이다.

https://www.hackerrank.com/challenges/diagonal-difference

(연습문제) 행렬의 대각선의 차 더보기

(연습문제) 대소문자변환

연습문제 : 대소문자 변환하기

입력받은 문자열의 대소문자를 반전하여 출력하는 프로그램을 작성하시오.

입력받은 각 글자의 문자 코드가 ‘a’ … ‘z’ 사이에 있으면 소문자, ‘A’~’Z’ 사이에 있으면 대문자이다. 그리고 그 변환은 해당 코드값에 a - A를 더하거나 빼주면 된다.

먼저 대소문자의 범위를 찾아보자.

print("azAZ".utf8.map(Int.init))
//=> [97, 122, 65, 90]

즉 문자 코드가 97~122 구간에 있으면 소문자, 65~90 구간에 있으면 대문자이다. 소문자->대문자 변환은 32를 빼고, 반대의 변환은 32를 더한다.

문자 코드 값으로 다시 문자를 만드는 방법은 다음과 같다.

  1. 문자열은 [Character] 타입으로 만들 수 있다.
  2. 코드값으로 Character 인스턴스를 만들기 위해서는 Int 값을 UnicodeScalar 값으로 변환하면 된다.

즉, 다음과 같이 변경할 수 있다.

let arr = [97, 122, 65, 90]
let str = String(arr.map{ Character(UnicodeScalar($0)) })
print(str)
//=> "azAZ"

전체 코드는 다음과 같다.

if let s = readLine(), case let u = s.utf8.map(Int.init) {
    let h: Int -> Int = { n in
        switch n {
        case 97...122: return n - 32
        case 65...96: return n + 32
        default: return n
        }
    }
    let result = String(u.map{ Character(UnicodeScalar(h($0))) })
    print(result)
}

위의 switch 문은 간단히 삼항 연산자의 중첩으로 쓸 수 있다. 범위(Range<Int>)에 특정 값이 있는지는 패턴매칭으로 찾는데, 패턴매칭은 연산자 ~=를 쓰므로 다음과 같이 코드를 줄일 수 있다.

/* shorter version */
if let s = readLine(), case let u = s.utf8.map(Int.init) {
    let h: Int -> Int = { n in return 97...122 ~= n ? n - 32 : 65...96 ~= n ? n + 32 : n }
    let result = String(u.map{ Character(UnicodeScalar(h($0))) })
    print(result)
}

하스켈 풀이

하스켈의 경우, Data.Char 모듈에 isUpper, toUpper, toLower 함수가 정의되어 있으므로 이를 이용할 수 있다.

import Data.Char
main = interact (map f) where f c = if isUpper c then toLower c else toUpperc

interact 함수는 String -> String -> IO () 타입의 함수로 입력받은 문자열을 처리해주는 함수를 받아서 입력과 출력을 연결해주는 함수이다. 즉,

main = getLine >= (\xs -> putStrLn . (map f) $ xs)

를 줄인 것이다.