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

hamayanhamayan's blog

One More aab aba baa [AtCoder Beginner Contest 215 C]

https://atcoder.jp/contests/abc215/tasks/abc215_c

解説

https://atcoder.jp/contests/abc215/submissions/25247677

今回要求されている問題を実装できるかという問題となる。
前半と後半でやればいい。

文字列Sの各文字を並べ替えて作ることが可能な文字列を辞書順にすべて列挙

こっちがほぼ本質。
並び替えて作れる文字列は順列の組み合わせ数となるので、8765432*1=40320通りとなる。
これは全列挙できる個数であるので、全列挙しよう。

C++であればdo-whileとnext_permutationを使えば、簡単に並び替えて作ることができる文字列を列挙できる。
これを使わない場合はdfsとかで頑張る感じになると思う。
「順列列挙」辺りのキーワードで探せば出てくると思う。

前からK番目にくる文字列を求める

こっちは単純に前からK番目を出力するだけ。

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

    vector<string> cands;

    sort(all(S));
    do {
        cands.push_back(S);
    } while (next_permutation(all(S)));

    cout << cands[K - 1] << endl;
}

Coprime 2 [AtCoder Beginner Contest 215 D]

https://atcoder.jp/contests/abc215/tasks/abc215_d

解説

https://atcoder.jp/contests/abc215/submissions/25246926

始めてこういう系統の問題を見る人は何から手を付けていいか分からないだろう。
gcdをして1になる数は基本的には互いに素である数となる。
だが、互いに素の数というのは少し列挙するのが厄介で、逆に観点を置いて考えることが競プロでは多いように思う。
(自分的にはですけど)
逆というのは、つまり、gcdをして1にならない数はどういった数であるかを考えてみる。

gcdをして1にならない数は?

gcd(6,k)について考えると、k=2,3,4,6,8,9,10,12みたいな数が1にならない数となる。
何か規則性というか、単純に考えることができないか?
gcdの求め方を中学までさかのぼって思い出してみると、素因数分解していることを思い出す。
6の素因数は2と3であり、それを倍数がkに含まれている。
これで単純に表現できそうだ。

gcd(x,k)!=1であるようなkというのは、xを素因数分解したときの素因数のいずれかの倍数となる。

素因数分解

よって、まずは素因数分解だが、これはO(sqrt(N))でやる方法がある。
ググると以下のようなサイトが見つかったが、他にもサイトがあるかもしれない。
素因数分解-数学アルゴリズム演習ノート-
より高速なアルゴリズムもあるが、O(sqrt(N))でC++なら今回は間に合う。

自分はenumPrimeOnly関数として、少しもじって素因数だけ持ってくるような実装にしている。
以下のng配列を用意する。

ng[x] := gcd(A[i],x)!=1となるiが存在する

すると、取得した素因数pにおいて、ng[p]=trueになる。
これをすべてのA[i]について行っていこう。

倍数について

これで素因数についてはダメなものはマーキングできたが、その倍数について考慮できていない。
このために、ng[p]=trueであれば、ng[2p], ng[3p], ng[4p], ...についてもtrueに変換していく。
実装を見ればやりたいことはだいぶ明らかなのだが、これをp=2から順番にやっていき、間に合うのかが問題。
この各iについて倍数を確認していくループは有名なもので、調和級数的な計算となりO(NlogN)になる。
競技プログラミングにおけるエラトステネスの篩・区間篩・調和級数計算量問題 - はまやんはまやんはまやん

これでngテーブルが完成するので、それを使って後は答えるだけ。

int N, M, A[101010];
bool ng[101010];
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N >> M;
    rep(i, 0, N) cin >> A[i];

    rep(i, 0, N) {
        auto ep = enumPrimeOnly(A[i]);
        fore(p, ep) ng[p] = true;
    }

    rep(p, 2, M) if(ng[p]) {
        for (int y = p + p; y <= M; y += p) ng[y] = true;
    }

    vector<int> ans;
    rep(x, 1, M + 1) if (!ng[x]) ans.push_back(x);

    printf("%d\n", ans.size());
    fore(x, ans) printf("%d\n", x);
}

Chain Contestant [AtCoder Beginner Contest 215 E]

https://atcoder.jp/contests/abc215/tasks/abc215_e

前提知識

解説

https://atcoder.jp/contests/abc215/submissions/25245742

