Rustで競プロがやりたい(2)

前回の続き、プロジェクトを hello_world から tester で作り直す。

build、run を一度実行しておき、launch.json を書き直しておく。 が、上側の Run/Build が消えてしまった。

再表示する方法が分からず、F5 を押しても再ビルドされるわけではない。 Ctrl + Alt + N で実行することで main.exe、main.pdb ファイルが作られて実行された。 正しい使い方ではないだろうが、ここで時間を使いすぎるのもあれなので一旦これでゴリ押す。

ここからは問題を解いてみる。 解く問題は鉄則本から。

amzn.asia

今回は A01 - The First Problem を解く。

atcoder.jp

use proconio::input; で入力を受け取れるようにして、input! {} で N の型を指定して受け取る。 最後に println!() で出力。

use proconio::input;

fn main() {
    input! {
        N: usize,
    }
    println!("{}", N*N);
}

これを実行するとエラーが出た。

調べた感じだと proconio が AtCoder でしか対応していないっぽい。

teratail.com

このまま提出してみると AC できた。

atcoder.jp

しかしなぜかコンパイルエラーが出ている。 他の提出を見ても同じエラーが出ている人がいない、なぜ。

調べたら、頭文字が大文字の変数がまずいらしい。 これは #[allow(non_snake_case)] を付けることで回避できた。

さらに出力を早くできるように use proconio::fastout;、#[fastout] も書き加えておく。

use proconio::input;
use proconio::fastout;

#[fastout]
#[allow(non_snake_case)]
fn main() {
    input! {
        N: usize,
    }
    println!("{}", N*N);
}

提出すると、先ほどのコンパイルエラーが消えた。

atcoder.jp

追記部分は以下の記事を参考にした。

qiita.com

今回はここまで、進めようとすると課題がどんどん増えていくような感じがする。

Rustで競プロがやりたい(1)

とりあえず Rust で競プロがやりたくなった。

環境構築から始める、とはいっても何からやればいいか分からないのでやり方を調べた。

qiita.com

vscode と Build Tools はすでにインストールしてるので、必要なものを順番に入れていく。

Rust のサイトに行く。

www.rust-lang.org

多分 RUSTUP-INIT.EXE(64-BIT)をダウンロードするの方でよさそう。

ダウンロードしたインストーラーを起動。

デフォルトのインストールでよさそうなので 1 で。

インストールが終わるのを待つ。

Enter で閉じて終わり。

とりあえずインストールはできたっぽい。

vscode に Rust の拡張機能を入れる。

marketplace.visualstudio.com

とりあえずのインストール。

と思ったら、非推奨でインストールできなくなっている。

代わりで指定されている rust-analyzer を入れておく。

これでインストールはできたっぽいのでコードを書く。 最初は hello world から。

まずはプロジェクトを作る必要があるっぽい。 cargo new [プロジェクト名] で作る。

自動でフォルダが作られている。 src の中にメインのコードを書く rs ファイルがあった。

cd で作ったプロジェクトのフォルダに移動して、build して走らせたらちゃんと出力した。

デバッグ機能を追加する。 直接 CodeLLDB を探してインストール。

実行のデバックの開始で、LLDB が候補として出るようになった。

LLDB を選ぶとエラーが出て launch.json が作られた。

11 行目の後ろの <> の部分を [プロジェクト名]/target/debug/[プロジェクト名].exe にして保存。

これで F5 で実行すればターミナルに結果が表示されるようになった。

と思ってコードを書き直しても再度 F5 で実行したときは前の結果が表示されてしまった。

とりあえず上の Debug 押せば再 build して今のコードの内容が出力されるようになった。

この辺の動きが正しいか分からないが今回はここまで。

ヒューリスティックコンテスト初心者の記事

はじめに

この記事は競プロ Advent Calendar 2022 の 10 日目の記事ですが、ヒューリスティックコンテスト初心者がだらだらと解くだけの記事です。 使用言語はPythonです。 基本的なことしか書いていません。

解く問題は AtCoder 10th Anniversaryになります。 理論値の大体 80 % ~ 90 %を取ることを目標にしています。

atcoder.jp

AtCoder 10th Anniversary

1. 得点を取る

まずは、得点を取らないとヒューリスティックは始まりません。(多分) なので、1 点でも得点を取るところから始めます。

得点に『カット回数が K 回を超える、または不正な直線を指定した場合は WA となる』、『一つ以上のテストケースで WA、または制限時間超過による TLE で提出全体の判定が WA や TLE になる』と書かれています。 K は入力で『K = 100』が確定しており、不正な直線に関しては問題文に『直線は2つの異なる整数座標 -109 ~ 109 を通る直線として指定する』と書かれています。

