少ないリソースを酷使する

低レイヤーとレトロPC(PC98,MSX)が好きな情報学生

競プロの過去問をPython, Go, Rust 使って黙々と解く_Day1

苦行

「競プロの過去問をPython, Go, Rust 使って黙々と解く」 という、1人アドベントカレンダーを立てました。
今日はその記念すべき1日目です!(遅刻)🎉

始めた理由

各言語を選定した理由は以下の通り。

  • Python・・・競プロで使いたいから
  • Go・・・面白そうだから
  • Rust・・・OSの研究とかで使いたいから

で、「各言語に慣れるには・・・」と考えたときに競プロの問題を例題に素振りをしようと思ったわけです。
この学習方法が良いのかはわかりませんが、とにかく「物は試し」やっていきます。💪

Qiitaとかに投稿するような内容でも無いので、はてなブログを新たに開設して書いていくことにしました。 ブログ初心者ですので悪しからず。

ルール

一応1日目なので、このアドベントカレンダー(1人)のルールを決めておきます。

  1. 言語は Python, Go, Rust それぞれを毎回使う
  2. 解く問題は「AtCoder Beginner Contest」の過去問、AB問題中心
  3. 「言語に慣れる」「特徴を理解する」が目的なので解く速さは問わない

各回の流れ

最初に過去問から問題を選定し、以下の流れを各言語で行います。

  1. 最初は言語リファレンスをみながら自分で解く努力をする
  2. 他の回答者のコードを見て、より良い書き方や、別解を学ぶ
  3. 知見をまとめる

問題1 Welcome to AtCoder

AtCoder Beginners Selection」
PracticeA - Welcome to AtCoder

まずはどの言語も初回なので、プラクティスをやっていきましょう。

Python

自分の回答

a = int(input())
b,c = map(int, input().split() )
s = input()

print(a+b+c, s)

Pythonはちょっとだけ触れていたので、「これは簡単だろう」と意気込んで書いたコード。

  • 標準入力はinput()
  • intへのキャストはint()
  • 文字列の切り分けはsprit()(デフォルトで空白を区切り文字とする)
  • map()map( 関数, [list] )の構文で、listの各要素に関数を適用

といった感じ。

別解

コード長が短い順で探して一番上位に出てきたコード。 https://atcoder.jp/contests/abs/submissions/4760515

a=input;print(sum(map(int,[a()]+a().split())),a())

こ、これは。。。 短いコードなので美しい・・・のか?
一瞬びっくりしましたが、どうやらinput関数をaに代入しているようです。

python関数をオブジェクトとして変数に格納することが可能なようで、上記のコードで言うa関数オブジェクトと呼ばれるものなんだとか。
私は手続き型言語に慣れているせいもあってか、ちょっと理解しづらいが、関数型言語ライクな考え方・・・? で合ってるのかな。 要勉強。

それから;区切りで1行で複数の処理を記述できるようです。下記が例コードです。
コード

print("hello");print("world")

出力

hello
world

知らなかった。(しかし使うことあるのか・・・?)

Go

自分の回答

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
)

var sc = bufio.NewScanner(os.Stdin)

func nextLine() string {
    sc.Scan()
    return sc.Text()
}

func toInt(str string) int {
    i, e := strconv.Atoi(str)
    if e != nil {
        panic(e)
    }
    return i
}

func main() {
    sc.Split(bufio.ScanWords)

    a := toInt(nextLine())
    b := toInt(nextLine())
    c := toInt(nextLine())
    s := nextLine()

    ans := a + b + c

    fmt.Println(ans, s)
}

Goフォーマットにより美しく整形されました。
色々とimportして便利に使えるようです。頼もしい!

var sc = bufio.NewScanner(os.Stdin)

入力にはバッファIOパッケージを使いました。
.NewScanner()でファイルを指定して新たなスキャナーを作る。今回は標準入力なのでos.Stdinを指定しました。

sc.Split(bufio.ScanWords)これをつけるとどうやら空白区切で読み込んでくれるらしい。

sc.Scan()で入力トークンをスキャンして、sc.Text()でスキャンしたトークンから文字列を取り出すようです。なぜ二段階に分けたのだろう。。。あとで調べてみます。

整数型へのキャストはstrconv.Atoi()でやりました。こいつはキャストした結果と一緒に標準エラー情報を返してくるようなのでハンドリングしました。

あと、全体的に:=は変数の初期化に使うと学びました。(var hoge = fugaの短縮?)

別解

package main
 
import "fmt"
 
func main() {
    var a, b, c int
    var s string
 
    fmt.Scanf("%d", &a)
    fmt.Scanf("%d %d", &b, &c)
    fmt.Scanf("%s", &s)
    fmt.Printf("%d %s\n", a+b+c, s)
}

実行速度を求めないのであればfmt.Scanf()を使う方が多いようですね。
確かに、C言語チックで書きやすい上にコード量も短くなるかも!
私とほとんど同じようなコードを書いている方も何人かいました。ちょっと安心。

Rust

自分の回答

use std::io;
use std::str::FromStr;

fn main() {
    /* 入力 */
    let mut a = String::new(); //空のStringを作る
    io::stdin().read_line(&mut a).expect("Failed to read line"); //1行読み込み
    let a: u32 = a.trim() //文字列前後の空白を取り除く
        .parse() //キャスト
        .expect("Please type a number!"); //エラーハンドリング

    let mut it = String::new();
    io::stdin().read_line(&mut it).expect("Failed to read line"); //1行読み込み
    let mut it = it.split_whitespace().map(|n| u32::from_str(n).unwrap());
    let (b, c) = (it.next().unwrap(), it.next().unwrap());

    let mut s = String::new();
    io::stdin().read_line(&mut s).expect("Failed to read line"); //1行読み込み

    /* 計算 */
    let ans = a+b+c;

    /* 出力 */
    println!("{} {}",ans,s);
}

