🐷

AtCoder Beginner Contest 326参加記(A~C)

2023/10/29に公開

パナソニックグループ プログラミングコンテスト2023(AtCoder Beginner Contest 326)に参加したので記録を残します。
https://atcoder.jp/contests/abc326

今回も3完です。

A - 2UP3DOWN

愚直に場合分けしました。もっと賢い実装方法があるはずとは思いましたが思いつかず。

fun main() {
    val (x, y) = readln().split(" ").map { it.toInt() }
    if(x < y) {
        if(y - x <= 2) {
            println("Yes")
        } else {
            println("No")
        }
    } else {
        if(x - y <= 3) {
            println("Yes")
        } else {
            println("No")
        }
    }
}

https://atcoder.jp/contests/abc326/submissions/46994757

こうすればもっと簡単だったようですね…

fun main() {
    val (x, y) = readln().split(" ").map { it.toInt() }
    if(y in (x-3..x+2)) {
        println("Yes")
    } else {
        println("No")
    }
}

https://atcoder.jp/contests/abc326/submissions/47059139

B - 326-like Numbers

愚直に調べました。3桁であると決まっているので、999まで全部調べればOK。答えは必ず存在するということなので、小さいほうから調べて最初に見つかったのを出力して抜けるだけです。

fun main() {
    val n = readln().toInt()
    for(i in n until 1000) {
        val list = i.toString().map { it.digitToInt() }
        if(list[0] * list[1] == list[2]) {
            println(i)
            return
        }
    }
}

https://atcoder.jp/contests/abc326/submissions/46999835

C - Peak

けっこう難しいと感じて詰まってしまいました。最初に考えたのは累積和ですが、単純に累積和を使うだけの方法なら値がない位置も0で埋めるとかする必要があります。しかし、それをやるには制約が大きすぎて無理そうでした。
けっこう考え込んでしまいましたが、最終的に二分探索を使った解法を思いつきます。各Nについて、N+M-1の位置がわかればその範囲内の個数がわかるので、それを二分探索を使って高速に探索すれば間に合うはずと。
(-1があるのは半開区間のため)

ただ一つ問題がありまして、それは一つの座標に複数のプレゼントが置いてある場合の考慮です。Kotlinの二分探索メソッドだと、同じ値が複数ある場合にはそれのうちどれの位置が返ってくるのかわかりません。なので、値が複数ある場合はそれのうちどれの値が返ってきたのか判別して補正する考慮が必要でした。

どれが返ってくるのか判別するために、整数そのままではなく、その整数と順番を保持するデータクラスを作ってそれに対して二分探索するようにしました。

大まかな手順として、まずはソートします。それから各座標に何個のプレゼントが置いてあるのか記録して、上記のデータクラスの配列を生成します。(同じ値があったら連番を振るだけ)
あとは各Nに対してN+M-1で二分探索します。値がなければ、挿入位置をもとにそのまま答えがわかります。値があった場合はそれが何番目の値なのか見て、それと同じ値の個数をもとに補正します。N+M-1の値の個数は答えに含めます。
N+M-1より小さい座標の個数も複数の可能性がありますが、それは全部そのまま答えに含まれるだけなので考慮不要です。

import kotlin.math.max

fun main() {
    val (n, m) = readln().split(" ").map { it.toInt() }
    val a = readln().split(" ").map { it.toInt() }
        .sorted().toIntArray()

    val map = mutableMapOf<Int, Int>()
    for(i in 0 until n) {
        map[a[i]] = map.getOrDefault(a[i], 0) + 1
    }
    val indexedA = mutableListOf<IndexedInt>()
    var before = -1
    for(i in 0 until n) {
        val index = if(a[i] == before) {
            indexedA[i-1].index + 1
        } else {
            0
        }
        indexedA.add(IndexedInt(a[i], index))
        before = a[i]
    }

    var ans = 0
    for(i in 0 until n) {
        val to = IndexedInt(a[i] + m - 1, 0)


        val index = indexedA.binarySearch(to, compareBy { it.value })
        ans = if(index >= 0) {
            val count = map[to.value]!!

            val gap = indexedA[index].index + 1

            max(ans, index - i + 1 - gap + count)
        } else {
            val index2 = -(index + 1)
            max(ans, index2 - i)
        }
    }
    println(ans)
}