ここから 100 回ほど不正とならないように 2 点をランダムで選び、出力することで点数を取ることができそうです。 試しに、以下のようなコードを提出してみます。

from random import randrange
def rand():
    return randrange(-10**9, 10**9+1)

N = 100
print(N)
for _ in range(N):
    while True:
        px = rand()
        py = rand()
        qx = rand()
        qy = rand()
        if px == qx and py == qy:
            continue
        print(px, py, qx, qy)
        break

結果は AC となりました、しかし点数は 0 点です。 流石にランダムで 0 点では今後の実装に活かせない為原因を探ることにします。

atcoder.jp

適当な出力結果をビジュアライザで確認したところ、一本も直線が出ませんでした。 値が大きすぎる為か、全てケーキと関係ないところを結んでいるようです。

問題文を見るとケーキの大きさは半径 104 であるため、そもそも半径より大きい座標を指定する必要はないように感じます。 そこで、最初のコードを以下のように変更して提出してみます。

from random import randrange
def rand():
    return randrange(-10**4, 10**4+1)

N = 100
print(N)
for _ in range(N):
    while True:
        px = rand()
        py = rand()
        qx = rand()
        qy = rand()
        if px == qx and py == qy:
            continue
        print(px, py, qx, qy)
        break

このコードでは得点を約 59,000,000 点取ることができます。 満点は 100,000,000 点の為、半分以上は取れる計算になります。

atcoder.jp

ビジュアライザでも直線を引けていることが確認できます。

点数の条件や出力方法が分かったので、ここからは実装してさらに点数を上げる方法を考えていきます。

2. アイデアを試す

一発で理想となる答えを求めれるのが理想ですが、ヒューリスティックコンテストではそのような考えでは厳しそうです。 ランダムで直線を選び続けるのも、運で得点が増減するのは避けたいです。 ここは、規則的に直線を引くことで得点の向上を目指します。

ところで、普通ホールケーキは中心を基準に 6 等分や 8 等分のピースに切られることがほとんどです。 これをそのまま応用できないでしょうか。

試しに、ケーキに 50 個の直線を等分に引いてみます。

print(50)
x = 10000
y = 0
for _ in range(25):
    print(x, y, -x, -y)
    x -= 400
    y += 400
for _ in range(25):
    print(x, y, -x, -y)
    x -= 400
    y -= 400

しかし、1 ピース辺りに乗る苺が多すぎて点数が取れないようです。

今度は、上限いっぱいの 100 本を等分に引いてみます。

print(100)
x = 10000
y = 0
for _ in range(50):
    print(x, y, -x, -y)
    x -= 200
    y += 200
for _ in range(50):
    print(x, y, -x, -y)
    x -= 200
    y -= 200

それでも、1 個だけ乗っているピースがなかったりして、かなり厳しいように感じます。

そもそも、この切り方だと最大でピースが 200 個しかできません。 出席者は最大 1000 人の為、このやり方ではかなり不利に感じます。 もっと別の切り方を考える必要があります。

今まではランダム、つまり様々な方向から直線を引いていました。 逆に、全ての直線を縦、または横にするときれいに切り分けれるように感じます。

試しに、等間隔で縦と横に直線を引くことを考えます。 中途半端な数値になりますが、48 本の直線を引いてみます。

print(48)
x = -10000
for _ in range(24):
    x += 800
    print(x, 0, x, 1)
y = -10000
for _ in range(24):
    y += 800
    print(0, y, 1, y)

すると、きれいに格子状に分けられて見栄えよく感じます。 しかし、必要な個数のピースが足りないものも多く、まだ 11 個以上存在するピースも目につきます。

今度は 98 本の直線を引いてみます。

print(98)
x = -10000
for _ in range(49):
    x += 400
    print(x, 0, x, 1)
y = -10000
for _ in range(49):
    y += 400
    print(0, y, 1, y)

逆に今度は 3 個以下のピースができすぎて、等分のし過ぎが問題になっています。

そこで、中間くらいの配分で直線を引いてみます。 11個以上がほぼなく、程よく分散しているのでこれを提出してみます。

print(63)
x = -10000
for _ in range(24):
    x += 800
    print(x, 0, x, 1)
y = -10000
for _ in range(39):
    y += 500
    print(0, y, 1, y)

このコードでは、得点としては約 67,000,000 点取ることができました。

atcoder.jp

3. スコアを求める

先ほどの方法で引いた直線を動かせば、さらに得点を伸ばすことができないでしょうか。 なんとなく、今ある直線を上下か左右に動かせば、得点が増えたり減ったりしそうです。 いわゆる山登り法と言われる方法で、得点の向上を目指します。

