ラムダ

ラムダの書き方や注意点を学びます。

はじめに

ラムダは、リストの操作等でよく使用されることが多いため、リストに対しての操作を例にとって解説していきます。以下は、リストに格納するPersonオブジェクトです。
名前、年齢、性別を表すプロパティを持つ、ごく単純なデータクラスとなっています。

data class Person(val name: String, val age: Int, val gender: Gender) {
}

enum class Gender {
    MAN,
    WOMAN,
}

リストは以下のようなものを使用します。
特に断りのない場合は以下のリストを操作します。

val people = listOf(
            Person("Bob", 20, Gender.MAN),
            Person("Alice", 19, Gender.WOMAN),
            Person("John", 21, Gender.MAN),
            Person("Ema", 30, Gender.WOMAN),
            Person("Brad", 18, Gender.MAN))

ラムダとは

平たく言ってしまうと、他の関数に渡すことのできる関数のことです。

基本の書き方

ラムダ式は、{}で囲むのが基本の書き方です。 ただし、いろいろと省略記法があるので、順に見ていきましょう。
以下の例では、Personオブジェクトのリストから、年齢が最大であるPersonを取得します。

//一番冗長な書き方。引数にラムダを渡している。
people.maxBy({p: Person -> p.age})
//引数の最後がラムダ式の場合、()の外に出せる
people.maxBy() { p: Person -> p.age }
//さらに、引数がラムダのみの場合、()は省略できる
people.maxBy { p: Person -> p.age }
//型が推論されるため、Person型であることを明示しなくとも良い
people.maxBy { p -> p.age }
//さらに、ラムダで使用できる変数itを使用することで、変数宣言も不要となる
people.maxBy { it.age }

いろいろなコレクション操作APIを試す

ラムダの基本的な書き方などを理解したところで、実際にコレクションに対してラムダを利用していろいろな操作を行ってみましょう。
以下では、コードと、その実行結果を記載しています。

filter

フィルターを使用すると、ある条件を満たす要素のみを残す新たなリストを取得できます。
例として、成人している(年齢が20以上)の要素を抜き出してみます。

println(people.filter { it.age >= 20 })
[Person(name=Bob, age=20, gender=MAN), Person(name=John, age=21, gender=MAN), Person(name=Ema, age=30, gender=WOMAN)]

map

mapを使用するとコレクションの要素を別の型に変換したリストを取得できます。
例として、Personオブジェクトを名前(String)へ変換してみましょう。

println(people.map { it.name })
[Bob, Alice, John, Ema, Brad]

all

allを使用すると、コレクションの全てが指定された条件を満たすかを判定できます。 以下の例では、コレクションのPersonオブジェクトが全て30歳以上かを判定しています。

println(people.all { it.age >= 30 })
false

any

anyを使用すると、コレクション中にある条件を満たす要素が存在しているかどうかを判定できます。コレクション中に1件でも条件に合致すればtrueとなります。

println(people.any { it.age >= 30 })
true

count

countを使用すると、コレクション中の要素の中から、条件に合う要素の個数を取得できます。 以下の例では、女性のカウントをしています。

println(people.count {it.gender == Gender.WOMAN})
2

find

findを使用すると、コレクションの中から、与えられた条件に合致するオブジェクトを取得します。 条件に合致するオブジェクトがない場合は、nullが返却されます。

    println(people.find { it.name == "Bob" })
    println(people.find { it.name == "Michel" })
Person(name=Bob, age=20, gender=MAN)
null

groupBy

groupByを使用すると、コレクションをある条件でグループ化したMapを取得できます。 取得できるMapの型は、Map<指定したキー, リスト>となります。
よって、以下の例では、Map<Gender, List<Person>>が取得できます。

println(people.groupBy { it.gender })
{MAN=[Person(name=Bob, age=20, gender=MAN), Person(name=John, age=21, gender=MAN), Person(name=Brad, age=18, gender=MAN)], WOMAN=[Person(name=Alice, age=19, gender=WOMAN), Person(name=Ema, age=30, gender=WOMAN)]}

flatMap

リストのリストなどを平坦に(flatに)するさいに利用します。 実際の例を見てみるとわかりやすいでしょう。 以下は、90年代のNBAチームから抜粋したチームオブジェクトを使用しています。

