橙になりたいaquarium

にゃーんにゃーんにゃにゃーん

codeforces赤(Grandmaster) になりました!

半年ぶりの色変記事です。

前回の色変記事(CodeChef 赤)はこちら。

shinchankosen.hatenadiary.jp

やったこと

  • Universal Cupに毎週でるようになった

  • codechef, codeforcesにほぼ毎回でるようになった

  • solvedacに取り組むようになった

  • 上記のupsolveをよりちゃんとやるようになった

自分が知識面で足りないと感じることはもうめったになくなった。それは、「あとは思考力と時間さえあれば解けるよね?」ということになる。

やらなくなったこと

新しい知識を付ける

やってないことをぱっと思い出してもalienDPくらいだと思う。

こどふぉバチャ(部内、運営してたdiscord鯖両方)

こどふぉにわりと出るようになったしUcupもでてるからいいかなって

atcoder埋め

自分に海外コン適正があることがわかってきた。暖色って数オリ勢の割合多くなってくるから数え上げで勝てない。AGCとかARCも最近はYes/Noとか構築に張ってレート上げてる。

ライブラリ整備

ICPCのことを考えると、個人のライブラリは整理してもしょうがない。あと、最近はチームメイトのDaylightがライブラリ整備を進めてくれていて、去年KowerKointがほぼ準備したkotamanegi_mijingiriのライブラリを僕たち仕様に改変してくれている(特に最近はkuhaku先生のスパルタ指導を受けているそう…?)。

今後の展望

AtCoder橙よりもcodeforces IMの方がなりたいと感じてきた。もっといえばICPCに比べたら色なんてものはあまり気にしなくなってきた。学生生活はもう充分やることやったと感じてるので、卒業までは研究と就活と競プロ(アルゴ)だけに打ち込むことになりそうです。

TUATPC2024Summer (Algorithm) で全体Testerをしました

shinchanです。10日間の仕事でしたが全体Testerとして走った経験がおそらくなかったので記事を書くことにしました。

mofecoder.com

きっかけ

これ

このあとの招待時に、実は前回もtesterをやっていたことがわかりました。なんも覚えてなかった…。

やったこと

Lで1WAが外れなくて数時間苦しみました。僕が解いた時点で、Iは本番のK、Jは本番のI、Kは本番のJです。また、このコメント時点ではLを通せてないです。

以下は本番の問題番号でのかかった時間です。L以外はノーペナで通しました(これすごくね?)。Lは10ペナくらいしました。

上の画像に書いてある内容の他にも、Iはダブリング+二分探索だがO(Nlog2 M)を落とすのかどうか、Lの誤差の話とかいろいろチェックしました。また、Kは当初N<=10000, M<=1000とかで、logKさんやNerveさんがかなりちゃんと決めた制約だったのですが、僕がふと2人の過去の会話を眺めていると、通したO(N2)が実は想定じゃないということに直前に気づいて冷や冷やした。

Iは当時の制約で愚直にダブリング+二分探索したら落ちるが、bitを上から立てるかどうかの二分探索をすることで僕が無理やり通したことで制約の修正がなされました。最初に走ったときは一応想定であろうNlogMで通しましたが。

反省点

一応以下の言われてた仕事自体はある程度ちゃんとやりましたが、部分点や図とかを全然見てなかったことでclarいっぱい来ちゃったなぁとなりました。

「 主な仕事内容としては以下の 3 点を予定しています。 ・セットを走る ・問題文チェック ・簡単な難易度評価 」

J(構文解析の問題)では、numberの長さが1000以下というのがなにを意味するのかに気づかず(その後も完全に忘却して報告し忘れ)、numberの長さに依存しない解法でがんばって通してしまったせいで、難易度評価を間違えてしまいました。ここはかなり反省。

TechFUL Programming Ekiden 2024 参加記

きっかけ

たぶん阪大競プロ部RAINBOUに案内が来たのかな?それで参加することに。

参加したい人を募集すると4人だったので他に参加できる人を強い順に声をかけると6人になりました。

レート順に組んで、以下の2チームを作りました。

kotamanegi_ganbaruby