今回は適当に直線を動かして、その得点が増えたら動かした状態を採用するということになります。 その為、その状態でのスコア、つまり、どの苺がどの部分に含まれるかを求める必要があります。 ただ求めるだけでは試行回数が減るので、なるべく高速に求める必要があります。 格子状に切るやり方だと、以下のような求め方が考えられます。

例えば、縦に切る線は固定し、横に切る線をランダムに動かしていくことを考えます。

まず縦にだけ切ると、 x 座標が縦のどの直線と直線の間にあるかが分かれば、苺がどこに属するか簡単に判明します。 なので、各苺を下図のように x 座標の位置でグループ分けします。 直線と被る場合は、問題文で『どのピースにも含まれない』と書かれている為、除外する必要があります。

次は横に切った時ですが、縦で分けた各グループごとに、y 座標が横のどの直線と直線の間にあるかが分かれば、その部分の苺の個数が分かります。 全部分の個数から、スコア計算式にあてはめ、スコアを求めることができます。

実装としては、まず縦のグループ分けをする部分は、あらかじめ苺を x 座標でソートしておけば求まります。 さらに各グループごとに y 座標をソートしておき、二分探索で部分ごとの苺の数を高速に求めることができます。 実装としては以下のようになります。

from bisect import bisect_left, bisect_right
# スコア計算用
def score_check():
    x = 0
    y = 0
    for i in range(10):
        x += min(a[i], cnt[i])
        y += a[i]
    ret = x / y * pow(10, 6)
    return round(ret)

# 入力
N,K = map(int,input().split())
a = list(map(int,input().split()))
Z = [list(map(int,input().split())) for _ in range(N)]
Z.sort() # x 座標でソート
x,y = [list(i) for i in zip(*Z)]

# 縦に切った後のグループ分け
G = [[] for _ in range(25)]
pos = -10000
cur = -1
for i in range(N):
    while pos < x[i]:
        pos += 800
        cur += 1
    if pos == x[i]:
        continue
    G[cur].append(y[i])

# グループごとに y 座標をソート
for i in range(25):
    G[i].sort()

# 横に切る予定の座標
P = [i for i in range(-10000, 10001, 500)]

# 苺が 1 ~ 10 個乗ったグループの個数を数える
cnt = [0] * 10
for i in range(25):
    for j in range(40):
        k = bisect_left(G[i], P[j+1]) - bisect_right(G[i], P[j])
        if 1 <= k <= 10:
            cnt[k-1] += 1

# スコア計算
score = score_check()

4. 山登り法を試す

スコア計算の実装が終わった為、ここからは実行制限時間まで探索を行います。 制約に『3 sec』と書いているので、大体 2.7 秒くらいまで探索します。

横に切る予定の座標からランダムで座標を増減させ、その得点によって更新するかを判定します。 今回は単純に得点が増えれば更新するだけです。 実装は以下のようになります。

from time import time
from random import randrange
from bisect import bisect_left, bisect_right
# スコア計算用
def score_check():
    x = 0
    y = 0
    for i in range(10):
        x += min(a[i], cnt[i])
        y += a[i]
    ret = x / y * pow(10, 6)
    return round(ret)

# 入力
N,K = map(int,input().split())
a = list(map(int,input().split()))
Z = [list(map(int,input().split())) for _ in range(N)]
Z.sort() # x 座標でソート
x,y = [list(i) for i in zip(*Z)]

# 開始時間
start_time = time()

# 縦に切った後のグループ分け
G = [[] for _ in range(25)]
pos = -10000
cur = -1
for i in range(N):
    while pos < x[i]:
        pos += 800
        cur += 1
    if pos == x[i]:
        continue
    G[cur].append(y[i])

# グループごとに y 座標をソート
for i in range(25):
    G[i].sort()

# 横に切る予定の座標
P = [i for i in range(-10000, 10001, 500)]

# 苺が 1 ~ 10 個乗ったグループの個数を数える
cnt = [0] * 10
for i in range(25):
    for j in range(40):
        k = bisect_left(G[i], P[j+1]) - bisect_right(G[i], P[j])
        if 1 <= k <= 10:
            cnt[k-1] += 1

# スコア計算
score = score_check()

# 実行制限ぎりぎりまで焼きなましをまわす
while time() - start_time < 2.70:
    Q = list(P)
    for i in range(1,40):
        if randrange(1,101) % 2:
            if randrange(1,101) % 2:
                Q[i] += randrange(1,101)
            else:
                Q[i] -= randrange(1,101)
    flg = False
    for i in range(1,40):
        if Q[i-1] >= Q[i] or Q[i] >= Q[i+1]:
            flg = True
    if flg:
        continue
    cnt = [0] * 10
    for i in range(25):
        for j in range(40):
            k = bisect_left(G[i], Q[j+1]) - bisect_right(G[i], Q[j])
            if 1 <= k <= 10:
                cnt[k-1] += 1

    # スコアが高くなれば更新する
    now_score = score_check()
    if now_score > score:
        score = now_score
        P = Q

