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

hamayanhamayan's blog

Vapor Pressure [第七回 アルゴリズム実技検定 B]

https://atcoder.jp/contests/past202107-open/tasks/past202107_b

解説

https://atcoder.jp/contests/past202107-open/submissions/24459788

シミュレーション問題。
計算一本でも求めることができるが、言われているシミュレーションを動かすことでも求めることができる。
「ボールの数が風船の数のC倍以下になる」が満たされるようになるまでボールを1つずつ減らしていき、
最終的なA÷Bが答えになる。

C++では注意点として整数としてABCを受け取った場合、単純にA/Bとすると答えは整数値となり、切り捨てのような動作となる。
よって、1.0を事前に書けるか、明示的なキャスト(double)を利用して計算結果がdoubleになるようにする必要がある。

小数の答え方も独特なので注意しよう。
小数点以下10位くらいで自分はいつも答えている。

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

check digit [第七回 アルゴリズム実技検定 A]

https://atcoder.jp/contests/past202107-open/tasks/past202107_a

解説

https://atcoder.jp/contests/past202107-open/submissions/24459496

シミュレーション問題。
問題で要求されていることを実装しよう。

まず、自分の実装ではコードを文字列で取り込んでいる。
問題文でも文字列ですという風に書いてある所を見ると、文字列で取り込むのがオススメなのかも。

偶数番目、奇数番目というのはループの添え字に対して2で割った余りを利用するのがいい。
上級者はbit演算を使うかもしれないが、もちろんそれでもいい。

文字列としてコードを取り込んだ場合、各桁は文字として扱われるが、これを数に変換する必要がある。
これにはいくつか方法があり、標準ライブラリでも使える関数があるが、自分はS[i]-'0'のようにして変換している。
これはAscii的な所を意識した部分であるが、パッと見て何を目的としたものか分かりにくいかもしれない。
文字を数に変換していると思ってほしい。

string S;
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> S;

    int tot = 0;
    rep(i, 0, 14) if (i % 2 == 0) tot += S[i] - '0';
    tot *= 3;
    rep(i, 0, 14) if (i % 2 == 1) tot += S[i] - '0';

    if (S[14] - '0' == tot % 10) cout << "Yes" << endl;
    else cout << "No" << endl;
}

Ring MST [AtCoder Beginner Contest 210 E]

https://atcoder.jp/contests/abc210/tasks/abc210_e

前提知識

解説

https://atcoder.jp/contests/abc210/submissions/24333006

題名にもあるように今回はMST,最小全域木を作ることが目的となっている。
上手く隠されているような感じがするが、クラスカル法を前提として問題を解くことができる。
クラスカル法についてはそれほど説明しないので、他で勉強することを勧める。
ちょっとだけ触れるし、核の部分ではないので、必須ではないかと思うが…どうだろう…

クラスカル

クラスカル法とは、最小全域木を作るアルゴリズムの1つで、使える辺の中でコストが最小のものから連結でないなら
使っていくというアルゴリズムである。
今回で言うとM種類の操作について費用が少ないものから使用を検討していくことにする。
とりあえず、M種類の操作は費用が少ない順でソートされているとこれ以降は考える。

とある操作を行うとき

クラスカル法なので、とある操作を行うときに連結成分を減らせるだけ減らすように毎回行っていく。
例えば、N=6でA=2の場合を考えてみる。

1->3  
2->4  
3->5  
4->6  
5->1  
6->2  

これで連結になる辺を先頭から順に選択すると最初の4つが使える。
最後の2つはすでに連結なので必要ない。
こうすると、2つの連結成分になるので、N=2になったと考えてもいい。

ここがほんとかポイントなのだが、例えばこの状態でA=3を考えてみると、

1->4  
2->5  
3->6  
4->1  
5->2  
6->3  

となり、連結している中で最小の数に変換したと考えると

1->2  
2->1  
1->2  
2->1  
1->2  
2->1  

となり、実質

1->2  
2->1  

なので、連結成分数を操作後のNとしてしまっても問題ない。

操作前にNで、とある操作がAであるとすると、最適に操作をすると連結成分数はgcd(N,A)となる。
これは今までに何となく似たような問題を見てきたのと、実験と、色々加味してひねり出した。

まとめ

説明が冗長になってきたので、まとめる。
操作は費用が少ない順に実施して、毎回の操作で、操作前の連結成分数がcurで、操作Aを行ったとすると、
連結成分数はgcd(cur, A)になり、その際に行われる操作回数はその差分なので、cur-gcd(cur,A)回。
つまり、かかる費用は(cur-gcd(cur,A))×Cとなる。
これの総和がかかる必要の総和となる。

最終的に連結成分数curが1でないなら、MSTが構築できていないので、-1を答えとする。

