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

hamayanhamayan's blog

Shortest Path [第八回 アルゴリズム実技検定 H]

https://atcoder.jp/contests/past202109-open/tasks/past202109_h

前提知識

解説

https://atcoder.jp/contests/past202109-open/submissions/26687334

今回の問題は制約が割と緩いので、基本的な典型アルゴリズムを理解していると解ける。

愚直に考えてみる

条件を満たす都市の組(i,j)が存在するかを判定することが要求されているが、
都市の組については全探索できる制約となっている。
なので、愚直に考えるとすると、都市の組(i,j)を全探索して、各組について距離を求めてXのものがあればYesと答えるようにする。

だが、都市の組(i,j)の全列挙はすでにO(N2)となるので、制約から見ると計算時間はあまり余裕がない。
残る問題は木上の2頂点間の距離を高速に求めるという部分であるが、これは典型アルゴリズムが存在する。

木上の2点間の距離

これを求める方法の1つとしてLCAと累積和を利用する手法がある。
典型だからググって読んでみてと書こうとしたがさっと探して見当たらなかったので、少し詳しく書いておく。
LCAと累積和についてはそれぞれ良質な記事が存在しているので書かない。
もし知らない場合は、検索して学習しよう。
自分はLCAはHL分解を利用したものを使っているが、ややこしいので普通にダブリングで実装したものを使うといい。

さて、木上の2点(a,b)について距離を求める。

  1. 木の根をどこでもいいので設定
  2. LCAの構築
  3. すべての頂点について根からの距離を累積和っぽくDFSで求めておく。配列totとする。

ここまでが事前準備で、利用するときは、

  1. 頂点(a,b)からLCAを求める。頂点lcとする
  2. 頂点(a,b)間の距離はtot[a] + tot[b] - 2 * tot[lc]となる

細かく見ていこう

手順1

根っこはどこでもいいが、ランダムにしておくと、いじわるなケースを意図せず回避できるかもしれない。
本質的な改善ではないから、普通に0にしとけばいい。

手順2

LCAの構築は、どこかを見て、調べてほしい。
自分はHL分解でやっているが、やや難しいので、ダブリングが楽。
どちらにしろ、ライブラリ化しておくといい。

手順3

手順1で決めた根から累積和で配列totを計算する。

tot[cu] := 根から頂点cuまでの経路の重みの和

累積和と書いているのは、とある頂点cuの子供toへは

tot[to] = tot[cu] + (cuからtoへの重み)

のように計算ができるので、累積和っぽく高速に求めることができるためである。

手順4

これはLCAがあればよい。
頂点aと頂点bのLCAを頂点lcとしておこう。

手順5

すると、頂点aから頂点bへの距離は、tot[a] + tot[b] - 2 * tot[lc]で求められる。
もう少しかみ砕くと、

「頂点a -> 頂点b」は「頂点a -> 頂点lc -> 頂点b」と経路を分解できる。
「頂点a -> 頂点lc」はtot[a] - tot[lc]と計算でき、「頂点lc -> 頂点b」はtpt[b] - tot[lc]と計算できる。
よって、足し合わせるとtot[a] + tot[b] - 2 * tot[lc]となる。

int N, X;
vector<pair<int, int>> E[3010];
//---------------------------------------------------------------------------------------------------
int tot[3010];
void dfs(int cu, int pa = -1) {
    fore(p, E[cu]) if (p.first != pa) {
        tot[p.first] = tot[cu] + p.second;
        dfs(p.first, cu);
    }
}
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N >> X;
    HLDecomposition hld;
    hld.init(N);
    rep(i, 0, N - 1) {
        int a, b, c; cin >> a >> b >> c;
        a--; b--;
        hld.add(a, b);
        E[a].push_back({ b, c });
        E[b].push_back({ a, c });
    }
    
    hld.build(0);
    dfs(0);

    rep(a, 0, N) rep(b, a + 1, N) {
        int lc = hld.lca(a, b);
        int len = tot[a] + tot[b] - 2 * tot[lc];
        if (len == X) {
            cout << "Yes" << endl;
            return;
        }
    }

    cout << "No" << endl;
}

K-th element [第八回 アルゴリズム実技検定 G]

https://atcoder.jp/contests/past202109-open/tasks/past202109_g

前提知識

解説

https://atcoder.jp/contests/past202109-open/submissions/26603092

この問題は正直典型問題として知っていないと解くのは難しいように思う。
二分探索を利用することで解くことができる。
全てそうかは分からないが、以下から類題を探せるかもしれない。
https://drken1215.hatenablog.com/archive/category/k%E7%95%AA%E7%9B%AE%E3%82%92%E6%B1%82%E3%82%81%E3%82%8B

「K番目の要素」

このK番目の要素という題名がヒントになっていて、やや典型テクとしてK番目の要素を求めるなら二分探索みたいな
テクというか、よく出る手法というかがある。
あまりピンとこないかもしれない。
二分探索の比較関数を定義しよう。

比較関数

check(lim) := 数列の要素の中でlim以下のものの個数を集計したときに、K以上個あればtrue、さもなければfalse