# 出力
print(63)
x = -10000
for _ in range(24):
    x += 800
    print(x, 0, x, 1)
for i in range(1,40):
    print(0, P[i], 1, P[i])

このコードでは約 85,000,000 点を取ることができました。

atcoder.jp

6. 得点アップのアイデアを試す

ところで、途中から縦線と横線の本数を固定していましたが、ケースによっては本数を調整した方がいいのではないでしょうか。 seed = 0 では、縦の直線を 7 本、横の直線を 31 本等間隔に引けば初期の得点だけでも 700,000 点が稼げます。

ところが、これを同じように seed = 10 に適用しても 600,000 点程度しか取れません。

seed = 10 では、縦の直線を 15 本、横の直線を 49 本等間隔に引けば初期の得点だけでも 750,000 点が稼げます。

このことから、まず縦の直線、横の直線を何本引くかを探索した方がよさそうです。 ランダムに動かす予定の横の直線が多くなる方に調整します。 これらをまとめた実装が以下になります。

from time import time
from random import randrange
from bisect import bisect_left, bisect_right
# スコア計算用
def score_check():
    x = 0
    y = 0
    for i in range(10):
        x += min(a[i], cnt[i])
        y += a[i]
    ret = x / y * pow(10, 6)
    return round(ret)

# 入力
N,K = map(int,input().split())
a = list(map(int,input().split()))
Z = [list(map(int,input().split())) for _ in range(N)]
Z.sort() # x 座標でソート
x,y = [list(i) for i in zip(*Z)]

# 開始時間
start_time = time()

# 縦に切った後のグループ分け
G = [[] for _ in range(25)]
pos = -10000
cur = -1
for i in range(N):
    while pos < x[i]:
        pos += 800
        cur += 1
    if pos == x[i]:
        continue
    G[cur].append(y[i])

# 直線の数を探索
score = 0
tate = 0
yoko = 0
for ta in range(2,100):
    if 20000 % ta != 0:
        continue
    t_wide = 20000 // ta

    # 縦に切った後のグループ分け
    G = [[] for _ in range(ta)]
    pos = -10000
    cur = -1
    for i in range(N):
        while pos < x[i]:
            pos += t_wide
            cur += 1
        if pos == x[i]:
            continue
        G[cur].append(y[i])
    # グループごとに y 座標をソート
    for i in range(ta):
        G[i].sort()

    for yo in range(ta,100):
        if 20000 % yo != 0:
            continue
        y_wide = 20000 // yo

        # 横に切る予定の座標
        P = [i for i in range(-10000, 10001, y_wide)]

        # 苺が 1 ~ 10 個乗ったグループの個数を数える
        cnt = [0] * 10
        for i in range(ta):
            for j in range(yo):
                k = bisect_left(G[i], P[j+1]) - bisect_right(G[i], P[j])
                if 1 <= k <= 10:
                    cnt[k-1] += 1

        # スコア計算
        now_score = score_check()
        if now_score > score:
            score = now_score
            tate = ta
            yoko = yo

t_wide = 20000 // tate
y_wide = 20000 // yoko

# 縦に切った後のグループ分け
G = [[] for _ in range(tate)]
pos = -10000
cur = -1
for i in range(N):
    while pos < x[i]:
        pos += t_wide
        cur += 1
    if pos == x[i]:
        continue
    G[cur].append(y[i])

# グループごとに y 座標をソート
for i in range(tate):
    G[i].sort()

# 横に切る予定の座標
P = [i for i in range(-10000, 10001, y_wide)]

# 苺が 1 ~ 10 個乗ったグループの個数を数える
cnt = [0] * 10
for i in range(tate):
    for j in range(yoko):
        k = bisect_left(G[i], P[j+1]) - bisect_right(G[i], P[j])
        if 1 <= k <= 10:
            cnt[k-1] += 1

# スコア計算
score = score_check()

# 実行制限ぎりぎりまで焼きなましをまわす
while time() - start_time < 2.70:
    Q = list(P)
    for i in range(1,yoko):
        if randrange(1,101) % 2:
            if randrange(1,101) % 2:
                Q[i] += randrange(1,101)
            else:
                Q[i] -= randrange(1,101)
    flg = False
    for i in range(1,yoko):
        if Q[i-1] >= Q[i] or Q[i] >= Q[i+1]:
            flg = True
    if flg:
        continue
    cnt = [0] * 10
    for i in range(tate):
        for j in range(yoko):
            k = bisect_left(G[i], Q[j+1]) - bisect_right(G[i], Q[j])
            if 1 <= k <= 10:
                cnt[k-1] += 1

    # スコアが高くなれば更新する
    now_score = score_check()
    if now_score > score:
        score = now_score
        P = Q