int N, M, A[101010], C[101010];
vector<pair<int, int>> CA;
//---------------------------------------------------------------------------------------------------
ll solve() {
    rep(i, 0, M) CA.push_back({ C[i], A[i] });
    sort(all(CA));

    int cur = N;
    ll ans = 0;
    fore(p, CA) {
        int c = p.first;
        int a = p.second;

        ans += 1LL * (cur - gcd(cur, a)) * c;
        cur = gcd(cur, a);
    }

    if (cur == 1) return ans;
    return -1;
}
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N >> M;
    rep(i, 0, M) cin >> A[i] >> C[i];
    cout << solve() << endl;
}

National Railway [AtCoder Beginner Contest 210 D]

https://atcoder.jp/contests/abc210/tasks/abc210_d

解説

https://atcoder.jp/contests/abc210/submissions/24331674

難しい問題。
なお、i,jをx,yと言い換えて説明している。

何から始めるか

問題の中で一番厄介な部分が絶対値であり、これをどうしようか考える。
2つのマスについて

2  
 1  

みたいになっていれば1の方がxもyも大きいので、絶対値を外した計算ができる。
具体的には、

A[y1][x1] + A[y2][x2] + C×(y1 - y2 + x1 - x2)

となる。絶対値が外れるおかげで、以下のように変換できる。

A[y1][x1] + A[y2][x2] + C×(y1 - y2 + x1 - x2)
= { A[y1][x1] + C×(y1 + x1) } + { A[y2][x2] - C×(y2 + x2) }

前提として左上と右下の組になっていれば、このように2つの値を独立して考えることができるようになる。
こうすることで、1番目のマスを全探索し、1番目のマスを固定して、最適な2番目のマスを高速に求めることで解くことができる。
1番目のマスが固定されれば、2番目のマスを1番目のマスよりも左上にあるマスで{ A[y2][x2] - C×(y2 + x2) }が最大のものが最適解となる。
これは矩形区画の最大値を求める問題になるので高速に求められそうな感じがする(実際できる)

ここまでの理解がまずは重要。

位置の前提は適切か?

実は

 2  
1  

というパターンもあり、その場合はこれに対応できない。
これを解決するために盤面を90度回転して同様の問題を解くというものがある。
盤面を90度回転させると、左下と右上の組について、左上と右下の組として考えることができるので、
回転させて同じ問題を解くと、考慮できなかったパターンも含めることができる。
なので、「そのまま解く」と「90度回転させて解く」の最適解が答えとなる。

2番目の最適なマスは?

1番目のマスを固定したときの最適な2番目のマスを求める方法について考えよう。
1番目のマスが(x1, y1)であるとき、最適な2番目のマス(x2, y2)が満たすべき条件は以下のようになる。

  • x2≦x1、かつ、y2≦y1、かつ、(x1,y1)!=(x2,y2)
  • A[y2][x2] - C×(y2 + x2)が条件を満たすものの中で最小

よくよく見るとほぼ矩形の領域に対して最小値を考えるというものであるが、矩形の領域に対して最小値を考える問題を少しもじればいいので、
矩形の領域に対する最小値を求めることを考える。
これには累積和というか、DPというかを前計算しておくことで対応する。

mi[y][x] := (1,1)~(x,y)の矩形領域のA[y2][x2] - C×(y2 + x2)の最小値

これの計算は

mi[y][x] = min(A[y][x] - C×(y + x), mi[y-1][x], mi[y][x-1])

という感じにすれば問題ない(領域外参照に注意すること)。
これで完全な矩形に対する最小値は求まった。
今回求めたいのは完全な矩形ではなく、(x1,y1)は使えないので、単にmin(mi[y1-1][x1], mi[y1][x1-1])を使えばいい。

これでACに必要な要素は整ったので、全部実装すれば解ける。

int H, W, C;
vector<vector<int>> A;
//---------------------------------------------------------------------------------------------------
ll mi[1010][1010];
ll solve() {
    rep(y, 0, H) rep(x, 0, W) mi[y][x] = A[y][x] - 1LL * C * (y + x);
    rep(y, 0, H) rep(x, 0, W) {
        if (0 < y) chmin(mi[y][x], mi[y - 1][x]);
        if (0 < x) chmin(mi[y][x], mi[y][x - 1]);
    }

    ll res = infl;
    rep(y, 0, H) rep(x, 0, W) {
        ll opt = infl;
        if (0 < y) chmin(opt, mi[y - 1][x]);
        if (0 < x) chmin(opt, mi[y][x - 1]);
        chmin(res, A[y][x] + 1LL * C * (y + x) + opt);
    }

    return res;
}
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> H >> W >> C;
    A.resize(H);
    rep(y, 0, H) rep(x, 0, W) {
        int a; cin >> a;
        A[y].push_back(a);
    }

    ll ans = infl;
    rep(_, 0, 2) {
        chmin(ans, solve());
        A = rotateClockwise(A);
        swap(H, W);
    }
    cout << ans << endl;
}

Colorful Candies [AtCoder Beginner Contest 210 C]

https://atcoder.jp/contests/abc210/tasks/abc210_c

前提知識

解説

https://atcoder.jp/contests/abc210/submissions/24333038