//各チームには文字列リストでメンバーの名前を保持している
val bulls = Team("bulls", listOf("Michael Jordan", "Scottie Maurice Pippen", "Dennis Keith Rodman"))
val jazz = Team("jazz", listOf("Karl Malone", "John Stockton"))

val teams = listOf(bulls, jazz)

println(teams.flatMap { it.members })
[Michael Jordan, Scottie Maurice Pippen, Dennis Keith Rodman, Karl Malone, John Stockton]

withとapply

with

まずはwithから見ていきましょう。

fun alphabet(): String {
    val sb = StringBuilder()
    return with(sb) { //with(sb)とすることで、このブロック内ではsbがレシーバとなる。
        for (letter in 'A'..'Z') {
            append(letter) //appendは変数sbに対して実行される
        }
        append("¥nNow I know the alphabet!")
        toString() //最終的にtoString()した結果がreturnされる
    }
}

withを使用することで、ラムダ内で使用するオブジェクトを指定できます。 このようにすることで、ラムダないで変数名を何度も書くことなくラムダをすっきりと書くことができるようになります。

apply

applyも、withとほぼ同様の動きをします。withとの違いは、applyは常に引数の値として渡されたオブジェクトを返すことです。
先ほどと同じalphabet関数をapplyを使用してリファクタリングしてみましょう。

fun alphabet() = StringBuilder().apply {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("¥nNow I know the alphabet!")
}.toString()

applyを使用すると、レシーバオブジェクトを直接返却するようになります。 そのため、上記の例ではオブジェクトの返却の前にtoString()を呼び出しています。
toString()を呼び出さない場合はStringBuilderオブジェクトが返却されます。

終わりに

ラムダについて、その書き方や使い方などをみてきました。 コレクションAPIとの親和性が高いので、上手に使えるようになると可読性の向上など、メリットは大きそうですね。

今回は以上となります。それではまた。

拡張

Kotlinの拡張について。

拡張とは

ある既存のクラスに新しくメソッドやプロパティなどを定義する方法です。

拡張関数

fun main(args: Array<String>) {
    //拡張関数の例:既存のIntクラスにisOddメソッドを追加する
    fun Int.isOdd(): Boolean = (this % 2 != 0)
    5.isOdd() //true
    8.isOdd() //false

    //なお、既存のメソッドと同一のシグネチャの拡張関数を定義してもその拡張関数は実行されない
    fun Int.plus(n: Int): Int = 0 //plusメソッドを常に0を返すように拡張する??
    val ans = 10.plus(2)
    println(ans)//12 (シグネチャが同じ場合は拡張関数ではなく既存のメソッドが呼ばれる)

}

拡張プロパティ

//拡張プロパティの例:IntクラスにプロパティisOddを追加する
val Int.isOdd: Boolean
    get() = (this % 2 != 0)


//拡張プロパティを呼び出す
println(7.isOdd)

所感

やっていることはごく自然な印象を受けますが、うまく使わないといけないかもしれませんね。

data class

Kotlinのデータクラスです。

はじめのまとめ

Kotlinに、値を表すクラスを作成する際に便利な機能のひとつとして、data classがあります。
data classを用いると、そのclassにはコンパイラ側で以下の3つのメソッドが実装されます。
1. toString
2. equals
3. hashCode

実践

単純な例として、Personクラスを見ていきましょう。

data class Person(val name: String, val age: Int)

データクラスを作成するには、予約語dataをクラスの前に付与します。 data classを用いると、前述したように3つのメソッドがプライマリコンストラクタに渡される値(この例ではnameage)を適切に評価する形でオーバーライドされます。具体例を見てみましょう。

fun main(args: Array<String>) {
    val person1 = Person("taro", 20)
    val person2 = Person("taro", 20)
    val person3 = Person("hanako", 30)

    println(person1)
    println(person1 == person2)
    println(person2 == person3)

    println(person1.hashCode())
    println(person2.hashCode())
    println(person3.hashCode())
}

結果

Person(name=taro, age=20)
true
false
110131178
110131178
692713294

toString()

まずはじめにPersonオブジェクトを標準出力へ出力していますが、人から見てとても見やすい形に整形されているのがわかると思います。

equals()とhashCode()