チームの写真はjag夏合宿懇親会のときにkowerkointと僕が持ってた黒澤ルビィを一緒に撮ったものです。そのときにチーム名も勝手に僕が付けました。

  • KowerKoint (1区) 早解き最強

  • shinchan (2区) 早解きはできません

  • hint908 (3区) レートがこの中で一番高い

kotamanegi_daisuki_club

  • littlegirl112

  • shogo314

  • Daylight

早解きって観点だと、shinchan, hint908以外の4人の方が早解きタイプ。優勝はできないだろうと思ってゆるふわでいこうとしてたが、参加者を見ると案外黄色上位以上がいないかも?と思ったので優勝をとりにいくことに。東大の2チームとかkotamanegi_daisuki_clubにさえ勝てたら?とかこのときは思ってた。

本番

1区前

キーワードがEkiden!らしいのでこれを出力させるコードをkowerkointに用意させる。

1区

予定通りAのFA。しかし、Bで問題文読解に苦しんだとはいえあのkowerkointが1位になれないということは、上の東大はともかく謎のぼっち参加者が只者ではなさそう。

ググるとこれがでてきて、techful coding battleでkotamanegiに勝ってるということは橙以上と考えてよい。この時点で、優勝ではなくFAに狙いを定める。特に、後に優勝するこの人はDから解いてるっぽいので、2,3区も賞金理論値のためにDから解くと考え、僕とhint908はCのFAを狙うことに。

2区

とりあえず4問とも開いてDを読んだが、このタイプだとFAは取れないと思った。A, Bはちょっとめんどくさそう。やっぱりCを狙う。Cは全探索は必須っぽかったので書くしかないなぁと思って全探索を書いた。先に1位の人がFAだったものの、DもFAだったらしく、予定通り僕がFAとなった。

その後Dを考えて嘘を生やすこと2回。いったんA, Bを通してDに戻る。セグ木貼れば脳死でできそうなので貼った。全完。

ペナなし早解きなので全体的に何も考えず脳死で書けたのは良かったと思うがDの嘘に気づけるくらいは考えた方が良かった。

以下、回答したコード

  • A
#include <bits/stdc++.h>
using namespace std;
#define all(v) (v).begin(),(v).end()
#define pb(a) push_back(a)
#define rep(i, n) for(int i=0;i<n;i++)
#define foa(e, v) for(auto& e : v)
using ll = long long;
const ll MOD7 = 1000000007, MOD998 = 998244353, INF = (1LL << 60);
#define dout(a) cout<<fixed<<setprecision(10)<<a<<endl;

int main() {
    cin.tie(0);
    ios::sync_with_stdio(false);
    string s;
    getline(cin, s);
    vector<int> cnt(26, 0);
    foa(e, s) {
        int num = e - 'A';
        if(0 <= num and num < 26) cnt[num] ++;
        num = e - 'a';
         if(0 <= num and num < 26) cnt[num] ++;
    }
    cout << (count(all(cnt), 0) ? "No" : "Yes") << endl;
    return 0;
}

B

#include <bits/stdc++.h>
using namespace std;
#define all(v) (v).begin(),(v).end()
#define pb(a) push_back(a)
#define rep(i, n) for(int i=0;i<n;i++)
#define foa(e, v) for(auto& e : v)
using ll = long long;
const ll MOD7 = 1000000007, MOD998 = 998244353, INF = (1LL << 60);
#define dout(a) cout<<fixed<<setprecision(10)<<a<<endl;

bool calc(int x) {
    int h = x / 60;
    int m = x % 60;
    if(h == m) return true;
    if(m == 0 or h == 0) return false;
    string s = to_string(h);
    string t = to_string(m);
    reverse(all(s));
    reverse(all(t));
    while(s.back() == '0') s.pop_back();
    while(t.back() == '0') t.pop_back();
    s += t;
    int n = s.length();
    for(int i = 0; i < n - 1; i ++) if(s[i] != s[i + 1]) return false;
    return true;
}
int main() {
    cin.tie(0);
    ios::sync_with_stdio(false);
    int a, b, c, d;
    cin >> a >> b >> c >> d;
    int ans = 0;
    for(int i = a * 60 + b; i <= c * 60 + d; i ++) {
        if(calc(i)) ans ++;
    }
    cout << ans << endl;
    return 0;
}
  • C
