見出し画像

AtCoder精選良問「D - Preparing Boxes」Diff 926

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

問題リンク

問題概要

空の箱がN個横一列に並んでおり、左からi番目の箱には整数iが書かれている。すぬけさんは各箱にボールを1個入れるか、何も入れないかを選べる。以下の条件を満たすボールの入れ方を「いいボールの入れ方」と定める。

  • 1以上N以下の任意の整数iについて、iの倍数が書かれた箱に入っているボールの個数の和を2で割った余りがaiである。

「いいボールの入れ方」が存在するかどうかを判定し、存在する場合は1つ求めて以下を出力する。

  • ボールを入れた箱の数

  • ボールを入れた箱の番号

存在しない場合は-1を出力する。

考え方

解いた後で面白い、と思った。
まず問題文の「1以上N以下の任意の整数iについて、iの倍数が書かれた箱」という部分を整理してみる。
例えば1~6までの箱がある場合。
1の倍数:1,2,3,4,5,6
2の倍数:2,4,6
3の倍数:3,6
4の倍数:4
5の倍数:5
6の倍数:6

というような形になる。

これを見てみると、前から番号を追っていくと難しいことが分かる。
例えば6の2と3の倍数なので、2の時に正しいと思って6にボールを入れたけど、3で見るとボールを入れるのは間違いだったというようなことが起きる。

こういう時は発想を転換させて「後ろから考える」という手法を使う。
大きい数字から順番に追っていけば、操作の矛盾が起きない。

aiのリストは仮にこういう風にしておこう。

0 1 0 0 0 1

まずは、6にボールを入れる必要があるので、ボールを入れる。
5と4は0なので入れない。
3を見た時に3と6の合計を2で割った余りを0にする必要があるので3にもボールを入れる。
2を見た時は同様に余りを1にする必要がある。6に1個だけボールが入っているので、何もしなくていい。

このような感じで処理を追っていく。

次に計算量だが、箱のサイズの最大は200,000。
愚直に倍数をたどっていくと、TLEしそうな気配があるが意外とそうでもない。
全部繰り返さないといけないのは1の時だけで、それ以外の数字の時は数が大きいとそれだけ繰り返しが減る。

2のときは2,4,6,8…
3のときは3,6,9,12…

この辺は事前に検証してみてもいいだろう。

function main() {
    const n = 200000
    let cnt = 0
    for (let i = 1; i <= n; i++) {
        // nをiを割った商がだいたいの計算量
        cnt += Math.floor(n / i)
    }
    log(cnt)
}
2472113

このように最大値で250万回程度のループで終えられる。
あとは実装をすればいい。

実装

上のルールで行っていけば、必ず「いいボールの入れ方」となるので、答えが-1になることはない。

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

function main() {
    const n = nextNum()
    // 1インデックスにしておく。
    const arr = [0, ...nextNums(n)]
    // ボールを入れた回数
    let countOperation: number = 0
    // ボールを入れた箱
    const boxWithBalls: number[] = []
    // 箱の状態を管理 これも1インデックスに対応
    const boxes: number[] = new Array(n + 1).fill(0)
    // 後ろから最初まで
    for (let i = n; i > 0; i--) {
        let totalBall: number = 0
        let p = 2
        // iの倍数を順番に見て入れたボールの個数を計算
        while (i * p <= n) {
            totalBall += boxes[i * p]
            p += 1
        }
        // 偶奇が違う場合は箱に入れる
        if (totalBall % 2 != arr[i]) {
            boxes[i] = 1
            countOperation++
            boxWithBalls.push(i)
        }
    }
    log(countOperation)
    boxWithBalls.reverse()
    log(boxWithBalls.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();
}


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