はまやんはまやんはまやん

hamayanhamayan's blog

kcal [AtCoder Beginner Contest 205 A]

https://atcoder.jp/contests/abc205/tasks/abc205_a

解説

https://atcoder.jp/contests/abc205/submissions/23457255

単位数である1mLを経由することで計算をしていく。

100 mL -> A kcal
1 mL -> A / 100 kcal
B mL -> A / 100 * B kcal

ということでA/100*Bが答えになる。
実はC++だとAをintで取っている場合はA/100で切り捨てが発生してしまうので、

  • Aをdoubleで取る
  • (double)A/100*Bみたいにキャストする
  • 1.0A/100Bみたいにdoubleで書けることで暗黙的にキャストする

みたいな工夫をする必要がある。
あとは小数の出力も知らないとできないと思うので、できるようにしておこう。
C++だと自分はprintfでやっている。

int A, B;
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> A >> B;
    double ans = 1.0 * A / 100 * B;
    printf("%.10f\n", ans);
}

Hanjo 2 [AtCoder Beginner Contest 204 F]

https://atcoder.jp/contests/abc204/tasks/abc204_f

前提知識

解説

https://atcoder.jp/contests/abc204/submissions/23263864

今回の問題はとても難しいのだが、典型度は高い。
まず、制約がかなりとがっているので、何を要求されているかは制約を見れば慣れてくれば分かる。
類題を見たことがあるので、解法までは一直線だったので、恐らく一部発想が飛んでいる。

敷き詰め対象について

組合せ計算をするという観点で考えると、2×1のタイルの置き方が決まれば、1×1はそれ以外に置くだけで置き方は一意に定まるので、
2×1のタイルをどのような形で置くかということだけを考えることにする。

H≦6とは何か?

パッと見て明らかにビットを意識した制約に見える。
今回の敷き詰めの問題を行っていくので、ビットで管理できそうなのは2×1のタイルが置かれているかどうかである。
その辺から組合せ計算という部分から考えるとビットDPが思いつく。

dp[i][msk] := i-1列目まで2×1のタイルの置き方が確定していて、i列目のタイルの置き方がmskである場合の置き方の組み合わせ

タイルが2×1のサイズしかないので、左から順に畳を置いていくように方針を決めれば、置こうとしている列の1つ前の列くらいしか置き方には影響しないことが分かる。
なので、i+1列目の置き方はi列目の置き方さえ分かればいいことになり、DPの状態として置き方のmaskが置かれることになる。

これでとりあえず状態圧縮は成功している。

Wの上限が1012

1012なので、log(W)かlog(1)を目指すしかない。
先のDPを見ると、i列目の状態からi+1列目の状態を作ることができる(それより前は関係ない)ので、行列累乗によるDP高速化が行えそうである。

行列累乗によるDP高速化

詳細を書くにはちょっとガッツが必要なので、コンセプトについては、ABC(AtCoder Beginner Contest)の009Dの解説を見るのがいいと思う。
Abc009
他にも「行列累乗 DP」とかでもいい解説が出てくるんじゃないかな?

ざっくり書くと
dp[i][any]からdp[i+1][any]への遷移について

  • dp[i+1][any]への遷移はすべてdp[i][any]からの遷移になっている
  • 1回の遷移は毎回同じように(同じ係数で)行われる
  • 行列計算で|any|^3の計算をするのでそれが間に合うか

が満たされれば使用することができる。

1番目は満たしていて、2番目も毎回状態が変化するわけではないので同じ遷移になるし、3番目が今回の条件に特にマッチする。
anyは26通りなので、218となり、ちょうどいい計算量となる。

後は実装

正直ここまで理解できるかが第一関門であり、実装もやや大変。
行列ライブラリを作る必要があるし、遷移のための行列を作るのも正直簡単ではない。
フィボナッチ数列の計算を行列累乗でできるかくらいの練習を先に挟むのがいいと思う。
そこで行列計算ライブラリか、行列計算の実装の手ごたえをつかむのがオススメ。

遷移の行列作り

さて、行列計算はOKと仮定すると、最後の関門は遷移の行列作りである。
色々実装はあると思うが、自分の実装では縦の組み合わせを先に計算してから、横の置き方を全探索することで遷移を試していくことにした。