#include <bits/stdc++.h>
using namespace std;
#define all(v) (v).begin(),(v).end()
#define pb(a) push_back(a)
#define rep(i, n) for(int i=0;i<n;i++)
#define foa(e, v) for(auto& e : v)
using ll = long long;
const ll MOD7 = 1000000007, MOD998 = 998244353, INF = (1LL << 60);
#define dout(a) cout<<fixed<<setprecision(10)<<a<<endl;

int main() {
    cin.tie(0);
    ios::sync_with_stdio(false);
    int m;
    cin >> m;
    vector<vector<int>> v;
    auto dfs = [&](auto dfs, vector<int> vec, int now) -> void {
        if(now == 1) return;
        if(now == 0) {
            v.push_back(vec);
            return;
        }
        for(int i = 1; i <= now; i ++) {
            vec.pb(i);
            dfs(dfs, vec, now - i);
            vec.pop_back();
        }
    };
    dfs(dfs, vector<int> (), m);
    sort(all(v));
    foa(e, v) {
        foa(e1, e) cout << e1 << " ";
        cout << endl;
    }
    return 0;
}
  • D
#include <bits/stdc++.h>
using namespace std;
#define all(v) (v).begin(),(v).end()
#define pb(a) push_back(a)
#define rep(i, n) for(int i=0;i<n;i++)
#define foa(e, v) for(auto& e : v)
using ll = long long;
const ll MOD7 = 1000000007, MOD998 = 998244353, INF = (1LL << 60);
#define dout(a) cout<<fixed<<setprecision(10)<<a<<endl;

struct SegmentTree {
private:
    ll n;
    vector<ll> node;
    ll ID = INF; // min -> INF, max, sum -> 0
    inline ll func(ll a, ll b){return min(a, b);} /////////

public:
    SegmentTree(vector<ll> v) {
        int sz = v.size();
        n = 1; while(n < sz) n *= 2;
        node.resize(2*n-1, ID);
        for(int i=0; i<sz; i++) node[i+n-1] = v[i];
        for(int i=n-2; i>=0; i--) node[i] = func(node[2*i+1], node[2*i+2]);
    }

    void update(ll x, ll val) {
        x += (n - 1);
        node[x] = val;
        while(x > 0) {
            x = (x - 1) / 2;
            node[x] = func(node[2*x+1], node[2*x+2]);
        }
    }

    ll get(ll a, ll b, ll k=0, ll l=0, ll r=-1) {
        if(r < 0) r = n;
        if(r <= a || b <= l) return ID;
        if(a <= l && r <= b) return node[k];

        ll vl = get(a, b, 2*k+1, l, (l+r)/2);
        ll vr = get(a, b, 2*k+2, (l+r)/2, r);
        return func(vl, vr);
    }
};

int main() {
    cin.tie(0);
    ios::sync_with_stdio(false);
    int n;
    cin >> n;
    SegmentTree seg(vector<ll> (300000, INF));
    rep(i, n) {
        ll a, b;
        cin >> a >> b;
        seg.update(a, min(seg.get(a, a + 1), a + b));
    }
    int ans = 0;
    ll now = 0;
    while(1) {
        ll num = seg.get(now, 300000);
        if(num == INF) break;
        now = num;
        ans ++;
    }
    cout << ans << endl;
    return 0;
}

Dの解法はいろいろあって、O(max(T)N), O(N), O(NlogN)などがある。セグ木を使ったO(NlogN)が一番簡単に書けると思ったのでそうした。区間スケジューリングによるO(NlogN)、後ろから累積minを持つO(N)、max(T)が小さいことを利用したO(max(T)N)とか。

閉会式

去年今年ICPCでチームを組んだkowerkoint, daylight, hint908がしゃべったのに僕だけマイク不調でしゃべれてないのもあれなので3位の代表としてしゃべることにした。

↓ミュートになってない人がいて謎の電車音で不機嫌になりながらしゃべったshinchan。

www.youtube.com

とりあえずは、個人で3000円もらえたので嬉しいです。マイクラで走ってる姿とか面白かったので是非来年もやってほしいです。ありがとうございました!!

JAG 夏合宿 2024 参加記

国内予選はこちら

チーム