# 出力
print(tate + yoko - 2)
x = -10000
for _ in range(tate-1):
    x += t_wide
    print(x, 0, x, 1)
for i in range(1,yoko):
    print(0, P[i], 1, P[i])

しかし、これでは得点が変更前と大体同じになってしまいました。

https://atcoder.jp/contests/ahc012/submissions/37015442

7. 感想

今回は約 85 %の点数を取ったところで断念しました。 というより、6 で考えた渾身のアイデアがあまり点数が伸びないことに心がやられました。 考えたアイデアが実はあまり役に立たなかったり、いいアイデアを思いついてもそれが実装できなかったりで、かなり沼だと思いました。

ヒューリスティックでは普段の競プロでは使わないランダムに動かす山登り法や焼きなまし法、スコア上位 K 番目までを残して探索し続けるビームサーチなどがあります。 今回はこれらの練習の為に記事を書こうと思いましたが、焼きなまし法は得点の減少を許す時間などの指定が上手くできなかったのか伸びず、ビームサーチに至っては考えていた実装ができなくて泣きました。

9 割超えると意気込んでいた割に成果を出すことができなかったので、来年リベンジします。(予定)

スパイス&ハーブ検定3級に合格した

スパイス&ハーブ検定3級に合格したので、どんな勉強をしたのか書き残しておきます。

スパイス&ハーブ検定とは

ざっくりいうとスパイスの正しい知識を学んで、日々の料理や暮らしに取り込んでいこうという感じの検定です。 詳しくは下の公式サイトから見てください。 yamazakispice-promotionfdn.jp

勉強方法

認定テキストであるこの本が試験範囲を全てカバーしています。 なのでこの本を読んでおけば問題ないと言えます。 f:id:tnodino:20220409143121p:plain

ちなみに協力会社であるエスビー食品のホームページにもスパイス&ハーブの情報が載っています。
www.sbfoods.co.jp

勉強方法 + α

上の本を読めば確かに知識としては得られますが、実際に使って覚えるのもありです。 3級の範囲のスパイス&ハーブはでかいショッピングモールやスーパーならほとんど置いていると思います。 置いていないものは通販などを検討してみましょう。

購入したら、実際に使ってみましょう。 協力会社であるエスビー食品のホームページにはスパイス&ハーブを使ったレシピが公開されています。 使うスパイス&ハーブの種類や、ご飯物、汁物などで細かくフィルターをかけて検索できるので大変便利です。 www.sbfoods.co.jp

感想

気まぐれで受けることになった試験ですが、なんとか完走できました。 本を読むだけで試験範囲はカバーできますが、科名や別名などの暗記が多かったので大変でした。

本とは別にスパイスとハーブを買いあさりましたが、ほとんどが使いきれずに残っています。 やっぱりブラックペッパーとガーリックしか勝たん。 あとエスビー食品が有能すぎる。 f:id:tnodino:20220409145750j:plain

カレーに入れる最適なカレールーの量を二分探索して調べたら地獄だった

1.はじめに

この記事は競プロ Advent Calendar 2021の19日目の記事です。

私は競プロを始めて1年半くらいが経過しました。専門学生時代には毎日問題を解き、社会人になった今でも仕事と回線の問題で毎日ではありませんが、気が向いたときに問題を解いています。

競プロでは様々なアルゴリズムを使いますが、私は思いました。

現実で使わん!!!

少なくとも、私が過ごしている中では Union-Find やダイクストラ等使う必要性が出てこないし、仕事では VBA で if、for、while を使って簡単なマクロを作るレベル。

せっかく覚えたアルゴリズムを使わずに終わるのはもったいないということで、今回は現実でも扱いやすそうな二分探索に焦点を当てていこうと思いました。

2.二分探索のテーマ決め

ところで、私はスパイスハーブ検定を受けるためにスパイスについて勉強してきました。そういえば、カレーはたくさんのスパイスを混ぜ合わせて作られている、それを凝縮したのがカレールーということをふと思い出しました。そんな凝縮されたカレールーなのだから、分量より多く入れた方がおいしさが爆上がりするのでは?、と考えてませんが、誰かにそそのかされたのでやることにしました。

結果的に、この誰かを恨むことになるとは、この時はまだ思っていませんでした。

f:id:tnodino:20211218220539p:plain

3-1.カレーの作り方