init[msk] := 2×1の畳を縦に置くことでmskの状態を作ることができるか

これを先に作っておく。
これは全ての組み合わせについて上から2個グループが過不足無く作れるかを見ていけばいい。

E[to][from] := dp[i+1][to]に対して足し合わされる時のdp[i][from]に対する係数

を作る。fromとtoを全探索して係数を決めていくが、普通にやるのは難しい。
fromとtoを全探索するが、この時toは畳を縦に置いた場合のみを考える。その後、各列について横に畳を置く組合せを全探索して、
置けるなら置いて、置いた場所にビットを立たせてfromとtoを確定させて、組合せを足し合わせていく。
このやり方だとfrom, to, 各列の横置きの全探索で218通りかかるが、間に合うのでこれで実装した。

後は、initを初期ベクトルとして、Eを行列累乗でW-1回掛け合わせて、initとかけて総和を取れば答えになる。

int H; ll W;
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> H >> W;

    Mat m(1 << H, Vec(1 << H, 0));
    Vec v(1 << H, 0);

    Vec init(1 << H, 0);
    rep(msk, 0, 1 << H) {
        bool nextPlace = false;
        bool ok = true;
        rep(i, 0, H) {
            if (nextPlace) {
                if (msk & (1 << i)) nextPlace = false;
                else ok = false;
            }
            else {
                if (msk & (1 << i)) nextPlace = true;
            }
        }
        if (nextPlace) ok = false;
        if (ok) {
            init[msk] = 1;
        }
    }

    rep(msk, 0, 1 << H) v[msk] = init[msk];

    rep(from, 0, 1 << H) rep(to, 0, 1 << H) {
        rep(used, 0, 1 << H) {
            bool ok = true;
            rep(i, 0, H) if (used & (1 << i)) {
                if ((from & (1 << i)) || (to & (1 << i))) ok = false;
            }
            if (ok) {
                int to2 = to;
                rep(i, 0, H) if (used & (1 << i)) to2 += 1 << i;
                m[to2][from] = add(m[to2][from], init[to]);
            }
        }
    }

    m = fastpow(m, W - 1);
    v = mulMatVec(m, v);

    int ans = 0;
    rep(msk, 0, 1 << H) ans = add(ans, v[msk]);
    cout << ans << endl;
}

Rush Hour 2 [AtCoder Beginner Contest 204 E]

https://atcoder.jp/contests/abc204/tasks/abc204_e

前提知識

解説

https://atcoder.jp/contests/abc204/submissions/23263588

問題設定が有向グラフの最短経路問題なので、ダイクストラ法が最初に思いつく。
ダイクストラ法をもじって解くことをまずは考えていこう。

特殊な条件

一般的なダイクストラと違う点が2点ある。

  • 各都市で整数時間待機できる
  • コストが現在の時間で決定する

まず、上の問題であるが、整数時間待機できるが、各頂点で到達できる時間は一般的なダイクストラ法と同じく最短経路を考えればいい。
時間がかかれば場合によってはコストが小さくなる可能性があるが、それを狙うのであれば、最短経路で進んでから整数時間待機すればいい。
なので、1番目の条件はほとんど問題にならない。
問題は2番目である。

2番目をどう扱うか

以下の問題が解ければ、そのほかはダイクストラ法と全く同じメソッドが使える。
「頂点Aに最短時間cuでいた場合、有向辺で遷移可能な頂点Bに動く場合の最短距離は?」

自明な所から考えていけば、C[i] + floor(D[i] / (t + 1))という式からtがD[i]以上の場合は割り算部分が0になるので、tはD[i]以下だけ考えればいい。
と考えると、遷移の可能性は最短時間cuからD[i]のいずれかとなる。
もっと言うと、最短時間cuがD[i]を超えている場合もあるので、その場合は待つメリットがないので、最短距離cuを採用すればいい。

遷移先を減らすことができたが、この後が問題。
というか三分探索してWAして、絶望してた。

最適な時間は?

