😊

AtCoder Beginner Contest 320参加記(A~D)

2023/09/17に公開

トヨタ自動車プログラミングコンテスト2023#5(AtCoder Beginner Contest 320)に参加したので記録を残します。
コンテスト中にはA~Cを解いて、Dはコンテスト後に通しました。
https://atcoder.jp/contests/abc320

A - Leyland Number

問題文の通り実装です。Kotlinの場合はpow()メソッドを使えます。レシーバの型がDoubleなので変換して呼び出して、計算後にLongにしています。
制約をちゃんと見ていなかったですが、別にIntでもいいですね。

import kotlin.math.pow

fun main() {
    val (a, b) = readln().split(" ").map { it.toInt() }

    println((a.toDouble().pow(b) + b.toDouble().pow(a)).toLong())
}

https://atcoder.jp/contests/abc320/submissions/45581656

B - Longest Palindrome

全部試します。substring()はなかなか慣れなくていつも混乱します。半開区間なので、内側のループはi + 1から始めます。
reversed()で反転させた文字列を得られるので、それと比較して同じなら回文といえます。

import kotlin.math.max

fun main() {
    val s = readln()

    var ans = 0
    for(i in s.indices) {
        for(j in i + 1..s.length) {
            val ss = s.substring(i ,j)
            if(ss == ss.reversed()) {
                ans = max(ans, j - i)
            }
        }
    }
    println(ans)
}

https://atcoder.jp/contests/abc320/submissions/45590854

C - Slot Strategy 2 (Easy)

これも全部試して、最小だったものが答えでした。
1秒が最小単位の時間で、一つの時間で押せるのは最大一つ(押さなくてもいい)、押す順番は自由に選べる、押したボタンに対応するリールは止まるがそれ以外の止めてないやつは回り続ける、という感じ。
リールは環状で、最後まで行った次はまた最初の数字に戻るので、3つのリール内に一つでも共通の数字があれば目的は必ず達成できます。逆に、一つもなければ達成不可能です。

なのでまずは達成可能かどうかを判定しました。
達成可能であれば、後は全部調べます。押す順番は自由なので、1→2→3と止めるパターン、1→3→2と止めるパターン... など全9通り調べます。
それぞれについて、0秒目に止めた場合、1秒目に止めた場合... と調べますが、今回は1つ目に止めたリールで表示されている数字と同じもの(最初に出現するもの)を2つ目、3つ目について調べるという感じでやりました。
しかし、あるリールを止めても他のリールは回り続けるため、2つ目と3つ目については単純に先頭からの探索だとダメで、リールを止めた時間の次の時間に表示される位置から調べます。

まあ素直にそのように探索すればよかったのですが、今回はループカウンタ自体は0から始めるが、経過した時間の文を足した位置の情報と比較、みたいなややこしいことをしてしまいました。

import kotlin.math.min

fun main() {
    val m = readln().toInt()
    val sList = List(3) {
        readln()
    }
    val set2 = sList[1].toSet()
    val set3 = sList[2].toSet()

    var possible = false
    for(c in sList.first()) {
        if(c in set2 && c in set3) {
            possible = true
            break
        }
    }
    if(!possible) {
        println(-1)
        return
    }

    var ans = Int.MAX_VALUE
    for(i in sList.indices) {
        val s1 = sList[i]

        val s2: String
        val s3: String
        when (i) {
            0 -> {
                s2 = sList[1]
                s3 = sList[2]
            }
            1 -> {
                s2 = sList[0]
                s3 = sList[2]
            }
            else -> {
                s2 = sList[0]
                s3 = sList[1]
            }
        }

        // 探索
        var cur = Int.MAX_VALUE
        for(t in s1.indices) {
            val c = s1[t]

            for(j in s2.indices) {
                if(s2[(j + t + 1) % m] == c) {
                    for(k in s3.indices) {
                        val num = k + t + 1 + j + 1
                        if(s3[num % m] == c) {
                            cur = min(cur, num)
                        }
                    }
                }
            }

            for(j in s3.indices) {
                if(s3[(j + t + 1) % m] == c) {
                    for(k in s2.indices) {
                        val num = k + t + 1 + j + 1
                        if(s2[num % m] == c) {
                            cur = min(cur, num)
                        }
                    }
                }
            }
        }
        ans = min(ans, cur)
    }
    println(ans)
}