では早速カレーを作ろう!と思いましたが、カレールー以外の条件は固定しないと、味に変化があり、正確な結果にならないと考えました。そこで、以下のルールを決めました。

  • カレールーはスパイス&ハーブ検定に協力している S&B(エスビー食品株式会社)のものを採用
  • カレールーは 1 箱に 2 パック入っている。1 パックの 1/4 を 1 個、1 パックを 4 個、1 箱を 8 個とする
  • 野菜を毎回切っていては大きさや品質にばらつきが出ると思い、冷凍されているカット野菜を採用
  • カレーに使う材料はカット野菜(100g)、水(400ml)と、二分探索でばらつきが出るカレールー、少量のサラダ油のみトッピングや具を足すことは絶対にしない
  • 別途ご飯(1合)を炊く

f:id:tnodino:20211218223001j:plain

また、作り方も灰汁取りなどの工程は省き、シンプルにしました。

  • サラダ油でカット野菜を炒める
  • 水を入れて沸騰するまで待つ
  • カレールーを入れてふたをする(時々かき混ぜる)
  • 完全にカレールーが溶けたら皿にご飯と一緒に盛って完成

今回はこのルールで作っていきます。

ちなみに、このルールで作ると 2 人前になると思います。

3-2.二分探索の疑似コード作成

ルールが決まったところで、今回の二分探索の疑似コードを書こうと思います。

ok = 0
ng = 32

while ng - ok > 1
    x = floor((ok + ng) / 2)
    Curry = x 個のカレールーで作ったカレー
    if Curry は美味しい
        ok = x
    else
        ng = x

とりあえず(予算の都合で)カレールー 2 箱分から始まるように値を調整しました。頑張ってカレーを作りたいと思います。

3-3.二分探索開始

探索の状況は分かりやすく図にしておきます。

f:id:tnodino:20211218223527p:plain

では、最初のカレーはルー 2 箱分のカレーです。早速作っていきましょう。

f:id:tnodino:20211218224529p:plain

野菜を炒めて、

f:id:tnodino:20211218223734j:plain

水を入れて、沸騰した水にカレールーを入れ...

f:id:tnodino:20211218223858j:plain

2 箱分のカレールーを入れたカレー、思った以上にひどかった。

そのまま煮込むと...

f:id:tnodino:20211218224045j:plain

なんか泥ができた、これは泥でしかない。

f:id:tnodino:20211218224142j:plain

皿に盛ってみたが、カレーにスプーンがささって立った。 絶対に美味しくないと思うが、一応食べた。

パクッ

まず口の中に広がったのは食塩の風味、そして食塩の味が口いっぱいに広がり続けました。さらに食塩の後味が口の中に残り続けた。近くにあったアクエリアスを飲み干してもなお、残り続ける食塩感に嫌気がさした。カレールーには結構食塩が使われているみたいです。

f:id:tnodino:20211219014915j:plain

このカレーは致死量のカレーと呼ぶことにしました。もちろんおいしくなかったので ng = x です。

f:id:tnodino:20211218224606p:plain

ここで重大なミスをしました。8 個と 4 個の写真を撮り忘れてしまいました。 どうしようかと悩みましたが、泥と少しマシな泥をもう一度作りたくなかったので妥協することにしました、時間もなかったし。

f:id:tnodino:20211218224852p:plain f:id:tnodino:20211218224918p:plain f:id:tnodino:20211218224933p:plain f:id:tnodino:20211218224951p:plain

ここまで、4 個以上カレールーを入れるとまずいことが分かりました。スパイスを入れすぎるということをオーバースパイスといい、今回はオーバースパイスで濃いカレーができてまずいかな、と考えていましたが、普通に食塩で死ぬと思いませんでした。

f:id:tnodino:20211219013545p:plain

次はカレールー 2 個のカレーです。

f:id:tnodino:20211219013703j:plain

普通のカレーができました。3 日間泥を見続けたからか、普通のカレーができただけでかなり嬉しかったです。

一口食べて、あまりのおいしさにスプーンが止まらず、勢いで完食してしまいました。この企画初めてのおいしいカレーに涙が止まりませんでした。もちろん ok = x です。

f:id:tnodino:20211219013820p:plain

そして、今回最後となるカレールー 3 個のカレーです。

f:id:tnodino:20211219013926p:plain

f:id:tnodino:20211219014007j:plain

今までの泥と比べたら食べれないことはないものの、塩気が強く少し美味しくないと思いました。ng = x とします。

f:id:tnodino:20211219014124p:plain

3-4.二分探索結果

二分探索の結果、カレールー 2 個のカレーが一番おいしいとわかりました。というより、分量をキチンと守って料理作れカスって話でした。