一番辛いっす・・・。
今回1つの問題にしかチャレンジできなかったのは、こいつの理解にすごい時間がかかったからです・・・

さて、いったいどこで何をしているのかじっくり見ていきます。 言語の基本的な理解は、公式チュートリアルの日本語翻訳版が非常に役立ちました。

まず標準入力を受け取りたい

入力された文字列を格納する空の文字列を作ります。

let mut a = String::new(); //空のStringを作る

なんとなくインスタンス生成を彷彿とさせるが、公式曰くどうも自分のイメージとは違うらしい。

これは String のインスタンスではなく String 自体に関連付けられているということです。 これを「スタティックメソッド」と呼ぶ言語もあります。

うーん、わからん。 これについては要勉強。

次に1行読み込んでさっきのaに入れます。

io::stdin().read_line(&mut a).expect("Failed to read line"); //1行読み込み

stdin()クラスのread_line()メソッドを呼び出してる感じかな?
しかし公式チュートリアルの日本語訳ではio::stdin()はクラスではなく関数という呼ばれ方をしている。

mutについて

read_line()はその名の通り1行読む。で、&mut aで代入先を指定している。
地味なところではあるが、&mutをつける意味が、わかったようなわかってないような...まだ若干考えが定着しない部分だと思った。
詳しくは公式の ミュータビリティ(mutability) に書いている。

【ここからふわふわ(にわか)説明】
前提知識として、rustはデフォルトで「変数の再代入は許さない」「値の変更は許さない」という特徴があります。
なのでlet hogeで宣言される変数は、いわゆるconst定数となります。
再代入可能な変数にしたい場合mutをつけます。

let mut x = 5;
let y = &mut x;

で、このようなコードを書くと、ybの参照を渡すこととなる。
そして、let yは不変なのでyは参照先を束縛されます。
よって、以下のように参照渡しをもう一度行うことは不可能ということです。

let mut x = 5;
let y = &mut x;

y = &mut z; //これダメ

しかし、&mutをつけているのでyに束縛されているものを変化させることは可能です・・・はい。

let mut x = 5;
let y = &mut x;

*y = 6;

色々長いこと書きましたが、要はコードの以下の部分は、再代入可能な変数itの参照渡しを行なっているということです。多分。(投げやり)

.read_line(&mut it)

さて、ここまで調べたところでアドベントカレンダー初日から記事投稿の遅刻が確定しました😇
しかしまだまだ問題解答には遠いです、初日から気合い入れていきましょう!

さて、mutについてだいぶ時間を割いてしまいましたが、話を標準入力に戻しましょう。
と言っても残っているのはこの部分だけ、

.expect("Failed to read line");

これはエラーハンドリングですね、私は面倒くさがりなので最初この部分を書きませんでしたが、コンパイラに「書け」と怒られました(warning)
これは結構言われるので、従順にしたがっていれば安全なコードが書けそうです。

キャスト

let a: u32 = a.trim() //文字列前後の空白を取り除く
        .parse() //キャスト
        .expect("Please type a number!");

さっき読み込んだデータは文字列として格納されてるので、整数型にキャストしましょう。
読み込んだ文字列には改行とかも含まれているので、trim()で切り落とします。
.parse()がキャスト関数なんですが、何にキャストするかは変数の型から推測するそうです(賢い)
そのためにlet a: u32で明示的に型を指定しています。
aはさっきまでString型でしたが、rustは変数の定義を新しいもので隠すことができる(上書きなのかな?)らしいので、名前を再利用しています。

これで整数が1つ読み込めました・・・! えらい。

複数の値を読み込む

2 3 4

のように、1行から空白区切の複数の値を読み込むのが以下の部分です。

let mut it = it.split_whitespace().map(|n| u32::from_str(n).unwrap());

.split_whitespace()は、名前のまんまの仕事をしています。わかりやすい。
さて、これで切り分けられた複数の文字列が手に入るわけですが、これを全部整数型にキャストしなければなりません。

.map(|n| u32::from_str(n).unwrap());

先ほど、pythonmap()に触れていたので考え方はそんなに難しくなかったです。
要素の1つ1つをnとして、そのnに対してu32::from_str(n)でキャスト変換をします。
.unwrap()from_str()の戻り値がResult<T,Err>という型で返って来るんですが、それのT部分を抜き出す関数です。とことん安全について考えられてますね・・・

別解

別解というか、競プロのテクニックだと思うんですが、やはり入力は真面目に毎回書いてたら手間なので、多くの人が例えば以下のように関数としてまとめているようですね。

fn read<T: FromStr>() -> T {
    let stdin = stdin();
    let stdin = stdin.lock();
    let token: String = stdin
        .bytes()
        .map(|c| c.expect("failed to read char") as char) 
        .skip_while(|c| c.is_whitespace())
        .take_while(|c| !c.is_whitespace())
        .collect();
    token.parse().ok().expect("failed to parse token")
}

確かに・・・1から書いてみると納得。

まとめ

1日目から早速苦行でした・・・・
文字ばっかりでわかりづらいまとめになったので、「図も入れたいな」と思うけどそんな余裕はないかも・・・
しかしながら、今日で入出力部分はしっかり理解できたと思うので、明日からやっと、実際にもっと計算する問題を解いていきましょー!
1日目で得た教訓は「Rustは強いけど最初辛い」ということでした。
以上、また明日!