data class IndexedInt(
    val value: Int,
    var index: Int,
)

https://atcoder.jp/contests/abc326/submissions/47036107

面倒な実装になりましたが、C++の場合はlower_boundupper_bound、Pythonの場合はbisect_leftbisect_rightを使えばサクッと求められるようでした。羨ましい…w
Kotlinでもlower_bound相当のことはできると思っていたのですが、lower_boundのほうは複数の値があった場合はその最初の位置が返ると保証されているっぽいので、lower_boundの劣化版という感じになりますね…
lower_boundupper_boundみたいなのも作っておこうかなあ。

あとは、尺取り法でも解けるみたいでした。公式解説のコードが非常に簡潔で驚いたのですが、それは尺取り法を使った解法のようでした。せっかくなので尺取り法も勉強しようと思います。
解説記事を読んで、雰囲気だけ掴んだのですが実装は少し慣れが必要そう。

追記

しゃくとり法で解き直しました。

考え方としては以下の記事を参考にしました。
https://qiita.com/ngtkana/items/0b8f619a406d5f5b89de

左手、右手で考えるのはめちゃくちゃわかりやすい…
右手は進む一方で、戻ってまた数え直す動作がないということですね。

実装自体は解説コードに近いです。
左手を固定して右手を可能な限り伸ばし、Mを足した数以上になってしまったらその時点の右手の位置から左手の位置を引いたらそれがその時点での答えとなります。その後、左手を一つ右にずらして同じことを繰り返していきます。
右手がAの範囲を超えてもエラーにならないように末尾にダミーの値を足しています。

import kotlin.math.max

fun main() {
    val (n, m) = readln().split(" ").map { it.toInt() }
    val a = readln().split(" ").map { it.toInt() }
        .sorted()
        .plus(listOf(Int.MAX_VALUE))

    var ans = 0
    var r = 0
    for(l in 0 until n) {
        while (a[r] - a[l] < m) {
            r++
        }
        ans = max(ans, r - l)
    }
    println(ans)
}

https://atcoder.jp/contests/abc326/submissions/47067701

さらに追記

lower_boundupper_boundを作ろうかなと書きましたが、実際には作らなくていいことに気づきました。
以前他の問題の解説をもとに作った二分探索実装があるのですが
https://zenn.dev/dhirabayashi/articles/ca2ff0ad126888#d---factorial-and-multiple

これが一般的には「めぐる式二分探索」として知られており、また普通に使うとlower_boundと同じ動きをすることがわかりました。また、lower_boundがあればupper_boundの動きもできるということで…

import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min

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

    var ans = 0
    for(i in 0 until n) {
        val x = a[i]
        val xm = x + m

        val index = binarySearch(i, n) {
            a[it] >= xm
        }
        ans = max(ans, index - i)
    }
    println(ans)
}

fun binarySearch(minNg: Int, maxOk: Int, isOk: (Int) -> Boolean): Int {
    var ng = minNg
    var ok = maxOk

    while(abs(ok - ng) > 1) {
        val mid = (ok + ng) / 2
        if(isOk(mid)) {
            ok = mid
        } else {
            ng = mid
        }
    }
    return ok
}

https://atcoder.jp/contests/abc326/submissions/47158626

めぐる式二分探索はお気持ちがわかっていないとコードから意図を読み取りにくいかなと思って、使いこなすにはもうちょっと慣れが必要そうです。

感想

レートとしては負けなのですがけっこう楽しめました。なんとかCが解けて少しホッとしていまい。緑とか目指すならホッとしている場合じゃないのですが、まあ現状は緑になれる実力ではないので仕方ないです。今回の問題のおかげでlower_boundとかupper_bound、尺取り法のお気持ちがよくわかりました。こういう小さな成長を積み上げていきたいところです。
(執筆時間: 36分18秒)

Discussion