Bitap Algorithmを理解する (using asearch)
自分で作ったコードをcode readingするってどゆこと?と思うかもしれないが、takker.iconはnode-asearchのコードを移植しただけでアルゴリズムの仕組みは全く理解していないので、code readingする必要がある 用語を決めておきたい
「ビットパターン」だの「変数」だの「状態」だのを適当に使ったため、どれがどれを示しているのかよくわからなくなってきた
なるほどtakker.icon
各種初期値の定義
32bit目だけを立てたビットパターン
code:mod.ts
const INITPAT = 0x80000000;
code:mod.ts
const MAXCHAR = 0x10000;
大文字小文字変換
code:mod.ts
const isupper = (c: number) => (c >= 0x41) && (c <= 0x5a);
const islower = (c: number) => (c >= 0x61) && (c <= 0x7a);
const tolower = (c: number) => isupper(c) ? (c + 0x20) : c;
const toupper = (c: number) => islower(c) ? (c - 0x20) : c;
各種初期値の定義その2
wildCardに指定した文字をwild card*として解釈する
code:mod.ts
const wildCard = 0x20;
code:mod.ts
export function Asearch(source: string) {
前処理
遷移可能な場所を記録する
表現方法
それぞれの文字に対応するビットパターン
bitが立っている場所でそれらの文字が来ることを表す
例:検索文字列の2,5,8文字目にaが来る場合、対応する整数は2進数で01001001になる
ここで使用するビットパターンの最大ビット長で、検索可能な最大文字数が決まる
全ての文字に対応するために、任意の文字コードからビットパターンに写す写像を作成する必要がある
本実装では予め全ての文字コードごとにビットパターンを用意している
これは検索に使わない文字のビットパターンも用意しなければならないので、かなりメモリ効率が悪いと思われる
後ほどHash mapMapを使った方法に切り替えるつもり
もちろんベンチマークをとって実際に速くなったかどうかを調べる
疎行列のデータ表現方法に、この手のメモリを節約する方法がないかな?takker.icon code:mod.ts
const shiftpat = new Float64Array(MAXCHAR);
let epsilon = 0;
code:mod.ts
let mask = INITPAT;
for (const i of unpack(source)) {
if (i === wildCard) {
ワイルドカードの場合は別途用意したビットパターンepsilonに記録する
またwild cardの場合はmaskのビットの場所を進めない
これはなぜだろう?takker.icon
code:mod.ts
epsilon |= mask;
} else {
文字の場所をビットパターンに記録する
ここをいじれば、いろんな同一視方法を組み込める
e.g.
ひらがなカタカナ無視
全角半角無視
見た目が似ている文字の違いを無視
𝟙と1とか
|= maskで、mask内で立っている全てのbitを転写できる
今回の場合は1本しか立っていないが
code:mod.ts
mask >>>= 1;
}
}
acceptpatが状態遷移機械の受理状態を表す
sourceからwild cardを除いた文字列の長さと同じ位置にビットが立っている
code:mod.ts
const acceptpat = mask;
それ以上は全ての変数が未受理になるので計測できない
もちろんその分計算量が増える
code:mod.ts
function getState(str = "") {
NFAの初期化
code:initial_state
i3 0000 0000 0000 0000 0000 0000 0000 0000
i2 0000 0000 0000 0000 0000 0000 0000 0000
i1 0000 0000 0000 0000 0000 0000 0000 0000
i0 1000 0000 0000 0000 0000 0000 0000 0000
code:mod.ts
被検索文字列の全ての文字に対して機械を動かしている
全部なめる必要なくない?takker.icon
変数のどれかが受理状態になった時点で遷移を打ち切っていいのでは
ビット演算の意味を考える
i0 = (i0 & epsilon) | ((i0 & mask) >>> 1)
前のi0から新しいi0を作っている
新しいi0に以下の場所を書き込んでいる
前のi0のうちwildcardの場所と一致する場所((i0 & epsilon))
前のi0のうち文字コードcに該当する文字の検索文字列中の出現箇所と一致する場所((i0 & mask))を右に1つずらしたもの((i0 & mask) >>> 1)
1つ下の状態を上の状態に転写している
うーん、適当な文字で1stepごとに具体化して調べないと、何をやっているのかわからなそうだなtakker.icon
適当な短い文字で試してみるか
wild cardありとwild cardなしとで試したい
ちょっとわかった
例:
sourceの1文字目がaだった時
strの1文字目がaのとき
mask:0b1...
i0:0b1...
初回のループなのでi0の先頭ビットも1
よって(i0 & mask) >>> 1は0b01...となる
これが文字を受理して状態遷移したということ
strの1文字目がaでないとき
(i0 & mask) >>> 1は0b00...となる
maskが0b0xxxxなのでi0の先頭ビットが削られる
この時削られたi0のビット情報自体はi1に転記されているので消えはしない
sourceの1文字目が (wild card)だった時
どんな文字が来ても(i0 & epsilon)によってi0の先頭ビットは常に1に保たれる
部分一致検索できる原理はこれだなtakker.icon
code:mod.ts
for (const c of unpack(str)) {
i3 = (i3 & epsilon) | ((i3 & mask) >>> 1) | (i2 >>> 1) | i2;
i2 = (i2 & epsilon) | ((i2 & mask) >>> 1) | (i1 >>> 1) | i1;
i1 = (i1 & epsilon) | ((i1 & mask) >>> 1) | (i0 >>> 1) | i0;
i0 = (i0 & epsilon) | ((i0 & mask) >>> 1);
i1 |= (i0 >>> 1);
i2 |= (i1 >>> 1);
i3 |= (i2 >>> 1);
}
}
一致判定を行う函数
code:mod.ts
function test(str: string, distance = 0) {
const state = getState(str);
distance = Math.min(INITSTATE.length - 1, distance);
return (statedistance & acceptpat) !== 0; }
検索文字列とどのくらい一致したかがわかるので、検索結果の並び替えに使える
code:mod.ts
function match(str: string) {
const state = getState(str);
return { found: false };
}
const distance = state.findIndex((i) => (i & acceptpat) !== 0);
return { found: true, distance };
}
code:mod.ts
return {
test,
match,
source,
};
}
しかしそれをやるにはMAXCHARを0x10000より大きくする必要があるが、容易にはできない
shiftpatのメモリ消費量が大変なことになる
code:mod.ts
function* unpack(str: string) {
for (const char of str) {
yield char.charCodeAt(0);
}
}