kotamanegi_hint_kureya

  • Daylight  問題文読解、実装、写経、データ構造の知識担当

  • shinchan  いろいろ担当

  • hint908  天才担当

1日目

結果

E, H, I, J, K、5完

  • I(shinchan): 問題読解が下手で1ペナ。

  • E, H, J, K: チームメイト。途中BITを使うときにソラ書きだけしてあげた。

  • A: チームメイトがrollback付きmoで解けることに気づいたが実装間に合わず

  • B: がんばりたくない見た目をしていたので解いてない

  • C: チームメイトがコーナーケースにやられた

  • G: 実は2乗が通るのでは?と思ってがんばって実装して投げたがさすがにTLE

感想

韓国セット難読だった。

構築とグラフをくれ

2日目

結果

ABEFGIKN8完

A(shinchan): daylightが問題を見て「畳み込みです」と言いながら写経を始めた。10分くらいたって僕が問題を見ると掛け算ではなく足し算だった。実装をかわりAC。

E(daylight): わりと早い段階から、daylightが優先度低めで実装を始めた。WAとTLEがいりまじって6ペナの末、TLEは僕がdaylightのコードだけ見て定数倍高速化をがんばると外れた。

F, G, I: たぶんhint908

K(shinchan): Moで解いたがどうやら想定ではないらしい。moの写経はもちろんdaylightに任せた。BITはソラ書きした。B=2000としてそれ以下の約数は前処理でなんとかして、それより大きい約数だけmoとBITとにぶたんでやった。O((100以下の高度合成数の約数の個数)N√N logN)になったが祈ると通った。高度合成数で埋まってるテストケースはなかったらしい?

N(shinchan): だれも解いてなくてチームメイトもわからんと言ってたが好きな見た目をしていたので問題を教えてもらって解くことにした。素因数列挙して過去の最新の素因数の場所からの遷移をすると最大値はDPでできることにすぐ気づいた。そこまでいったん書いて、重複の扱いに一瞬困ったが、最大値をとりうる素因数の場所を列挙して重複をsetで省けばいいと気づいてAC。FAはギリとれなかった。

J(shinchan): チームメイトから考察を変わって進めて考察自体は終わったがサンプルがあわず…

M: チームメイト2人が考えてたが惜しかったらしい。

C: 最初しばらく考えたがわからず

感想

JとMは解けるべきだったが、この2つを解いても順位が変わらないらしい。上がちょっと怖くなった。それはそうとEはよくがんばった、daylightえらい。

序盤の練習をしていたことで、序盤はかなりうまくいってて2位とかだった。それによってペナを生やしまくっても時間で勝つことができたのはかなり成長だと思う。

3日目

結果

ABDGIK6完。

A: daylight

B(shinchan, daylight): まずdaylightにunionfindを写経させた。壁を越えてから1方向に進めると2人とも勘違いしてmaxを取っていて2人で10ペナを生やしたのだが、「これ去年kowerkointが用意したライブラリなんだけどバグってんじゃねぇの?」とか言っていた。なんとか80分近くたって気づいてAC。2人の時間を計120分くらい使ってしまった。

D, I: たぶんhint908

G(shinchan, hint908): チームメイトがわからんと言ってたが好きな見た目をしてたのでやることにした。2×Nはすぐ思いついて、端判定をちょっとサボることで回数を減らせることにも気づいた。完全な解法もすぐ思いついて実装したが、チームメイトに見てもらったりしながらも6ペナになった。最終的に、「これって投げる文字の長さの制限とかあったっけ?」と言われ、確認すると1000以下らしい。どう考えてもそれを超えているので超えないように実装してもらい、AC。

K(shinchan): 2人が嘘を生やしてペナを生やしてわからんわからんと言っていて、好きな見た目をしてたので奪うことにした。 UnionFindとマージテク使っていい感じに変換をしてやると解けるなーとなった。UFはBで書いたのをそのままコピペ。変換を3重にやる必要があってしんどかったがなんとか実装しきってAC。

F: hint908と一緒にわからんなぁと言ってたがdaylightにも伝えると解けたと言われ、そのまま任せたが間に合わず、、。終了7分後にAC。

C: hint908と一緒に考えてたがまともに時間が残っておらず…

E: 「構築あったよ!」とチームメイトに渡されて15分くらい考えたところで「これはおそらくコンテスト中にACは現れない」と考えて捨てることに。

