Permutation in String

# 問題

2つの文字列s1s2が与えられる.s1の順列がs2の部分文字列として含まれるか判定せよ.

# 解法1

  • Blute-force
  • s1の順列を全部調べ上げる
  • 時間計算量:$O(l_1 !)$
    • ただし$l_1$はs1の長さ
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Solution:
    def checkInclusion(self, s1: str, s2: str) -> bool:
        flag = False

        def permutation(s, sofar):
            nonlocal flag
            if len(s) == 0:
                if sofar in s2:
                    flag = True
                return
            for i in range(len(s)):
                permutation(s[:i] + s[i+1:], sofar + s[i])

        permutation(s1, "")
        return flag

# 解法2

  • s1の順列がs2の部分文字列として含まれるか判定せよ.」なので,実際にs2に含まれる順列がどの順列なのかは不要
  • 要するに「s1と同じ文字種・同じ個数で構成される」s2の部分文字列があるかを調べれば良い
  • s1をソートしたのとs2の部分列をソートしたのが一致すれば十分
  • 時間計算量:$O(l_1 \log l_1 + (l_2 - l_1) l_1 \log l_1)$
    • $l_1 \log l_1$:s1のソート
    • $(l_2 - l_1) l_1 \log l_1$:長さlen(s1)の部分列がlen(s2) - len(s1)個存在してそれぞれソートされる
1
2
3
4
5
6
7
class Solution:
    def checkInclusion(self, s1: str, s2: str) -> bool:
        s1 = "".join(sorted(s1))
        for i in range(len(s2) - len(s1) + 1):
            if s1 == "".join(sorted(s2[i:i+len(s1)])):
                return True
        return False

# 解法3

  • 解法2と同じ発想で別のやり方
  • s1と同じ文字種が同じ個数存在するか」を辞書の比較で実現
  • 時間計算量:$O(l_1 + (l_2 - l_1) l_1 )$
    • $l_1$:s1に含まれる文字種がそれぞれ何個存在するかの辞書を作る
    • $(l_2 - l_1) l_1$:len(s2) - len(s1)個の部分文字列のそれぞれについて辞書を比較
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
    def checkInclusion(self, s1: str, s2: str) -> bool:
        if len(s1) > len(s2):
            return False

        # 辞書作る
        cnt1 = Counter(s1)
        for i in range(len(s2) - len(s1) + 1):
            # 辞書作る
            cnt2 = Counter()
            for j in range(len(s1)):
                if s2[i + j] in cnt2:
                    cnt2[s2[i + j]] += 1
                else:
                    cnt2[s2[i + j]] = 1

            # 辞書同士の比較
            if cnt1 == cnt2:
                return True
        return False

# 解法4

  • 解法3を辞書じゃなくてkeyを位置で代用した配列でやる
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution:
    def checkInclusion(self, s1: str, s2: str) -> bool:
        if len(s1) > len(s2):
            return False

        cnt1 = [0 for _ in range(26)]
        for ch in s1:
            cnt1[ord(ch) - ord('a')] += 1

        for i in range(len(s2) - len(s1) + 1):
            cnt2 = [0 for _ in range(26)]
            for ch in s2[i:i+len(s1)]:
                cnt2[ord(ch) - ord('a')] += 1

            flag = True
            for i in range(26):
                if cnt1[i] != cnt2[i]:
                    flag = False
                    break
            if flag:
                return True
        return False

# 解法5

  • 解法4を改良する
  • len(s2) - len(s1)個ある部分文字列のそれぞれに対して,ゼロから辞書を毎回作るのは無駄では?
    • 一文字部分文字列がズレるときの辞書は,部分文字列から消える文字が何で,部分文字列に新しく加わる文字が何かを追えば差分を更新するだけで作れる
  • 時間計算量:$O(l_1 + (l_2 - l_1) * 26)$
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution:
    def checkInclusion(self, s1: str, s2: str) -> bool:
        if len(s1) > len(s2):
            return False

        cnt1 = [0 for _ in range(26)]
        cnt2 = [0 for _ in range(26)]
        for i in range(len(s1)):
            ch = s1[i]
            cnt1[ord(ch) - ord('a')] += 1
            ch = s2[i]
            cnt2[ord(ch) - ord('a')] += 1

        def match(cnt1, cnt2):
            for i in range(26):
                if cnt1[i] != cnt2[i]:
                    return False
            return True

        for i in range(len(s2) - len(s1)):
            if match(cnt1, cnt2):
                return True

            left = s2[i]
            right = s2[i + len(s1)]
            cnt2[ord(left) - ord('a')] -= 1
            cnt2[ord(right) - ord('a')] += 1

        return match(cnt1, cnt2)

# 解法5

  • 解法4を改良する
  • match(cnt1, cnt2)をいい感じにしたい
  • 個数が一致する文字種の数が一致するかを部分文字列の差分から計算できれば,辞書のすべての要素について個数をチェックするループが消せそう
    • 定数倍計算量が削減できそう
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Solution:
    def checkInclusion(self, s1: str, s2: str) -> bool:
        if len(s1) > len(s2):
            return False

        cnt1 = [0 for _ in range(26)]
        cnt2 = [0 for _ in range(26)]
        for i in range(len(s1)):
            ch = s1[i]
            cnt1[ord(ch) - ord('a')] += 1
            ch = s2[i]
            cnt2[ord(ch) - ord('a')] += 1

        matched = 0 # 個数が一致する文字種の数
        for i in range(26):
            if cnt1[i] == cnt2[i]:
                matched += 1

        for i in range(len(s2) - len(s1)):
            left = ord(s2[i]) - ord('a')
            right = ord(s2[i + len(s1)]) - ord('a')

            if matched == 26:
                return True

            cnt2[left] -= 1 # 次のiで左端の文字は部分文字列から消える
            if cnt2[left] == cnt1[left]:
                matched += 1 # 左端の文字が部分文字列から消えた結果,個数が一致した文字種が増えた
            elif cnt2[left] + 1 == cnt1[left]:
                matched -= 1 # 左端の文字が部分文字列から消えた結果,個数が一致した文字種が減った

            cnt2[right] += 1 # 次のiで右端の文字は部分文字列に追加される
            if cnt2[right] == cnt1[right]:
                matched += 1 # 右端の文字が部分文字列に追加された結果,個数が一致した文字種が増えた
            elif cnt2[right] - 1 == cnt1[right]:
                matched -= 1 # 右端の文字が部分文字列に追加された結果,個数が一致した文字種が減った

        return matched == 26

# ref

https://leetcode.com/problems/permutation-in-string/

Hugo で構築されています。
テーマ StackJimmy によって設計されています。