ちょっとわかりにくいかもしれない。
少し例を使って説明してみよう。

check(0)は、数列の要素の中で0以下のものを集計していて、そんな要素はないので、かならずfalseになる。
check(∞)は、数列の要素の中で∞以下のものを集計していて、すべての要素が含まれるため、かならずtrueになる。
これはよくって…

例えば答えがansであった場合を考えてみよう。
check(ans)は、数列の要素の中でans以下のものを集計している。
K番目がansという定義で進めているので、数列の要素の中でans以下のものはK個はあるはずである。
よって、K以上個あるのでtrueになる。

check(ans - 1)を考えてみると、K番目がansであるはずなので、数列の要素の中でans-1以下の個数は
K個未満のはずである。
そのため、check(ans - 1)はfalseとなる。

二分探索的にはちょうど境界線上に答えが浮かび上がってくることになる!
なので、比較関数を高速にさばくことができれば、二分探索で答えを導くことができる。

lim以下の要素の個数

全ての数列それぞれに対して、lim以下の要素の個数を求めてその総和を取ればいい。
なので、とある数列に対してlim以下の要素が何個あるかについて高速に計算したい。
これはmin(1LL * A[i], 1LL + (lim - B[i]) / C[i])で求めることができる。

minを使っているのは個数の最大値はA[i]個なので、その上限を決めるために使っている。
なお、1LLを使っているのはlong longに型変換させるために使っている。明示キャストでも問題ない。

実際の個数計算は1LL + (lim - B[i]) / C[i]である。
例えば3 5 7 9 11という数列があって、10以下の個数を求める場合を考えてみる。
最初の3は含まれるとカウントして、全部の数から初項を引いてみよう。
0 2 4 6 8という数列で7以下の個数を求める問題になる。
これは丁度倍数の個数を求めているような感じになるので、7から公差である2を割ったときの商を使えばいい。
7÷2の商は3なので、初項の1つと合わせて1+3の4個が答え。
これを数式に落とし込むと、1LL + (lim - B[i]) / C[i]になる。

int N; ll K;
int A[101010], B[101010], C[101010];
//---------------------------------------------------------------------------------------------------
bool check(ll lim) {
    ll cnt = 0;
    rep(i, 0, N) if (B[i] <= lim) cnt += min(1LL * A[i], 1LL + (lim - B[i]) / C[i]);
    return K <= cnt;
}
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N >> K;
    rep(i, 0, N) cin >> A[i] >> B[i] >> C[i];

    ll ng = 0, ok = infl;
    while (ng + 1 != ok) {
        ll md = (ng + ok) / 2;
        if (check(md)) ok = md;
        else ng = md;
    }

    cout << ok << endl;
}

Incomplete Permutation [第八回 アルゴリズム実技検定 F]

https://atcoder.jp/contests/past202109-open/tasks/past202109_f

解説

https://atcoder.jp/contests/past202109-open/submissions/26602840

構築問題。
構築問題の基本は、シンプルな構築ルールである。
なるべく簡単に簡単に条件を満たせるルールを使うことが大切。

-1のとき

構築できない場合はどういった場合かを考えてみると、0が1つの場合になる。
0が1つであるので、他はすべて1ということになる。
1の部分は添え字と同じ数字になっている必要があるが、そうすると、0の部分で使える数字は
その添え字の数字しかなくなり、添え字と違う数字を割り当てることができない。

構築できない場合をざっくり考えてみると他に無さそうではある。
(構築法を考えると、0が1つのときだけだなという確信が増してくる)

どういった構築ルールにするか

適当に並び替えてやれば条件を満たしそうではあるが、それは人間視点であり、
どんな状況でも単一ルールで条件を満たすようなものを考えてみよう。

自分は0の部分の添え字を1つずつ横にずらすことで、自分の添え字とは違うようにした。
実装ではdequeを使った実装を行った。
1つずつ横にずらす必要があるので、順番にdequeに入れておいて、そのあと、
先頭の1つを取り出して、末尾に追加してやれば1つずらしたような感じになる。

int N;
string S;
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N >> S;
    
    deque<int> zero, one;
    rep(i, 0, N) {
        if (S[i] == '0') zero.push_back(i + 1);
        else one.push_back(i + 1);
    }

    if (zero.size() == 0) { }
    else if (zero.size() == 1) {
        cout << -1 << endl;
        return;
    }
    else {
        int top = zero.front();
        zero.pop_front();
        zero.push_back(top);
    }

    rep(i, 0, N) {
        if (i) printf(" ");
        if (S[i] == '0') {
            printf("%d", zero.front());
            zero.pop_front();
        }
        else {
            printf("%d", one.front());
            one.pop_front();
        }
    }
    printf("\n");
}

Colorful T-Shirts [第八回 アルゴリズム実技検定 E]

https://atcoder.jp/contests/past202109-open/tasks/past202109_e

解説

https://atcoder.jp/contests/past202109-open/submissions/26602632