さらに見ていきましょう。
Personオブジェクトを==を用いて比較しています。
この場合、data classではプロパティの全ての値が一致しているかを判定してbool値を返します。Personクラスの例で言うと、nameとageが一致している場合ですね。 また、hashCode()も同様に各プロパティにそった値を返すようになります。

なお、ひとつ注意点ですが、Kotlinの場合、Javaと異なりオブジェクトの同値性は==で実施します。 一方、オブジェクトの同一性については===での比較となります。

 val person1 = Person("taro", 20)
val person2 = Person("taro", 20)

println(person1 == person2) //こちらはtrueだが、
println(person1 === person2) //こちらはfalse

上記のように、==で比較した場合はnameとageの値が同じため、結果はtrueとなりますが、===で比較すると同一のオブジェクトではないためfalseを返していることがわかります。

終わりに

data classを用いると従来Javaなどで実装していたequalstoStringといったメソッドを自動生成してくれるようになります。 単純な値を持つクラスを作る際には使っていくことにしましょう。

初めましてKotlin

はじめに

Kotlinの基本的な文法等を学びます。 取り扱っていないトピックも多々あるかと思いますが、最低限持っていたい知識をまとめました。 また、所々Javaとの比較も織り交ぜております。

Hello, World!

まずはこれを書かないことにはプログラミングは始まらないですよね。 Javaと比べるとだいぶスッキリした印象です。

fun main() {
    println("Hello, World!")
}

それでは以降、基本的な文法などを見ていきます。

変数

変数宣言には大きく分けて2つの方法があります。 valは、読み取り専用の変数を宣言し、varは書き換え可能な変数を定義できます。

val a: Int = 1
a = 10 //valで宣言しているため、コンパイルエラーとなります

var b: Int = 2
b = 3

関数

Kotlinでの関数定義方法を見ていきます。 Kotlinでは予約後funを使用することで関数を定義できます。以下は、足し算をする関数です。

fun sum(a: Int, b: Int): Int {
    return a + b
}

なお、上記の関数は以下のように書くこともできます。 この場合、返却される型は推論されます。

fun sum(a: Int, b: Int) = a + b

呼び出し方法はJavaなどの構文と同じです。

fun main() {
    println(sum(3, 5)) // 8
}

分岐と繰り返し

プログラムの基本である分岐と繰り返しです。 これさえ書ければ大半のロジックは組めます。(多分)

分岐

if

Kotlinでは、ifを式として扱います。 そのため、下記のような書き方が可能となります。

fun main() {
    val a = 10
    val b = 20
    //ifを式で扱えるため、ifが値を返す
    val max = if (a < b) b else a
    println(max)
}

//以下は、比較のため、ifを文として扱った場合です。
//Kotlinでもこのように書けますが、IDEによっては「もっとシンプルに書けるよ」と警告が出ます。
if (a < b) {
    max = b
} else {
    max = a
}

when

ifで分岐が多くなってしまう場合はwhenが利用できます。 場面に合わせて効果的な方を使いましょう。

when(age) {
    in 0..3 -> println("baby")
    in 4..12 -> println("child")
    in 13..19 -> println("youth")
    in 20.. 100 -> println("adult")
}

なお、whenは値を取らなくとも使用できます。 この場合、各式でtrueとなった行の右辺が実行されます。 途中でtrueとなった場合は以降の行は評価されないことに注意しましょう。 (Javaなどのswith文で、breakがあることをイメージすると良いかもしれません)

when {
    age in 0..3 -> println("baby")
    age in 4..12 -> println("child")
    age in 13..19 -> println("youth")
    age in 20.. 100 -> println("adult")
}

繰り返し

for

forは、いくつか書き方があります。

fun forExample(){
    //1,2,3,4,5,6,7,8,9,10
    for (i in 1..10) {
        println(i)
    }
    
    //stepを使用すると、指定した数値だけ飛ばしていきます。
    //1,3,5,7,9
    for (i in 1..10 step 2) {
        println(i)
    }
    
    //downToを使用すると数値を逆順で出せます。
    //10,8,6,4,2,0
    for (i in 10 downTo 0 step 2) {
        println(i)
    }

    //コレクションの全要素に対して処理をする場合です。
    //Javaの拡張For文に似ていますね。
    var arr: Array<String> = arrayOf("one", "two", "three")
    for (s in arr) {
        println(s)
    }
}