もう少しカレールーを使うと思って買いだめをしたんですが、だいぶカレールーが余ってしまいました。

f:id:tnodino:20211219014716j:plain

「もうカレーはこりごりだよ~!」

f:id:tnodino:20211219015449j:plain

4.あとがき

今回は二分探索を取り上げました。毎回半分の要素が探索済みになるため、10の18乗の要素数でも約60回くらいの探索で答えを求めることができます。

「伝家の宝刀二分探索」とも言われ、応用がしやすいので絶対に使えるようになりたいアルゴリズムです。

リストが昇順か降順、または答えが線型でないと使えない点に気を付けましょう。

スパイス&ハーブ検定、受かってるといいな。

ABC207 C - Many Segments 解説

問題

Difficulty : 397 atcoder.jp

解説

$N$ の最大が 2000 と小さいため、組み合わせを全探索しても間に合う。 問題は共通部分の判定である。

区間の種類ごとに場合分けしてもいいのだが、もっと分かりやすくしたい。 複雑になっているのは、「$X$ 以上」や「$Y$ 未満」など、統一性がないことである。 これをどちらかに統一することを考える

「以上」「以下」はその数字を含むため、変換が難しそうに感じる。 一方「大きい」「未満」はその数字を含まないため、値を足し引きすることで「以上」「以下」にすることが容易である。

分かりやすく足し引きする値を 1 としてみる。 区間 $(4,8)$ を変換すると $[5,7]$ となり、元の区間の条件を満たしつつ共通部分を持つかの判定が容易にできる。

共通部分を持つかの判定は、$max(l_i,l_j)≤min(r_i,r_j)$ でできる。 $max(l_i,l_j)>min(r_i,r_j)$ だと、例えば $l_i,r_i,l_j,r_j,$のような順に並んでいる。 小さい $l_i$ と大きい $l_j$ の間に $r_i$ が存在するため、小さい側 $(l_i,r_i)$ と大きい側 $(l_j,r_j)$ で共通部分を持たない別々の区間になってしまう。 つまり、そうでないなら 端点を含むどこかで共通部分ができる。

これで AC と言いたいが、先ほどのように足し引きする値を 1 にした場合、おかしなことになる。 例えば、$(2,3)$ という区間を変換すると $[3,2]$ と明らかにおかしい。

そこで値をもっと下げてみる。 0.1 にすると、$[2.1,2.9]$ となり、条件を満たすし、先ほどのようにおかしなことにもならない。 この値の許容範囲は 0.5 まであり、解説でも 0.5 を扱っている。

以上のことから、区間ごとに決めた値を足し引きし、共通部分を持つか判定することでこの問題を AC できた。

AC コード

atcoder.jp

ABC191 振り返り

はじめに

誤読祭りに誤差祭り...大変な回だったので戒めとして記事にすることにしました。

A - Vanishing Pitch
B - Remove It
C - Digital Graffiti
D - Circle Lattice Points
E - Come Back Quickly
F - GCD or MIN

A - Vanishing Pitch

Difficulty : 25(AC)

いつもの A より問題文が複雑な気がする。
$V \times T$ から $V \times S$ の間は球が消えているので、判定する。

v,t,s,d = map(int,input().split())
if v*t <= d <= v*s:
    print('No')
else:
    print('Yes')

B - Remove It

Difficulty : 20(AC)

A より DIff 低いってどういうこと。
前から順番に見ていって、$X$ と違うならリストに加えていく。

n,x = map(int,input().split())
a = list(map(int,input().split()))
ans = []
for i in range(n):
    if a[i] != x:
        ans.append(a[i])
print(*ans)

C - Digital Graffiti

DIfficulty : 1063(-)

これは何角形ですか? f:id:tnodino:20210207134518p:plain

そうです八角形ですね。 f:id:tnodino:20210207134612p:plain

多くの人を誤読させた問題。
上の図を三角形と考えた人は多いはず(自分も考えた一人)。
解説を見て、ようやくこの問題の正しい答えが分かりました。
そのマスの各頂点(
左上$(i,j)$、右上$(i,j+1)$、左下$(i+1,j)$、右下$(i+1,j+1)$
)の黒マスの合計が奇数なら答えが +1 されるようです。

h,w = map(int,input().split())
s = [list(input()) for i in range(h)]
ans = 0
for i in range(h-1):
    for j in range(w-1):
        cnt = 0
        for n,m in ((0,0),(1,0),(0,1),(1,1)):
            ni,mj = n+i,m+j
            if s[ni][mj] == '#':
                cnt += 1
        if cnt % 2 == 1:
            ans += 1
print(ans)

D - Circle Lattice Points

DIfficulty : 1953(-)

なんでACできないんですか?
そうです誤差のせいですね。

