开源地址

为了便于大家学习,所有实现均已开源。欢迎 fork + star~

https://github.com/houbb/leetcode

题目

题目描述 给你一个字符串 s ,请你返回满足以下条件的最长子字符串的长度:每个元音字母,即 ‘a’,’e’,’i’,’o’,’u’ ,在子字符串中都恰好出现了偶数次。

示例 1:

输入:s = “eleetminicoworoep” 输出:13 解释:最长子字符串是 “leetminicowor” ,它包含 e,i,o 各 2 个,以及 0 个 a,u 。 示例 2:

输入:s = “leetcodeisgreat” 输出:5 解释:最长子字符串是 “leetc” ,其中包含 2 个 e 。 示例 3:

输入:s = “bcbcbc” 输出:6 解释:这个示例中,字符串 “bcbcbc” 本身就是最长的,因为所有的元音 a,e,i,o,u 都出现了 0 次。

提示:

1 <= s.length <= 5 x 10^5 s 只包含小写英文字母。

v1-基本前缀和+HashMap-错误的思路

思路

1) 构建好前缀好,方便计算子数组的和,是否为 k

2)如何能做到让元音字母相同呢?

一开始的思考是非元音字母直接为0,以 a 为例子,奇数和偶数分别对应 1 -1

但是缺点是不同的元音会冲突。

所以想到了拆分为 5 个数组,当然会导致代码特别冗余。

初步实现

  [java]
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/** * 构建对应的数组 * * 子数组:所以使用前缀和来解决? * @param s * @return */ public int findTheLongestSubstring(String s) { // 如果是拆分为5个数组呢? final int len = s.length(); char[] chars = s.toCharArray(); int[] aSum = initPrefixSum(chars, 'a'); int[] eSum = initPrefixSum(chars, 'e'); int[] iSum = initPrefixSum(chars, 'i'); int[] oSum = initPrefixSum(chars, 'o'); int[] uSum = initPrefixSum(chars, 'u'); // 找到最长的为0的数组 Map<Integer, Integer> countMapA = new HashMap<>(); Map<Integer, Integer> countMapE = new HashMap<>(); Map<Integer, Integer> countMapI = new HashMap<>(); Map<Integer, Integer> countMapO = new HashMap<>(); Map<Integer, Integer> countMapU = new HashMap<>(); // 兼容从零开始的情况 countMapA.put(0, -1); countMapE.put(0, -1); countMapI.put(0, -1); countMapO.put(0, -1); countMapU.put(0, -1); int maxLen = 0; for(int i = 0; i < len; i++) { int sumA = aSum[i]; int sumE = eSum[i]; int sumI = iSum[i]; int sumO = oSum[i]; int sumU = uSum[i]; if(countMapA.containsKey(sumA) && countMapE.containsKey(sumE) && countMapI.containsKey(sumI) && countMapO.containsKey(sumO) && countMapU.containsKey(sumU) ) { // 必须对应的前缀相同? int startIxA = countMapA.get(sumA); int startIxE = countMapE.get(sumE); int startIxI = countMapI.get(sumI); int startIxO = countMapO.get(sumO); int startIxU = countMapU.get(sumU); if (startIxA == startIxE && startIxE == startIxI && startIxI == startIxO && startIxO == startIxU ) { maxLen = Math.max(maxLen, i - startIxA); } } // 更新下 countMapA.put(sumA, i); countMapE.put(sumE, i); countMapI.put(sumI, i); countMapO.put(sumO, i); countMapU.put(sumU, i); } return maxLen; } private int[] initPrefixSum(char[] chars, char targetChar) { final int n = chars.length; int[] prefixSum = new int[n]; Map<Character, Integer> charCount = new HashMap<>(); prefixSum[0] = getCharVal(chars[0], targetChar, charCount); for(int i = 1; i < n; i++) { prefixSum[i] = prefixSum[i-1] + getCharVal(chars[i], targetChar, charCount); } return prefixSum; } private int getCharVal(char c, char targetChar, Map<Character, Integer> charCount) { int count = charCount.getOrDefault(c, 0); count++; charCount.put(c, count); if(c != targetChar) { return 0; } // 让元音字母成对出现 if(count % 2 == 0) { return 1; } return -1; }

效果

这个是错误的,以后有时间,做一下处理优化。

TBC

V2-问题出在哪里了?

思路

最大的问题在于5个元素的压缩不能这么做。

比如很多答案中有位运算,个人不是很喜欢这种。平时不太常用,很难想到。

这一题最大的难点就在于属性压缩。

方便理解的思路

因为一共 5 个元音,出现的次数分为奇偶两个状态。所以需要 2^5=32 位的状态来标识。

有很多种方式,说两种方便理解的:

1)”00000” 5位的字符串

2)int[] 5 位的数组。

0 标识偶数次,1代表奇数次。

实现

  [java]
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class Solution { private void toggleState(int[] states, int index) { states[index] = states[index] == 1 ? 0 : 1; } /** * 构建对应的数组 * * 子数组:所以使用前缀和来解决? * @param s * @return */ public int findTheLongestSubstring(String s) { // 如果是拆分为5个数组呢? final int len = s.length(); char[] chars = s.toCharArray(); // 状态数组 int[] states = new int[5]; Map<String, Integer> map = new HashMap<>(); // 初始化 从零开始的场景 map.put(Arrays.toString(states), -1); int maxLen = 0; for(int i = 0; i < len; i++) { char c = chars[i]; if(c == 'a') { toggleState(states, 0); }else if(c == 'e') { toggleState(states, 1); }else if(c == 'i') { toggleState(states, 2); }else if(c == 'o') { toggleState(states, 3); }else if(c == 'u') { toggleState(states, 4); } // 如果已经有 说明差值相同 String stateString = Arrays.toString(states); if(map.containsKey(stateString)) { maxLen = Math.max(maxLen, i - map.get(stateString)); } else { // 记录状态 map.put(stateString, i); } } return maxLen; } }

效果

  [plaintext]
1
338ms 6.67%

性能比较差,为什么比较差呢?

v3-性能优化

思路

下面的方法比较难想到一些,就是借助位运算提升性能。

直接用一下官方解法。

代码

  [java]
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
public int findTheLongestSubstring(String s) { int n = s.length(); int[] pos = new int[1 << 5]; Arrays.fill(pos, -1); int ans = 0, status = 0; pos[0] = 0; for (int i = 0; i < n; i++) { // 直接通过位运算,更新对应的数据。 char ch = s.charAt(i); if (ch == 'a') { status ^= (1 << 0); } else if (ch == 'e') { status ^= (1 << 1); } else if (ch == 'i') { status ^= (1 << 2); } else if (ch == 'o') { status ^= (1 << 3); } else if (ch == 'u') { status ^= (1 << 4); } if (pos[status] >= 0) { ans = Math.max(ans, i + 1 - pos[status]); } else { pos[status] = i + 1; } } return ans; }

效果

11ms 97.87%

参考资料

https://leetcode.cn/problems/find-the-longest-substring-containing-vowels-in-even-counts/submissions/579064348/?envType=problem-list-v2&envId=prefix-sum