while

whiledo whileJavaとほとんど同じようです。

fun whileExample() {
    var x :Int = 10
    while(x > 0) {
        x--
        println(x)
    }

    do {
        x++
        println(x)
    } while(x < 10)
}

FizzBuzzを書いてみよう

分岐と繰り返しが書ければFizzBuzzが書ける! というわけで、前述までの知識でFizzBuzzを書いてみましょう。

fun fizzBuzz(count : Int) {
    for (i in 1..count) {
        when {
            i % 15 == 0 -> println("FizzBuzz")
            i % 5 == 0 -> println("Fizz")
            i % 3 == 0 -> println("Buzz")
            else -> println(i)
        }
    }
}

もっといい書き方があれば教えてください

オブジェクト指向

クラスやインターフェースなど、オブジェクト指向の基本的な記述方法です。

クラスの宣言

classキーワードを使用することでクラスを宣言することができます。 以下、コンストラクタと初期化ブロック(init)を持つPersonクラスです。

class Person(val firstName: String, val lastName: String) {
    init{
        println("hello, $firstName")
    }
}

Kotlinでは、コンストラクタはクラス名の直後に記載します。(val firstName: String, val lastName: String)←この部分です。 また、initを使用することで、オブジェクト生成時の挙動を定義することもできます。

オブジェクトの生成

Kotlinではオブジェクトの生成時にnew演算子を用いません。 前述したPersonクラスを用いたオブジェクト生成方法を下記に示します。

fun main() {
    val tarou: Person = Person("tarou", "tanaka")
    println(tarou.firstName)
}

結果

hello, tarou  //initブロック内の実行結果
tarou //main関数内で実行したprintlnの結果

継承

クラスの継承方法です。 注目して欲しいのはopenキーワードです。 Javaと異なり、Kotlinでは全てのクラスはデフォルトでfinalとなっています。 そのため、継承をさせたい場合は親クラスにopenキーワードを付与する必要があります。

open class Parent(x: Int) {

}

class Child(x: Int) : Parent(x) {
    
}

インターフェース

KotlinにおけるインターフェースもJavaと大きく違いはありません。予約語interfaceを使用します。 以下のように抽象メソッドを定義できますし、具象メソッドも定義できます。

//インターフェース
interface Calculator {
    fun calc(a : Int, b: Int): Int
}
//実装クラス
class Plus : Calculator {
    override fun calc(a: Int, b:Int):Int {
        return a + b
    }
}

コレクション

使用頻度が高いデータ型の代表格であるコレクションについて、詳しくみておこうと思います。 kotlinでは、コレクションを変更可能か、変更不能かで明確に分けていることに注意しましょう。

リスト

fun main() {
    //MutableList<E>は名前の通りミュータブル(変更可能)なリスト
    val numbers1: MutableList<Int> = mutableListOf(1,2,3,4,5)

    //List<E>型はイミュータブル(変更不可)なリスト
    val numbers2: List<Int> = listOf(1,2,3,4,5)

    //numbers1はミュータブルなリストのため、要素の追加と削除が可能
    numbers1.add(6)
    numbers1.removeAt(3)
    
    //numbers2はイミュータブルなリストのため、参照用のメンバしか呼び出せない
    numbers2.size
    val n = numbers2[0]
}

セットとマップ

セットとマップもリストと同様にイミュータブルとミュータブルで分かれています。 また、下記のコードにあるように、mapの生成はkey to valueの形式で、1 to "one"のように書けます。

fun main() {
    //setの生成
    val set1: MutableSet<Int> = mutableSetOf(1,2,3,4,5)
    val set2: Set<Int> = setOf(1,2,3,4,5)
    
    //mapの生成
    val map1: MutableMap<Int, String> = mutableMapOf(1 to "one", 2 to "two")
    val map2: Map<Int, String> = mapOf(1 to "one", 2 to "two")
    //mapの要素へアクセスする場合
    val value = map2[1] // one
}

Null安全

