見出し画像

AtCoder Beginner Contest 304 A-EをTypeScriptで解く。

先週に引き続きAtCoder Beginner Contest 304に参加をした。
コンテスト自体はPythonで参加をしたのだけど、Typescriptで振り返り記事を書く。

結果

A,B,C,D,Eのノーペナ5完。
今回は判定の遅れが発生しコンテストを20分延長するものの、unratedとなってしまった。順位は1,500位前後と自分の中では好成績だったので、残念だった。気を取り直して、振り返る。

A - First Player

二人以上のN人が時計回りに円卓に座っている。
それぞれの名前と年齢が与えられるので、年齢が一番低い人を起点として、時計回りでN人の名前を出力する問題。

ちょっとA問題にしては難しいと思った。
一番低い年齢を調べてそこからfor loopを回す。インデックスで余りを取る。

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

function main() {
    const n = nextNum()
    // 最少の年齢
    let minAge = 10 ** 10
    type person = [string, number]
    const persons: person[] = []
    for (let i = 0; i < n; i++) {
        const name = next()
        const age = nextNum()
        minAge = Math.min(minAge, age)
        persons.push([name, age])
    }
    for (let i = 0; i < n; i++) {
        const [_, age] = persons[i]
        if (age == minAge) {
            for (let j = 0; j < n; j++) {
                // 余りをとる
                const idx = (i + j) % n
                const name = persons[idx][0]
                log(name)
            }
        }
    }
}

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();
}

B - Subscribers

整数 N が与えられ、以下の指示に従って N の近似値を出力する

  • N が 10^3 - 1 以下ならば、N をそのまま出力する。

  • N が 10^4 - 1 以下ならば、N の一の位を切り捨てて出力する。

  • N が 10^5 - 1 以下ならば、N の十の位以下を切り捨てて出力する。

  • N が 10^6 - 1 以下ならば、N の百の位以下を切り捨てて出力する。

  • N が 10^7 - 1 以下ならば、N の千の位以下を切り捨てて出力する。

  • N が 10^8 - 1 以下ならば、N の一万の位以下を切り捨てて出力する。

  • N が 10^9 - 1 以下ならば、N の十万の位以下を切り捨てて出力する。

コンテスト中はIF文を地道に書いたが…
桁が一つ上がるにつれて、段々と切り捨てる桁も増えている。
例えば9999 みたいな数字があったとすると、答えは9990。
99999だったら99900。
一般すると10の(桁数-3)乗で切り下げで割って、またかけると答え。
9999だったら10の(4-3)乗で10で割る。
999になるので、また10をかけて9990となる。

function main() {
    const n = nextNum()
    const digit = String(n).length
    const wari = 10 ** (digit - 3)
    let ans = Math.floor(n / wari)
    log(ans * wari)
}

C - Virus

1,2,...,N の番号がついた N 人の人が二次元平面上いる。人 i は座標 (Xi, Yi) で表される地点にいる。人 1 はウイルスに感染している。ウイルスに感染した人からユークリッド距離が D 以内にいる人にウイルスはうつる。各 i について人 i がウイルスに感染しているか判定する、と言う問題。

まず、距離がD以内にいる人を順番に辿っていくことが要件なんだな、と考える。
距離がD以内にいる人を探索するのが厄介そうな感じがあるが、Nの制約は2000までと小さい。そのため、予め組み合わせを全探索しておけばいい。

function calcDist(a: number[], b: number[]): number {
    const [x1, y1] = a
    const [x2, y2] = b
    return Math.abs(x2 - x1) ** 2 + Math.abs(y2 - y1) ** 2

}

function main() {
    const [n, d] = nextNums(2)
    const positions: number[][] = []
    const graph: Record<number, number[]> = {}
    for (let i = 0; i < n; i++) {
        const [x, y] = nextNums(2)
        positions.push([x, y])
        // graphを初期化しておく
        graph[i] = []
    }
    for (let i = 0; i < n - 1; i++) {
        for (let j = i + 1; j < n; j++) {
            const a = positions[i]
            const b = positions[j]
            // ユークリッド距離がd以内だったらつなげる
            const dist = calcDist(a, b)
            if (dist <= d ** 2) {
                graph[i].push(j)
                graph[j].push(i)
            }
        }
    }
    const answer: String[] = new Array(n).fill('No')
    // 0から順番にたどって答えを更新
    const que: number[] = []
    que.push(0)
    const vis: Set<number> = new Set()
    vis.add(0)
    while (que.length > 0) {
        const now = que.pop()!
        answer[now] = 'Yes'
        for (const ver of graph[now]) {
            if (vis.has(ver)) continue
            vis.add(ver)
            que.push(ver)
        }
    }
    for (const ans of answer) {
        log(ans)
    }
}