感想

かなり悪い結果になったがday3こそ優勝の可能性があるセットだったように思う。

GはともかくBが本当に戦犯だった。優勝はともかく8完はしなければ…。

全体の感想

運営のみなさん、話してくれたみなさん、3日間ありがとうございました。

3人ともインターンやバイトや研究で忙しくしててチーム練ができてなく、3日間とも下振れたように思います。他の2人が解けないときにもう1人が解けたケースがわりと多くて、そういうのをもっと早く判断できればなーと思いました。

mijingiriのときのライブラリをそのまま使ってるのでもっと短いライブラリを用意しましょう…。

しばらく研究に時間を使う必要があるのですが、なんとかしてチーム練を重ねたいところです。

来年も参加権があるのでたぶん合宿も参加します!今年のregional以降含め対戦よろしくお願いします!次こそ優勝します!

丸紅プログラミングコンテスト2024(AtCoder Regular Contest 183)D問題-Keep Perfectly Matched 解説

問題

atcoder.jp

 N 頂点の完全マッチングがあることが保証されている木が与えられる。葉の頂点を  2 つ選んで、同時に消すことを繰り返す。ただし、毎回削除後も常に完全マッチングが存在しなければならない。操作列を一つ出力。

  •  N \leq 250000
  •  N は偶数

解説

本質

重心となる頂点を選び根とします。完全マッチングの条件を無視すると、根を通るように葉の頂点を2つ選んで適当な順番で削除していけばよいです。

完全マッチングの条件を考慮すると、本質なのは「各部分木の頂点数の偶奇」だと思います。

重要な性質があります。

  • 頂点  v がある部分木の根のとき、部分木の頂点数が奇数なら、  v はそのさらに親とマッチングする。逆に偶数なら親とマッチングしない。

根と隣接している各頂点の部分木について考えます。部分木の頂点数は、1つが奇数、残りがすべて偶数のはずです。そうでなければ、根とマッチングしなければならない頂点が複数存在してしまいます。

よって、葉を選ぶ部分木は、片方は頂点数が奇数、もう片方が偶数です。それだけだと部分木のサイズが偏ってしまうので、偶数のもののうち一番頂点数が大きいものを選べばよいです。

あとは各部分木の葉となる頂点の選択順序について考えます。

各部分木について

根と隣接するとは限らない頂点  v を根とする部分木から選ぶことになったとします。このときも、 v の子の部分木の頂点数が奇数であるものは高々1つです。

そのような子が1つ存在したときは、その子の部分木に含まれる葉を選ぶ必要があります。ここでも頂点数が偶数である部分木を選んでしまうと、 v とマッチングしなければならない頂点が複数存在してしまいます。

1つも存在しないときは、どれから選んでもかまいません。そして、選んだ部分木に探索していきます。結果的になんらかの探索を用いて、各頂点では、部分木の頂点数が奇数の子についてすべて探索したあと、部分木の頂点数が偶数の子についてすべて探索することになります。

葉の削除による影響と削除順序

ある葉  v を削除するとき、 v の先祖にのみ影響がでます。具体的には、各先祖  a について、部分木の頂点数が奇数である子の個数が1であるとき、その子の部分木の頂点数が偶数になるので、  a の子の部分木はすべて頂点数が偶数になります。一方、子の部分木の頂点数がもともとすべて偶数だったとき、1つ奇数がうまれることになります。

すべて偶数になったとするとどの頂点を選んでもよいのでさっき選んだ葉が属する部分木を連続して選んでかまわないですし、1つ奇数になったとしたらさっき選んだ葉が属する部分木を選ばなければなりません。結局、ある部分木を選んだなら、その根自体が消えてなければその部分木を連打する必要があります。

葉に近い部分に着目して少し考えると、深さ優先探索の帰りがけ順に削除していけばよいことがわかります。

以上でこの問題が解けました。上述の「偶数のもののうち一番頂点数が大きいものを選べばよい」の部分で優先度付きキューを使用することがボトルネックとなり計算量は  O(N \log N) です。

