今回は paiza の「文字列の重複カウント」の問題に挑戦!
久しぶりに正規表現を使った!
問題概要
入力
- 1行目:文字列
s
(探したいパターン) - 2行目:文字列
t
(検索対象の文字列) - 両方とも半角アルファベットで構成される
目的
- 文字列
t
の中でs
が何回出現するかをカウントする - 重なり部分も含めて数える(例えば "AA" が "AAA" に 2 回含まれる)
出力
-
s
がt
に出現する回数を整数で出力
入力例:
AA
abdeeAAbAAAbfde
出力例:
3
❌ NG例:
const rl = require('readline').createInterface({ input: process.stdin });
const lines = [];
rl.on('line', (input) => lines.push(input));
rl.on('close', () => {
const s = lines[0];
const t = lines[1];
// ❌
const result = t.match(/(?=${s})/g).length;
console.log(result);
});
-
/…/
は 正規表現リテラル で、文字列展開(${s}
)は効かない -
つまり
s
変数の内容は反映されず、文字列 ${s} を探すことになる
✅ OK例:
const rl = require('readline').createInterface({ input: process.stdin });
const lines = [];
rl.on('line', (input) => lines.push(input));
rl.on('close', () => {
const s = lines[0]; // 探したいパターン
const t = lines[1]; // 対象の文字列
// 動的に正規表現を生成
const regex = new RegExp('(?=${s})', 'g');
const matches = t.match(regex);
console.log(matches ? matches.length : 0);
});
1️⃣ 入力データの取り出し
const s = lines[0]; // 探したいパターン
const t = lines[1]; // 対象の文字列
-
s
が検索する部分文字列。 -
t
がその中で何回出現するかをカウントする対象文字列。
2️⃣ 正規表現を動的に作成
const regex = new RegExp(`(?=${s})`, 'g');
-
(?=…)
は 先読みアサーション(後ろに続く文字列がs
である位置をマッチ)
- 先読みアサーションは文字を消さずにマッチ位置だけを検出できるので、重複して出現するパターンも正しく数えられる。
-
‘(?=${s})’
のように テンプレートリテラル を使うことで、変数 s を正規表現に組み込んでいる。
-
‘g’
フラグで 全体検索、マッチするすべての位置を取得。
3️⃣ マッチングしてカウント
const matches = t.match(regex);
console.log(matches ? matches.length : 0);
-
t.match(regex)
は、マッチした全ての位置の配列を返す。 -
matches.length
が出現回数。
- マッチがなければ
matches
はnull
になるので、三項演算子で0
を返している。
4️⃣ ポイント
-
先読みアサーションを使うことで、文字列を消さずに重なりも含めて数えられる
-
new RegExp
で動的に文字列を正規表現に組み込む -
.match()
の戻り値は配列、ない場合はnull
💡先読みアサーションとは
- 正規表現の中で
(?=pattern)
の形で使う。
-
「この位置の後ろに
pattern
があるかを確認する」という意味。 -
実際には文字を消費しない(マッチしても文字列自体は消えない)。
🔍 ポイント
-
普通の正規表現はマッチすると文字を「読んだ」とみなす。
-
先読みは文字を読まずに「ここから
pattern
が続いているか」をチェックするだけ。 -
だから重なっているパターンも正しく数えられる。
🔍 例
const t = "AAAA";
const regex = /(?=AA)/g;
const matches = t.match(regex);
console.log(matches.length); // 3
解説:
- t = “AAAA” の中で “AA” は重なりながら出現している:
- 1文字目から: “AA”
- 2文字目から: “AA”
- 3文字目から: “AA”
-
📌 通常の
/AA/g
だとマッチした“AA”
の文字を消費するので 2回しかカウントできない。 -
先読み
(?=AA)
は文字を消費しないので、すべての開始位置をカウントできる →3
🗒️ まとめ
-
/…/
の中では${…}
は変数展開されない
- 変数を正規表現に組み込みたい場合は
new RegExp(s, “g”)
のように書く。
- 例えば:
const regex = new RegExp(`(?=${s})`, "g");
const matches = t.match(regex);
これなら s
の値が反映され、正しくマッチングできる
- 先読みアサーション
(?=pattern)
は「この位置の後ろにpattern
があるかチェックするが文字は消費しない」ので、重なった部分も含めてマッチを数えられる。
おまけ:正規表現を使わない
const fs = require("fs");
// 入力を読み込み、改行で分割して s と t に代入
const input = fs.readFileSync("/dev/stdin", "utf-8").trim();
const [s, t] = input.split("\n");
let count = 0;
// t の先頭から順に s と同じ長さの部分文字列を比較
for (let i = 0; i + s.length <= t.length; i++) {
if (t.substr(i, s.length) === s) {
count++;
}
}
// 出現回数を出力
console.log(count);