Kotlinでは、Javaと異なりNullを許容する変数と許容しない変数を明示的に分けて書くことが可能となっています。そのため、コンパイル時点でNullPointerExceptionの危険性に気づくことができます。 公式ドキュメントにもあるように、プログラマを悩ませてきたNullPointerException撲滅のため、しっかり理解して上手に扱えるようにしましょう。(自戒)

基本

まずは基本的な使い方から見ていきます。 以下のように、変数宣言時に型指定において、末尾に?を付けるとNullを参照できる変数となります。

var hoge: String = "hoge"
hoge = null //コンパイルエラー!!

//String?とすることでnullが設定可能となる    
var huga: String? = "huga"
huga = null

null参照の可能性がある場合、コンパイルエラーとなる

例えば、以下のようなコードを考えてみましょう。 変数hugaString?型のため、nullがありうる値となっています。 そのため、huga.lengthの呼び出しの際にNullPointerExceptionが発生する可能性があることに気づくと思います。 Kotlinではこのような場合に、実行時ではなくコンパイル時にNPEを抑止できるようになります。

var huga: String? = getHuga()
if (huga.length > 5) { //hoge.lengthでNPEの恐れがあることをコンパイラが気づいてくれる!
    println("hugehuga")
}

Nullableに対する処置

さて、コンパイルエラーになってしまうのではしょうがないので、コードを修正しなければいけません。 いくつか方法がありますので順に見ていきましょう。

nullチェックをする

原始的な(?)やり方ではありますが、これが一番ポピュラーな方法でしょう。 nullチェックを一度噛ませれば当然NPEが起こらないことが保証できるので、コンパイラも怒ったりしません。

if (huga != null) {
    if (huga.length > 5) {
        println("hugehuga")
    }
}

セーフコール

?.を使用することで、その変数がnullの場合はnullを、nullでない場合は呼び出したメソッドに応じた値を取れるようになります。 なお、以下の例の場合に返却される値の型はInt?になることに注意しましょう。

fun main() {
    var huga: String? = null
    var len1 = huga?.length //len1はnullになる

    var hoge: String? = "hoge"
    var len2 = hoge?.length //len2は4
}

エルヴィス演算子

?:を使用することもできます。 ?:を使用すると、左辺がnullの場合に右辺で指定した値を式の結果として返すようになります。 なお、右辺の式は左辺がnullである場合にのみ評価されます。

//変数hugaがnullでなければlengthの値を、hugaがnullならば−1を返す
var len = huga?.length ?: -1

!!演算子

3つ目の方法として、!!を使用できます。1 この演算子を使用すると、その変数がnullかどうかに関わらず、メソッドを呼び出すことが可能となります。 つまり、NullPointerExceptionが発生する恐れがあるということです。 本来NPEの発生抑止を目的としているのがNull安全の考え方だと思いますので、可能な限り使用は控えた方がいいかと思います。

var hoge: String? = null
var len = hoge!!.length //NullPointerExceptionが発生してしまう!

セーフキャスト

上記3つとは趣旨が若干異なりますが、Kotlinではセーフキャストという仕組みを利用することで、ClassCastExceptionを抑止することができます。 キャスト時に、想定した型にキャストできない場合、通常ClassCastExceptionがスローされますが、as?を使用すると例外が発生しなくなります。

var str: String? = getHoge() as? String?

例外

例外の補足などを学びます。

try-catch

try-catchは、Javaと大きく違いありません。 ただし、try-catchもKotlinでは式として使用することができます。 単純な例を示します。

fun main() {
    val result: Int = try {
        something()
    } catch (e: NullPointerException) {
        0
    }
}

fun something():Int {
    return 10
}

他いろいろ

その他、Kotlinの細かい部分についてもほんの少し触れておきます。

コメント

コメントの書き方もJavaと同じですね。

//1行コメント
    
/*
複数行はこのように書く
 */

セミコロンは不要

お気づきの方も多いと思いますが、Javaと異なりKotlinでは行末のセミコロンは不要です。

終わりに

公式ドキュメントや書籍から、Kotlinの基礎的な知識を記載しました。 適宜更新していこうと思います!


  1. なんと発音すれば良いのだろうか

Reactで勉強すること

こんなTweetが流れてきたので、ひとつひとつできるように勉強。

フック

React16.8から導入されたフックの基本を学びます。

概要

簡単に言うと、関数型のコンポーネントにstateを持たせる仕組みのことです。以下、公式ドキュメントからの引用です。