まとめ

  1. 重心を求めて根とする
  2. 各部分木の頂点数を計算しておく
  3. 根と隣接する各頂点vから深さ優先探索をしていき、子は部分木の頂点数が奇数のものを先に探索していき、帰りがけ順にvのキューにpushする
  4. 根から隣接する頂点のうち、部分木の頂点数が奇数のものと、偶数のうち最も頂点数が大きいものを選び、決めた順に1つずつ葉を削除する。
  5. 頂点数が2になるまで4を繰り返す。
  6. 残った頂点(根そのもの、部分木の頂点数が奇数の子)を削除して終わり

実装例

https://atcoder.jp/contests/arc183/submissions/57174749

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n; cin >> n;
    vector<vector<int>> v(n);
    for(int i = 0; i < n - 1; i ++) {
        int a, b; cin >> a >> b;
        a --; b --;
        v[a].push_back(b);
        v[b].push_back(a);
    }

    auto center = [&](auto center, int now, int p) -> pair<int, int> {
        int sum = 1, c = -1, M = 0;
        for(int e : v[now]) {
            if(e == p) continue;
            auto [se, ce] = center(center, e, now);
            sum += se;
            M = max(M, se);
            if(ce != -1) c = ce;
        }
        M = max(M, n - sum);
        if(n >= M * 2) c = now;
        return {sum, c};
    };
    auto [_, root] = center(center, 0, -1);

    vector<int> sz(n, 1);
    auto dfs1 = [&](auto dfs1, int now, int p) -> void {
        for(int e : v[now]) {
            if(e == p) continue;
            dfs1(dfs1, e, now);
            sz[now] += sz[e];
        }
    };
    dfs1(dfs1, root, -1);

    vector<queue<int>> ord(n);
    auto dfs2 = [&](auto dfs2, int now, int p, int anc) -> void {
        for(int e : v[now]) {
            if(e == p) continue;
            if(sz[e] % 2 == 1) dfs2(dfs2, e, now, anc);
        }
        for(int e : v[now]) {
            if(e == p) continue;
            if(sz[e] % 2 == 0) dfs2(dfs2, e, now, anc);
        }
        ord[anc].push(now);
    };

    int odd = -1;
    priority_queue<pair<int, int>> pq;
    for(int e : v[root]) {
        if(sz[e] & 1) odd = e;
        else pq.push({sz[e], e});
        dfs2(dfs2, e, root, e);
    }

    while(!pq.empty()) {
        auto [_, even] = pq.top();
        pq.pop();

        cout << ord[odd].front() + 1 << " " << ord[even].front() + 1 << endl;

        ord[odd].pop();
        ord[even].pop();
        sz[odd] --;
        sz[even] --;

        if(sz[odd]) pq.push({sz[odd], odd});
        odd = even;
    }

    cout << root + 1 << " " << odd + 1 << endl;
    return 0;
}

実装をサボりたい話

はじめに

書き始めたところです。未完成です(2024/08/13現在)。

競技プログラミングでは実行時間制限(TL)にさえ間に合えばいいので必要以上に高速化をする必要はあまりない。TLに間に合う範囲で、細部の考察や実装を速くする方法を考えていきたい。

二分探索をサボりたい

二分探索で条件分岐とかでバグらせるのが怖いのは黄色になってもそう。

時間が無限にあれば、1とかから順番に試していくのが一番楽。こういうのはどうだろう。whileでざっくりとした境界を見つける。明らかに1000000を足していいか?明らかに10000を足していいか?明らかに100を足していいか?のようにして、終わったあとに1ずつ増やして順番に試していく。こうするだけで計算量は100の定数倍になる。もちろんこれを極限まで突き詰めたものが二分探索なのだが。

Damage over Time (ABC303-F)

atcoder.jp

どこまで今の呪文を続けていいかは二分探索またはO(1)で求められるが、細部の考察がしんどい。

愚直に時間を1ずつ進めていって、最後の呪文まで考慮できたらあとはO(1)でやるという方針を取ると、109くらいの計算量でできて、一応これだけでも間に合う(想定かは知らない)。

100ターン後まで今の呪文を続けていいか、または続く100ターンは次の呪文の等差数列で表せるか。を付け加えるだけで単純に考えれば愚直よりも100倍の高速化になり、二分探索やO(1)で境界を求める必要はないと考えられる。下のコードだけを見れば実装量が多いように感じるが、細部の考察は境界を見つけるよりも非常に楽だと個人的に思う。