D - A Piece of Cake

W*HのXY平面上にある長方形のケーキにN個のイチゴが載っている。
ケーキをA本の直線でx軸に平行に切り、さらにB本の直線でy軸に平行に切ることによって、(A+1)(B+1)個のピースに分割する。そのうちの1つを選んで食べるとき、選んだピースに含まれるイチゴの個数の最小値と最大値を求める。

考えることが多くて難しいと思った。

こういう問題はまず図を描いてみる。
入力例1の場合はこんな感じ。


合計で9個に分割されたことが分かる。
ここで、答えは何もない0個が最小で、最大は2個のエリア。

これを求めることを考える。
素朴に考えると、エリアを全探索して、イチゴも全探索して、数え上げれば答えは出せそう。ただし、A,Bは10^5まであるので最大でエリアだけで10^10くらいまでなる。そのため、明らかに厳しい。

発想を転換して、イチゴが含まれるエリアを記録する、という風に考える。
左下の1点にイチゴを集中させるイメージ。


イチゴの数の最大は2 * 10^5ほど。そのため、このように転換させれば、計算量が大幅に減らせる。

次に左下に寄せる動作を行う方法について考える。
Aの配列は[2,5]
Bの配列は[3,4]
ここにそれぞれ番兵を入れて…
A:[0,2,5,W]
B:[0,3,4,H]
というようにする。

たとえば(6,2)のイチゴについて、6を超えないAの最大値、2を超えないBの最大値を求めと、(5,0)が左下の点。これは二分探索を用いれば高速に判定できる。
あとは左下の点ごとにハッシュテーブルで数を足し上げていく。
その最小と最大が答えとなる。
注意点はこの方法だとイチゴが存在しないエリアは検知できない。
逆に言うとハッシュテーブルの要素数が、エリア数に満たない場合は必ずイチゴが0個のエリアがある。

/**
 * 
 * @param arr ソート済みの配列
 * @param x 検索値
 * @returns 
 * 値 x が最初に現れるインデックスを返す。
 * x が arr 内に存在しない場合は、x を挿入するためのインデックスを返す
 */
export const bisectLeft = (arr: number[], x: number, lo: number = 0, hi: number = arr.length): number => {
    while (lo < hi) {
        const mid = Math.floor((lo + hi) / 2);
        if (arr[mid] < x) {
            lo = mid + 1;
        } else {
            hi = mid;
        }
    }
    return lo;
}

function main() {
    const [w, h] = nextNums(2)
    const n = nextNum()
    const strawberries: number[][] = []
    // イチゴの位置を記録
    for (let i = 0; i < n; i++) {
        const [x, y] = nextNums(2)
        strawberries.push([x, y])
    }
    // 0と最大値で番兵を置いて配列を作る
    const A = nextNum()
    const arrA: number[] = [0]
    const positionsA = nextNums(A)
    for (const pos of positionsA) {
        arrA.push(pos)
    }
    arrA.push(w)
    const B = nextNum()
    const arrB: number[] = [0]
    const positionsB = nextNums(B)
    for (const pos of positionsB) {
        arrB.push(pos)
    }
    arrB.push(h)
    // イチゴの数のカウンター
    const counter: Record<string, number> = {}
    // イチゴを繰り返す
    for (const strawberry of strawberries) {
        const [x, y] = strawberry
        // 二分探索でxとyを超えない最大値を検出
        const posX = bisectLeft(arrA, x) - 1
        const posY = bisectLeft(arrB, y) - 1
        // 文字列で交点を示すキーを作る
        const key = `${posX},${posY}`
        if (counter[key]) {
            counter[key]++
        } else {
            counter[key] = 1
        }
    }
    let ansMin = 10 ** 10
    let ansMax = 0
    for (const key in counter) {
        const cnt = counter[key]
        ansMin = Math.min(ansMin, cnt)
        ansMax = Math.max(ansMax, cnt)
    }
    // エリアの数は(A+1)*(B+1)
    // カウンターの要素数がこの数に満たなければ必ず0がある
    if (Object.entries(counter).length < (A + 1) * (B + 1)) ansMin = 0
    log(ansMin, ansMax)
}

E - Good Graph

