2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Node.js 競プロ 標準入力 メモ

Last updated at Posted at 2022-07-17

Node.js の標準入力に取り憑かれています。

導入

以前も似たような記事を書いたのですが、ただの自作モジュールの紹介記事でした。

今回は「競プロっぽい」をテーマに自分が見てきた・考えてきたコード例を載せます。

自分が考えた例には★印を付けておきます。「信用しないでね」の意味です。

何か面白いパターンを思いついたら随時更新する予定です。

fs.readFileSync を利用する例

こちらは変数に標準出力の内容を一度に受けてしまおうというものです。

split して配列を保持する

const inputs = require("fs").readFileSync("/dev/stdin", "utf-8").trim().split("\n");

// 1 行目は inputs[0]
// 2 行目は inputs[1]
// ...

今日日よく見られるものな気がします。

こちらは、N 行目を得るために inputs[N-1] をする必要があるのですが、インデックスの管理が必要になって面倒だなあという印象を受けます。

★ スキャナみたいなものを書いてみるといいかも

一度変数に受けたのだから、スキャナのようなものを書くといいんじゃないと思って考えてみました。現状次のものが一番楽に書ける気がします。

const scan = {
  _buf: require("fs").readFileSync(0, "utf-8"),
  line() {
    const [s] = this._buf.split("\n", 1);
    this._buf = this._buf.substring(s.length + 1);
    return s;
  },
};
// scan.line() で一行読む
const n = Number(scan.line());
for (let i = 0; i < n; ++i) scan.line();

8 行必要になりますが、一度 _buf に文字列として標準入力を受けておいてどんどん消費していくようなものを書きました。ただし、改行文字が \n であることが前提です。

何度も _buf.split(...) を呼んでいて遅そうに見えるかもしれませんが、個人的に上のようにあらかじめ split して配列を持っておく例とそんなに速度的に変わらないような気がしています。

for (let i = 0; i < n; ++i) {
  const str = scan.line();
  console.log(str);
  console.log(scan);
}

こんなふうに scan の内容を出力してみると _buf の内容がどんどん削れていく様子が見えて気持ちいいです。

実行例

readline モジュールを利用する例

readline モジュールという一行ずつ読み取るためのものがあります。お誂え向きに見えますが、どちらかというと CLI で対話するためのものなのでややしんどい思いをすることになります。

一度入力が終わるまで行を全て読み取り、配列に保持する

こちらも競プロで標準入力を取る例としてよく見かけるものな気がします。

const rl = require("readline").createInterface(process.stdin);

const inputs = [];

rl.on("line", (line) => {
  inputs.push(line);
});

rl.on("close", () => {
  // メイン処理...
});

イベント駆動だということで競プロでは見かけないようなコードをしています。

結局配列に全て受けるところは fs.readFileSync の初めの例と変わらないので、そこに気をつければ同じようにコーディングできる気がします。

close イベントを見て「入力が完了した」ということでメイン処理を開始する感じになっています。

event モジュールと組み合わせて「一行読む」関数を作る(async/await

色々考えて作ってみました(楽しかった)。

const { stdin, exit } = process;
const { log, error } = console;

const rl = require("events").on(require("readline").createInterface(stdin), "line");
const ln = () => rl.next().then(({ value: [l] }) => l);

async function main() {
  const [n, m, q] = (await ln()).split(" ").map(Number);
  const mat = [];
  for (let i = 0; i < n; ++i) mat.push((await ln()).split(" ").map(Number));
  // ...
}

main().then(() => exit(0)).catch(e => error(e) || exit(1));

ゴチャゴチャしていて見にくいかもしれませんが、async function main の中で await ln() とすればその場で一行読み出せる(ように見える)コーディングができるようになっています。

悩んで作った割にはそんなに良いコードではないのですが、「必要になったら待ち受ける」といった書き方ができるようになっています。そういうコーディングがしたい場面があればこちらの方が良いでしょう。

途中で入力が終わってしまった場合はどういうエラーが起こるんでしょうか。この辺よくわかっていません。

実行例

2022/11/07 追記

fs.readFileSync の内容を分割して受け取る関数を main で利用する例

上の fs.readFileSync の例で、入力が必要ない段階で scan オブジェクトに fs.readFileSync を受けるのがなんとなく気に食わなくなって考え直していたのですが、以下の例を思いつきました。

/** @arg {() => string} gets */
function main(gets) {
  console.log(gets().split(""));
  console.log(gets().split(""));
}

((_buffer) => main(() => {
  let [s] = _buffer.split("\n", 1);
  _buffer = _buffer.substring(s.length + 1);
  return s;
}))(require("fs").readFileSync(0, "utf-8")); // prettier-ignore

main 関数内でのみ引数の gets 関数を利用できるということで、main 実行直前に readFileSync してくれる感じです。関数名は他とぶつからないように好きなようにすればいいと思います(引数の名前を変えるだけ)。入力云々が必要なのは main だけということにすれば IO の切り分けもそれっぽいですし、ロジックが入力の上を走る感じでちょっと気に入っています。

main に渡す関数の中身が 3 行から縮まらないのが JS パワーが足りていないという感じですが、良いものがあればお教えください。

★ 上の readline

同様の方針で readline のものも書き直しました。

/** @arg {() => Promise<string>} gets */
async function main(gets) {
  console.log((await gets()).split(""));
  console.log((await gets()).split(""));
}

// prettier-ignore
(i => main(() => i.next().then(({ value: [l] }) => l)).then(_ => process.exit(0)))
(require("events").on(require("readline").createInterface(process.stdin), "line"))

2022/11/14 追記

★ 「fs.readFileSync の内容を〜」を短くした

/** @arg {() => string} gets */
function main(gets) {
  console.log(gets().split(""));
  console.log(gets().split(""));
}

// 3 行から雑に短くした
((b, s) => main(() => ([s] = b.split("\n", 1), b = b.substring(s.length + 1), s)))
(require("fs").readFileSync(0, "utf-8")); // prettier-ignore

多分お行儀は良くないんですが、ざっくり短くしてみました。

★ 配列を取っておいてインデックス管理

「短くするだけなら配列・インデックス管理を押し込めただけの関数が楽なんでは?」ということに気がついて、当初の fs.readFileSync して split してインデックスを管理して... という方針に戻ったものです。

/** @arg {() => string} gets */
function main(gets) {
  console.log(gets().split(""));
  console.log(gets().split(""));
}

((l, i) => main(() => l[i++]))
(require("fs").readFileSync(0, "utf-8").split("\n"), 0); // prettier-ignore

終わりに

競プロ環境に入ったら使いやすいんじゃないかなと思ってこういうの作ったんですけど、もちろん採用例はないです。

実行例

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?