#include <bits/stdc++.h>
using namespace std;
#define all(v) (v).begin(),(v).end()

using ll = long long;

int main() {
    ll n, h;
    cin >> n >> h;
    vector<pair<ll, ll>> v;
    for(int i = 0; i < n; i ++) {
        ll t, d;
        cin >> t >> d;
        v.push_back({d, t});
    }
    sort(all(v));
    reverse(all(v));
    
    ll now = 0, i = 0, Ma = 0;
    while(h > 0) {
        while(i < n and v[i].second <= now + 1) {
            Ma = max(Ma, v[i].first * v[i].second);
            i ++;
        }
        if(i == n) {
            now += (h + Ma - 1) / Ma;
            break;
        }
        // 境界を見つけるのをサボる
        if(Ma > v[i].first * (now + 101) and h >= Ma * 100) {
            h -= Ma * 100;
            now += 100;
            continue;
        }
        if(v[i].first * (now + 1) > Ma and v[i].second > now + 101) {
            ll sum = now * 100 + 5050;
            if(sum * v[i].first <= h) {
                h -= sum * v[i].first;
                now += 100;
                continue;
            }
        }
        h -= max(Ma, v[i].first * (now + 1));
        now ++;
    }
    cout << now << endl;
    return 0;
}

条件分岐をサボりたい

CodeQUEEN2024決勝

atcoder.jp

A問題では、if, else if, elseを計3回書けばよいですが、こういうのは個人的に好きではありません。vectorやsetに10, 15, 17を入れておいてlower_boundすると、こちらの方が微妙にカッコイイです。

#include <bits/stdc++.h>
using namespace std;
#define all(v) (v).begin(),(v).end()

int main() {
    vector<int> v = {10, 15, 17};
    int x; cin >> x;
    cout << *lower_bound(all(v), x) << endl;
    
    return 0;
}

B問題では、問題文で与えられてる規則を全部コピペとかすることで解けますが、そこに現れるちょっとした法則を見抜くことで実装の短縮になります。詳しくは公式解説の通りです。

再帰で書けるものは再帰で書いた方が実装が楽

再帰で書いた方が高速だが実装は再帰の方が楽であることの方が多い。

atcoder.jp

atcoder.jp

応用情報技術者試験に合格しました(2024, R6春期)

著者スペック

  • 明石高専電気情報工学科⇒大阪大学工学部電子情報工学科⇒大阪大学大学院情報科学研究科M1

  • 高専でも大学でも成績は上位2, 30%くらい?(推測)

  • 高専2年秋(2018, H30)に基本情報不合格 (午前65, 午後35)

  • 高専4年秋(2020, R2)に応用情報不合格 (午前72.5, 午後50)

  • 国語と暗記が本当に苦手(中3~高1のとき特に国語がひどかった、30点とか平気でとってた)

  • その分、数学だけはかなり自信がある

  • AtCoder黄色

きっかけ

過去に基本情報と応用情報に落ちた原因はわかっていた。国語が壊滅的にできないからである。その上勉強不足なのもわかっていたが、午後の残り10点を上げるのが自分にとって途方もないことを自覚していて、学生のうちに取得するのを諦めていた。

そんな中、就職活動とやらを意識する必要が出てきた。競プロとかでガクチカはかなり自信があるものの、面接で会話がちゃんとできるかはあまり自信がなかった。編入とか院試くらいは耐えたが、この際なので応用情報を取ることにした。逃げてきたものを取り返すというのは自分にとってかなり意味があるとも考えていた。

対策

本の購入

本番は4月中旬である。受けることは正月くらいから考え始めていた。阪大競プロ部主催のOUPCが終わったところで、卒業研究に本気を出し始めたとき。

過去問道場はやるとして、やはりキタミ式は外せないだろうと思った。それから、午後を別で対策したいと考え、メルカリでいっぱい転がってたもので、わりと最近で比較的安かったものを購入した。

壁1

さて、卒研の進捗としては、そもそも卒論を書き始められていなかった。もちろん応用情報の勉強なんてできるはずがなく。

なんとか卒研を突破し、応用情報…と思いきや、自分にはやることがあった。

壁2

