Q1、找出长度为 K 的特殊子字符串
1、题目描述
给你一个字符串 s
和一个整数 k
。
判断是否存在一个长度 恰好 为 k
的子字符串,该子字符串需要满足以下条件:
- 该子字符串 只包含一个唯一字符 (例如,
"aaa"
或"bbb"
)。 - 如果该子字符串的 前面 有字符,则该字符必须与子字符串中的字符不同。
- 如果该子字符串的 后面 有字符,则该字符也必须与子字符串中的字符不同。
如果存在这样的子串,返回 true
;否则,返回 false
。
子字符串 是字符串中的连续、非空字符序列。
2、解题思路
我们可以使用滑动窗口 或遍历统计连续相同字符段 的方法来解决此问题。
本题代码采用的是遍历统计连续相同字符段的方法:
- 遍历字符串 ,统计连续相同字符的长度
ret
:- 如果当前字符
s[i]
与前一个字符s[i-1]
相同,则ret++
。 - 如果当前字符
s[i]
与前一个字符s[i-1]
不同:- 说明前一个连续相同的字符段结束,检查
ret
是否等于k
:- 若等于
k
,则该子串满足条件,返回true
。
- 若等于
- 然后重置
ret = 1
,表示新的字符段开始。
- 说明前一个连续相同的字符段结束,检查
- 如果当前字符
- 遍历结束后 ,需要额外检查
ret
是否等于k
(因为s
可能以满足条件的子串结尾)。
3、代码实现
class Solution {
public:
bool hasSpecialSubstring(string s, int k) {
int ret = 1; // 记录当前连续相同字符的长度
int n = s.size();
for (int i = 1; i < n; ++i) {
// 当前字符与前一个字符相同, 增加计数
if (s[i - 1] == s[i]) {
ret++;
} else {
// 当前字符与前一个字符不同, 检查前面连续段的长度是否等于 k
if (ret == k) {
return true;
}
// 重置 ret 为 1, 开始新的连续字符段
ret = 1;
}
}
// 由于 s 可能以满足条件的子串结尾, 因此最后再检查一次
return ret == k;
}
};

4、复杂度分析
时间复杂度 : O ( n ) O(n) O(n),其中 n n n 是字符串 s
的长度。我们只遍历了一次 s
,因此时间复杂度是线性的。
空间复杂度 : O ( 1 ) O(1) O(1),仅使用了常数额外空间(一个 int ret
变量)。
5、其他解法
我们还可以使用滑动窗口的方法来解决此问题:
class Solution {
public:
bool hasSpecialSubstring(string s, int k) {
int n = s.size();
if (n < k) {
return false;
}
for (int i = 0; i <= n - k; ++i) {
// 检查长度为 k 的子串是否符合条件
if (count(s.begin() + i, s.begin() + i + k, s[i]) == k) {
// 确保前后字符不同
if ((i == 0 || s[i - 1] != s[i]) &&
(i + k == n || s[i + k] != s[i])) {
return true;
}
}
}
return false;
}
};
- 时间复杂度 : O ( n ⋅ k ) O(n \cdot k) O(n⋅k)。
- 空间复杂度 : O ( 1 ) O(1) O(1)。
相较而言,遍历统计连续相同字符段的方法更优 ,因为它只需要 O(n)
时间。
Q2、吃披萨
1、题目描述
给你一个长度为 n
的整数数组 pizzas
,其中 pizzas[i]
表示第 i
个披萨的重量。每天你会吃 恰好 4 个披萨。由于你的新陈代谢能力惊人,当你吃重量为 W
、X
、Y
和 Z
的披萨(其中 W <= X <= Y <= Z
)时,你只会增加 1 个披萨的重量!体重增加规则如下:
- 在 奇数天 (按 1 开始计数 )你会增加
Z
的重量。 - 在 偶数天 ,你会增加
Y
的重量。
请你设计吃掉 所有 披萨的最优方案,并计算你可以增加的 最大 总重量。
**注意:**保证 n
是 4 的倍数,并且每个披萨只吃一次。
2、解题思路
由于体重的增加依赖于吃掉的 4 片披萨中的最大值(奇数天)或次大值(偶数天),我们应当让这些天所吃到的披萨尽可能重。因此,我们可以采取以下策略:
- 先对披萨重量降序排序,这样较重的披萨排在前面。
- 奇数天 :
- 设总天数为
days = n / 4
(因为每天吃 4 片)。 - 其中奇数天数 为
(days + 1) / 2
(向上取整)。 - 在奇数天,我们应该选择排序后的第
i * 4
块披萨(即每组 4 个中的最大值)。
- 设总天数为
- 偶数天 :
- 偶数天数 为
days / 2
。 - 在偶数天,我们应该选择排序后的第
i * 4 + 1
块披萨(即每组 4 个中的次大值)。
- 偶数天数 为
3、代码实现
class Solution {
public:
long long maxWeight(vector<int>& pizzas) {
int n = pizzas.size();
long long ret = 0;
// 1. 先对披萨重量降序排序 (从大到小)
sort(pizzas.rbegin(), pizzas.rend());
int days = n / 4; // 总共的天数
int odd = (days + 1) / 2; // 计算奇数天的数量 (向上取整)
// 2. 计算奇数天的总体重增加, 加法具有交换律, 可以先计算奇数天的体重增加
for (int i = 0; i < odd; ++i) {
ret += pizzas[i]; // 选取每组 4 个披萨中的最大值
}
// 3. 计算偶数天的总体重增加
for (int i = 0; i < days / 2; ++i) {
ret += pizzas[odd + i * 2 + 1]; // 选取每组 4 个披萨中的次大值
}
return ret;
}
};

