見出し画像

AtCoder精選良問「D - Ki」Diff 920

この記事はAtCoderの精選良問集マガジンの中の記事です。
私が良問だと思った問題を解説付きでまとめています。

https://note.com/kuri_tter/m/m622eeb3d9ca2

問題リンク

問題概要

はじめ、1からNまでの番号が振られたN頂点の根付き木が与えられる。
i番目の辺は頂点a_iと頂点b_iを結んでいる。
各頂点にはカウンターが設置されている。
下記のようなQ回の操作を行う。
頂点pを根とする部分木に含まれるすべての頂点のカウンターの値にxを足す。
すべての操作後の各頂点のカウンターの値を答える、という問題

考え方

まずは入力例1の図を見てみる。根付き木とは以下のような親子関係のあるグラフ構造。

行う操作は要は以下のようになる。
「指定された親頂点とその全ての子供のカウンターをX足す」
上から水を流すイメージをするといいかもしれない。

ちなみに私はタイトルが「Ki」とあるので、なんとなく「気」のようなものが下に流れるイメージも持った。作問者が「木」とかけたダジャレを放っていたのかは分からないが、私は気の利いたダジャレだと思った。ちなみに「気の利いた」という言葉もダジャレである。

問題に話を戻そう。
制約はNが100,000でQも100,000。
操作が与えられるたびに足し上げていっても答えは出せるが、制約的にその都度やっていたら明らかに間に合わない。

そのため、なんとか一発で答えを求める方法を考える。

まずは各クエリの都度に探索をするのではなく
{ 頂点1:足される量, 頂点2:足される量… }
というような辞書を用意する方針でいこう。

そのうえでグラフを1番から探索していくことを考えて、幅優先か深さ優先のどちらで攻めるかを考える。

結論を言うと深さ優先の方を選択するのが正しい。

幅優先で探索をしていくと上の図の場合、以下のような順番でたどっていく。

1 => 2 => 3 => 4

なんとなく3と4に枝わかれしてしまっているので、2に水を流した後に3に水を流すと計算がややこしくなりそうな気がする。順番に足し上げていくと、4に流れる量が正しいものよりも多くなってしまう。

この「ややこしくなりそう」という直感をひとまず信じて、深さ優先の順番をみてみる。深さ優先の場合は以下のような順番でたどる。

1 => 2 => 3 => 2 => 4

深さ優先探索は猪突猛進的に掘り進むイメージ。
言い換えると今回のケースで必ず「親=>子」という順番に進む。
先にすすめる子がいなくなったら「子=>親」に戻る、という感じだ。

こちらの順番なら解ける気がする。
各クエリのデータは以下のような辞書にまとめていた。

{ 頂点1:足される量, 頂点2:足される量… }

まずは1番の親から頂点を順番にたどっていって、流れる水の量をどんどん足していけばいい。親に流れた水は必ずその子供に流れていくからだ。

そうして「子=>親」の遷移をした際は、子に流した水を減らすような感じで流量を調整する。
そのような実装をする。

実装

import { createInterface } from "readline";
import * as fs from "fs";
import { log } from "console";
import { exit } from "process";


function main() {
    const [n, q] = nextNums(2)
    // graphを構築
    const graph: Record<number, number[]> = []
    for (let i = 0; i < n - 1; i++) {
        const [a, b] = nextNums(2)
        if (graph[a]) {
            graph[a].push(b)
        } else {
            graph[a] = [b]
        }
        if (graph[b]) {
            graph[b].push(a)
        } else {
            graph[b] = [a]
        }
    }
    // 各頂点に流れる量を保持
    const flowDict: Record<number, number> = {}
    // 0で初期化
    for (let i = 1; i < n + 1; i++) {
        flowDict[i] = 0
    }
    for (let i = 0; i < q; i++) {
        const [a, x] = nextNums(2)
        flowDict[a] += x
    }
    // 訪問済みの頂点を記録
    const vis: Set<number> = new Set()
    vis.add(1)
    // 答えの配列
    const answer: number[] = new Array(n + 1).fill(0)
    // 流れている量
    let flow = 0
    // 深さ優先探索
    const stack: number[] = [1]
    while (stack.length > 0) {
        const now = stack.pop()!
        if (now > 0) {
            // 水を足す
            flow += flowDict[now]
            // 答えに足す
            answer[now] += flow
            if (!graph[now]) continue
            // 子供を見ていく
            for (const child of graph[now]) {
                // 訪問済みはスキップ
                if (vis.has(child)) continue
                vis.add(child)
                // マイナスを先に入れる
                stack.push(-1 * child)
                stack.push(child)
            }
        } else {
            // マイナスの頂点が返ってきた
            // つまり次は子=>親の遷移になる
            // あげた水を減らす
            flow -= flowDict[-1 * now]
        }
    }
    // 答えの出力。1インデックスにしているためsliceする。
    log(answer.slice(1).join(' '))
}

let inputs = "";
let inputArray: string[];
let currentIndex = 0;

function next() {
    return inputArray[currentIndex++];
}
function nextNum() {
    return +next();
}
function nextBigInt() {
    return BigInt(next());
}
function nexts(length: number) {
    const arr = [];
    for (let i = 0; i < length; ++i) arr[i] = next();
    return arr;
}
function nextNums(length: number) {
    const arr = [];
    for (let i = 0; i < length; ++i) arr[i] = nextNum();
    return arr;
}
function nextBigInts(length: number) {
    const arr = [];
    for (let i = 0; i < length; ++i) arr[i] = nextBigInt();
    return arr;
}


// デバッグ環境がWindowsであれば条件分岐する
if (process.env.OS == "Windows_NT") {
    const stream = createInterface({
        input: process.stdin,
        output: process.stdout,
    });
    stream.on("line", (line) => {
        inputs += line;
        inputs += "\n";
    });
    stream.on("close", () => {
        inputArray = inputs.split(/\s/);
        main();
    });
} else {
    inputs = fs.readFileSync("/dev/stdin", "utf8");
    inputArray = inputs.split(/\s/);
    main();
}

この記事が気に入ったらサポートをしてみませんか?