Long Luo's Life Notes

每一天都是奇迹

By Long Luo

Leetcode 1044. 最长重复子串 题目如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1044. 最长重复子串

给你一个字符串s,考虑其所有重复子串:即,s的连续子串,在s中出现2次或更多次。这些出现之间可能存在重叠。

返回**任意一个**可能具有最长长度的重复子串。如果s不含重复子串,那么答案为""。


示例 1:
输入:s = "banana"
输出:"ana"

示例 2:
输入:s = "abcd"
输出:""


提示:
2 <= s.length <= 3 * 10^4
s由小写英文字母组成

这是一道Hard难度的题,我们将展示这道题的几种解决方法:

方法一:从暴力到二分

思路与算法:

首先想到的是暴力法,用一个哈希表存储每个字母的出现次数,那么答案字符串的首字母至少出现 \(2\) 次,中间字符至少出现一次。

  1. 第一层循环 \(0 < i < len - 1\),遍历整个字符串;
  2. 第二层循环,子字符串长度,\(i < j < len - i\)
  3. 第三层循环,判断第二层循环的字符串在后续字符串中是否出现。

于是写出了下列Ver 1的代码:

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
public String longestDupSubstring(String s) {
if (s == null || s.length() <= 1) {
return "";
}

Map<Character, Integer> freq = new HashMap<>();
for (char ch : s.toCharArray()) {
freq.put(ch, freq.getOrDefault(ch, 0) + 1);
}
StringBuilder sb = new StringBuilder();
int len = s.length();
int maxLen = 0;
String ans = "";
for (int i = 0; i < len; i++) {
sb.delete(0, sb.length());
char ch = s.charAt(i);
if (freq.getOrDefault(ch, 0) <= 1) {
continue;
}
for (int j = i; j < len - sb.length(); j++) {
sb.append(s.charAt(j));
for (int k = i + 1; k < len; k++) {
String str = s.substring(k, len);
if (str.contains(sb.toString())) {
if (sb.length() >= maxLen) {
ans = sb.toString();
}
maxLen = Math.max(maxLen, sb.length());
break;
}
}
}
}

return ans;
}

上述代码,时间复杂度为 \(O(n^4)\),会超时

通过观察,发现Ver 1代码有很多改进地方,于是更新到Ver 2

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
// BF Opt time: O(n^3) space: O(n)
// Timeout
public static String longestDupSubstring_bf_opt(String s) {
int len = s.length();
int[] cnt = new int[26];
for (char ch : s.toCharArray()) {
cnt[ch - 'a']++;
}

int maxLen = 0;
String ans = "";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < len - 1; i++) {
char ch = s.charAt(i);
if (cnt[ch - 'a'] <= 1) {
continue;
}

for (int segLen = len - i; segLen > maxLen; segLen--) {
sb.delete(0, sb.length());
sb.append(s, i, i + segLen);
String str = s.substring(i + 1, len);
if (str.contains(sb.toString()) && sb.length() > maxLen) {
ans = sb.toString();
maxLen = Math.max(maxLen, sb.length());
break;
}
}
}

return ans;
}

Ver 2时间复杂度降低到 \(O(n^3)\),但仍然会超时,所以还需要优化!

很明显,题目要求的子字符串的长度范围为 \([0, n - 1]\),那么枚举每个位置作为起点,二分搜索以找到最大长度,于是有了下面的Ver 3代码:

阅读全文 »

By Long Luo

Leetcode 686. 重复叠加字符串匹配 题目如下:

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
686. 重复叠加字符串匹配

给定两个字符串a和b,寻找重复叠加字符串a的最小次数,使得字符串b成为叠加后的字符串a的子串,如果不存在则返回-1。

注意:字符串"abc"重复叠加0次是"",重复叠加1次是"abc",重复叠加2次是"abcabc"。

示例 1:
输入:a = "abcd", b = "cdabcdab"
输出:3
解释:a 重复叠加三遍后为"ab**cdabcdab**cd", 此时b是其子串。

示例 2:
输入:a = "a", b = "aa"
输出:2

示例 3:
输入:a = "a", b = "a"
输出:1

示例 4:
输入:a = "abc", b = "wxyz"
输出:-1

提示:
1 <= a.length <= 10^4
1 <= b.length <= 10^4
a 和 b 由小写英文字母组成

分析