C[i] + floor(D[i] / (t + 1))の最短時間はどの辺だろうということを考えると、相加相乗平均を思い出すとsqrt(D[i])あたりだろうなぁという想像がつく。
言われてみれば確かにという感じ…
床関数を外した場合の最小値が、外さなかった場合の最小値とほぼ一緒(全く一緒なのかな?分からない)というのは確かにどこかで見たことがある気がする。
そのあたりで最適な時間を探して答えを見つけよう。

基本的には遷移は無駄に確認しても、最適解は導くことができる。
なので自分の実装では、とりあえず全く待たない場合にかかる時間を求めておいて、そのあとに、sqrt(D[i])の±10くらいの時間で、
かつ、現在の最短時間以降の時間を使って計算するようにしている。
このことに気が付けば、それ以外は特に難しくない。

int N, M;
int A[101010], B[101010], C[101010], D[101010];
vector<int> E[101010];
//---------------------------------------------------------------------------------------------------
template<typename T> using min_priority_queue = priority_queue<T, vector<T>, greater<T>>;
int vis[101010];
ll dist[101010];
//---------------------------------------------------------------------------------------------------
ll calc(ll cur, ll c, ll d) {
    return cur + c + d / (cur + 1);
}
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N >> M;
    rep(i, 0, M) {
        cin >> A[i] >> B[i] >> C[i] >> D[i];
        A[i]--; B[i]--;
        E[A[i]].push_back(i);
        E[B[i]].push_back(i);
    }

    rep(i, 0, N) dist[i] = infl;
    rep(i, 0, N) vis[i] = 0;

    min_priority_queue<pair<ll, int>> que;

    dist[0] = 0;
    que.push({ 0, 0 });

    while (!que.empty()) {
        auto q = que.top(); que.pop();

        ll cst = q.first;
        int cu = q.second;

        if (cu == N - 1) {
            cout << cst << endl;
            return;
        }

        if (vis[cu]) continue;
        vis[cu] = 1;

        fore(idx, E[cu]) {
            int to = A[idx] + B[idx] - cu;

            ll cst2 = calc(cst, C[idx], D[idx]);

            ll sqr = sqrt(D[idx]);
            rep(d, -10, 10) if (cst <= sqr + d) chmin(cst2, calc(sqr + d, C[idx], D[idx]));
            if (chmin(dist[to], cst2)) que.push({ dist[to], to });
        }
    }

    cout << -1 << endl;
}

Cooking [AtCoder Beginner Contest 204 D]

https://atcoder.jp/contests/abc204/tasks/abc204_d

解説

https://atcoder.jp/contests/abc204/submissions/23262989

この問題はDPで解ける。
いかにDPで解くという所に帰着させるかを説明していこう。

まず、制約がとても小さい。
パッと見て貪欲で行けそうな感じがあるが、貪欲ならもうちょっと制約が大きくても解けるので、
ギリギリを目指す問題設定という方針を考えると怪しい。
一旦、一般的なアルゴリズムの形に落とし込めないか考えてみよう。

正直もうこの時点で、最短というのとか、制約があまり大きくないとか、今までの経験とかでDP感がしている。

性質を見つける

最終的にはとある料理はどちらかのオーブンに割り当てられることになる。
かつ、料理がオーブンに割り当てられているときに、とあるオーブンが料理がどのような順番で調理されても最終的な最短時間は変わらない。
つまり、選択肢としては料理はどちらかに割り当てられているかだけが関係していて割り当て順番は関係ないことが分かる。
なので、適切に2グループに分けて、大きい方を最小化する問題に帰着できる。

DP

dp[i][t] := i番目まで料理作成が終わっていて、オーブンAの使用時間がtであるときのオーブンBの使用時間の最小値

本番はこのDPで解いたが、何番目まで料理作成が終わっていて、オーブンAの使用時間が分かっていればオーブンBの使用時間は一意に定まるので、最小値もなにもない。
なので、特にメモる必要もないのだが、書いてしまったので、これで説明する。

遷移は、料理作成時にオープンAを使うなら、dp[i][t] -> dp[i + 1][t + A[i]]と遷移させる。
オーブンBを使うなら、dp[i][t] + A[i] -> dp[i + 1][i]と遷移させる。

最後はmax(t, dp[N][t])の最小値を取れば答え。