色々解法があるが、特別なデータ構造が必要ないのは尺取り法(正確にはっぽいアルゴリズム)だろう。
この問題は尺取り法の入門問題として適切であるので、アルゴリズムも含めて説明しておく。
ただ、有名所で説明が各所でなされているので、図解されている記事を探して読んだ方が手っ取り早いかもしれない。

尺取り法とは

尺取り法と言っているが、基本的には差分計算である。
[1,K]の区間についての計算結果から[2,K+1]の区間についての計算結果を差分だけを計算することで高速に求める
というのがモチベーションとなる。
尺取り法はもうちょっと広い解釈ができるのだが、差分計算だけを行うことで区間をうまく動かしていくという共通認識がある。

ちょっと、ややこしく説明しすぎた気もする。問題に合わせた具体的な説明に戻ろう。

差分計算をする

今回はすべての区間を確認して、種類数の最大値を求める問題であり、実際これをそのまま行っていく。
この際に、すべての区間で種類数をただ求めるには時間がかかるので、差分計算だけを行ってすべての区間を確認していく。

では、最初の区間である[1,K]について計算していこう。
この区間の種類数を計算するために以下を用意する。

kinds := 今見ている区間の種類数
cnt[x] := 今見ている区間に数xが何個含まれているか

これを構築する方法は、[1,K]の数を見て、cnt[x]をインクリメントしていく。
cnt[x]が0からインクリメントされるタイミングだけkindsをインクリメントするようにすれば種類数も数えられる。

さて、この状態から差分計算をして[2,K+1]を計算しよう。
差分としては1番目の要素が取り除かれて、K+1番目の要素が追加されている。
1番目の要素についてcnt[x]を1つ減らして、その結果cnt[x]が0になるなら種類数が1つ減るのでkindsを1つ減らす。
次に、K+1番目の要素についてcnt[x]を1つ増やして、cnt[x]が0からインクリメントされているなら種類数が1つ増えるのでkindsを1つ増やす。
こうすると2回の計算だけで[2,K+1]の場合のkindsとcntを構築することができた。

これをどんどんやっていくと1つ横にずらすための計算はO(1)で済むため、すべての区間を確認してもO(N)で間に合う。
あとは、各区間のkindsについて最大値を取れば答え。

尺取り法とは

一般には区間の右端を1つ動かしたら条件を満たすように左端を右に動かして調整するアルゴリズムのことなのだが、
今回のテクもそれほど変わりないので一緒として説明した。
なので、この問題を理解した後に尺取り法の違う記事や問題も解いてみると、色々なバリエーションが見られるだろう。

int N, K, c[301010];
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N >> K;
    rep(i, 0, N) cin >> c[i];

    map<int, int> cnt;
    int kinds = 0;
    int ans = 0;

    rep(i, 0, K) {
        if (cnt[c[i]] == 0) kinds++;
        cnt[c[i]]++;
    }
    chmax(ans, kinds);
    
    rep(i, K, N) {
        if (cnt[c[i - K]] == 1) kinds--;
        cnt[c[i - K]]--;
        if (cnt[c[i]] == 0) kinds++;
        cnt[c[i]]++;
        chmax(ans, kinds);
    }

    cout << ans << endl;
}

Bouzu Mekuri [AtCoder Beginner Contest 210 B]

https://atcoder.jp/contests/abc210/tasks/abc210_b

解説

https://atcoder.jp/contests/abc210/submissions/24333088

シミュレーションして勝敗を決していこう。
先頭から順番に、高橋君、青木君の順番でカードを見ていき悪いカードを受け取った人が負けなので、その人を答える。
配列を0-indexed、つまり、0から始めた番号で見ている場合は、番号が偶数の時に高橋君の手番で、
奇数の場合は青木君の手番となる。
よって、先頭から見て最初に1が出てくる時の添え字の偶奇を使って答える。

注意としては、先頭から順番に見ていくので処理途中でプログラムを終了させる必要がある。
自分は関数として切ってreturnすることで処理を中断しているが、exit(0)みたいなものを使ってもいい。

int N; string S;
//---------------------------------------------------------------------------------------------------
string solve() {
    rep(i, 0, N) if (S[i] == '1') {
        if (i % 2 == 0) return "Takahashi";
        else return "Aoki";
    }
}
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N >> S;
    cout << solve() << endl;
}

Cabbages [AtCoder Beginner Contest 210 A]

https://atcoder.jp/contests/abc210/tasks/abc210_a

解説

https://atcoder.jp/contests/abc210/submissions/24333120

場合分けして問題を解こう。
状況が変わる条件はキャベツをA個以下買うか、A個よりも多く買うかである。

キャベツをA個以下買う場合はすべてX円で買うので、NXが答え。
キャベツをA個よりも多く買う場合はA個まではX円で買って、それ以降はY円で買うのが最適なので、AX+(N-X)Yが答え。
maxやminをうまく使って一行でも書くことができるが、どっちでもいい。

int N, A, X, Y;
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N >> A >> X >> Y;

    int ans = 0;
    if (N <= A) ans = N * X;
    else ans = A * X + (N - A) * Y;

    cout << ans << endl;
}