可以分为下面\(3\)种情况:

  1. \(\textit{A}\) ==\(\textit{B}\),复制 \(1\) 次;

  2. \(lenA > lenB\),直觉是只需要复制 \(2\) 次;

  3. \(lenA < lenB\),首先至少将 \(\textit{A}\) 复制几次后长度 \(>=lenB\),然后至多再复制 \(\textit{A}\) 一次。

下面简单证明下,也就是复制次数的下限上限

  1. 至少将字符串 \(\textit{A}\) 复制 \(N\) 次之后主串 \(\textit{S}\),那么 \(lenS >= lenB\) ,才有可能匹配,至此我们得到了复制次数的下限\(\frac{lenB}{lenA}+1\)

  2. 如果从主串 \(\textit{S}\) 中找到子串 \(\textit{B}\),因此可以明确子串的起始位置,不会超过 \(lenA\);因为如果起始匹配位置 \(>= lenA\),那么说明在之前已经被匹配过了。

综上分析可知:

\[ \frac{lenB}{lenA} + 1 \le repeatedTimes \le \frac{lenB}{lenA} + 2 \]

方法一:暴力

思路与算法:

首先想到的是暴力法。最开始没有去分析复制次数上下限时,使用了一个朴素的想法:

  1. \(\textit{B}\) 中含有 \(\textit{A}\) 中没有的字母,那么直接返回 \(-1\)
  2. 最少复制次数:\(\textit{B}\) 中字母出现次数 \(\textit{A}\) 中字母次数的最大值。

使用 \(\texttt{HashMap}\) 存储字母出现次数。

代码如下所示:

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
public int repeatedStringMatch(String a, String b) {
if (a.equals(b) || a.contains(b)) {
return 1;
}

int min = 1;
Map<Character, Integer> mapA = new HashMap<>();
Map<Character, Integer> mapB = new HashMap<>();
for (char ch : a.toCharArray()) {
mapA.put(ch, mapA.getOrDefault(ch, 0) + 1);
}
for (char ch : b.toCharArray()) {
mapB.put(ch, mapB.getOrDefault(ch, 0) + 1);
}

for (char ch : mapB.keySet()) {
if (!mapA.containsKey(ch)) {
return -1;
} else {
int times = mapB.get(ch) / mapA.get(ch);
min = Math.max(min, times);
}
}

StringBuilder res = new StringBuilder(a);
for (int i = 2; i <= min + 1; i++) {
res.append(a);
if (res.toString().contains(b)) {
return i;
}
}

return -1;
}

因为字符串均由小写英文字母组成,所以可以直接用数组进行计数,速度提高了很多。

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
public int repeatedStringMatch(String a, String b) {
if (a.equals(b) || a.contains(b)) {
return 1;
}

int[] cntA = new int[26];
int[] cntB = new int[26];

for (char ch : a.toCharArray()) {
cntA[ch - 'a']++;
}
for (char ch : b.toCharArray()) {
cntB[ch - 'a']++;
}

int min = 1;

for (int i = 0; i < 26; i++) {
if (cntB[i] > 0 && cntA[i] == 0) {
return -1;
}

if (cntA[i] > 0) {
int times = cntB[i] / cntA[i];
min = Math.max(min, times);
}
}

StringBuilder res = new StringBuilder(a);
for (int i = 2; i <= min + 1; i++) {
res.append(a);
if (res.toString().contains(b)) {
return i;
}
}

return -1;
}

值得一提的是,如果不用比较 \(\textit{A}\), \(\textit{B}\) 字母的话,直接进行比较的话,那么耗时会成倍增加,所以预先排除一些test case有很大帮助。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int repeatedStringMatch(String a, String b) {
if (a.equals(b) || a.contains(b)) {
return 1;
}

int minRepeatTimes = Math.max(1, b.length() / a.length() + 1);

StringBuilder res = new StringBuilder(a);
for (int i = 2; i <= minRepeatTimes + 1; i++) {
res.append(a);
if (res.toString().contains(b)) {
return i;
}
}

return -1;
}

复杂度分析:

  • 时间复杂度:\(O(n + m + nm)\),其中 \(n\)\(\textit{A}\) 的长度,\(m\)\(\textit{B}\) 的长度。

  • 空间复杂度:\(O(26)\)

阅读全文 »

By Long Luo

Leetcode 997. 找到小镇的法官 题解。

方法一:图论

