러스트로 문제 풀기
러스트는 C/C++ 수준으로 빠릅니다. 빡빡한 시간 제한이 있는 알고리즘 문제를 푼다면 러스트는 괜찮은 선택지입니다. 그런데 러스트로 문제를 풀다보면 좀 성가신 부분도 있습니다. 입력 파싱은 쉽게 너저분해지고, 조심하지 않으면 실행 시간의 대부분을 출력에 쓰게 됩니다. 다른 언어에서 마음껏 사용하던 전역 변수를 못 쓰니 그 변수들을 모두 함수의 인자로 넘겨주기 시작하고, 함수 인자가 자꾸 늘어납니다. 이 글은 러스트로 알고리즘 문제를 풀 때 흔히 겪는 문제와 해결책을 소개합니다.
입출력
한 번에 입력 받기
입력은 두 가지 이유로 가능하면 한 번에 String에 담는 것이 좋습니다. 먼저 Read 트레잇의 메서드보다 String의 메서드가 더 다양하고 쓰기 편합니다. 두 번째로 시스템 호출이 줄어들어 입출력 시간을 절약할 수 있습니다. 입력을 한 번에 String에 담으려면 아래처럼 하면 됩니다.
use std::io::{self, Read};
fn main() {
let mut buf = String::new();
io::stdin().read_to_string(&mut buf).unwrap();
// 여기서 `buf`를 사용하면 됩니다. `buf`에는 입력 전체가 담겨있습니다.
}
메모리 제한이 있는 일부 문제에서는 한번에 입력을 다 받아오기 어려울 수 있는데, 이런 경우에는 BufReader를 이용하여 적절한 크기의 버퍼를 설정하면 됩니다. BufReader를 사용하지 않더라도 러스트 표준 라이브러리는 8 KB의 버퍼를 가지고 입력을 처리합니다. 즉, 버퍼의 크기를 바꿔가며 실행 시간을 줄여보려고 한다면 적어도 8 KB보다는 크게 설정해야 합니다.
깔끔하게 입력 파싱하기
러스트는 C의 scanf() 같은 함수가 없는데다 명시적인 예외 처리를 요구하기 때문에 입력 파싱을 위해 아래처럼 같은 코드를 반복해서 작성하게 됩니다.
let mut slices = buf.split_whitespace();
let i = slices.next().unwrap().parse::<i32>().unwrap();
let j = slices.next().unwrap().parse::<i32>().unwrap();
let f = slices.next().unwrap().parse::<f32>().unwrap();
let d = slices.next().unwrap().parse::<f64>().unwrap();
이럴 때는 아래처럼 매크로를 선언하고, 호출하는 시점에서 타입 추론을 유도하면 반복되는 코드를 줄일 수 있습니다.
let mut slices = buf.split_whitespace();
macro_rules! next {
() => { slices.next().unwrap().parse().unwrap() };
}
let i: i32 = next!();
let j: i32 = next!();
let f: f32 = next!();
let d: f64 = next!();
한 번에 출력하기
여러 줄에 걸친 출력을 수행할 때에는 println!()의 사용을 최소화해야 합니다. println!()을 사용하면 출력이 버퍼링되지 않기 때문입니다. 러스트 표준 라이브러리는 표준 출력을 줄 단위로 버퍼링하기 때문에 (새줄 문자를 출력하는) println!()이 사용될 때마다 버퍼의 내용을 모두 내보냅니다. 너무 빈번한 내보내기는 긴 실행 시간으로 이어질 수 있습니다.
간단한 해결책은 출력을 String에 모았다가 한번에 내보내는 것입니다. writeln!() 매크로를 사용하면 println!() 매크로처럼 표준 포맷팅을 사용할 수 있어 간편합니다.
use std::fmt::Write;
fn main() {
let mut buf = String::new();
writeln!(buf, "{}", 42).unwrap(); // buf는 "42"가 됩니다.
println!("{}", buf); // 실제 출력은 한번에 수행합니다.
}
입력에서와 마찬가지로, 메모리 초과 문제가 발생하는 경우에는 한 번에 출력하는 대신 BufWriter를 사용하면 됩니다.
전역변수
러스트에서는 가변 전역변수의 사용이 상당히 까다롭습니다. 그래서 여러 함수가 상태를 공유해야하는 상황이 오면 (다른 언어에서는 으레 전역변수로 선언했을) 변수들을 인자로 넘겨가며 사용하다가 아래처럼 인자가 비슷비슷한 함수가 잔뜩 생기고 맙니다.
fn n_queen(ld: u16, col: u16, rd: u16, q: usize, n: usize) -> i32 { /* ommited */ }
fn n_queen_helper(ld: u16, col: u16, rd: u16, q: usize, n: usize) -> i32 { /* ommited */ }
fn n_queen_another_helper(ld: u16, col: u16, rd: u16, q: usize, n: usize) -> bool { /* ommited */ }
이런 경우에는 해당 변수들을 묶어 구조체를 정의합니다. 그러면 그 구조체의 메서드에서는 해당 변수들을 전역변수처럼 쓸 수 있습니다. (물론 self는 써야하지만요)
struct NQueen {
ld: u16,
col: u16,
rd: u16,
q: usize,
n: usize
}
impl NQueen {
fn solve(&mut self) -> u32 { /* ommited */ }
fn helper(&mut self) -> u32 { /* ommited */ }
fn another_helper(&mut self) -> u32 { /* ommited */ }
}
마치는 말
이 글은 러스트 1.54를 기준으로 작성되었습니다. 러스트는 빠르게 진화하는 언어이기 때문에 이 글의 내용이 이후 버전에서는 유효하지 않을 수 있습니다. 특히 출력의 줄 단위 버퍼링 문제는 곧 수정될 예정입니다. (관련 GitHub PR)
그럼, 즐거운 코딩 되세요!