フックとは、関数コンポーネントに state やライフサイクルといった React の機能を “接続する (hook into)” ための関数です。

フックのコード例

以下に、フックを利用した場合のコード例を示します。
やっていることは単純で、ボタンを押すたびに画面上のカウンタがインクリメントされていきます。

import React, {useState} from 'react';

//フックを使用した例
export function Counter3() {
  const [count, setCount] = useState(0);

  return(
    <div>
      <p>you clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}> click me</button>
    </div>
  )
}

解説

import React, {useState} from 'react';の部分でインポートしているuseStateがフックとなります。
useStateは、初期値となる値をひとつ引数にとり、初期化するstateと、stateを更新する関数を返します。 この場合、countが0で初期化され、setCountにより、stateを更新できるようになる、ということです。

フックの基本の使い方

最初からあれやこれやと手を出すよりも、まずは公式ドキュメントにある基本のフックを学びましょう。

useState

先ほどのカウンタで行ったものです。
const [state, setState] = useState(initialState)の形式でstateの初期値と、更新するための関数を定義します。
setState()を呼び出すことでstateの更新ができます。

useEffect

useEffectを使用すると、関数のレンダー後に副作用を起こすことができます。
以下は、前述したカウンタについて、副作用としてタイトルにカウント回数をレンダーします。

  useEffect(() => {
    document.title = `you clicked ${count} times`;
  });

useContext

useContextを知る前に、そもそもReactにおけるContextの使用方法を学ぶべきですね。 公式ドキュメントによれば、

コンテクストは各階層で手動でプロパティを下に渡すことなく、コンポーネントツリー内でデータを渡す方法を提供します。

とのことです。平たくいうと、通常はpropsを用いることで親から子へ子から孫へ・・という形で実現していたことを、明示的にpropsを利用しなくとも値を渡すことができる仕組みのようです。

さて、useContextの話に戻ります。
以下、公式ドキュメントからの引用コードになります。(importなど、一部実行させるために手を加えています。)

import React, {useContext} from 'react'

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

//Contextの生成
const ThemeContext = React.createContext(themes.light);

export function App2() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

//中間であるToolbarでは、値の中継を明示的にしなくとも良い
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  //useContextを使用して現在のContextを取得する
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

上記のコードでは、親であるApp2から、孫にあたるThemedButtonまで、値を引き渡すことに成功しています。子にあたるToolbarではContextを明示的に扱っていないことがポイントになります。

終わりに

以上、簡単ではありますがフックの使い方についてでした。
フックをはじめ、Reactはまだまだ勉強しないといけないことが多いですね。

それでは。

ABC176 復習

C問題までは簡単なんだけどなぁ・・。

A - Takoyaki

単純に、全体で作成したい個数を、一回あたりの個数で割った数に時間をかければ良いだけです。 余りが出る場合は1回分の時間を足すことを忘れずに。

n, x, t = map(int, input().split())

ans = n // x * t
if n % x > 0:
  ans += t

print(ans)

B - Multiple of 9

9で割り切れるか?をそのまま書いたら通ってしまいました。
公式解説によれば、Pythonはそのままでいけるみたいですね。

n = int(input())

if n % 9 == 0:
  print('Yes')
else:
  print('No')

C - Step

配列において、a[i] > a[i + 1]になる場合に差を足していけば答えが求まります。 また、その場合にa[i + 1]の値をa[i]と同じにすることを忘れずに。
あまり深く考えずとも解けるのではないでしょうか。

n = int(input())
a = list(map(int, input().split()))

ans = 0

for i in range(n - 1):
  if a[i] > a[i + 1]:
    diff = a[i] - a[i + 1]
    ans += diff
    a[i + 1] += diff

print(ans)

D - Wizard in Maze

おそらく、幅優先探索の亜種のように考えれば解けると思ったのですが、解けませんでした。 幅優先探索をしつつ、DP配列を持ってワープの最小値を更新していくようなイメージではないのでしょうか。
ひとまず解説を待つことにします。

総括

C問題までがとても簡単で、おそらくC問題も茶色Diffはないのではないでしょうか。
D問題以降を解けるようにならないと、レーティングは上がっていかないでしょうね・・。
精進あるのみ。