思路与算法:

  1. 如果存在法官,那么所有人都会信任法官,在结合条件\(1\),可以得出信任法官的人数为\(n - 1\)
  2. 如果不存在法官,那么也可能存在某些人被所有人信任,这个人的信任人数也为\(n - 1\),但是他也会信任别人
  3. 可以以此作为区分other和judge的条件,假设每个人都有信任值,那么定义一个数组长度为\(n\),用来存放\(n\)个人的信任值:
  • 如果一个人信任了别人,那么这个人的信任值\(-1\)
  • 如果一个人被别人信任,那么这个人的信任值\(+1\)

所以:

  1. 当一个人被所有人信任,并且他没有信任其它人时,这个人的信任值就是\(n - 1\),那么此人就是法官;
  2. 当一个人被所有人信任,但是他也信任了其他人时,这个人的信任值\(< n - 1\)。 其它情况下,每个人的信任值都会小于\(n - 1\)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public int findJudge(int N, int[][] trust) {
if (N <= 0) {
return -1;
} else if (N == 1) {
return 1;
}

// [0]: In [1]: Out
int[][] count = new int[N][2];
for (int[] item : trust) {
count[item[0] - 1][1]++;
count[item[1] - 1][0]++;
}

for (int i = 0; i < N; i++) {
if (count[i][0] == N - 1 && count[i][1] == 0) {
return i + 1;
}
}

return -1;
}
阅读全文 »

By Long Luo

Leetcode 419. 甲板上的战舰

方法一:一次遍历 + 不修改原始数组

一次扫描,\(O(m \times n)\) 额外空间,使用一个与矩阵同等大小的辅助数组 \(visited\) 记录访问过的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static int countBattleships(char[][] board) {
int ans = 0;
int m = board.length;
int n = board[0].length;
boolean[][] visited = new boolean[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (!visited[i][j] && board[i][j] == 'X') {
visited[i][j] = true;
ans++;

for (int k = i + 1; k < m && board[k][j] == 'X'; k++) {
visited[k][j] = true;
}

for (int k = j + 1; k < n && board[i][k] == 'X'; k++) {
visited[i][k] = true;
}
}
}
}

return ans;
}

复杂度分析:

  • 时间复杂度:\(O(m \times n \times \max(m, n))\)

  • 空间复杂度:\[O(m \times n)\]

方法二:一次遍历 + 修改原始数组

一次扫描,允许修改原始数组,遍历之后将原来的X修改为.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static int countBattleships(char[][] board) {
int ans = 0;
int m = board.length;
int n = board[0].length;
boolean[][] visited = new boolean[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (!visited[i][j] && board[i][j] == 'X') {
visited[i][j] = true;
ans++;

for (int k = i + 1; k < m && board[k][j] == 'X'; k++) {
visited[k][j] = true;
}

for (int k = j + 1; k < n && board[i][k] == 'X'; k++) {
visited[i][k] = true;
}
}
}
}

return ans;
}

复杂度分析:

  • 时间复杂度:\(O(m \times n \times \max(m, n))\)

  • 空间复杂度:\(O(1)\)

阅读全文 »

By Long Luo

Leetcode 1816. 截断句子 题解。

方法一:Regex

很简单,首先想到的是正则表达式,根据空格分割单词。

1
2
3
4
5
6
7
8
9
10
public String truncateSentence(String s, int k) {
String[] words = s.split("\\s+");
StringBuilder sb = new StringBuilder();
for (int i = 0; i < k; i++) {
sb.append(words[i]).append(" ");
}

sb.deleteCharAt(sb.length() - 1);
return sb.toString();
}

复杂度分析

  • 时间复杂度:\(O(N)\),其中\(N\)\(\textit{s}\)的长度。

  • 空间复杂度:\(O(N)\)

方法二:模拟

简单的模拟,统计空格与句子结尾的数目来统计单词数,当已经有\(k\)个单词时,返回答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public String truncateSentence(String s, int k) {
StringBuilder sb = new StringBuilder();
int len = s.length();
for (int i = 0; i < len; i++) {
sb.append(s.charAt(i));
if (s.charAt(i) == ' ') {
k--;
}
if (k == 0) {
sb.deleteCharAt(sb.length() - 1);
break;
}
}

return sb.toString();
}

复杂度分析

  • 时间复杂度:\(O(N)\),其中\(N\)\(\textit{s}\)的长度。

  • 空间复杂度:\(O(N)\)

方法三:优化

我们可以只记录最后截断的下标\(\textit{end}\),返回句子\(\textit{s}\)\(\textit{end}\)处截断的句子。

阅读全文 »
0%