1月中旬頃、ICPC Asia Pacific Championship招待のメールが来て、3月上旬にベトナムに行けることになった。それでパスポートの準備やらをして、卒研後はひたすら競プロをしていた。

壁3

また、その大会が終わってから体調を崩し、一か月近く咳が続いた。咳がでるだけで元気ではあり、3月中旬から新しくAI系のバイトを始めていた。それにこんなでかい大会にでてしまったら競プロモチベももちろん上がってしまった。

4月に入ってしまった。

壁4, 5

研究を再開しなければならなかった。6月上旬には学会発表があるからだ。そして院に進学した直後の諸々の手続きが非常にめんどくさかった。特にヤバかったのは奨学金申請。このくらいマイナンバーでできるようになってくれと思いながらやりきった。源泉徴収票を提出する必要があり、業務委託型のバイトは源泉徴収票を作ってくれなくて、それがめんどくさくなった半分くらいの原因である。今更プログラミング以外のバイトを続けていく理由がなくて、そのときはちょうど自分の仕事がなかったので辞めた。

遅すぎる勉強開始

はい、試験一週間前になりました(絶望)。

さすがに一週間は応用情報の勉強にフルコミットできそうでそこは安心。まずはキタミ式をかたっぱしから読んで完全に理解していく。さすがに前半は昨年院試をやったばかりのAtCoder黄の阪大生であればかなり知ってる範囲である。それに3年半前にも受けてたから午前の暗記パートはなんとかいけるはずだと思っていた。会計とかマネジメントとか監査とかのよくわからんパートもなんとか突破し、過去問道場も空き時間にやったりして、3日で70%を超えた。

午後対策

問題は午後である。知り合いには「データベースわりと勉強したらすぐできるようになるよ」とアドバイスを受けてたのでここからデータベースを完全に理解する。

情報セキュリティ、プログラミング、システムアーキテクチャ、データベース、組込みシステム開発を受けることに。

以上が難しかったら情報システム開発やシステム監査を受けることにした。

AtCoder黄なので、さすがに他の分野を勉強した方が合格するための効率がいいのでプログラミングはノー勉で行くことに。

セキュリティは必須なので特に力を入れて勉強、データベースも過去問でしっかり傾向を見抜く。システムアーキテクチャ、組込みシステム開発もある程度解いておく。午後は時間との勝負だ。解ける分野は爆速で解けるに越したことはない。

正直、解けなかったところを限られた時間で全部理解するのは不可能。簡単に理解できたり覚えられるところはそうすることにして、覚えるのに時間がかかるのはすぐに捨てた。また、できる限り知識がいらないところを積極的に慣れた。自分が短期記憶が下手なのはわかっているので、解き方、考え方をなんとかした方がよい。

システムアーキテクチャや組込みシステム開発は、まさに、間違えたところは解説を読んだら「そういうことかーーー」になることが多く、問題や図でどういうところが重要かに焦点を当てた。

試験本番

大阪工業大学というところになった。阪大近くからはモノレールを使用するとわりと楽だった。

午前

主にセキュリティやネットワークに手ごたえを感じず、それ以外に手ごたえを感じながら解いた。

さすがにこれ以上いても選択を変えることはないと思ったので残り30分くらいのタイミングで退出した。

昼休みはひたすらSQLとセキュリティとネットワークの用語を頭に詰め込んだ。

午後

まずやることはプログラミングを15分で終わらせることだ。低速なダイクストラがでてきて笑顔になりながら解いた。

その後、全体を見てなにが簡単か難しいかをちらっと確かめる。全部むずそうだなおい。

組込みシステム開発はいけそうだと感じた、解いた。

セキュリティは結局必須なので解いた。

データベース実はちゃんと読めばいけるなと思ったので解いた。

残り30分くらいだったのでさっさとアーキテクチャに決める、解いた。

自己採点

なんと、午前が60ちょうどだった。午後は最低70はあると確信していた(フラグ)。

結果

午前60、午後62で合格。

感想

午前は後から見直しても解けてないのは知らないとこばかりだった。知識・勉強不足。

午後はなんでこんなに低いのかわかってない。

まぁコスパ良く合格できたから良かったね。

教訓

最低1ヶ月は毎日やった方がいいです。