この問題はbitDPの応用問題となる。
とりあえずmod数え上げなので、DPがまず第一候補として挙がってくる。
コンテストの種類ごとでまとまりになっている必要があるので、以前やり終わったコンテストは利用できないということで、
コンテストに出たか出てないかという所でbitDP感がするし、コンテストも10種類ということでかなりそれっぽい。

bitDPについては説明しないので、理解してない場合はどこかで学習してきてほしい。

DP

正直、以下のDPテーブルが作れていればほぼ回答できているようなものである。

dp[i][msk][lst] :=
i回目のコンテストまで出場するかを決定していて、
出場済みコンテストの状況がmskで、
最後に出たコンテスト種類がlstである
場合の組み合わせ

mskだけだと条件判定に必要な連続で同じコンテストに出ているかの判定ができないので、最後にどのコンテストに出たかを
状態として含める必要がある部分が特徴的だが、bitDPではよく見られる条件。

どうやってこのテーブルを出したかという部分は何とも言えないが、必要そうな

  • いつもの何回目までか
  • コンテストに出たか出ないかを持っておく必要がある
  • 条件判定には最後に出たコンテストの情報が必要

という部分をまとめ上げて、制約を確認すると問題なかったから、遷移を考えると解けたという流れになる。

遷移

dp[i][msk][lst]からの遷移で考えると、

  • コンテストに出ない dp[i][msk][lst] -> dp[i + 1][msk][lst]
  • コンテストに出る
    • 出るが、mskに含まれていてlstと違ったら出れないので遷移できない
    • そうじゃない場合は出れるので、 dp[i][msk][lst] -> dp[i + 1][msk | (1<<nxt)][nxt]

mskに状態を追加する場合は、or計算で追加すると遷移分けをしなくてもよくなるので便利。

int N; string S;
mint dp[1010][1 << 10][11];
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N >> S;

    dp[0][0][10] = 1;
    rep(i, 0, N) rep(msk, 0, 1 << 10) rep(lst, 0, 11) if (dp[i][msk][lst] != 0) {
        dp[i + 1][msk][lst] += dp[i][msk][lst];
        
        int nxt = S[i] - 'A';
        if ((msk & (1 << nxt)) && lst != nxt) continue;
        dp[i + 1][msk | (1 << nxt)][nxt] += dp[i][msk][lst];
    }

    mint ans = 0;
    rep(msk, 0, 1 << 10) rep(lst, 0, 10) ans += dp[N][msk][lst];
    cout << ans << endl;
}

Dist Max 2 [AtCoder Beginner Contest 215 F]

https://atcoder.jp/contests/abc215/tasks/abc215_f

前提知識

解説

https://atcoder.jp/contests/abc215/submissions/25244049

二分探索と尺取り法の知識はこれ以降の解説を理解するのに必須であるので、
分かっていない場合は学んでくることを勧める。

まずは二分探索を思いつく必要がある。
最小値の最大値とか、その逆とかは二分探索っぽいみたいな話があるが、今回もそれにちょっと似ている感じがする。
それもあって、二分探索から考えてみると、解法にたどり着いた。
この部分がまず1つ目の山となる。

二分探索

どういった条件で考えるかというと、

check(lim) := 異なる2つの点の距離の最大値がlim以下であるか

というのを判定問題として二分探索をしていくことにする。
仮に答えが10であれば、

...
check(8) = false
check(9) = false
check(10) = true
check(11) = true
...

のようになるので、境界線の上側が答えになる。

判定条件

「異なる2つの点の距離の最大値がlim以下であるか」というのは、このままだとあまり意味のある条件には見えないような感じがする。
なので、言い換えよう。

「異なる2つの点の距離の最大値がlim以下であるか」
というのは、「全頂点間で min (xの差, yの差)≦lim を満たす」である場合に満たされることになる。
minというのはちょっと面倒なので、もう少しばらすと、
「全頂点間で xの差≦lim または yの差≦lim を満たす」
となる。

正直、高速に計算できそうなやつに寄せて作っている条件なので、あまり納得感がないかもしれない。
この判定を高速に行っていく。

2軸を1軸に

x軸とy軸でそれぞれ考えるのは大変なので、どちらかでも何とかしたいものである。
今回の点の集合は特に順番に意味はないので、ソートをしても問題ない。
あらかじめx座標でソートしておこう。
こうすることで、距離の区間に変換しやすくなる。