https://atcoder.jp/contests/abc320/submissions/45617937

コピペがダサいのと、コピペ後の修正漏れ(片方だけ修正)によって1ペナ貰う始末でしたが、一応解けてよかったです。

D - Relative Position

コンテスト中は解けませんでした。実はこれも全探索が間に合うのではないかと考えて実装していましたが、TLEが2つ出てそれを取り除けず…

コンテスト中に考えたのは以下のような感じです。
Aiからの距離は明示されていて、それも正方向とはっきり書いているので、Aiの位置が判明しているのであればBiは確実にわかります。しかしAiの位置が判明しているのは初期状態だとA1だけなので、そこを突破口として芋づる式に調べていく感じです。どういう順番で調べればいいのか賢く判断できればいいのですが、なかなか難しそう。
それで考えたのは、位置が既にわかっているほうを元に調べればいいのではということでした。位置がわかっているものからの相対位置が入力されている人についてはそれによって位置を特定して、位置が特定できたらまたそれをもとに位置を知ることができる人の位置を特定して... を繰り返せば、答えは得られるだろうと。同じような探索を何度もすることになりますが、Mapを使ってO(1)で探索すればゴリ押せるのではないかと…

まあコンテスト中は通せなかったのですが、コンテスト後にその方針のまま計算量を改善して突っ走ったら最終的には通すことができました。ペナを量産しましたのでコンテスト中にそれを通すのは結局無理だったでしょうけど、方針自体は間違っているとは言えなかったのですね。残念。

import java.io.PrintWriter

fun main() {
    val br = System.`in`.bufferedReader()

    val (n, m) = br.readLine().split(" ").map { it.toInt() }

    val aMap = mutableMapOf<Int, MutableList<Person>>()

    repeat(m) {
        val (a, b, x, y) = br.readLine().split(" ").map { it.toInt() }
        val xx = x.toLong()
        val yy = y.toLong()
        val p = Person(a, b, xx, yy)

        aMap.putIfAbsent(p.a, mutableListOf())
        aMap[p.a]?.add(p)

        val rp = Person(b, a, -xx, -yy)
        aMap.putIfAbsent(p.b, mutableListOf())
        aMap[p.b]?.add(rp)
    }

    val ans = mutableMapOf<Int, Pair<Long, Long>>()
    ans[1] = 0L to 0L

    var beforeSize: Int

    val list = mutableListOf<Int>()
    list.add(1)

    val queue = ArrayDeque<Int>()

    do {
        beforeSize = ans.size

        for(ii in 0 until list.size) {
            val i = list[ii]

            queue.add(i)

            while (queue.isNotEmpty()) {
                val a = queue.removeFirst()

                if(!aMap.containsKey(a)) {
                    continue
                }

                for(p in aMap[a]!!) {
                    if(ans.containsKey(p.b)) {
                        continue
                    }

                    if(ans.containsKey(p.a)) {
                        val knownA = ans[p.a]!!
                        ans[p.b] = knownA.first + p.x to knownA.second + p.y
                        list.add(p.b)
                        queue.add(p.b)
                    }

                    if(ans.containsKey(p.a)) {
                        continue
                    }

                    if(ans.containsKey(p.b)) {
                        val knownB = ans[p.b]!!
                        ans[p.a] = knownB.first + p.x to knownB.second + p.y
                        list.add(p.a)
                        queue.add(p.a)
                    }
                }
            }
        }

    } while (beforeSize != ans.size)

    val out = PrintWriter(System.out)

    for(i in 1..n) {
        if(ans.containsKey(i)) {
            val (x, y) = ans[i]!!
            out.println("$x $y")
        } else {
            out.println("undecidable")
        }
    }

    out.flush()
}