貪欲法で解いていく。
かかる値段を最小化したいと考えた場合、簡単な方針として、安いものから選択するという方針がある。
今回はこれで解いていこう。

安いものから選択する

具体的に方針を示すと、全てのTシャツを価格が安い順に並び替えて、買うかどうかを判定していく。
まだ買ってない色のTシャツなら購入して、そうでないなら買わないという感じで購入していき、
K枚買ったら終了。

実装について

自分のC++の実装では、pairとソートをうまく使って、順番に処理している。
pairの配列に対して昇順ソートを適用すると、第一要素で昇順にして、第一要素が同じなら第二要素で昇順にしてくれる。
今回はpairの第一要素に値段をおいてソートを行う。
第二要素はソート的には使っていない(別にどんな順番でもいい)のだが、
色を保持しておくことでソートしても対応する色を楽に取ってくることができる。
この辺は好き好きで実装するといい。

あと、購入済みの色を管理するためにsetを利用している。

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

    vector<pair<int, int>> orders;
    rep(i, 0, N) orders.push_back({ p[i], c[i] });
    sort(all(orders));

    set<int> used;
    ll ans = 0;
    rep(i, 0, N) {
        int price = orders[i].first;
        int color = orders[i].second;

        if (used.count(color)) continue;

        ans += price;
        used.insert(color);

        if (used.size() == K) {
            cout << ans << endl;
            return;
        }
    }

    cout << -1 << endl;
}

Divisor [第八回 アルゴリズム実技検定 D]

https://atcoder.jp/contests/past202109-open/tasks/past202109_d

前提知識

解説

https://atcoder.jp/contests/past202109-open/submissions/26602449

とある数の約数の個数が計算できれば、あとは判定するだけとなるので、
実質問題視されているのは、約数の部分である。
今回は制約的に実は約数列挙してしまっても問題ない。
今後のためにも、高速に約数列挙する方法を学んでおこう。
自分はライブラリ化してあるので、回答自体は貼るだけだった。

約数列挙

自分が下手に説明するより、
N の約数を全列挙するアルゴリズム | アルゴリズムロジック
を見る方がいいと思う。
計算量がO(sqrt(N))となっているが、sqrtは平方根のことである。
平方根を知らない学生については、そちらも学ぶといいかもしれない。

あと、自分の実装では少し計算量が悪くてlogがついてしまっているのだが、
今まで問題になったことはない。
自分の実装は上の投稿リンクからたどってほしい。

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

    int cntX = enumdiv(X).size();
    int cntY = enumdiv(Y).size();

    if (cntX > cntY) cout << "X" << endl;
    else if (cntX < cntY) cout << "Y" << endl;
    else cout << "Z" << endl;
}

Number of Apperance [第八回 アルゴリズム実技検定 C]

https://atcoder.jp/contests/past202109-open/tasks/past202109_c

解説

https://atcoder.jp/contests/past202109-open/submissions/26585529

問題で要求されていることを実装する問題。
入力の個数がやや多いので少し気を付ける必要があるかもしれない。
何故か通らないという場合は入力処理だけを残して提出してTLEになっていないか確認するといいだろう。
今はどうか分からないが、昔はcin.tie(0); ios::sync_with_stdio(false);を付けないとcinではTLEしていた。

あとは、それほど難しいものはない。
ループで配列Aをなめていって、Xと同じものがあればカウンターをインクリメントしてやるだけ。

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

    int ans = 0;
    rep(i, 0, N) if (A[i] == X) ans++;
    cout << ans << endl;
}

intersection [第八回 アルゴリズム実技検定 B]

https://atcoder.jp/contests/past202109-open/tasks/past202109_b

解説

https://atcoder.jp/contests/past202109-open/submissions/26585385

やや工夫が必要になる問題。
配列A,Bに含まれる数は制約からすべて別々であるということから、
重複とかは考えずに、単純にA[i] == B[j]を満たすようなものを選択して値の小さい順に答えていく。
配列Aと配列Bが値の小さい順になっているとは限らないので、一旦答えを配列ansに保持しておいて、
最後にソートしてから答えることにする。

A[i] == B[j]を満たすような(i,j)を探せばいいので、iとjをそれぞれ探索するような2重ループを書こう。
条件を満たすような数が見つかれば、それを配列ansに入れていく。

値の小さい順に答える必要があるので、答える前にソートをしよう。
C++であればsort関数を使う。
自分は特殊なマクロで書いているが、マクロなしだとsort(ans.begin(), ans.end());といった感じになるだろう。

後は答えるだけであるが、少し見栄えを気にして前の要素があれば空白を入れるなどしている。
多分昨今は無駄なスペースがあっても通るような気がするが、別の競プロサイトはAtCoderほどしっかりしてない可能性もあるので、
自分はなるべく要求通りに書くようにしている。

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

    vector<int> ans;
    rep(i, 0, N) rep(j, 0, M) if (A[i] == B[j]) ans.push_back(A[i]);
    sort(all(ans));

    rep(i, 0, ans.size()) {
        if (0 < i) printf(" ");
        printf("%d", ans[i]);
    }
    printf("\n");
}