4、复杂度分析
时间复杂度:
- 排序
O(n log n)
- 计算奇数天
O(n / 4)
- 计算偶数天
O(n / 4)
- 总时间复杂度:
O(n log n)
(排序为主要开销)
空间复杂度:
- 仅使用常数额外空间
O(1)
Q3、选择 K 个互不重叠的特殊子字符串
1、题目描述
给你一个长度为 n
的字符串 s
和一个整数 k
,判断是否可以选择 k
个互不重叠的 特殊子字符串 。
特殊子字符串 是满足以下条件的子字符串:
- 子字符串中的任何字符都不应该出现在字符串其余部分中。
- 子字符串不能是整个字符串
s
。
**注意:**所有 k
个子字符串必须是互不重叠的,即它们不能有任何重叠部分。
如果可以选择 k
个这样的互不重叠的特殊子字符串,则返回 true
;否则返回 false
。
子字符串 是字符串中的连续、非空字符序列。
2、解题思路
-
记录每个字符的出现位置
- 由于特殊子字符串内的字符不能在字符串的其他部分出现,我们需要知道每个字符在
s
中的 出现位置。
- 由于特殊子字符串内的字符不能在字符串的其他部分出现,我们需要知道每个字符在
-
尝试扩展有效的特殊子字符串
-
从每个字符
c
的第一次出现start
和最后一次出现end
开始,尝试扩展这个区间,使得所有位于start
到end
之间的字符的 出现位置 也完全落在该区间内。 -
通过这一过程,我们可以找到
s
中所有的 可能的特殊子字符串。
-
-
筛选出不等于
s
的子字符串- 需要确保子字符串不是整个
s
。
- 需要确保子字符串不是整个
-
使用贪心算法选择最多的不重叠子字符串
- 通过按结束位置排序 ,使用贪心策略 找到最多的不重叠子字符串,并判断是否能够找到
k
个。
- 通过按结束位置排序 ,使用贪心策略 找到最多的不重叠子字符串,并判断是否能够找到
3、代码实现
class Solution {
public:
struct Substring {
int start, end;
bool operator<(const Substring& other) const { return end < other.end; }
};
bool maxSubstringLength(string s, int K) {
int n = s.size();
// 记录每个字符的所有出现索引
vector<vector<int>> charPositions(26);
for (int i = 0; i < n; ++i) {
charPositions[s[i] - 'a'].push_back(i);
}
vector<Substring> validSubstrings;
// 遍历所有可能的起始字符
for (int c = 0; c < 26; ++c) {
if (charPositions[c].empty()) {
continue;
}
// 该字符的第一次出现和最后一次出现的位置
int left = charPositions[c][0], right = charPositions[c].back();
bool expanded = true;
// 尝试扩展当前特殊子字符串的范围
while (expanded) {
expanded = false;
for (int j = left; j <= right; ++j) {
int ch = s[j] - 'a';
if (charPositions[ch].empty()) {
continue;
}
int first = charPositions[ch].front();
int last = charPositions[ch].back();
// 若字符首次出现和最后一次出现均在当前区间内部, 跳过
if (first >= left && last <= right) {
continue;
}
// 否则扩展区间
if (first < left || last > right) {
left = min(left, first);
right = max(right, last);
expanded = true;
}
}
}
// 确保子字符串不等于整个 s
if (left > 0 || right < n - 1) {
validSubstrings.push_back({left, right});
}
}
// 按照结束位置排序, 使用贪心算法找最多不重叠的区间
sort(validSubstrings.begin(), validSubstrings.end());
int count = 0, lastEnd = -1;
for (const auto& sub : validSubstrings) {
if (sub.start > lastEnd) {
count++;
lastEnd = sub.end;
}
}
return count >= K;
}
};