具体的にはとある点iについてxの差がlim以下である点を考えると、ちょうどiを含む区間になることが分かるだろう。
とある点iについて、区間[L,R)についてはxの差がlim以下であるとすると、この区間については条件を満たすことになる。
逆に[0,L)の区間と、[R,N)の区間についてはxの差がlimより大きいことになる。
つまり、この区間に含まれる点はすべてyの差がlim以下である必要がある。
なので、この区間に含まれる点において、yの差がlimより大きいものがあるかを探すことで判定関数がfalseと返すかを決めることができる。

このように片方をソートすることで、問題を区間に変換することができ、区間内ではもう片方だけを考慮するだけで良くなったりする。
平面走査と呼んでもいい(し、まんまか)。

実装

すべての点iについて区間[L, R)を特定する方法として、尺取り法を用いる。
これはLもRもiが増えていくたびに広義単調増加していくのはイメージ付くだろう。
よって、尺取り法によって高速に区間[L, R)を特定できる。

[0,L)の区間と、[R,N)の区間についてはxの差がlimより大きいものがあるかを判定する方法について考える。
y[i]と最も距離がありそうな点というのは、その区間に含まれる最小のy座標と、最大のy座標となる。
なので、その区間において、y座標の最大値と最小値が得られればいい。
自分はSparseTableを用いた。これはざっくり言うと高速なセグメントツリーである。
取得がO(1)で行える。SparseTableを用いても、O(Nlog最大値)みたいな感じでNも2*105なので、セグメントツリーのO(Nlog2N)が
ちょっと怖くて、SparseTableにした。
だが、今思えば、先頭からと末尾からの区間なので、累積和としてmin/maxを用意するだけでよさそうである。

これで2つの区間の最大値と最小値を持ってきてy[i]との距離を測ってlimを超えていれば判定関数はfalseを返す。

int N;
vector<pair<int, int>> points;
SparseTableMin<int> stmin;
SparseTableMax<int> stmax;
//---------------------------------------------------------------------------------------------------
bool check(int lim) {
    int L = 0, R = 0;
    rep(i, 0, N) {
        while (R < N && points[R].first - points[i].first <= lim) R++;
        while (lim < points[i].first - points[L].first) L++;

        int ma = max(stmax.get(0, L), stmax.get(R, N));
        if (0 <= ma && lim < abs(ma - points[i].second)) return false;

        int mi = min(stmin.get(0, L), stmin.get(R, N));
        if (mi < inf && lim < abs(mi - points[i].second)) return false;
    }

    return true;
}
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N;
    rep(i, 0, N) {
        int x, y; cin >> x >> y;
        points.push_back({ x, y });
    }
    sort(all(points));
    
    vector<int> dic;
    rep(i, 0, N) dic.push_back(points[i].second);
    stmin.init(dic);
    stmax.init(dic);

    int ng = -1, ok = inf;
    while (ng + 1 != ok) {
        int md = (ng + ok) / 2;
        if (check(md)) ok = md;
        else ng = md;
    }
    cout << ok << endl;
}

テトラへドロン [パソコン甲子園2020 予選 F]

https://onlinejudge.u-aizu.ac.jp/challenges/sources/PCK/Prelim/0433

解説

https://onlinejudge.u-aizu.ac.jp/solutions/problem/0433/review/5788439/hamayanhamayan/C++14

かなり複雑な問題で何から手を付けたものかという感じだと思うが、規則性を見つけていこう。

規則性

まずは、△型のタイルは少し扱いにくいし、答えるときもxy座標で答えるので例で与えられている配色を
普通のタイルに当てはめて眺めてみよう。

y=5  YGRBYGRBYGRBYGRBYGRB  
y=4  BRGYBRGYBRGYBRGYBRGY  
y=3  YGRBYGRBYGRBYGRBYGRB  
y=2  BRGYBRGYBRGYBRGYBRGY  
y=1  YGRBYGRBYGRBYGRBYGRB  
y=0  BRGYBRGYBRGYBRGYBRGY  

さて、これを見て、何か思う所がないだろうかということなのだが、ここから規則性を見つけ出せないと
解くのは難しい。
簡潔でなくても、ある程度使えそうな規則性が見つかればそれをうまく使って答えることができる。
今回自分が見つけた規則性について説明する。

規則性①

y座標の偶奇が同じものは全く同じパターンを持つ。
よって、y=0,2,4は同じような並びだし、y=1,3,5も同じような並び。
なので、y座標は2で割った余りを使ってy=0,1のどちらかに移した状態で答えてしまっていい。

