RustでAtCoderをやってみた / Rustの良さを語る
この記事は 東京高専プロコンゼミ1年 Advent Calendar 2023 17日目の記事です。
RustでAtCoderをやってみたら想像以上に快適だったので書いてみる。
まずは環境構築
WSL2とVSCodeでやった。
Cargo CompeteというRustで競技プログラミングをやるための神ツールがあるので導入した。
cargo compete init atcoder .
で作業ディレクトリを登録して
cargo compete new abcXXX
でテストケースなどを取り込んだディレクトリを作成する。
するとこんな感じになる
作業ディレクトリ
├── abcXXX
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── src
│ │ └── bin
│ │ ︙
│ └── testcases
│ ︙
├── abcXXX
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── src
│ │ └── bin
│ │ ︙
│ └── testcases
│ ︙
├── abcXXX
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── src
│ │ └── bin
│ │ ︙
│ └── testcases
│ ︙
︙
└── target
│ ︙
└── compete.toml
└── template-cargo-lock.toml
cargo compete test a
でテストしたり
cargo compete submit a
でAtCoderに即座にSubmitしたりできる。
導入の際、cargo compete init atcoder .で生成されるディレクトリの下にある、「compete.toml」でlanguage_id = "5054"
と書き換えないとSubmitが失敗する点でやや躓いた。
自分はより速くタイプできるように、~/.bashrcに
alias cc='cargo compete'
alias cct='cargo compete test'
alias ccs='cargo compete submit'
ccn () {
cd ~/rust
cargo compete new $1
cd ~/rust/$1
code .
}
を追記していい感じにしてみた。参考にする場合はcd ~/rust
, code .
の部分を環境に応じて適宜変更していただきたい。
Rustのここがすごい!
イテレータ系メソッドがすごい!
use itertools::Itertools;
use proconio::input;
use proconio::marker::Chars;
fn main() {
input! {
n: usize,
s: Chars,
};
let mut l: Vec<(char, usize)> = Vec::new();
for i in 0..n {
if i == 0 || s[i] != s[i - 1] {
l.push((s[i], 1));
} else {
l.last_mut().unwrap().1 += 1;
}
}
let ans = l
.into_iter()
.sorted_by_key(|v| v.1)
.rev()
.unique_by(|v| v.0)
.fold(0, |acc, cur| acc + cur.1);
println!("{}", ans);
}
C++で同じようなことを書くとこうなると思います。
#include <bits/stdc++.h>
using namespace std;
int main() {
size_t n;
string s;
cin >> n;
cin >> s;
vector<pair<char, int>> l;
for (size_t i = 0; i < n; i++) {
if (i == 0 || s[i] != s[i - 1]) {
l.push_back(make_pair(s[i], 1));
} else {
l.back().second++;
}
}
sort(l.begin(), l.end(), [](pair<char, int> a, pair<char, int> b) {
if (a.first == b.first) {
return a.second > b.second;
}
return a.first < b.first;
});
l.erase(unique(l.begin(), l.end(), [](pair<char, int> a, pair<char, int> b) {
return a.first == b.first;
}), l.end());
int ans = accumulate(l.begin(), l.end(), 0, [](int acc, pair<char, int> p) {
return acc + p.second;
});
cout << ans << endl;
}
Rustの方が、メソッドが非破壊的であるためチェーンすることができ、「ペアの2コ目の値でソートして、反転して、1コ目の値でuniqueして、2コ目の値の和を取る」という操作が簡潔に記述できていると思います。
コンパイラが優しい
これはRustの型変換エラーの例です。「i32を期待したがusizeだった」「.try_into().unwrap()
を使って変換することができる」と親切に教えてくれます。嬉しい。
Result型やOption型がイケてる
Rustでは、失敗する可能性のあるメソッドはResult型(Ok()
かErr()
)を返すようになっていて、仮にそのメソッドが失敗したとしても「エラーであったことを示すモノ」が返ってくるだけです。例えば、
// "abc"をi32(32ビット整数)としてパースしろ、という意味、当然失敗する
let hoge = "abc".parse::<i32>();
と書いても、ここですぐpanicはしません。
let hoge = "abc".parse::<i32>().unwrap();
.unwrap()
によって値を取り出そうとして始めてpanicを起こします。
この仕様によって多様にエラーハンドリングをすることができます。
// エラーの場合にpanic
let hoge = "abc".parse::<i32>().unwrap();
// エラーの場合に0を返す
let hoge = "abc".parse::<i32>().unwrap_or(0);
// 成功ならtrue、失敗ならfalse
let hoge = "abc".parse::<i32>().is_ok();
// matchでよしなに
let result = "abc".parse::<i32>();
let hoge = match result {
Ok(num) => format!("Success: {}", num),
Err(err) => format!("Failure: {}", err),
};
// if letでよしなに
let result = "abc".parse::<i32>();
if let Ok(num) = result {
Ok(num) => println!("Success: {}", num),
}
一方Option型は、値が無い可能性のある場合の戻り値として使用されます。
// 「空の配列から最後の値を読み出す」という意味、無いものは無い
let empty: Vec<i32> = Vec::new();
let hoge = empty.last();
これも.unwrap()
や.unwrap_or()
などで値を取り出して扱うことができます。
良い
おしまい
Rustはいいぞ
Discussion