N 頂点 M 辺の無向グラフ G が与えられる。
すべての i=1,2,...,K について、頂点 xi と頂点 yi を結ぶパスが存在しないとき、Gは良いグラフと呼ばれる。
Q 個の質問で与えられたグラフ G に頂点 p と頂点 q を結ぶ無向辺を追加する。新しいグラフが良いグラフであるかどうかを判定する。

まず何を言っているかよくわからない、というのが初見の感想だった。
グラフ問題はとりあえず図を描いてみることが重要。

入力例1は以下。

6 6
1 2
2 3
2 3
3 1
5 4
5 5
3
1 5
2 6
4 3
4
2 5
2 6
5 6
5 4

まずグラフ部分を図にするとこうなる。


次にKの部分について、頂点を赤線で結んでみる。

解読するのに苦労するタイプの問題だが、図に書くと見えてくる。
ある頂点とある頂点を結ぶパスが存在する、ということは、島から島へ行き来するパスが存在するイメージ。

なのでそれぞれの島ごとにくくって…


大分見通しがよくなってきた。
この赤線があるとダメなんだな、という問題であることが分かる。

つまりQ個のクエリそれぞれで結合した場合で、赤線がある島同士がつながってしまったらNG。

とりあえずそれぞれの島はUnionFindすれば求められそう。

次につないだらNGな島の組み合わせを記録しておく。
Q個の質問のそれぞれでつながることになる島を調べて、NGな組み合わせでなかったらOKという手順で問題を解く。

export class UnionFind {
    private parents: number[];

    constructor(private n: number) {
        this.parents = Array.from({ length: n }, () => -1);
    }

    public find(x: number): number {
        if (this.parents[x] < 0) {
            return x;
        } else {
            this.parents[x] = this.find(this.parents[x]);
            return this.parents[x];
        }
    }

    public union(x: number, y: number): void {
        x = this.find(x);
        y = this.find(y);

        if (x === y) {
            return;
        }

        if (this.parents[x] > this.parents[y]) {
            [x, y] = [y, x];
        }

        this.parents[x] += this.parents[y];
        this.parents[y] = x;
    }

    public size(x: number): number {
        return -this.parents[this.find(x)];
    }

    public same(x: number, y: number): boolean {
        return this.find(x) === this.find(y);
    }

    public members(x: number): number[] {
        const root = this.find(x);
        return Array.from({ length: this.n }, (_, i) => i).filter(i => this.find(i) === root);
    }

    public roots(): number[] {
        return Array.from({ length: this.n }, (_, i) => i).filter(i => this.parents[i] < 0);
    }

    public groupCount(): number {
        return this.roots().length;
    }

    public allGroupMembers(): Map<number, number[]> {
        const groupMembers = new Map<number, number[]>();
        for (let member = 0; member < this.n; member++) {
            const root = this.find(member);
            if (!groupMembers.has(root)) {
                groupMembers.set(root, []);
            }
            groupMembers.get(root)!.push(member);
        }
        return groupMembers;
    }
}

function main() {
    const [n, m] = nextNums(2)
    const uf = new UnionFind(n + 1)
    for (let i = 0; i < m; i++) {
        const [u, v] = nextNums(2)
        // つながっている頂点をunionしてgroup分けする
        uf.union(u, v)
    }
    const k = nextNum()
    // NGになる組み合わせ
    const badPair: Record<number, Set<number>> = {}
    // 初期化しておく
    for (let ver = 1; ver < n + 1; ver++) {
        badPair[ver] = new Set()
    }
    for (let i = 0; i < k; i++) {
        const [u, v] = nextNums(2)
        // NGな島のrootを記録
        const uRoot = uf.find(u)
        const vRoot = uf.find(v)
        badPair[uRoot].add(vRoot)
        badPair[vRoot].add(uRoot)
    }
    const query = nextNum()
    for (let i = 0; i < query; i++) {
        const [p, q] = nextNums(2)
        // つなぐ島のrootを求める
        const pRoot = uf.find(p)
        const qRoot = uf.find(q)
        // NG組み合わせにあったらNo
        if (badPair[pRoot].has(qRoot) || badPair[qRoot].has(pRoot)) {
            log('No')
        } else {
            log('Yes')
        }
    }
}

おわりに

unratedになってしまったのは率直にいって残念だった。個人的には好きなアルゴリズムのUnionFindを使うことでE問題まで到達できたので、その点には満足している。AtCoder社には引き続き運営を頑張ってもらいたい。ではでは。

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