4、复杂度分析
预处理字符出现位置 :O(n)
查找可能的特殊子字符串 :O(26 * n) = O(n)
排序 :O(n log n)
贪心选择 :O(n)
总复杂度 :O(n log n)
Q4、最长 V 形对角线段的长度
1、题目描述
给你一个大小为 n x m
的二维整数矩阵 grid
,其中每个元素的值为 0
、1
或 2
。
V 形对角线段 定义如下:
- 线段从
1
开始。 - 后续元素按照以下无限序列的模式排列:
2, 0, 2, 0, ...
。 - 该线段:
- 起始于某个对角方向(左上到右下、右下到左上、右上到左下或左下到右上)。
- 沿着相同的对角方向继续,保持 序列模式 。
- 在保持 序列模式 的前提下,最多允许 一次顺时针 90 度转向 另一个对角方向。

返回最长的 V 形对角线段 的 长度 。如果不存在有效的线段,则返回 0。
2、解题思路
本题适合 深度优先搜索(DFS)+ 记忆化搜索,原因如下:
- 由于 线段必须严格按照
1 → 2 → 0 → 2 → 0 ...
进行扩展,所以递归搜索是合适的方式。 - 由于搜索过程中 可能会重复计算相同的子问题 ,所以使用 记忆化存储 避免重复计算,提高效率。
解题步骤
- 定义 4 个对角方向 ,存储在
DIRS
数组中。 - 使用
dfs(i, j, dir, can_turn, target)
进行搜索 :i, j
:当前搜索位置。dir
:当前方向(0-3,对应 4 个对角方向)。can_turn
:当前是否允许 顺时针转向。target
:当前位置需要匹配的值(2
或0
)。
- 递归扩展路径 :
- 继续沿着当前方向搜索 下一个符合序列规则的位置。
- 如果还可以转向,尝试 顺时针 90 度转向 并继续搜索。
- 记忆化存储已计算过的路径,避免重复计算。
- 遍历整个矩阵,以
1
为起点,遍历所有 4 种对角方向,并计算最长路径。
3、代码实现
class Solution {
private:
// 4 个对角方向 (左上到右下, 右下到左上, 右上到左下, 左下到右上)
static constexpr int DIRS[4][2] = {{1, 1}, {1, -1}, {-1, -1}, {-1, 1}};
public:
int lenOfVDiagonal(vector<vector<int>>& grid) {
int row = grid.size();
int col = grid[0].size();
// 记忆化存储: [行][列][方向][是否可转弯][当前目标值]
vector memo(row, vector<array<array<array<int, 2>, 2>, 4>>(col));
// 深度优先搜索
function<int(int, int, int, bool, int)> dfs = [&](int i, int j, int dir, bool can_turn, int target) -> int {
// 计算下一个位置
int x = i + DIRS[dir][0];
int y = j + DIRS[dir][1];
// 越界或不符合目标值, 终止搜索
if (x < 0 || x >= row || y < 0 || y >= col || grid[x][y] != target) {
return 0;
}
// 记忆化存储
int& ret = memo[x][y][dir][can_turn][target / 2];
if (ret) {
return ret; // 之前计算过, 直接返回
}
// 继续直行
ret = dfs(x, y, dir, can_turn, 2 - target);
// 尝试顺时针转向一次
if (can_turn) {
ret = max(ret, dfs(x, y, (dir + 1) % 4, false, 2 - target));
}
return ++ret; // 计入当前位置
};
int maxLength = 0;
for (int i = 0; i < row; ++i) {
for (int j = 0; j < col; ++j) {
// 只有 1 才能作为 V 形的起点
if (grid[i][j] == 1) {
// 遍历所有 4 种方向
for (int k = 0; k < 4; ++k) {
maxLength = max(maxLength, dfs(i, j, k, true, 2) + 1);
}
}
}
}
return maxLength;
}
};
4、复杂度分析
假设 grid
的大小为 m x n
:
- 遍历矩阵所有元素 : O ( m ∗ n ) O(m * n) O(m∗n)
- 每个点最多搜索 4 个方向,递归深度受
m, n
限制。 - 记忆化搜索避免重复计算,最坏情况为 O ( m ∗ n ) O(m * n) O(m∗n)。
- 总体复杂度: O ( m ∗ n ) O(m * n) O(m∗n)。
所有 4 种方向
for (int k = 0; k < 4; ++k) {
maxLength = max(maxLength, dfs(i, j, k, true, 2) + 1);
}
}
}
}
return maxLength;
}
};

### 4、复杂度分析
假设 `grid` 的大小为 `m x n`:
- **遍历矩阵所有元素**:$O(m * n)$
- **每个点最多搜索 4 个方向,递归深度受 `m, n` 限制**。
- **记忆化搜索避免重复计算,最坏情况为 $O(m * n)$**。
- **总体复杂度:$O(m * n)$**。
<br>
<br>