第437场周赛:找出长度为 K 的特殊子字符串、吃披萨、选择 K 个互不重叠的特殊子字符串、最长 V 形对角线段的长度

Q1、找出长度为 K 的特殊子字符串

1、题目描述

给你一个字符串 s 和一个整数 k

判断是否存在一个长度 恰好k 的子字符串,该子字符串需要满足以下条件:

  1. 该子字符串 只包含一个唯一字符 (例如,"aaa""bbb")。
  2. 如果该子字符串的 前面 有字符,则该字符必须与子字符串中的字符不同。
  3. 如果该子字符串的 后面 有字符,则该字符也必须与子字符串中的字符不同。

如果存在这样的子串,返回 true;否则,返回 false

子字符串 是字符串中的连续、非空字符序列。

2、解题思路

我们可以使用滑动窗口遍历统计连续相同字符段 的方法来解决此问题。

本题代码采用的是遍历统计连续相同字符段的方法:

  1. 遍历字符串 ,统计连续相同字符的长度 ret
    • 如果当前字符 s[i] 与前一个字符 s[i-1] 相同,则 ret++
    • 如果当前字符 s[i] 与前一个字符 s[i-1] 不同:
      • 说明前一个连续相同的字符段结束,检查 ret 是否等于 k
        • 若等于 k,则该子串满足条件,返回 true
      • 然后重置 ret = 1,表示新的字符段开始。
  2. 遍历结束后 ,需要额外检查 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 个披萨。由于你的新陈代谢能力惊人,当你吃重量为 WXYZ 的披萨(其中 W <= X <= Y <= Z)时,你只会增加 1 个披萨的重量!体重增加规则如下:

  • 奇数天 (按 1 开始计数 )你会增加 Z 的重量。
  • 偶数天 ,你会增加 Y 的重量。

请你设计吃掉 所有 披萨的最优方案,并计算你可以增加的 最大 总重量。

**注意:**保证 n 是 4 的倍数,并且每个披萨只吃一次。

2、解题思路

由于体重的增加依赖于吃掉的 4 片披萨中的最大值(奇数天)或次大值(偶数天),我们应当让这些天所吃到的披萨尽可能重。因此,我们可以采取以下策略:

  1. 先对披萨重量降序排序,这样较重的披萨排在前面。
  2. 奇数天
    • 设总天数为 days = n / 4(因为每天吃 4 片)。
    • 其中奇数天数(days + 1) / 2(向上取整)。
    • 在奇数天,我们应该选择排序后的第 i * 4 块披萨(即每组 4 个中的最大值)。
  3. 偶数天
    • 偶数天数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、解题思路

  1. 记录每个字符的出现位置

    • 由于特殊子字符串内的字符不能在字符串的其他部分出现,我们需要知道每个字符在 s 中的 出现位置
  2. 尝试扩展有效的特殊子字符串

    • 从每个字符 c 的第一次出现 start 和最后一次出现 end 开始,尝试扩展这个区间,使得所有位于 startend 之间的字符的 出现位置 也完全落在该区间内。

    • 通过这一过程,我们可以找到 s 中所有的 可能的特殊子字符串

  3. 筛选出不等于 s 的子字符串

    • 需要确保子字符串不是整个 s
  4. 使用贪心算法选择最多的不重叠子字符串

    • 通过按结束位置排序 ,使用贪心策略 找到最多的不重叠子字符串,并判断是否能够找到 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,其中每个元素的值为 012

V 形对角线段 定义如下:

  • 线段从 1 开始。
  • 后续元素按照以下无限序列的模式排列:2, 0, 2, 0, ...
  • 该线段:
    • 起始于某个对角方向(左上到右下、右下到左上、右上到左下或左下到右上)。
    • 沿着相同的对角方向继续,保持 序列模式
    • 在保持 序列模式 的前提下,最多允许 一次顺时针 90 度转向 另一个对角方向。

返回最长的 V 形对角线段长度 。如果不存在有效的线段,则返回 0。

2、解题思路

本题适合 深度优先搜索(DFS)+ 记忆化搜索,原因如下:

  1. 由于 线段必须严格按照 1 → 2 → 0 → 2 → 0 ... 进行扩展,所以递归搜索是合适的方式。
  2. 由于搜索过程中 可能会重复计算相同的子问题 ,所以使用 记忆化存储 避免重复计算,提高效率。

解题步骤

  1. 定义 4 个对角方向 ,存储在 DIRS 数组中。
  2. 使用 dfs(i, j, dir, can_turn, target) 进行搜索
    • i, j:当前搜索位置。
    • dir:当前方向(0-3,对应 4 个对角方向)。
    • can_turn:当前是否允许 顺时针转向
    • target:当前位置需要匹配的值(20)。
  3. 递归扩展路径
    • 继续沿着当前方向搜索 下一个符合序列规则的位置
    • 如果还可以转向,尝试 顺时针 90 度转向 并继续搜索。
    • 记忆化存储已计算过的路径,避免重复计算。
  4. 遍历整个矩阵,以 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;
}

};

![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/6414b6a6d7b14a64836d9049841931a9.png)


### 4、复杂度分析

假设 `grid` 的大小为 `m x n`:

- **遍历矩阵所有元素**:$O(m * n)$
- **每个点最多搜索 4 个方向,递归深度受 `m, n` 限制**。
- **记忆化搜索避免重复计算,最坏情况为 $O(m * n)$**。
- **总体复杂度:$O(m * n)$**。


<br>
<br>
相关推荐
程序员-King.2 小时前
【接口封装】——13、登录窗口的标题栏内容设置
c++·qt
学编程的小程3 小时前
LeetCode216
算法·深度优先
leeyayai_xixihah3 小时前
2.21力扣-回溯组合
算法·leetcode·职场和发展
01_3 小时前
力扣hot100——相交,回文链表
算法·leetcode·链表·双指针
萌の鱼3 小时前
leetcode 2826. 将三个组排序
数据结构·c++·算法·leetcode
Buling_03 小时前
算法-哈希表篇08-四数之和
数据结构·算法·散列表
AllowM3 小时前
【LeetCode Hot100】除自身以外数组的乘积|左右乘积列表,Java实现!图解+代码,小白也能秒懂!
java·算法·leetcode
RAN_PAND3 小时前
STL介绍1:vector、pair、string、queue、map
开发语言·c++·算法
fai厅的秃头姐!6 小时前
C语言03
c语言·数据结构·算法