y=1  YGRBYGRBYGRBYGRBYGRB  
y=0  BRGYBRGYBRGYBRGYBRGY  

規則性②

x座標に着目すると4個毎に同じパターンがどちらも連続している。
よって、x座標も4で割った余りにして考えてしまって問題ない。

y=1  YGRB YGRB YGRB YGRB YGRB  
y=0  BRGY BRGY BRGY BRGY BRGY  

みたいな感じだったのが、

y=1  YGRB  
y=0  BRGY  

だいぶ簡潔になった。
もう少し言うとy=1はy=0を左右ひっくり返した形になっているのでyの偶奇を見て、
必要ならひっくり返した状態で答えてあげればいいので、特徴点だけあえて取り出すと

y=0  BRGY  

これが連続しているような感じになる。

そして、この2つの規則性は初期の色が何であっても成り立つ性質となる。
ここまで分かれば、次に問題になるのが初期の色が2つ分かっているときにy=0のx=0..3の配色が分かれば
高速に答えが出せることになる。

初期配色

この部分を自分はgetTable関数で実装している。
ここが正直一番説明が難しい。
やり方は色々あると思う。

初期配色の組み合わせは12通りしかないので、最悪根性で書き上げてもいいが、
手動全列挙はケアレスミス紙一重であることを忘れてはいけない。
自分は多少の手間があっても全列挙部分を減らすように努力することを勧める。

自分の実装では三菱の形で色をメモっておいて特定する方法で実装した。
dic配列を手動で作って定義している。
dic[c] := 色cを中心としたときに以下のような順番で色を記録したもの

 2  
1c3  

ちょうど左下にこの図形が現れて1に当たるのがc1で、cに当たるのがc2になる。
よって、dic[c2]を使って、この配色を回転させながらc1が1になるとき、回転させてc1が2になるとき、
c1が3になるときをループで確認していって、色を探していく。
これで色が

 b  
acd  

のように特定できたら、y=0の状態に合わせるためにacdbの順番で答えてやれば目的の初期配置が得られる。

char c1, c2;
int N, x[101], y[101];
//---------------------------------------------------------------------------------------------------
string getTable() {
    map<char, string> dic;
    dic['R'] = "BYG";
    dic['G'] = "YBR";
    dic['B'] = "RGY";
    dic['Y'] = "GRB";

    string res = "";
    res += c1;
    res += c2;
    rep(i, 0, 3) if (dic[c2][i] == c1) {
        res += dic[c2][(i + 2) % 3];
        res += dic[c2][(i + 1) % 3];
    }
    return res;
}
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> c1 >> c2;
    cin >> N;
    rep(i, 0, N) cin >> x[i] >> y[i];

    auto table = getTable();

    rep(i, 0, N) {
        x[i] %= 4;
        
        if (y[i] % 2 == 0) cout << table[x[i]] << endl;
        else cout << table[3 - x[i]] << endl;
    }
}

写真の回転 [パソコン甲子園2019 予選 E]

https://onlinejudge.u-aizu.ac.jp/challenges/sources/PCK/Prelim/0432

解説

https://onlinejudge.u-aizu.ac.jp/solutions/problem/0432/review/5788072/hamayanhamayan/C++14

若干の高速化は含まれるが、大体は実装問題である。
盤面の回転処理を実装する必要があるので、そこが大変ポイントだろう。

問題を簡単化していく

回転処理をどうするかという課題がすぐに見えるが、そこの解決をちょっと後回しにしておいて、
問題そのものを良い感じに解釈できないか考えてみよう。

クエリとして最大20万回回転させた後、最終的な結果を問う問題。
毎回クエリで要求されていることを実施して回転処理を行う方針がまず考えられるが、20万回回転させるのはちょっと大変そう。
しかも1で時計回りに回転して、その後-1で反時計回りで回転させると元に戻ってしまう。
そして、どのように回転させても最終的には0度回転、90度回転、180度回転、270度回転のどれかになるということを考慮すると、
全部の回転を考慮したときに最終的に最初から何度回転すればOKかが分かれば回転処理はその分だけやればいいので
かなり高速化できそうな雰囲気がある。
この方針で進めてみよう。

操作をまとめる

今回は必須ではないが、操作をまとめることができるので、それをやってみよう。
色々な操作があるとその分処理を場合分けする必要が出てきてしまう、今回で言うと時計回りと反時計回りを区別しているせいで
場合分けが存在してしまうのはあまりうれしくない。
反時計回り1回というのは時計回りで考えると3回に相当する。
そのため、操作をすべて時計回りに考えてしまって、1は時計回りに1回、-1は時計回りに3回回転させるような感じでカウントしていく。

