【アルゴリズム】ワイルドカードを実装してみました

ワイルドカードと正規表現はどちらも検索や置換でよく使われている馴染みのある[**形式言語**](https://ja.wikipedia.org/wiki/%E5%BD%A2%E5%BC%8F%E8%A8%80%E8%AA%9E)です。
ワイルドカードは正規表現より弱い(表現力低い)ことが知られています。下図のように完全包含関係です。
![Picture1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120601/6cd1088a-7afc-4fe5-bf91-93b501565ff5.png)
実装難易度と応用場面の広さのトレードオフになりますが、どちらでもよく使われています。

ワイルドカード実用例 (shellでのファイルマッチング)

```bash
ls *.log
```

正規表現の実用例 (メールアドレス Python [from here](r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)") )

```py
r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
```

今回は簡単なほうのワイルドカードのマッチング判定ロジック`isMatch()`を自分の手で実装してみました。

サポートするパターン記号は *(簡単な実装なので、Unicode対応やエスケープ対応などは不要)*

- `?` 任意の1文字(char)とマッチ
- `*` 任意長さ(0も含む)の文字列(string)とマッチ
- `その他` 文字そのまま(文面)とマッチ

**例**
- 文字列 `"aa"` パターン `"?a"` 結果: ⭕ マッチ
- 文字列 `"aa"` パターン `"*"`  結果: ⭕マッチ
- 空文字列 `""` パターン `"**"` 結果: ⭕マッチ
- 文字列 `"aa"` パターン `"a"`  結果: ❌アンマッチ
- 文字列 `"aa"` パターン `"b*"` 結果: ❌ アンマッチ

厳密の問題定義は以下にもご参考ください。
(コード提出可能なので、回答を見る前にまずチャレンジしてみたほうが良い)
[LeetCode](https://leetcode.com/problems/wildcard-matching/)
[GeeksForGeeks](https://practice.geeksforgeeks.org/problems/wildcard-pattern-matching/1)

### TL;DR

最近Goでアルゴリズムを書くのに嵌っています。Goで実装を書いてみました。

## 問題全体的な思考
`*`を考慮しなければ、マッチした文字列とパターンは長さが一致するはずです。
そして、両者はきれいに1文字対1文字で単純にチェックすれば解決です。
`*`の効果を含めて考えれば、文字列とパターンのマッチングの場所は不確定になり、マッチング対応関係が何通りもできる場合もあります。

以下の例には、文字列s=`aabb`、パターンp=`*a*b*`のマッチング対応は4通りあります。
(対応関係を表すために空白を入れています。スペース文字と関係はありません。)

```
s:   a a b b

p: * a * b *
p: * a *   b *
p: *   a*b *
p: *   a * b *
```

## 実装方法1 (再帰を使って深さ優先探索)
まずは愚直的に再帰で実装してみました。

以下の例は`a*b` => `ab` `a?b` `a??b` `a???b` ...の展開を示す分岐ツリーです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120601/b3878d46-a10b-64b1-a690-c54d9b39e60d.png)

1. `*`が0文字とマッチする (↑のツリー図では、左に行く)場合
1. `*`が1以上の文字とマッチする(↑のツリー図では、右に行く)場合

2.の場合なら、再帰で判断します。

実装コードは以下になります。

```go
func isMatch(s string, p string) bool {
	for p != "" {
		if p[0] == '*' {
			// case when * matches 0 charactor
			if isMatch(s, p[1:]) {
				return true
			}
			// case when * matches 1 or more charactor
			if s == "" {
				return false
			}
			s = s[1:]
			continue
		}

		if s == "" || p[0] != '?' && p[0] != s[0] {
			return false
		}

		p, s = p[1:], s[1:]
	}
	// when p == ""
	return s == ""
}
```

簡単なインプットで試して、予想通りな結果が出ましたが、LeetCodeに提出したら、性能悪すぎて、TimeLimitExceedになりました。

最初に落ちたケースは以下です。

```ini
s="bbbababbbbabbbbababbaaabbaababbbaabbbaaaabbbaaaabb"
p="*b********bb*b*bbbbb*ba"
```

ワイルドカードの定義から見ると、連続のアスター`********`は、意味上は1個の`*`と一緒ですね。
複数の連続`*`を1個にまとめる事前処理を入れてみました。

```go
import "strings"

func simplify(p string) string {
	var sb strings.Builder
	isLastStar := false
	for _, ch := range p {
		if ch == '*' {
			if isLastStar {
				continue
			}
			isLastStar = true
		} else {
			isLastStar = false
		}
		sb.WriteRune(ch)
	}

	return sb.String()
}

func isMatchHelper(s string, p string) bool {
	// もともとのisMatchの実装
}

func isMatch(s string, p string) bool {
	return isMatchHelper(s, simplify(p))
}

```

結果は、もっと多くのケースが通りましたが、まだTLE(Time Limit Exceeded)エラーですべて通らなかったです。
今回は最初に落ちたケースは以下です。

```ini
s="abbabaaabbabbaababbabbbbbabbbabbbabaaaaababababbbabababaabbababaabbbbbbaaaabababbbaabbbbaabbbbababababbaabbaababaabbbababababbbbaaabbbbbabaaaabbababbbbaababaabbababbbbbababbbabaaaaaaaabbbbbaabaaababaaaabb"
p="**aa*****ba*a*bb**aa*ab****a*aaaaaa***a*aaaa**bbabb*b*b**aaaaaaaaa*a********ba*bbb***a*ba*bb*bb**a*b*bb"
simplified_p="*aa*ba*a*bb*aa*ab*a*aaaaaa*a*aaaa*bbabb*b*b*aaaaaaaaa*a*ba*bbb*a*ba*bb*bb*a*b*bb"
```

ローカルで試しても、おおよそ7グループ以上の`*`が存在すれば、1秒〜無限長がかかってしまいました。
やはり、大きいインプットに対して再帰での実装が無力でした。

次はDPで書き直してみました。

## 実装方法2 (DP 動的計画法)

2次元の計算表`dpt[len(s)+1][len(p)+1]`を順番に埋めていく手順を踏まえて、1マスを定数時間で計算できれば、全体的に`O (len(s+1) * len(p+1))`の計算量(時間も空間も)で解けますね。

`dpt[i][j]`はBoolean値で、その意味を「小規模のサブ問題`s[0:i]`と`p[0:j]`がマッチするか」とします。

まずは、些細なケースから埋めていきます。
明らかに、空文字と空パターンがマッチするので、`dpt[0][0] = true`
空ではない文字と空パターンが絶対マッチしないので、`dpt[i][0] = false`, 1 <= i <= len(s) 
そして、空文字とパターン`p[0:j]`のマッチング結果は、パターン最後の文字`p[j-1]`が`*`かどうかで判断します。
このDPの遷移ルールは以下となります。

```go
// 1 <= j <= len(p)
dpt[0][j] = dpt[0][j] = dpt[0][j-1] // when p[j-1] == "*"
dpt[0][j] = false // otherwise
```

実装コードは以下です。

```go
func isMatchDP(s string, p string) bool {
	dpt := make([][]bool, len(s)+1)
	for i := range dpt {
		dpt[i] = make([]bool, len(p)+1)
	}
	dpt[0][0] = true
	for j := 1; j <= len(p); j++ {
		if p[j-1] == '*' {
			dpt[0][j] = dpt[0][j-1]
		}
	}
	for i := 1; i <= len(s); i++ {
		for j := 1; j <= len(p); j++ {
			switch p[j-1] {
			case '?':
				fallthrough
			case s[i-1]:
				dpt[i][j] = dpt[i-1][j-1]
			case '*':
				dpt[i][j] = dpt[i-1][j] || dpt[i][j-1]
			default:
				dpt[i][j] = false
			}
		}
	}
	return dpt[len(s)][len(p)]
}
```

`dpt`を埋めていく中で、当行は1行前まで参照するので、メモリ上は常に当行と前行の2行のみ保持しても問題ありません。
そうすると、空間計算量 `O(len(p))`まで削減できます。

もっと最適化すると、空間計算量定数`O(1)`のやり方もあります。

## 実装方法3 (4つのポインタ)

文字列`s`とパターン`p`それぞれの現在処理位置のポインタ`i`と`j`以外に、もう2つのポインタ`asterI`と`asterJ`を使うことによって、定数空間計算量での実装が可能です。

アルゴリズム概要は以下です。

- `asterI`は現在処理中の`s`の中の`*`にマッチする最新の場所を指します。
`asterJ`は現在処理中の`p`の中の`*`を指します。
- `*`ではない場合は、単純比較で、マッチしたらポインタ`i`と`j`ともに次に移動します。
- `*`の場合、`asterI`と`asterJ`を現在の場所に移動します。
- `*`ではないし単純比較もマッチしない場合は、`i`と`j`をそれぞれ`asterI`と`asterJ`の次の場所に移動します。

ソースは以下になります。

```go
func isMatchConstSpace(s string, p string) bool {
	if p == "" {
		return s == ""
	}

	var i, j = 0, 0
	var asterI, asterJ = -1, -1

	for i < len(s) {
		if j < len(p) && (p[j] == '?' || s[i] == p[j]) {
			i++
			j++
		} else if j < len(p) && p[j] == '*' {
			asterI, asterJ = i, j
			j++
		} else if asterJ != -1 {
			i, j = asterI+1, asterJ+1
			asterI++
		} else {
			return false
		}
	}

	for ; j < len(p); j++ {
		if p[j] != '*' {
			return false
		}
	}
	return true
}
```

参考元は [ここ](https://www.geeksforgeeks.org/dynamic-programming-wildcard-pattern-matching-linear-time-constant-space/) ですが、この記事最後に、時間計算量は線形`O(len(p))`と書いてありますが、実は`i`,`j`ポインタの移動は単一方向ではなく、往復があるので、最悪の場合は`O(len(s)*len(p))`でしょうね。
確かに、補助のメモリを使っていないので、空間計算量は定数です。

本当に思いつきにくい方法でした。(^o^;)