誤差WAが多すぎて難易度爆上がりな問題。
自分は格子点の内部の求め方が分からなかったので、コンテスト中に以下の記事を見つけて参考にしました。

任意の円の内部の格子点の数を求める(@Tukaene様)

これを参考に実装しても誤差のせいでサンプル3が合わず断念しました。 が、コンテスト後に他の人のACを見たところ、Decimalを使って計算すれば正確な精度となり、ACできたようです。

from math import ceil,floor
from decimal import Decimal
x,y,r = map(Decimal,input().split())
low = ceil(x-r)
high = floor(x+r)

ans = 0
for i in range(low,high+1):
    d = Decimal(r**2-(x-i)**2).sqrt()
    top = floor(y+d)
    bottom = ceil(y-d)
    ans += top - bottom + 1
print(ans)

E - Come Back Quickly

Difficulty : 1323(AC(1))

実行制限時間が3秒、$N$ も $ M $ も 2000 と小さい。ならワーシャルフロイド使っても問題ないのでは?(TLE)

TLEしたので、それぞれの頂点からダイクストラしました。 使ったら早くなるんじゃないかな、という感覚で一応heapqを採用しています。

from heapq import heappop,heappush
n,m = map(int,input().split())
INF = 10**10
graph = [[] for i in range(n)]
for i in range(m):
    a,b,c = map(int,input().split())
    a -= 1
    b -= 1
    graph[a].append((c,b))
for i in range(n):
    cost = [INF] * n
    queue = []
    for j,k in graph[i]:
        heappush(queue,(j,k))
    while queue:
        c,k = heappop(queue)
        if cost[k] > c:
            cost[k] = c
            for x,y in graph[k]:
                heappush(queue,(c+x,y))
    ans = cost[i]
    if ans == INF:
        print(-1)
    else:
        print(ans)

F - GCD or MIN

Difficulty : 2333(-)

普通に難しすぎるやんけ。

コンテスト中の考察
操作して残る数字候補を考える。サンプル3を見て思ったのが、「与えられた数字で、一番小さい整数(以下 min とする)以下の整数しか残らないのでは?」ということ。

$gcd(A,B)$ を考えると、$min(A,B)$ が最大公約数になることはあっても、それを超える整数が最大公約数になることはない。min 以下で、何か法則があって選ばれるのでは?

min の約数かと考えたが、サンプル3で残る数字が 3 とか 6 とか 7 とかで全部が 27 の約数というわけではない。でも、min 以外の整数には約数としてありえる。ならこの約数と何かを使えば答えが求まるのでは?

$N$ が 2000 なので、$gcd(A_i,A_j)(1 \le i,j \le 2000)(i \neq j)$ で各最大公約数を求め、最大公約数、約数で出た整数が答えの候補かと考えたがWA、時間切れ

コンテスト後解説を見て
どうやら答えが min 以下になることと、約数を使うのは間違ってなかったようです。

連想配列 $t$ を用意します。 $A_i$ の約数を求め、約数を $j_1...$ とします。連想配列に $t_j$ がないなら $t_j = A_i$ とし、あるなら $t_j = gcd(t_j,A_i)$ で更新します。最終的に $t_j = j$ なら残る整数としてあり得ます。

この操作は、その約数同士が含まれる整数同士を $gcd$ して、その整数を作ることができるか調べています。サンプル2では、全ての整数に共通する約数は $(1,2)$ ですが、全て $gcd$ しても 1 になることはないので残せません。サンプル 3 では、$gcd(30,49)$ で 1 になるので、この二つを $gcd$ し、後の操作を全て $min$ にすることで 1 が残るように操作できます。これで作れる整数が分かりました。

考察に書いてある通り、最終的な答えは全て min より小さいので、min 以下の整数の個数を数える必要があります。$2,5$ のような配列が与えられた場合、連想配列では 5 が残る整数候補になります。しかし、2 との操作をする必要があり、残すことはできません。約数を求める時点で min を超える約数を除外するか、作れる整数を求めた後に min 以下の整数のみを数えるなどで、正しい答えが出ます。

from math import floor,sqrt,gcd
n = int(input())
a = list(map(int,input().split()))
b = [[] for i in range(n)]
m = min(a)
for i in range(n):
    x = a[i]
    for k in range(1,floor(sqrt(x))+1):
        if x % k == 0:
            b[i].append(k)
            if x // k != k:
                b[i].append(x//k)
t = {}
for i in range(n):
    for j in b[i]:
        if not j in t:
            t[j] = a[i]
        else:
            t[j] = gcd(t[j],a[i])
ans = 0
for i,j in t.items():
    if i == j and i <= m:
        ans += 1
print(ans)