int N, T[101];
int dp[101][102010];
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N;
    rep(i, 0, N) cin >> T[i];

    rep(i, 0, N + 1) rep(t, 0, 101010) dp[i][t] = inf;
    dp[0][0] = 0;
    rep(i, 0, N) rep(t, 0, 101010) if (dp[i][t] != inf) {
        chmin(dp[i + 1][t + T[i]], dp[i][t]);
        chmin(dp[i + 1][t], dp[i][t] + T[i]);
    }

    int ans = inf;
    rep(t, 0, 101010) chmin(ans, max(t, dp[N][t]));
    cout << ans << endl;
}

Tour [AtCoder Beginner Contest 204 C]

https://atcoder.jp/contests/abc204/tasks/abc204_c

前提知識

解説

https://atcoder.jp/contests/abc204/submissions/23261589

この問題では実装方針から正しく計算量を見積もれるかがポイントとなる。
計算量見積もりをして、オーダーで上限を当てはめたときに107くらいに抑える必要がある。
逆に、107くらいまでは使ってもいいともいえる。

オーソドックスな問題

最後の都市の組として考える問題は珍しいので、もうちょっとオーソドックスな問題で考えてみよう。
普通に始点が固定だった場合は到達性問題になるので、その場合はBFSで解くことができる。
この問題が解けない場合は「BFS」や「到達可能性問題」あたりで調べて、理解してから次に進める。

BFSを普通にやってみると、O(N+M)となるので、これは十分に間に合う。
というか、計算量が余っている。
計算量が余っているので、もうちょっと無理ができる。

始点を全探索

よくよく考えるとスタート地点が全探索される必要があり、やっても間に合ってしまう。
スタート地点はN通りなので、O(N(N+M))が計算量となり、これは制約を見ると間に合う。
これが間に合うということが分かることが今回はキモとなる。

始点が変われば事象は独立になるので、それぞれの組み合わせを足せば答えになる。

int N, M;
vector<int> E[2010];
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N >> M;
    rep(i, 0, M) {
        int a, b; cin >> a >> b;
        a--; b--;
        E[a].push_back(b);
    }

    int ans = 0;
    rep(st, 0, N) {
        queue<int> que;
        set<int> vis;

        que.push(st);
        vis.insert(st);

        while (!que.empty()) {
            int cu = que.front(); que.pop();
            fore(to, E[cu]) if (!vis.count(to)) {
                que.push(to);
                vis.insert(to);
            }
        }

        ans += vis.size();
    }
    cout << ans << endl;
}

Nuts [AtCoder Beginner Contest 204 B]

https://atcoder.jp/contests/abc204/tasks/abc204_b

解説

https://atcoder.jp/contests/abc204/submissions/23260128

シミュレーションする問題。要求されていることを実装しよう。
それぞれの木の実について、

  • 10以下であれば何もしない
  • 10より大きいならA[i] - 10を収穫する

ように実装すればいい。

自分の実装

自分の実装ではそうなっておらず、max(0, A[i] - 10)を足し合わせる実装にしている。
これは基本はA[i] - 10が収穫されるが、10未満の場合はマイナスになってしまい、かつ、その場合は何もしないことになっているので、
A[i] - 10をして負ならば0になるようにしてやれば画一的な操作で実装ができる。
よって、下限がある場合はmaxを使ったこのような実装が使える。
手数として持っておくといい実装テクだと思う。

int N, A[1010];
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N;
    rep(i, 0, N) cin >> A[i];

    int ans = 0;
    rep(i, 0, N) ans += max(0, A[i] - 10);
    cout << ans << endl;
}

Rock-paper-scissors [AtCoder Beginner Contest 204 A]

https://atcoder.jp/contests/abc204/tasks/abc204_a

解説

https://atcoder.jp/contests/abc204/submissions/23259935

あいこになるのは全部同じか全部違う場合なので、その2つの状況で場合分けして答えることにしよう。
x=yであれば全部同じの場合なので、xを答えればいい。
x!=yであれば全部違う場合なので、xでもyでもないものを答えればいい。
これはx+y+ans=3であることを利用して、ans=3-x-yと答えることができる。
単純に場合分けしても問題ない。

int x, y;
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> x >> y;
    if (x == y) cout << x << endl;
    else cout << (3 - x - y) << endl;
}