はじめに
JavaScriptでAtCoderやってますか?前回F#とか言ってなかったっけ?
2025年5月のTIOBE Indexでは、各言語の順位は次のようになっています。
順位 | 言語 | 割合 |
---|---|---|
1 | Python | 25.35% |
2 | C++ | 9.94% |
3 | C | 9.71% |
4 | Java | 9.31% |
5 | C# | 4.22% |
6 | JavaScript | 3.68% |
7 | Go | 2.70% |
6位は高いと言えるか微妙なところですが、十分メジャーな言語だと言えましょう。WebのフロントエンドやNode.jsなど、活躍の場は多いかと。
一方で、AtCoder ProblemsのLanguage Ownersを見てみると、各言語の提出状況は次のようになっています。(※6/1 20時頃の時点)
TIOBE順位 | 言語 | 1stAC数 | 1st~3rd平均AC数(X) | 20thAC数(Y) | Y / X(%) |
---|---|---|---|---|---|
1 | Python | 5430 | 5074 | 3570 | 70.35% |
2 | C++ | 7013 | 6302 | 4380 | 69.50% |
3 | C | 4925 | 3711 | 1028 | 27.70% |
4 | Java | 3926 | 3836 | 1938 | 50.52% |
5 | C# | 4549 | 4314 | 2009 | 46.56% |
6 | JavaScript | 2923 | 2410 | 638 | 26.47% |
7 | Go | 5236 | 3954 | 1048 | 26.50% |
上位7言語の中で一番提出が少なく、最多提出でもPythonの20位にすら及んでません(Python思ったより多い……)。
また1st~3rdの平均AC数(X)と20thのAC数(Y)の比率も一番低いです。この比率が低いと言うことは、活発に提出しているのは上位数人だけで人口が少ない(かもしれない)と考えられます。少なくとも、熱心な人は少ないですよね。
上位7言語以外でJavaScriptよりも提出が多い言語も下表のようにいくつかあります。JavaScriptを上回る箇所を太字にしています。
TIOBE順位 | 言語 | 1stAC数 | 1st~3rd平均AC数(X) | 20thAC数(Y) | Y / X(%) |
---|---|---|---|---|---|
6 | JavaScript | 2923 | 2410 | 638 | 26.47% |
15 | PHP | 3611 | 2747 | 400 | 14.56% |
19 | Rust | 4103 | 3687 | 2136 | 57.93% |
21 | Ruby | 3271 | 3154 | 1477 | 46.82% |
30 | Haskell | 3435 | 2835 | 820 | 28.92% |
44 | D | 3466 | 2712 | 611 | 22.52% |
なんだかんだ定番の言語ばかりですが、JavaScriptよりも下位にある言語の方が人気ありますね。
何故JavaScriptはこんなにAtCoderでの人気がないのか。実際にやってみた上での私個人による感想として説明していきます。
理由の1つは実行速度。少し古いですが、次のポストが参考になるでしょうか。
まず、提出数少ないせいか、箱ひげ図がほぼ線ですね。
肝心な速度ですが、早くはないがめちゃ遅いわけでもないくらい。PHPやRubyなんかの方が遅いですが、それぞれの最速コードはJavaScriptと同等なので大差無しと見たほうがいいでしょう。
次に数値型の扱いの問題。
10^18を扱うにはNumberでは精度を維持できないのでBigIntを使うことになります。すると計算が余計に遅くなります。
またビット演算は32ビットまでしか扱えないので、64ビットでのビット演算ができないです。
これらの問題についてはいつか別で記事を書くとして、最後の問題が入力に関する問題となります。
入力の問題
やっと本題。
ABSを見るとJavaScriptでの入力例は次のようになっています。
// inputに入力データ全体が入る
function Main(input) {
// 1行目がinput[0], 2行目がinput[1], …に入る
input = input.split("\n");
tmp = input[1].split(" ");
//文字列から10進数に変換するときはparseIntを使います
var a = parseInt(input[0], 10);
var b = parseInt(tmp[0], 10);
var c = parseInt(tmp[1], 10);
var s = input[2];
//出力
console.log('%d %s',a+b+c,s);
}
//*この行以降は編集しないでください(標準入出力から一度に読み込み、Mainを呼び出します)
Main(require("fs").readFileSync("/dev/stdin", "utf8"));
Main関数に対し、何やら値を渡しています。渡しているのはファイルから取得したデータ、文字列です。読み込んでいる/dev/stdin
は標準入力を表しています。
得られた文字列なんですが、これは入力値すべてを改行区切りしたものです。
なので\n
で分割し、それを1行1行読んでいるわけです。
これがまた面倒くさい。例えば入力が
$N$
$A_1$
$A_2$
...
$A_N$
$Q$
$Query_1$
$Query_2$
...
$Query_Q$
こういうの。これを読み込もうとすると次のようになります。
function Main(input) {
input = input.split("\n");
let n = parseInt(input[0]);
for (let i = 0; i < n; i++)
{
let a_i = parseInt(input[i + 1]);
}
let q = parseInt(input[n + 1]);
for (let i = 0; i < q; i++)
{
let query_i = parseInt(input[n + i + 2]);
}
}
Main(require("fs").readFileSync("/dev/stdin", "utf8"));
$A_N$まではまあいいんですが、$Q$以降の読み取りが煩雑になってきて、インデックス位置を考えるという余計な時間を浪費してしまいます。
解決策
function getReader(input) {
input = input.split("\n");
// 配列アクセス用の変数
let idx = 0;
// 配列から1行読み込む関数
return () =>
{
return input[idx++];
}
}
function Main(input) {
// 読み込み用クロージャを取得
let Read = getReader(input);
let n = parseInt(Read());
for (let i = 0; i < n; i++)
{
let a_i = parseInt(Read());
}
let q = parseInt(Read());
for (let i = 0; i < q; i++)
{
let query_i = parseInt(Read());
}
}
Main(require("fs").readFileSync("/dev/stdin", "utf8"));
普通に考えればわかることですが、配列にアクセスする変数を定義して、読み込む度にインクリメントしていけばいいですね。
ただインクリメントを都度書くのも面倒で、忘れるとバグを起こすので関数化したい。
けれどアクセス用の変数は関数の外側(例えばグローバル変数)で定義しなければならない。しかしMain関数からアクセスできないようにしたい。
というわけで、getReader関数の中に読み込み関数の実行環境を閉じ込めることで対応しています。
注意事項としてはインタラクティブな問題には対応できないこと。お察しかと思いますが入力を全部一気に受け取る仕組みですので、こちらの出力で入力が変化するインタラクティブな問題では上手く値が取れないのです。
インタラクティブな問題への対応は別の方が書かれてたのでこのあたりを参考にしてください。
正直なところインタラクティブな問題以外も同じ方法で入力してもいいんですが、awaitがちょっと面倒くさいかもしれません。
おわりに
実は今までずっと素直に配列アクセスでやってきたんですが、先日のコンテストの直前に思い立って今回のようなコードに書き換えてみました。
面倒な入力パターンは出てこなかったんですが、なんだかちょっと楽になったような気はします。
先述の通り、JavaScriptが抱える問題は他にも色々あるんですけれど、一番大きな入力の問題の解決によってもう少し人口が増えると良いなと思います。
追記
入力にはもう一個問題があって、Windows環境では実行できません。これについては別記事を作成しました。