こうすると時計回りに14回回転とか、34回回転とかというのが最終的に集計されてくるわけだが、4回回転させると元に戻るので、
4で割った余りの回数が簡単化した場合に必要な回数となる。
これで最後に残った要求事項は、0~3回与えられた盤面を時計回りに回転させるという操作になる。

時計回りに回転

ややバグりやすいのでモジュール化(関数化)して単体でテストしながら実装していくのもいいだろう。
自分はrotateClockwise関数として実装しているので、実装例として見てみてほしい。
回転させた場合に(x,y)がどこに移動するかをいくつか列挙してパターンを見つければ実装が行える。
x座標もy座標も0から始まる(0-indexed)であるとすると、(x,y)が(n - y - 1,x)となるのでこれで変換する。
実装時の注意点としては新しく配列を作らずに同じ配列でやった場合は、変換後の値で変換してしまうみたいな事態が発生したり
するので、自分のように別の配列を用意するか、何かしらうまくやる必要がある。

int N;
vector<string> row;
int Q, r[201010];
//---------------------------------------------------------------------------------------------------
vector<string> rotateClockwise(vector<string> g) {
    int n = g.size();
    int m = g[0].size();
    vector<string> res(m, string(n, '#'));
    rep(i, 0, n) rep(j, 0, m) res[j][n - i - 1] = g[i][j];
    return res;
}
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N;
    row.resize(N);
    rep(i, 0, N) cin >> row[i];
    cin >> Q;
    rep(i, 0, Q) cin >> r[i];

    int cnt = 0;
    rep(i, 0, Q) {
        if (0 < r[i]) cnt++;
        else cnt += 3;
    }
    cnt %= 4;
    
    rep(i, 0, cnt) row = rotateClockwise(row);

    rep(i, 0, N) cout << row[i] << endl;
}

カラフル円盤通し [パソコン甲子園2020 予選 D]

https://onlinejudge.u-aizu.ac.jp/challenges/sources/PCK/Prelim/0431

解説

https://onlinejudge.u-aizu.ac.jp/solutions/problem/0431/review/5787948/hamayanhamayan/C++14

問題で要求されている問題をさばいていこう。
要求されている事項をプログラムで計算できるようにもう少しアルゴリズムに寄せた感じで解釈していく。
「真上から見えない」という状況は与えられている半径で言うと、下に積まれている円盤の方が半径が小さい場合に成り立つ。
よって、円盤群を上から見下ろしたときに見えている円盤というのは、その円盤よりも上にあるどの円盤よりも
半径が大きい円盤であるものが見えていて、その条件を満たす枚数を数えていけばいい。

少し遅いがACできる回答

よって、各円盤についてその円盤よりも上にあるすべての円盤がその円盤の半径よりも小さいことを確かめて、
上から見えている円盤の数を数えていく解法があげられる。
これはN通りの円盤に対して、他の円盤、最大N-1枚を確認していくので、全体で大体N2回確認作業が発生するが、
Nの上限が1000であることを考えると間に合うので、これでも答えるためには問題ない。

今回実装した高速化した解法

自分が実装した解法は少し高速化をしている。
上から見えている円盤の条件を少し言い換えてみると、上から順番に円盤の最大値を取るような操作を行っているのが分かる。
例えば上から3 4 1 6 5 9となっていれば、

  • 3はとりあえず使う
  • 4はこれまでの最大値が3なので、使う
  • 1はこれまでの最大値が4なので、見えてないので使わない
  • 6がこれまでの最大値が4なので、使う
  • 5はこれまでの最大値が6なので、見えてないので使わない
  • 9はこれまでの最大値が6なので、使う

となり、4が答えとなる。
なので、これまでの最大値を持ちながら上から見ていけば、最大値が更新されたタイミングで答えもインクリメントしていけば
答えを導くことができる。
これならN回ループを書くだけで済むので、だいぶ計算が早くなる。

ちなみに…

入力は下から順の入力になっているが、ループの関係上、反転させて上から順の入力にしている。

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

    reverse(r, r + N);

    int ma = -1;
    int ans = 0;
    rep(i, 0, N) if (ma < r[i]) {
        ma = r[i];
        ans++;
    }

    cout << ans << endl;
}