data class Person(val a: Int, val b: Int, var x: Long, var y: Long)

https://atcoder.jp/contests/abc320/submissions/45657777

AiまたはBiがキー、入力行全体を値としたMapを使って、既に位置が判明しているものを元に位置を特定できる人を拾って特定する、特定したらその人を「既に位置が判明しているもの」側に入れて探索、結果はansというMapに格納、それをansの件数が変わらなくなるまで繰り返すという強引な手法です。まさか通るとは…
通らなかった版だとAiをキーとしたMapとBiをキーとしたMapの2つを作ってそれぞれに対して探索ということをしていたので遅くなっていました。通った版だとこれを一つにまとめています。XiYiの符号を反転させればまとめることができると。これに気づいていれば、ですねぇ。

なお、グラフとみなしてBFSなりDFSなりして調べるのが想定解だったようです。そりゃそうか…

BFSで解いてみたのがこちら。

import java.io.PrintWriter

fun main() {
    val br = System.`in`.bufferedReader()

    val (n, m) = br.readLine().split(" ").map { it.toInt() }

    val graph = Array(n + 1) { mutableListOf<Person>() }

    repeat(m) {
        val (a, b, x, y) = br.readLine().split(" ").map { it.toInt() }
        graph[a].add(Person(a, b, x.toLong(), y.toLong()))

        graph[b].add(Person(b, a, -x.toLong(), -y.toLong()))
    }

    val ans = mutableMapOf<Int, Pair<Long, Long>>()
    ans[1] = 0L to 0L

    // BFS
    val queue = ArrayDeque<Int>()
    queue.add(1)

    val seen = BooleanArray(n + 1)
    seen[1] = true

    while (queue.isNotEmpty()) {
        val v = queue.removeFirst()

        for(node in graph[v]) {
            if(seen[node.b]) {
                continue
            }

            val (fromX, fromY) = ans[v]!!

            ans[node.b] = fromX + node.x to fromY + node.y

            queue.add(node.b)
            seen[node.b] = true
        }
    }

    val out = PrintWriter(System.out)
    for(i in 1..n) {
        if(i in ans) {
            val (x, y) = ans[i]!!
            out.println("$x $y")
        } else {
            out.println("undecidable")
        }
    }

    out.flush()
}

data class Person(val a: Int, val b: Int, val x: Long, val y: Long)

https://atcoder.jp/contests/abc320/submissions/45661306

AiBiを頂点とする無向グラフを作って探索していきます。色々なやり方があると思いますが、今回は遷移先の頂点には頂点番号だけでなく全ての情報をもたせるようにしました。
当初は頂点番号をもとにXiYiを引き出せるMapを別途作ってやったのですが、それだとTLEになってしまいました。今回の場合は訪問済みの頂点はそのまま素直に訪問済みとすればよく、計算量としては問題ないはずなので、定数倍でやられたということですね。Mapの定数倍って重いのですね…
Mapを集約して、Mapから値を取得するオーバーヘッドを減らしたら通りました。

今回は訪問済の頂点はそのまま探索済みフラグを設定するだけで特に解除とか考えなくてよく、実装も楽だし計算量としても問題ないのですが、まさかの定数倍でやられました。うーん、難しいのやら簡単なのやら。

感想

Cが比較的早めに解けて緑パフォが出たのでレート的には勝ちでした。Dは解けてもよかったはずの問題なので残念ですが、最近は精進できていないのでむしろよくCが解けたなあという感想でした。
ABCに出る気力がわかないくらい体調が良くない週が続いていましたが、そのあたりも改善してきました。しばらくはこんな感じでやっていきます。

Mapの定数倍はけっこう重いぞというのは覚えておきます。Kotlin(というかJVM言語)の場合はオートボクシングの問題もあるので、なおさら重いのでしょうね。悩ましい。
(執筆時間: 57分28秒)

Discussion