909. 蛇梯棋 - 力扣(LeetCode)
timer:2025-5-31
class Solution {
public:
int snakesAndLadders(vector<vector<int>>& board) {
int n = board.size();
vector<int8_t> vis(n * n + 1); // 记录每个位置是否被访问过
vis[1] = true; // 起点标记为已访问
vector<int> q; // BFS队列
q.push_back(1); // 从位置1开始
// BFS主循环,i表示当前步数
for(int i = 0; !q.empty(); i++){
auto t = q; // 保存当前层的所有位置
q.clear(); // 清空队列准备下一层
// 遍历当前层的所有位置
for(auto& x : t){
if(x == n * n){ // 到达终点
return i;
}
// 模拟掷骰子,枚举所有可能的下一步位置
for(int y = x + 1; y <= min(x + 6, n * n); y++){
// 将一维位置y转换为二维棋盘坐标
int r = (y - 1) / n, c = (y - 1) % n;
if(r % 2) // 奇数行需要镜像列坐标(注意这里的奇偶行指的是数组的奇偶行,也就是从最上方开始为第0行)
c = n - c - 1;
// 获取棋盘上该位置的值
int nex = board[n - 1 - r][c];
if(nex == -1) // 没有梯子或蛇
nex = y;
// 如果下一步位置未被访问过
if(!vis[nex]){
q.push_back(nex); // 加入队列
vis[nex] = true; // 标记为已访问
}
}
}
}
return -1; // 无法到达终点
}
};
2025-6:
2929. 给小朋友们分糖果 II - 力扣(LeetCode)
timer:2025-6-1
class Solution {
// 计算组合数C(n,2) = n*(n-1)/2
long long C2(long long n) {
return n > 1 ? n * (n - 1) / 2 : 0;
}
public:
/*
* 计算将n个无区别糖果放入3个有区别盒子的合法分配方案数
* 每个盒子最多放limit个糖果
*
* 容斥原理应用:
* 总方案数(允许空盒) = C(n+2,2) (隔板法)
* 设Ai表示第i个盒子超过limit的方案集合
* 需要计算 |A1∪A2∪A3| 并从总方案中扣除
*
* 根据容斥公式:
* |A1∪A2∪A3| = Σ|Ai| - Σ|Ai∩Aj| + |A1∩A2∩A3|
*
* 单集合大小:|Ai| = C(n - limit, 2) (共3个)
* 双集合交集:|Ai∩Aj| = C(n - 2*limit - 1, 2) (共3个)
* 三集合交集:|A1∩A2∩A3| = C(n - 3*limit - 2, 2) (若存在)
*/
long long distributeCandies(int n, int limit) {
return C2(n + 2) // 总方案数
- 3 * C2(n - limit + 1) // 减去单集合
+ 3 * C2(n - 2 * limit) // 加上双集合交集
- C2(n - 3 * limit - 1); // 减去三集合交集
}
};
135. 分发糖果 - 力扣(LeetCode)
timer:2025-6-2
class Solution {
public:
int candy(vector<int>& ratings) {
int n = ratings.size();
if (n == 0) return 0;
// 初始化每个孩子至少有一个糖果
vector<int> candies(n, 1);
// 第一次遍历:从左到右,确保右边评分高的孩子比左边的多
for (int i = 1; i < n; i++) {
if (ratings[i] > ratings[i-1]) {
candies[i] = candies[i-1] + 1;
}
}
// 第二次遍历:从右到左,确保左边评分高的孩子比右边的多
for (int i = n-2; i >= 0; i--) {
if (ratings[i] > ratings[i+1]) {
candies[i] = max(candies[i], candies[i+1] + 1);
}
}
// 计算总糖果数
int total = 0;
for (int num : candies) {
total += num;
}
return total;
}
};
1298. 你能从盒子里获得的最大糖果数 - 力扣(LeetCode)
timer:2025-6-3
// DFS
// 记录状态
// 1、找到钥匙(状态0也表示为有钥匙)
// 2、找到箱子
// 3、当钥匙和箱子都找到我们就能拿到糖果
class Solution {
public:
int maxCandies(vector<int>& status, vector<int>& candies, vector<vector<int>>& keys, vector<vector<int>>& containedBoxes, vector<int>& initialBoxes) {
auto& has_key = status;
vector<uint8_t> has_box(status.size());
// 标记已知箱
for(auto& x : initialBoxes)
has_box[x] = true;
int ans = 0;
// 开箱函数
auto dfs = [&](this auto&& dfs, int x) -> void {
ans += candies[x]; // 拿糖
has_box[x] = false;
// 拿到钥匙
for(int y : keys[x]){
has_key[y] = true;
if(has_box[y]) // 有钥匙对应的箱子
dfs(y);
}
// 拿到箱子
for(int y : containedBoxes[x]){
has_box[y] = true;
if(has_key[y]) // 有箱子对应的钥匙
dfs(y);
}
};
// 开始开箱
for(auto& x : initialBoxes)
if(has_box[x] && has_key[x])
dfs(x);
return ans;
}
};
3403. 从盒子中找出字典序最大的字符串 I - 力扣(LeetCode)
timer: 2025-6-4
一:枚举子串左端点
// 枚举子串左端点解法
class Solution {
public:
string answerString(string word, int numFriends) {
int n = word.size();
string ans = "";
// 特殊情况:当每个朋友恰好分到1个字符时,整个字符串即为答案
if(numFriends == 1)
return word;
// 枚举所有可能的子串左端点i
for(int i = 0; i < n; i++){
// 计算从i开始的有效子串长度(受numFriends限制)
// 子串长度至少为1,且不能超过原字符串长度
int len = n - max((numFriends - 1), i);
if(len <= 0) continue; // 跳过无效长度
// 更新最大字典序子串
ans = max(ans, word.substr(i, len));
}
return ans;
}
};
二: 计算字典序最大的后缀
// 双指针优化解法:线性时间复杂度
class Solution {
public:
string answerString(string word, int numFriends) {
// 特殊情况处理
if (numFriends == 1) {
return word;
}
int n = word.size();
int i = 0; // 当前最大字典序子串的起始位置
int j = 1; // 探索指针,尝试寻找更大的子串
while (j < n) {
int len = 0; // 公共前缀长度
// 计算i和j开始的子串的最长公共前缀
while (j + len < n && word[i + len] == word[j + len]) {
len++;
}
// 如果j位置子串在公共前缀后更大,则更新i
if (j + len < n && word[i + len] < word[j + len]) {
int t = i; // 保存旧的i值
i = j; // 更新i为新的更大子串起始位置
// 优化j的下一个位置:跳过已知不可能的起始点
j = max(j + 1, t + len + 1);
} else {
// 否则直接跳过公共前缀长度+1的距离
j += len + 1;
}
}
// 计算最终子串长度(受numFriends限制)
// 确保子串长度至少为1,且不超过原字符串长度
int len = n - max(numFriends - 1, i);
return word.substr(i, len);
}
};
1061. 按字典序排列最小的等效字符串 - 力扣(LeetCode)
timer:2025-6-5
class Solution {
public:
string smallestEquivalentString(string s1, string s2, string baseStr) {
// 初始化并查集数组:parent[x] 表示字符 x 的父节点,初始时每个节点的父节点是自身
int parent[26];
for (int i = 0; i < 26; ++i) {
parent[i] = i;
}
// 查找字符 x 所在集合的根节点(带路径压缩)
function<int(int)> findRoot = [&](int x) {
if (parent[x] != x) {
parent[x] = findRoot(parent[x]); // 路径压缩:直接将节点连接到根节点
}
return parent[x];
};
// 合并两个字符所在的集合,确保每个集合的根节点是字典序最小的字符
auto unionChars = [&](char c1, char c2) {
int root1 = findRoot(c1 - 'a');
int root2 = findRoot(c2 - 'a');
if (root1 != root2) {
// 让较大的根节点指向较小的根节点,确保每个集合的根是最小字符
if (root1 > root2) {
parent[root1] = root2;
} else {
parent[root2] = root1;
}
}
};
// 处理所有等价关系,建立字符间的连通性
for (int i = 0; i < s1.size(); ++i) {
unionChars(s1[i], s2[i]);
}
// 将 baseStr 中的每个字符替换为其等价类中的最小字符
for (char& c : baseStr) {
c = findRoot(c - 'a') + 'a';
}
return baseStr;
}
};
2434. 使用机器人打印字典序最小的字符串 - 力扣(LeetCode)
timer:2025-6-6
class Solution {
public:
string robotWithString(string s) {
int n = s.size();
// 预处理后缀最小值数组
// suf_min[i] 表示从索引i到字符串末尾的最小字符
vector<char> suf_min(n + 1);
suf_min[n] = 'z'; // 初始化边界值为最大字符'z'
for (int i = n - 1; i >= 0; i--) {
suf_min[i] = min(suf_min[i + 1], s[i]);
}
string ans; // 存储最终结果的字符串
stack<char> st; // 模拟机器人的栈操作
// 遍历输入字符串的每个字符
for (int i = 0; i < n; i++) {
st.push(s[i]); // 将当前字符压入栈
// 核心逻辑:如果栈顶字符小于等于剩余字符中的最小字符
// 则应该立即将其添加到结果中,确保字典序最小
while (!st.empty() && st.top() <= suf_min[i + 1]) {
ans += st.top();
st.pop();
}
}
return ans;
}
};
3423. 循环数组中相邻元素的最大差值 - 力扣(LeetCode)
timer: 2025-6-12
class Solution {
public:
int maxAdjacentDistance(vector<int>& nums) {
int n = nums.size();
int ans = abs(nums[n - 1] - nums[0]);
for(int i = 1; i < n; i++){
ans = max(abs(nums[i] - nums[i - 1]), ans);
}
return ans;
}
};
2566. 替换一个数字后的最大差值 - 力扣(LeetCode)
timer: 2025-6-14
// 简单贪心
// 最大值: 将最前面的数字替换为最大‘9’
// 最小值: 将最前面的数字替换为最小‘0’
// 注: 要注意全部替换
class Solution {
public:
int minMaxDifference(int num) {
string s = to_string(num);
// 替换成最大值
int mmax = num;
for (char ch : s) {
if (ch != '9') {
string tmp = s;
ranges::replace(tmp, ch, '9');
mmax = stoi(tmp);
break;
}
}
// 替换成最小值
int mmin;
char ch0 = s[0];
ranges::replace(s, ch0, '0');
mmin = stoi(s);
return mmax - mmin;
}
};
2016. 增量元素之间的最大差值 - 力扣(LeetCode)
timer: 2025-6-16
// 方法一:暴力枚举法
// 时间复杂度:O(n²),空间复杂度:O(1)
class Solution {
public:
int maximumDifference(vector<int>& nums) {
int n = nums.size(); // 获取数组长度
int ans = -1; // 初始化最大差值为-1(题目要求未找到正差值时返回-1)
// 外层循环遍历每个可能的起始元素
for(int i = 0; i < n; i++)
// 内层循环遍历每个可能的结束元素(需在起始元素之后)
for(int j = i + 1; j < n; j++)
// 若当前元素对满足正差值条件,则更新最大差值
if(nums[j] - nums[i] > 0)
ans = max(ans, nums[j] - nums[i]);
return ans; // 返回最大正差值,若不存在则返回-1
}
};
// 方法二:一次遍历优化法
// 时间复杂度:O(n),空间复杂度:O(1)
class Solution {
public:
int maximumDifference(vector<int>& nums) {
int n = nums.size(); // 获取数组长度
int ans = 0; // 初始化最大差值为0
int mmin = INT_MAX; // 初始化最小值为INT_MAX,用于跟踪已遍历元素中的最小值
// 一次遍历数组
for(int i = 0; i < n; i++){
// 计算当前元素与历史最小值的差值,并更新最大差值
ans = max(ans, nums[i] - mmin);
// 更新已遍历元素中的最小值
mmin = min(nums[i], mmin);
}
// 若最大差值为0,说明不存在正差值,返回-1;否则返回最大差值
return ans ? ans : -1;
}
};
1432. 改变一个整数能得到的最大差值 - 力扣(LeetCode)
timer: 2025-6-16
class Solution {
public:
int maxDiff(int num) {
// 将数字转换为字符串以便逐位处理
string s = to_string(num);
// 定义一个lambda函数用于替换字符串中的指定字符并转换回整数
// oldChar: 需要替换的字符
// newChar: 替换后的新字符
auto replace_num = [&](char oldChar, char newChar) {
int x = 0;
for (auto& ch : s) {
// 如果当前字符等于oldChar,则替换为newChar,否则保持不变
char c = ch == oldChar ? newChar : ch;
// 逐位构建新的整数值
x = x * 10 + (c - '0');
}
return x;
};
// 计算替换后能得到的最大数值
int mmax = num;
// 遍历字符串,找到第一个不是'9'的字符
for (char ch : s) {
if (ch != '9') {
// 将该字符替换为'9'以获得可能的最大值
mmax = replace_num(ch, '9');
break;
}
}
// 计算替换后能得到的最小数值
int mmin = num;
// 如果第一位不是'1',则将第一位替换为'1'
if (s[0] != '1') {
mmin = replace_num(s[0], '1');
} else {
// 否则从第二位开始寻找第一个大于'1'的字符
for (int i = 1; i < s.size(); i++) {
if (s[i] > '1') {
// 将该字符替换为'0'以获得可能的最小值
mmin = replace_num(s[i], '0');
break;
}
}
}
// 返回最大值和最小值的差值
return mmax - mmin;
}
};
3405. 统计恰好有 K 个相等相邻元素的数组数目 - 力扣(LeetCode)
timer: 2025-6-17
const int MOD = 1'000'000'007;
const int MX = 100'000;
typedef long long ll;
ll F[MX]; // 阶乘
ll INV_F[MX]; // 对应阶乘的逆元
// 快速幂
ll qpow(ll x, int n){
ll res = 1;
for(; n; n /= 2){
if(n % 2){
res = res * x % MOD;
}
x = x * x % MOD;
}
return res;
}
// 初始化阶乘
auto init = []{
F[0] = 1;
for(int i = 1; i < MX; i++)
F[i] = F[i - 1] * i % MOD;
INV_F[MX - 1] = qpow(F[MX - 1], MOD - 2);
for(int i = MX - 1; i > 0; i--){
INV_F[i - 1] = INV_F[i] * i % MOD;
}
return 0;
}();
// 计算组合
ll comb(int n, int m){
return F[n] * INV_F[m] % MOD * INV_F[n - m] % MOD;
}
class Solution {
public:
int countGoodArrays(int n, int m, int k) {
return comb(n - 1, k) * m % MOD * qpow(m - 1, n - k - 1) % MOD;
}
};
// 作者:灵茶山艾府
// 链接:https://leetcode.cn/problems/count-the-number-of-arrays-with-k-matching-adjacent-elements/solutions/3033292/chun-shu-xue-ti-pythonjavacgo-by-endless-mxj7/
// 来源:力扣(LeetCode)
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2966. 划分数组并满足最大差限制 - 力扣(LeetCode)
timer:2025-6-18
class Solution {
public:
vector<vector<int>> divideArray(vector<int>& nums, int k) {
int n = nums.size();
sort(nums.begin(), nums.end());
vector<vector<int>> ans;
// 遍历每个分组,每组3个元素
for (int i = 0; i < n; i += 3) {
// 检查当前分组的最大值和最小值之差是否不超过k
if (nums[i+2] - nums[i] > k) {
return {};
}
// 将当前分组加入结果集
ans.push_back({nums[i], nums[i+1], nums[i+2]});
}
return ans;
}
};
2294. 划分数组使最大差为 K - 力扣(LeetCode)
timer:2025-6-19
class Solution {
public:
int partitionArray(vector<int>& nums, int k) {
int n = nums.size();
sort(nums.begin(), nums.end());
int pre = nums[0], ans = 1; // 要注意先排序在初始化pre
// if (n == 0) return 0;
for(int i = 1; i < n; i++){
if((nums[i] - pre) > k){
pre = nums[i];
ans++;
}
}
return ans;
}
};
3443. K 次修改后的最大曼哈顿距离 - 力扣(LeetCode)
timer: 2025-6-20
/*
* 方法一:曼哈顿距离最大化算法
*
* 坐标简化:
* - 向左(l) 向右(r) 向上(u) 向下(d)
*
* 曼哈顿距离公式:
* distance = |l - r| + |u - d|
*
* 优化目标:
* 通过修改方向字符,最大化 |l - r| 和 |u - d|
*
* 优化策略:
* 1. 每次修改可以将一个方向字符变为其相反方向(例如:E→W 或 N→S)
* 2. 每次修改可以使对应方向的计数变化量+2
* 3. 最优修改次数为 min(l, r, k) 或 min(u, d, k)
*
* 最终距离计算公式:
* distance = |l - r| + 2 * move
* distance = |u - d| + 2 * move
* 其中 move = min(同向计数, 反向计数, 剩余修改次数k)
*/
class Solution {
public:
int maxDistance(string s, int k) {
// 统计四个方向的总计数
int l = 0, r = 0, u = 0, d = 0;
int ans = 0;
// 遍历字符串逐步更新方向计数并计算最大可能距离
for (char ch : s) {
// 更新方向计数
if (ch == 'N') u++;
else if (ch == 'S') d++;
else if (ch == 'E') r++;
else l++;
// 剩余可用修改次数
int x = k;
// 计算单个轴向的最大可能距离
auto func = [&](int a, int b) -> int {
// 计算可转换的最大次数:受限于同向计数、反向计数和剩余修改次数
int move = min({a, b, x});
// 扣除已使用的修改次数
x -= move;
// 计算转换后的轴向距离:原始距离 + 2 * 转换次数
return abs(a - b) + 2 * move;
};
// 更新最大曼哈顿距离:x轴向距离 + y轴向距离
ans = max(ans, func(l, r) + func(u, d));
}
return ans;
}
};
// 方法二
class Solution {
public:
int maxDistance(string s, int k) {
int ans = 0; // 记录最大可能的曼哈顿距离
int x = 0, y = 0; // 当前位置的坐标,初始化为原点(0,0)
// 遍历字符串中的每个字符
for (int i = 0; i < s.size(); i++) {
// 根据字符更新当前位置
if (s[i] == 'N') y++; // 北:y坐标增加
else if (s[i] == 'S') y--; // 南:y坐标减少
else if (s[i] == 'E') x++; // 东:x坐标增加
else x--; // 西:x坐标减少
// 计算当前可能的最大曼哈顿距离
// abs(x) + abs(y):当前位置的曼哈顿距离
// k * 2:使用k次修改机会可能增加的最大距离(每次修改增加2个单位)
// i + 1:当前已处理的字符数,即最多可以修改的次数
// min(...):确保修改次数不超过可用次数
// max(ans, ...):更新最大距离
ans = max(ans, min(abs(x) + abs(y) + k * 2, i + 1));
}
return ans; // 返回最大可能的曼哈顿距离
}
};
3085. 成为 K 特殊字符串需要删除的最少字符数 - 力扣(LeetCode)
timer: 2025-6-21
// 方法一: 贪心枚举最小频率基准值
// 时间复杂度: O(n + 26²) ≈ O(n)
// 空间复杂度: O(26) ≈ O(1)
class Solution {
public:
int minimumDeletions(string word, int k) {
// 统计每个字符的出现次数
vector<int> count(26, 0);
for (char ch : word) {
count[ch - 'a']++;
}
// 按频率升序排序,便于贪心枚举基准值
sort(count.begin(), count.end());
int maxSave = 0; // 最多可保留的字符数
// 枚举所有可能的最小频率基准值 base = count[i]
// 即假设保留的字符中最少出现次数为 base
for (int i = 0; i < 26; i++) {
int base = count[i];
int sum = 0;
// 遍历所有频率 ≥ base 的字符(频率 < base 的字符已被自动排除)
for (int j = i; j < 26; j++) {
// 对于频率 ≥ base 的字符,保留其原始频率或 base + k 中的较小值
// 确保保留的字符频率不超过 base + k
sum += min(count[j], base + k);
}
// 更新最大保留字符数
maxSave = max(maxSave, sum);
}
// 总删除次数 = 原字符串长度 - 最多可保留的字符数
return word.size() - maxSave;
}
};
// 方法二: 滑动窗口优化
// 时间复杂度: O(n + 26 log 26) ≈ O(n)
// 空间复杂度: O(26) ≈ O(1)
class Solution {
public:
int minimumDeletions(string word, int k) {
// 统计每个字符的出现次数
vector<int> count(26, 0);
for (char ch : word) {
count[ch - 'a']++;
}
// 按频率升序排序,便于贪心枚举
sort(count.begin(), count.end());
int maxSave = 0; // 最多可保留的字符数
int windowSum = 0; // 当前窗口内字符的总出现次数
int right = 0; // 滑动窗口的右边界
// 枚举所有可能的最小频率值 base(贪心策略)
for (int base : count) {
// 扩展窗口右边界,直到窗口内所有字符的频率 ≤ base + k
// 这些字符可以完整保留(频率在 [base, base+k] 范围内)
while (right < 26 && count[right] <= base + k) {
windowSum += count[right];
right++;
}
// 计算当前方案的总保留字符数:
// 1. 窗口内的字符按原始频率保留(windowSum)
// 2. 窗口外的字符按最大可能保留(每个字符保留 base + k 个)
int currentSave = windowSum + (base + k) * (26 - right);
maxSave = max(maxSave, currentSave);
// 窗口左边界右移,移除当前基准值 base
// 为下一轮更大的 base 做准备
windowSum -= base;
}
// 总删除次数 = 原字符串长度 - 最多可保留的字符数
return word.size() - maxSave;
}
};
2138. 将字符串拆分为若干长度为 k 的组 - 力扣(LeetCode)
timer:2025-6-22
// class Solution {
// public:
// vector<string> divideString(string s, int k, char fill) {
// vector<string> ans;
// int l = 0, r = k - 1;
// while(s.size() % k){
// s += fill;
// }
// int n = s.size();
// int count = n / k;
// while(count--){
// string path = "";
// for(int i = l; i <= r; i++){
// path += s[i];
// }
// ans.push_back(path);
// l += k;
// r += k;
// }
// return ans;
// }
// };
class Solution {
public:
vector<string> divideString(string s, int k, char fill) {
vector<string> result;
int n = s.size();
// 计算需要填充的字符数
int padding = (k - (n % k)) % k;
// 一次性填充所有需要的字符
s.append(padding, fill);
// 遍历字符串,每次处理k个字符
for (int i = 0; i < s.size(); i += k) {
// 直接使用子字符串,避免逐个字符拼接
result.push_back(s.substr(i, k));
}
return result;
}
};
2081. k 镜像数字的和 - 力扣(LeetCode)
timer: 2025-6-23
// 存储各进制(2-9)下的k镜像数,ans[k]表示k进制下的k镜像数列表
vector<long long> ans[10];
// 每个进制需要收集的k镜像数的最大数量
const int mmax = 30;
/**
* 判断数字x在k进制下是否为回文数
* @param x 待判断的十进制数
* @param k 目标进制
* @return true表示x在k进制下是回文数,false表示不是
*/
bool isKthBasePalindrome(long long x, int k) {
// 排除k进制下有前导零的情况(若x能被k整除且x不为0,则k进制表示有前导零)
if (x % k == 0 && x != 0) {
return false;
}
// 反转k进制数字
long long rev = 0;
// 优化循环条件,减少不必要的计算
while (rev < x / k) {
rev = rev * k + x % k;
x /= k;
}
// 比较反转前后的数字,考虑数字长度为奇数的情况
return rev == x || rev == x / k;
}
/**
* 检查是否已为每个进制收集到足够数量的k镜像数
* @param x 待检查的十进制回文数
* @return true表示所有进制都已收集满mmax个k镜像数,false表示未收集满
*/
bool checkCountN(long long x) {
bool flag = true;
// 遍历2到9的所有进制
for (int k = 2; k <= 9; k++) {
// 如果当前进制收集的k镜像数未满且x在该进制下是回文数,则添加到列表
if (ans[k].size() < mmax && isKthBasePalindrome(x, k)) {
ans[k].push_back(x);
}
// 只要有一个进制未收集满,就继续收集
if (ans[k].size() < mmax) {
flag = false;
}
}
if (!flag) {
return false;
}
// 计算每个进制下k镜像数的前缀和(用于快速获取前n项和)
for (int k = 2; k <= 9; k++) {
partial_sum(ans[k].begin(), ans[k].end(), ans[k].begin());
}
return true;
}
// 全局初始化lambda表达式,程序启动时自动执行
auto init = []() {
// 按位数生成十进制回文数,base表示当前处理的位数的基数(1, 10, 100, ...)
for (int base = 1;; base *= 10) {
// 生成奇数位十进制回文数
for (int i = base; i < base * 10; i++) {
long long x = i;
// 通过反转前半部分生成奇数位回文数(如i=123,生成12321)
for (int j = i / 10; j > 0; j /= 10) {
x = x * 10 + j % 10;
}
// 检查该回文数是否为各进制的k镜像数
if (checkCountN(x)) {
return 0; // 所有进制收集满后退出初始化
}
}
// 生成偶数位十进制回文数
for (int i = base; i < base * 10; i++) {
long long x = i;
// 通过反转前半部分生成偶数位回文数(如i=1234,生成1234321)
for (int j = i; j > 0; j /= 10) {
x = x * 10 + j % 10;
}
// 检查该回文数是否为各进制的k镜像数
if (checkCountN(x)) {
return 0; // 所有进制收集满后退出初始化
}
}
}
}();
class Solution {
public:
/**
* 获取k进制下的第n个k镜像数
* @param k 目标进制(2-9)
* @param n 第n个k镜像数
* @return k进制下的第n个k镜像数(1-based索引)
*/
long long kMirror(int k, int n) {
return ans[k][n - 1]; // 转换为0-based索引获取结果
}
};
2200. 找出数组中的所有 K 近邻下标 - 力扣(LeetCode)
timer: 2025-6-24
class Solution {
public:
vector<int> findKDistantIndices(vector<int>& nums, int key, int k) {
vector<int> ans; // 存储结果的数组
// 初始化last为-k-1,确保当数组前k个元素中没有key时,last < i-k
// 这样可以正确处理数组起始部分的边界情况
int last = -k - 1;
// 初始化阶段:从k-1到0逆序查找第一个key的位置
// 目的是为数组的第一个窗口(索引0到k)设置正确的last值
for(int i = k - 1; i >= 0; i--){
if(nums[i] == key){
last = i; // 找到key后更新last为该索引
break; // 只需要最近的key,找到后立即退出循环
}
}
// 主循环:遍历数组中的每个元素
for(int i = 0; i < nums.size(); i++){
// 检查当前位置i的右侧k位置是否为key
// 如果是,则更新last为该位置,因为这是当前能找到的最近的key
if(i + k < nums.size() && nums[i + k] == key)
last = i + k;
// 判断条件:如果last >= i - k,表示在i的左侧或当前位置存在key
// 且该key与i的距离不超过k,因此i符合条件
if(last >= i - k)
ans.push_back(i);
}
return ans;
}
};
2040. 两个有序数组的第 K 小乘积 - 力扣(LeetCode)
timer:2025-6-25
class Solution {
public:
long long kthSmallestProduct(vector<int>& a, vector<int>& b, long long k) {
int n = a.size(), m = b.size();
// 找到数组a和b中第一个大于等于0的位置,作为区域划分的分界线
int i0 = ranges::lower_bound(a, 0) - a.begin();
int j0 = ranges::lower_bound(b, 0) - b.begin();
// 二分查找的检查函数:判断是否有至少k个乘积小于等于mx
auto check = [&](long long mx) -> bool {
long long cnt = 0;
if (mx < 0) {
// 情况1:目标值mx为负数,只有正负相乘或负正相乘可能小于mx
// 右上区域:a[i]为正,b[j]为正,乘积为正,不可能小于mx,跳过
// 右下区域:a[i]为正,b[j]为负,乘积可能小于mx
int i = 0, j = j0;
while (i < i0 && j < m) {
if (1LL * a[i] * b[j] > mx) {
j++; // 当前乘积太大,j右移增大负数的绝对值
} else {
cnt += m - j; // 当前i及之后的所有i与当前j及之后的所有j的乘积都满足条件
i++; // 尝试更大的正数a[i]
}
}
// 左下区域:a[i]为负,b[j]为正,乘积可能小于mx
i = i0;
j = 0;
while (i < n && j < j0) {
if (1LL * a[i] * b[j] > mx) {
i++; // 当前乘积太大,i右移增大负数的绝对值
} else {
cnt += n - i; // 当前j及之后的所有j与当前i及之后的所有i的乘积都满足条件
j++; // 尝试更大的正数b[j]
}
}
} else {
// 情况2:目标值mx为非负数
// 右上区域:a[i]为正,b[j]为负,乘积为负,必然小于等于mx
// 左下区域:a[i]为负,b[j]为正,乘积为负,必然小于等于mx
cnt = 1LL * i0 * (m - j0) + 1LL * (n - i0) * j0;
// 左上区域:a[i]为正,b[j]为正,乘积可能小于等于mx
int i = 0, j = j0 - 1;
while (i < i0 && j >= 0) {
if (1LL * a[i] * b[j] > mx) {
i++; // 当前乘积太大,i右移尝试更大的正数
} else {
cnt += i0 - i; // 当前j及之前的所有j与当前i及之后的所有i的乘积都满足条件
j--; // 尝试更小的正数b[j]
}
}
// 右下区域:a[i]为负,b[j]为负,乘积可能小于等于mx
i = i0;
j = m - 1;
while (i < n && j >= j0) {
if (1LL * a[i] * b[j] > mx) {
j--; // 当前乘积太大,j左移尝试更小的负数
} else {
cnt += j - j0 + 1; // 当前i及之后的所有i与当前j及之前的所有j的乘积都满足条件
i++; // 尝试更大的负数a[i]
}
}
}
return cnt >= k;
};
// 确定二分查找的上下界
long long corners[4] = {1LL * a[0] * b[0], 1LL * a[0] * b[m - 1],
1LL * a[n - 1] * b[0], 1LL * a[n - 1] * b[m - 1]};
auto [left, right] = ranges::minmax(corners);
left--; // 确保left小于可能的最小值
// 二分查找
while (left + 1 < right) {
long long mid = left + (right - left) / 2;
(check(mid) ? right : left) = mid;
}
return right;
}
};
2311. 小于等于 K 的最长二进制子序列 - 力扣(LeetCode)
timer:2025-6-26
/*
核心思路:
1、从s的后缀中选出尽可能长且值小于等于k的子序列
2、在该子序列的前面添加s中剩余的前导零
3、计算最终子序列的最大可能长度
*/
class Solution {
public:
int longestSubsequence(string s, int k) {
int n = s.size(); // 字符串 s 的总长度
int m = bit_width((uint32_t)k); // k 的二进制位数(即满足 2^m > k 的最小 m )
// 若k的位数超过字符串长度,整个字符串都可入选
if(m > n) {
return n;
}
// 截取 s 的最后 m 位并转换为十进制数值
// 这是为了检查长度为 m 的后缀是否满足 ≤ k 的条件
int t = stoi(s.substr(n - m), nullptr, 2);
// 若后缀值 ≤ k,最优解为 m 位后缀加前导零
// 否则选择 m - 1 位后缀加前导零
int ans = t <= k ? m : m - 1;
// 统计前 n - m 位中 '0' 的数量,这些 0 可以无成本添加到子序列前
return ans + count(s.begin(), s.end() - m, '0');
}
};
2014. 重复 K 次的最长子序列 - 力扣(LeetCode)
timer:2025-6-27
/*
1、预处理字符串,构建 next 数组用于快速查找字符位置
2、统计字符频率,确定每个字符可能出现的最大次数
3、生成所有可能的排列组合,从最长的开始尝试
4、使用预处理的 next 数组快速验证每个排列是否满足条件
*/
class Solution {
private:
// 生成所有可能的排列组合(允许重复字符)
void generatePermutations(const string& chars, auto callback) {
string path;
vector<bool> used(chars.size(), false);
// 回溯函数:递归生成排列
function<void()> backtrack = [&]() {
callback(path); // 处理当前生成的排列
// 当排列长度达到最大时停止递归
if (path.size() == chars.size()) return;
for (int i = 0; i < chars.size(); ++i) {
// 跳过已使用的字符或重复的情况(剪枝优化)
if (used[i] || (i > 0 && chars[i] == chars[i-1] && !used[i-1])) continue;
used[i] = true;
path += chars[i];
backtrack();
path.pop_back();
used[i] = false;
}
};
backtrack();
}
public:
// 寻找字符串s中重复k次的最长子序列
string longestSubsequenceRepeatedK(string s, int k) {
// 预处理:构建next数组用于快速查找字符位置
int n = s.size();
vector<array<int, 26>> next(n + 1);
fill(next[n].begin(), next[n].end(), n); // 越界标记
// 从后向前构建next数组
for (int i = n - 1; i >= 0; --i) {
next[i] = next[i + 1];
next[i][s[i] - 'a'] = i;
}
// 检查sub字符串是否在s中重复k次
auto isKSubsequence = [&](const string& sub) -> bool {
int pos = -1; // 当前位置
for (int i = 0; i < k; ++i) {
for (char c : sub) {
pos = next[pos + 1][c - 'a'];
if (pos == n) return false; // 无法找到字符c
}
}
return true;
};
// 统计每个字符的频率并构建候选字符集
vector<int> freq(26, 0);
for (char c : s) ++freq[c - 'a'];
string candidates;
// 从Z到A的顺序添加候选字符,确保优先处理字典序大的字符
for (int i = 25; i >= 0; --i) {
candidates.append(freq[i] / k, 'a' + i);
}
string result;
// 生成所有可能的排列并检查是否满足条件
generatePermutations(candidates, [&](const string& perm) {
// 只处理长度大于等于当前结果的排列,并且字典序更大的排列
if (perm.size() < result.size() ||
(perm.size() == result.size() && perm <= result)) return;
if (isKSubsequence(perm)) {
result = perm;
}
});
return result;
}
};
/*
1、首先统计字符串中每个字符的频率,确定每个字符最多可以在结果中出现的次数(freq/k)
2、生成所有可能的字符排列,从最长和字典序最大的开始检查
3、使用高效的子序列检查方法,验证每个排列是否可以在原字符串中重复 k 次
*/
class Solution {
private:
// 生成所有可能的排列,并对每个排列执行回调函数
void generatePermutations(const string& chars, auto callback) {
string path;
vector<bool> used(chars.size(), false);
// 回溯函数:生成所有可能的排列
function<void()> backtrack = [&]() {
callback(path); // 处理当前排列
if (path.size() == chars.size()) return; // 排列已完成
for (int i = 0; i < chars.size(); ++i) {
// 跳过已使用的字符或重复的情况
if (used[i] || (i > 0 && chars[i] == chars[i-1] && !used[i-1])) continue;
used[i] = true;
path += chars[i];
backtrack();
path.pop_back();
used[i] = false;
}
};
backtrack();
}
// 检查字符串sub是否是s的k次子序列
bool isKSubsequence(const string& sub, int k, const string& s) {
int count = 0; // 已匹配的子序列次数
int pos = 0; // 当前在s中的位置
for (char c : s) {
if (c == sub[pos]) {
++pos;
if (pos == sub.size()) {
++count;
pos = 0;
if (count == k) return true;
}
}
}
return false;
}
public:
// 寻找字符串s中重复k次的最长子序列
string longestSubsequenceRepeatedK(string s, int k) {
// 统计每个字符出现的次数
vector<int> freq(26, 0);
for (char c : s) ++freq[c - 'a'];
// 构建候选字符集:每个字符最多出现freq/k次
string candidates;
for (int i = 25; i >= 0; --i) { // 从Z到A,确保字典序最大
candidates.append(freq[i] / k, 'a' + i);
}
string result;
// 生成所有排列并检查
generatePermutations(candidates, [&](const string& perm) {
// 如果当前排列更长或字典序更大,并且满足k次子序列条件,则更新结果
if (perm.size() > result.size() ||
(perm.size() == result.size() && perm > result)) {
if (isKSubsequence(perm, k, s)) {
result = perm;
}
}
});
return result;
}
};
2099. 找到和最大的长度为 K 的子序列 - 力扣(LeetCode)
timer: 2025-6-28
class Solution {
public:
vector<int> maxSubsequence(vector<int>& nums, int k) {
int n = nums.size();
vector<pair<int, int>> numWithIndex(n);
for (int i = 0; i < n; ++i) {
numWithIndex[i] = {nums[i], i};
}
// 按照数值降序排序
sort(numWithIndex.begin(), numWithIndex.end(),
[](const auto& a, const auto& b) {
return a.first > b.first;
});
// 提取前k个元素,并按原索引排序
vector<pair<int, int>> topK(numWithIndex.begin(), numWithIndex.begin() + k);
sort(topK.begin(), topK.end(),
[](const auto& a, const auto& b) {
return a.second < b.second;
});
// 构建结果
vector<int> ans(k);
for (int i = 0; i < k; ++i) {
ans[i] = topK[i].first;
}
return ans;
}
};
1498. 满足条件的子序列数目 - 力扣(LeetCode)
timer:2025-6-20
class Solution {
public:
int numSubseq(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
int n = nums.size();
const int MOD = 1e9 + 7;
// 预处理2的幂次
vector<int> pow2(n, 1);
for (int i = 1; i < n; i++) {
pow2[i] = (pow2[i-1] * 2) % MOD;
}
int ans = 0;
int left = 0, right = n - 1;
while (left <= right) {
if (nums[left] + nums[right] > target) {
right--;
} else {
// 以left为最小值,right为最大值的子数组的所有子序列都满足条件
ans = (ans + pow2[right - left]) % MOD;
left++;
}
}
return ans;
}
};
594. 最长和谐子序列 - 力扣(LeetCode)
timer:2025-6-30
class Solution {
public:
int findLHS(vector<int>& nums) {
// 定义哈希表统计每个数字出现的次数
unordered_map<int, int> cnt;
// 第一次遍历数组统计频次
for(int x : nums){
cnt[x]++;
}
int ans = 0;
// 第二次遍历哈希表,查找每个数x及其后续数x+1的频次之和
for(auto& [x, c] : cnt){
// 检查是否存在x+1,确保和谐子序列存在
if(cnt.contains(x + 1)){
// 更新最长和谐子序列的长度
ans = max(ans, c + cnt[x + 1]);
}
}
return ans;
}
};
2025-7:
3330. 找到初始输入字符串 I - 力扣(LeetCode)
timer:2025-7-1
// class Solution {
// public:
// int possibleStringCount(string word) {
// int ans = 0;
// vector<int> cnt(26, 0);
// char pre = word[0];
// for(int i = 1; i < word.size(); i++){
// if(word[i] == pre){
// cnt[word[i] - 'a']++;
// }
// pre = word[i];
// }
// for(int i = 0; i < 26; i++){
// if(cnt[i] >= 1){
// ans += cnt[i];
// }
// }
// return ans + 1;
// }
// };
class Solution {
public:
int possibleStringCount(string word) {
int ans = 1;
for (int i = 1; i < word.length(); i++) {
if (word[i - 1] == word[i]) {
ans++;
}
}
return ans;
}
};
3333. 找到初始输入字符串 II - 力扣(LeetCode)
timer:2025-7-2
class Solution {
public:
int possibleStringCount(string word, int k) {
int n = word.size();
if (n < k) { // 无法满足要求
return 0;
}
const int MOD = 1'000'000'007;
vector<int> cnts;
long long ans = 1;
int cnt = 0;
for (int i = 0; i < n; i++) {
cnt++;
if (i == n - 1 || word[i] != word[i + 1]) {
// 如果 cnt = 1,这组字符串必选,无需参与计算
if (cnt > 1) {
if (k > 0) {
cnts.push_back(cnt - 1);
}
ans = ans * cnt % MOD;
}
k--; // 注意这里把 k 减小了
cnt = 0;
}
}
if (k <= 0) {
return ans;
}
int m = cnts.size();
vector f(m + 1, vector<int>(k));
ranges::fill(f[0], 1);
vector<int> s(k + 1);
for (int i = 0; i < m; i++) {
// 计算 f[i] 的前缀和数组 s
for (int j = 0; j < k; j++) {
s[j + 1] = (s[j] + f[i][j]) % MOD;
}
// 计算子数组和
for (int j = 0; j < k; j++) {
f[i + 1][j] = (s[j + 1] - s[max(j - cnts[i], 0)]) % MOD;
}
}
return (ans - f[m][k - 1] + MOD) % MOD; // 保证结果非负
}
};
3304. 找出第 K 个字符 I - 力扣(LeetCode)
timer:2025-7-3
class Solution {
public:
char kthCharacter(int k) {
return 'a' + bitset<32>(k - 1).count();
}
};
3307. 找出第 K 个字符 II - 力扣(LeetCode)
timer:2025-7-4
class Solution {
public:
// 递归函数:返回经过一系列操作后生成的字符串中的第k个字符
// 每次操作会将当前字符串长度翻倍,并对右半部分的每个字符应用转换操作
char kthCharacter(long long k, vector<int>& operations) {
// 基准情况:没有任何操作时,字符串仅包含初始字符 'a'
if (operations.empty()) {
return 'a';
}
// 获取并移除最后一次操作(注意:这会修改原始向量)
int op = operations.back();
operations.pop_back();
// 当前剩余操作次数,决定了当前字符串的长度为 2^m(m为原始操作次数)
int m = operations.size();
// 判断k位于当前字符串的左半部分还是右半部分
// 当前字符串长度为 2^(m+1),左半部分范围是 1~2^m,右半部分范围是 2^m+1~2^(m+1)
if (m >= 63 || k <= (1LL << m)) {
// 情况1:k在左半部分(或n过大导致溢出)
// 左半部分的字符与上一次操作后的字符相同,无需修改
return kthCharacter(k, operations);
} else {
// 情况2:k在右半部分
// 右半部分的字符是由左半部分对应位置的字符 + op 转换而来
// 例如:如果左半部分对应字符是 'a',op=1,则右半部分对应字符是 'b'
char leftChar = kthCharacter(k - (1LL << m), operations);
return 'a' + (leftChar - 'a' + op) % 26;
}
}
};
class Solution {
public:
char kthCharacter(long long k, vector<int>& operations) {
// 因为题目中k从1开始计数,而代码中使用0-based索引,所以减1
k--;
// 计算k的二进制表示的最高有效位的位置,确定需要考虑的操作次数
int m = bit_width((uint64_t) k);
// 初始化增量值,用于确定最终字符在字母表中的位置
int inc = 0;
// 从最高有效位到最低有效位遍历k的每一位
for (int i = m - 1; i >= 0; i--) {
// 如果k的第i位是1,则将对应的operations[i]累加到inc中
// k >> i右移i位后与1进行按位与操作,结果为1表示该位为1
inc += k >> i & operations[i];
}
// 将累加的增量值对26取模,得到字母表中的偏移量,再加上'a'得到对应的字符
return 'a' + inc % 26;
}
};
1394. 找出数组中的幸运数 - 力扣(LeetCode)
// 方法一:哈希表统计法
// 时间复杂度O(n),空间复杂度O(max(arr))
class Solution {
public:
int findLucky(vector<int>& arr) {
// 利用数组下标作为哈希表,统计每个数出现的次数
// 题目保证数组元素范围[1,500],故开510大小的数组
vector<int> cnt(510, 0);
int ans = -1; // 初始化结果为-1,表示未找到幸运数
// 遍历数组,统计每个数出现的次数
for (int x : arr)
cnt[x]++;
// 再次遍历数组,检查每个数x是否满足出现次数等于自身
// 若满足则更新结果为较大值
for (int x : arr) {
if (cnt[x] == x)
ans = max(ans, x);
}
return ans;
}
};
// 方法二:优化空间的数组统计法
// 时间复杂度O(n),空间复杂度O(n)
class Solution {
public:
int findLucky(vector<int>& arr) {
int n = arr.size();
// 数组大小设为n+1,因为幸运数最大可能为n(当所有元素都是n时)
vector<int> cnt(n + 1);
// 遍历数组,仅统计不超过n的元素出现次数
// 因为超过n的元素不可能成为幸运数(出现次数不可能达到n+1)
for(int x : arr)
if(x <= n)
cnt[x]++;
// 从大到小遍历可能的幸运数,找到第一个满足条件的数即为最大幸运数
for(int i = n; i > 0; i--)
if(cnt[i] == i)
return i;
return -1; // 未找到符合条件的幸运数
}
};
1865. 找出和为指定值的下标对 - 力扣(LeetCode)
timer:2025-7-6
class FindSumPairs {
vector<int> nums1, nums2; // 存储两个输入数组
unordered_map<int, int> cnt; // 记录nums2中每个元素的出现次数
public:
// 构造函数,初始化两个数组并统计nums2中元素的频率
FindSumPairs(vector<int>& nums1, vector<int>& nums2) {
this->nums1 = nums1;
this->nums2 = nums2;
for (int x : nums2) {
cnt[x]++; // 统计nums2中每个元素的出现次数
}
}
// 将nums2[index]的值增加val
void add(int index, int val) {
cnt[nums2[index]]--; // 更新前先减少原数值的计数
nums2[index] += val; // 增加指定位置的值
cnt[nums2[index]]++; // 更新后增加新数值的计数
}
// 计算满足nums1[i] + nums2[j] == tot的数对数量
int count(int tot) {
int ans = 0;
for(int x : nums1){ // 遍历nums1中的每个元素
ans += cnt[tot - x]; // 累加nums2中存在的补数的出现次数
}
return ans;
}
};
/**
* Your FindSumPairs object will be instantiated and called as such:
* FindSumPairs* obj = new FindSumPairs(nums1, nums2);
* obj->add(index,val);
* int param_2 = obj->count(tot);
*/
1353. 最多可以参加的会议数目 - 力扣(LeetCode)
timer:2025-7-7
class Solution {
public:
int maxEvents(vector<vector<int>>& events) {
// 边界条件:如果没有会议,直接返回0
if (events.empty()) return 0;
// 1、计算最大结束时间,确定遍历的天数上限
int maxDay = 0;
for (const auto& e : events) {
maxDay = max(maxDay, e[1]);
}
// 2、按会议开始时间分组 (开始天 -> 结束天列表)
vector<vector<int>> startGroups(maxDay + 2); // +2 确保足够大
for (const auto& e : events) {
startGroups[e[0]].push_back(e[1]);
}
// 最小堆:存储当前可参加会议的结束时间
priority_queue<int, vector<int>, greater<>> minHeap;
int attended = 0;
int currentDay = 1; // 从第1天开始遍历,更符合实际场景
// 3、遍历所有可能的天数,直到处理完所有会议
while (currentDay <= maxDay || !minHeap.empty()) {
// 如果堆为空且当天没有会议,跳到下一个有会议的日期
if (minHeap.empty() && currentDay <= maxDay && startGroups[currentDay].empty()) {
currentDay++;
continue;
}
// 将当天开始的会议加入堆中
for (int endDay : startGroups[currentDay]) {
minHeap.push(endDay);
}
// 移除所有过期的会议(结束时间早于当前天)
while (!minHeap.empty() && minHeap.top() < currentDay) {
minHeap.pop();
}
// 如果有可用会议,选择结束最早的参加
if (!minHeap.empty()) {
attended++;
minHeap.pop();
}
currentDay++;
}
return attended;
}
};
1751. 最多可以参加的会议数目 II - 力扣(LeetCode)
timer:2025-7-8
// dp + 二分
class Solution {
public:
int maxValue(std::vector<std::vector<int>>& events, int k) {
// 1、处理边界情况:若只能参加一个活动,直接返回最大价值
if (k == 1) {
return max_element(events.begin(), events.end(),
[](const auto& a, const auto& b) { return a[2] < b[2]; })->at(2);
}
// 2、按活动结束时间排序,确保动态规划的无后效性
sort(events.begin(), events.end(),
[](const auto& a, const auto& b) { return a[1] < b[1]; });
int n = events.size();
// dp数组:f[i][j]表示前i个活动中选择最多j个不重叠活动的最大价值
vector<vector<int>> f(n + 1, vector<int>(k + 1, 0));
for (int i = 0; i < n; ++i) {
// 3、二分查找:找到第一个结束时间 >= 当前活动开始时间的活动索引h
// 即h-1是最后一个结束时间 < 当前活动开始时间的活动
int h = lower_bound(
events.begin(), events.begin() + i,
events[i][0],
[](const auto& e, int start) { return e[1] < start; }
) - events.begin();
// 4、状态转移:对于每个可能的j,选择当前活动或不选
for (int j = 1; j <= k; ++j) {
// 不选当前活动:继承前i个活动选j个的最大价值
// 选当前活动:前h个活动选j-1个的最大价值 + 当前活动价值
f[i + 1][j] = max(f[i][j], f[h][j - 1] + events[i][2]);
}
}
return f[n][k]; // 返回前n个活动中选择最多k个的最大价值
}
};
3439. 重新安排会议得到最多空余时间 I - 力扣(LeetCode)
timer:2025-7-9
class Solution {
public:
int maxFreeTime(int eventTime, int k, vector<int>& startTime, vector<int>& endTime) {
int n = startTime.size();
auto get = [&](int i) -> int {
if (i == 0) // 第 1 个事件的空闲时间 = 在事件发生之前的时间
return startTime[0];
if (i == n) // 第 n 个事件的空闲时间 = 活动的总时间 - 事件最后结束的时间
return eventTime - endTime[n - 1];
// 第 i 个事件的空闲时间 = 当前事件的开始时间 - 上一个时间的结束时间
return startTime[i] - endTime[i - 1];
};
int s = 0, ans = 0;
for(int i = 0; i <= n; i++){
// 进窗口
s += get(i); // 获取到第 i 个事件的空闲时间
if (i < k) // 空闲时间段数不够
continue;
// 更新答案
ans = max(ans, s);
// 出窗口
s -= get(i - k); // 减去最左侧的空闲时间
}
return ans;
}
};
3440. 重新安排会议得到最多空余时间 II - 力扣(LeetCode)
timer:2025-7-10
class Solution {
public:
int maxFreeTime(int eventTime, vector<int>& startTime,
vector<int>& endTime) {
int n = startTime.size();
if (n == 0)
return eventTime; // 无事件时,整个时间段都是空闲
// 计算第i个空闲段的长度(共n+1个空闲段)
// i=0: 活动开始到第一个事件开始的时间
// i=n: 最后一个事件结束到活动结束的时间
// 其他i: 第i个事件结束到第i+1个事件开始的时间
auto get = [&](int i) -> int {
if (i == 0)
return startTime[0];
if (i == n)
return eventTime - endTime[n - 1];
return startTime[i] - endTime[i - 1];
};
// 维护最大的三个空闲段索引(贪心策略的关键)
int max1 = 0, max2 = -1, max3 = -1;
for (int i = 1; i <= n; i++) {
int current = get(i);
if (current > get(max1)) {
max3 = max2;
max2 = max1;
max1 = i;
} else if (max2 < 0 || current > get(max2)) {
max3 = max2;
max2 = i;
} else if (max3 < 0 || current > get(max3)) {
max3 = i;
}
}
int ans = 0;
// 枚举移除每个事件后的最大空闲时间
for (int i = 0; i < n; i++) {
int len = endTime[i] - startTime[i]; // 当前事件持续时间
// 判断是否可以将当前事件的持续时间加入空闲时间
// 条件:事件不在最大/次大空闲段旁边,或者事件持续时间足够小
if (((i != max1 && i + 1 != max1) && len <= get(max1)) ||
((i != max2 && i + 1 != max2) && len <= get(max2)) ||
(len <= get(max3))) {
// 情况1:可以利用当前事件的持续时间
ans = max(ans, get(i) + len + get(i + 1));
} else {
// 情况2:只能合并前后空闲段(原代码此处存在逻辑问题,正确实现应为(end[i+1]-start[i-1]))
// 此处保留原代码逻辑,但实际应使用合并后的真实长度计算
ans = max(ans, get(i) + get(i + 1));
}
}
return ans;
}
};
3169. 无需开会的工作日 - 力扣(LeetCode)
class Solution {
public:
int countDays(int days, vector<vector<int>>& meetings) {
sort(meetings.begin(), meetings.end(), [](auto& e1, auto& e2) {
return e1[0] < e2[0];}); // 按会议开始时间排序
int start = 1, end = 0; // 当前合并区间的左右端点
for (auto& p : meetings) {
if (p[0] > end) { // 新区间与当前合并区间不重叠
days -= end - start + 1; // 扣除当前合并区间的天数
start = p[0]; // 更新合并区间的起点为新区间的起点
}
end = max(end, p[1]); // 更新合并区间的终点
}
days -= end - start + 1; // 扣除最后一个合并区间的天数
return days;
}
};
1900. 最佳运动员的比拼回合 - 力扣(LeetCode)(⭐)
** timer:2025-7-12 **
class Solution {
public:
vector<int> earliestAndLatest(int n, int firstPlayer, int secondPlayer) {
// 预处理:检查输入合法性
if (firstPlayer <= 0 || secondPlayer <= 0 || firstPlayer >= secondPlayer || secondPlayer > n) {
return {-1, -1}; // 非法输入返回错误标记
}
// 记忆化数组:memo[n][a][b] 存储当总人数为n,A在位置a,B在位置b时的最早和最晚回合
vector memo(n + 1, vector(n + 1, vector<pair<int, int>>(n + 1, {0, 0})));
// 核心DFS函数,使用lambda表达式实现递归
function<pair<int, int>(int, int, int)> dfs = [&](int n, int a, int b) -> pair<int, int> {
// 检查是否已缓存结果
if (memo[n][a][b].first != 0) {
return memo[n][a][b];
}
// 基础情况:A和B在当前回合相遇
if (a + b == n + 1) {
return memo[n][a][b] = {1, 1};
}
// 确保a在左半部分,b在右半部分,简化状态空间
if (a + b > n + 1) {
return dfs(n, n + 1 - b, n + 1 - a);
}
int m = (n + 1) / 2; // 下一轮人数
int earliest = INT_MAX, latest = 0;
// 计算可能的中间保留人数范围
int minMid = b <= m ? 0 : b - (n / 2 + 1);
int maxMid = b <= m ? b - a : m - a;
// 枚举A左边和AB中间保留的人数
for (int left = 0; left < a; ++left) {
for (int mid = minMid; mid < maxMid; ++mid) {
// 剪枝:如果剩余位置不足,跳过当前组合
if (left + mid + 2 > m) continue;
auto [nextE, nextL] = dfs(m, left + 1, left + mid + 2);
earliest = min(earliest, nextE);
latest = max(latest, nextL);
}
}
// 存储结果并返回
return memo[n][a][b] = {earliest + 1, latest + 1};
};
auto [earliest, latest] = dfs(n, firstPlayer, secondPlayer);
return {earliest, latest};
}
};
2410. 运动员和训练师的最大匹配数 - 力扣(LeetCode)
timer:2025-7-13
class Solution {
public:
int matchPlayersAndTrainers(vector<int>& players, vector<int>& trainers) {
// 首先对选手和训练师的能力值数组进行排序
// 使用标准库的sort函数,确保数组按非递减顺序排列
sort(players.begin(), players.end());
sort(trainers.begin(), trainers.end());
int n = players.size(); // 选手数量
int m = trainers.size(); // 训练师数量
int matches = 0; // 记录匹配成功的数量
int trainerIdx = 0; // 指向当前训练师的索引
// 遍历每个选手,为其寻找合适的训练师
for (int player : players) {
// 跳过能力值不足以匹配当前选手的训练师
while (trainerIdx < m && trainers[trainerIdx] < player) {
trainerIdx++;
}
// 如果所有训练师都已遍历完,无法继续匹配,退出循环
if (trainerIdx >= m) {
break;
}
// 找到合适的训练师,匹配成功,计数器加1
matches++;
// 训练师索引后移,指向下一位训练师
trainerIdx++;
}
return matches;
}
};
1290. 二进制链表转整数 - 力扣(LeetCode)
timer:2025-7-14
class Solution {
public:
// 功能:将二进制链表转换为对应的十进制整数
// 输入:链表头指针head,每个节点的值为0或1
// 输出:对应的十进制整数
// 优化点:使用位运算替代乘法,提高性能
int getDecimalValue(ListNode* head) {
int result = 0;
while (head) {
// 左移一位相当于乘以2,然后加上当前节点的值
result = (result << 1) | head->val;
head = head->next;
}
return result;
}
};
3136. 有效单词 - 力扣(LeetCode)
timer:2025-7-15
class Solution {
public:
bool isValid(string word) {
// 检查字符串长度是否至少为3,不满足则直接判定无效
if (word.size() < 3) {
return false;
}
// 标记数组:f[0]表示是否存在辅音字母,f[1]表示是否存在元音字母
bool f[2] = {false};
// 遍历字符串中的每个字符
for (char ch : word) {
// 如果当前字符是字母
if (isalpha(ch)) {
// 转换为小写以便统一处理元音判断
ch = tolower(ch);
// 判断是否为元音字母(a,e,i,o,u),并更新对应标记
f[ch == 'a' || ch == 'e' || ch == 'i' || ch == 'o' || ch == 'u'] = true;
}
// 如果当前字符不是数字(此时也不是字母)
else if (!isdigit(ch)) {
// 字符串包含非法字符(既非字母也非数字),判定无效
return false;
}
}
// 必须同时存在元音字母和辅音字母才有效
return f[0] && f[1];
}
};
3201. 找出有效子序列的最大长度 I - 力扣(LeetCode)
timer:2025-7-16
// 方法一 动态规划
// class Solution {
// public:
// int maximumLength(vector<int>& nums) {
// int ans = 2;
// vector<vector<int>> dp(2, vector<int>(2, 0)); // dp[y][x]表示最后两个元素模k分别为y和x的子序列的最大长度
// for (int x : nums) {
// int mod_x = x % 2; // 当前元素对 2 取模的结果
// // 遍历所有可能的前一个元素的模值y
// for (int y = 0; y < 2; y++) {
// // 状态转移:如果当前元素的模为mod_x,前一个元素的模为y,
// // 则可以将当前元素添加到以y和mod_x为结尾的子序列中
// // 新的子序列长度为dp[mod_x][y] + 1
// dp[y][mod_x] = dp[mod_x][y] + 1;
// // 更新最大长度
// ans = max(ans, dp[y][mod_x]);
// }
// }
// return ans;
// }
// };
// 方法二
class Solution {
public:
int maximumLength(vector<int>& nums) {
// 由于题目隐含模2条件,元素奇偶性只有0(偶)和1(奇)两种情况
// 需要考虑三种可能的连续子序列:
// 1. 奇偶交替序列(如1,2,1,2)
// 2. 全奇数序列(如1,3,5)
// 3. 全偶数序列(如2,4,6)
int c1 = 1; // 记录奇偶交替序列的最大长度,初始为1(单个元素)
int c2 = 0; // 记录当前连续奇数序列的长度
int c3 = 0; // 记录当前连续偶数序列的长度
int pre = nums[0]; // 记录前一个元素的奇偶性
for(int x : nums){
// 统计整个数组中奇数的总数
if(x % 2 == 0){
c3++;
} else {
c2++;
}
// 统计奇偶交替次数
if(x % 2 != pre % 2){
c1++;
pre = x;
}
}
// 返回三种情况中的最大值
return ranges::max({c1, c2, c3});
}
};
3202. 找出有效子序列的最大长度 II - 力扣(LeetCode)
timer:2025-7-17
/*
(a + b) % k == (b + c) % k == (c + d) % k
=> a % k == c % k == d % k
=> 子序列的奇数项取模相等
子序列的偶数项取模相等
**子序列是个取模交替的序列**
=> 知道子序列的前两个数,就知道整个子序列
知道子序列的后两个数,就知道整个子序列
=> dp[y][x]:为最后两个数对 k 取模的值为 y 和 x 的子序列长度
dp[y][x] = dp[x][y] + 1 **交替**
*/
// class Solution {
// public:
// int maximumLength(vector<int>& nums, int k) {
// int ans = 2;
// vector<vector<int>> dp(k, vector<int>(k, 0)); // dp[y][x]表示最后两个元素模k分别为y和x的子序列的最大长度
// for (int x : nums) {
// int mod_x = x % k; // 当前元素对k取模的结果
// // 遍历所有可能的前一个元素的模值y
// for (int y = 0; y < k; y++) {
// // 状态转移:如果当前元素的模为mod_x,前一个元素的模为y,
// // 则可以将当前元素添加到以y和mod_x为结尾的子序列中
// // 新的子序列长度为dp[mod_x][y] + 1
// dp[y][mod_x] = dp[mod_x][y] + 1;
// // 更新最大长度
// ans = max(ans, dp[y][mod_x]);
// }
// }
// return ans;
// }
// };
class Solution {
public:
int maximumLength(vector<int>& nums, int k) {
int maxLen = 0;
// 枚举所有可能的余数 m,寻找满足 (prev + curr) % k == m 的最长子序列
for (int m = 0; m < k; m++) {
vector<int> dp(k, 0); // dp[x] 表示当前余数为x时的最长子序列长度
for (int x : nums) {
int mod = x % k;
// 计算前一个数需要满足的余数条件:(prev + mod) % k == m
int requiredPrev = (m - mod + k) % k;
// 更新当前余数的最长子序列长度
dp[mod] = dp[requiredPrev] + 1;
// 更新全局最长长度
maxLen = max(maxLen, dp[mod]);
}
}
return maxLen;
}
};
2163. 删除元素后和的最小差值 - 力扣(LeetCode)
timer:2025-7-18
class Solution {
public:
long long minimumDifference(std::vector<int>& nums) {
int m = nums.size();
int n = m / 3;
// ===== 预处理后缀最大和:维护后n个元素的最大和 =====
// 使用最小堆维护后n个最大元素
priority_queue<int, vector<int>, greater<>> minHeap;
long long sum = 0;
// 初始化:将最后n个元素加入最小堆并计算总和
for (int i = m - n; i < m; ++i) {
minHeap.push(nums[i]);
sum += nums[i];
}
// sufMax[i] 表示从索引i到数组末尾的n个元素的最大和
vector<long long> sufMax(m - n + 1);
sufMax[m - n] = sum;
// 从右向左遍历,逐步扩展窗口,维护最大和
for (int i = m - n - 1; i >= n; --i) {
int current = nums[i];
// 若当前元素大于堆顶元素(当前最小值),则替换堆顶并更新和
if (current > minHeap.top()) {
sum += current - minHeap.top();
minHeap.pop();
minHeap.push(current);
}
sufMax[i] = sum;
}
// ===== 预处理前缀最小和:维护前n个元素的最小和 =====
// 使用最大堆维护前n个最小元素
priority_queue<int> maxHeap;
long long preMin = 0;
// 初始化:将前n个元素加入最大堆并计算总和
for (int i = 0; i < n; ++i) {
maxHeap.push(nums[i]);
preMin += nums[i];
}
// 初始答案:前n个元素的和减去剩余部分的最大和
long long ans = preMin - sufMax[n];
// 遍历中间部分元素,动态调整前缀最小和并计算最小差值
for (int i = n; i < m - n; ++i) {
int current = nums[i];
// 若当前元素小于堆顶元素(当前最大值),则替换堆顶并更新和
if (current < maxHeap.top()) {
preMin += current - maxHeap.top();
maxHeap.pop();
maxHeap.push(current);
}
// 更新最小差值:前缀最小和 减去 剩余部分的最大和
ans = min(ans, preMin - sufMax[i + 1]);
}
return ans;
}
};
1233. 删除子文件夹 - 力扣(LeetCode)
timer:2025-7-19
// 方法一:字典序排序
class Solution {
public:
vector<string> removeSubfolders(vector<string>& folder) {
// 首先对文件夹路径列表进行字典序排序
sort(folder.begin(), folder.end());
// 初始化结果列表,先将排序后的第一个路径加入
vector<string> ans = {folder[0]};
// 遍历排序后的文件夹路径列表
for (int i = 1; i < folder.size(); ++i) {
// 获取当前结果列表中最后一个路径的长度
int m = ans.back().size();
// 获取当前遍历到的路径的长度
int n = folder[i].size();
// 判断条件:如果当前结果列表中最后一个路径长度大于等于当前遍历路径的长度
// 或者 当前结果列表中最后一个路径不等于当前遍历路径的前m个字符 或者 当前遍历路径的第m个字符不是'/'
// 则将当前遍历路径加入结果列表
if (m >= n || !(ans.back() == folder[i].substr(0, m) && folder[i][m] == '/')) {
ans.emplace_back(folder[i]);
}
}
// 返回结果列表
return ans;
}
};
// 方法二:字典树
// class Trie {
// public:
// // 插入文件夹路径到Trie树
// void insert(int fid, const string& f) {
// Trie* node = this;
// vector<string> parts = split(f, '/');
// // 从i=1开始,跳过第一个空字符串(因路径以'/'开头)
// for (int i = 1; i < parts.size(); ++i) {
// const string& part = parts[i];
// if (!node->children.count(part)) {
// node->children[part] = new Trie();
// }
// node = node->children[part];
// }
// node->fid = fid; // 标记该节点为一个完整路径的末尾
// }
// // 深度优先搜索Trie树,收集所有父文件夹的索引
// vector<int> search() const {
// vector<int> res;
// function<void(const Trie*)> dfs = [&](const Trie* node) {
// if (node->fid != -1) {
// res.push_back(node->fid);
// return; // 剪枝:遇到父文件夹则停止搜索其子文件夹
// }
// for (const auto& [_, child] : node->children) {
// dfs(child);
// }
// };
// dfs(this);
// return res;
// }
// private:
// // 按分隔符分割字符串
// vector<string> split(const string& s, char delim) const {
// vector<string> res;
// stringstream ss(s);
// string part;
// while (getline(ss, part, delim)) {
// res.push_back(part);
// }
// return res;
// }
// unordered_map<string, Trie*> children; // 使用智能指针管理子节点
// int fid = -1; // 文件夹索引,-1表示非完整路径
// };
// class Solution {
// public:
// // 移除所有子文件夹,只保留父文件夹
// vector<string> removeSubfolders(vector<string>& folder) {
// Trie trie; // 栈上分配Trie对象,自动管理内存
// // 将所有文件夹路径插入Trie树
// for (int i = 0; i < folder.size(); ++i) {
// trie.insert(i, folder[i]);
// }
// // 搜索Trie树,收集所有父文件夹的索引
// vector<string> ans;
// for (int i : trie.search()) {
// ans.push_back(folder[i]);
// }
// return ans;
// }
// };
1948. 删除系统中的重复文件夹 - 力扣(LeetCode)
timer:2025-7-20
// Trie树节点类,用于表示文件系统中的文件夹结构
class Trie {
public:
// 插入文件夹路径到Trie树
// path: 文件夹路径,例如 ["a", "b", "c"] 表示路径 /a/b/c
void insert(const vector<string>& path) {
Trie* node = this;
for (const string& part : path) {
// 如果当前节点的子节点中不存在该部分路径,则创建新节点
if (!node->children.count(part)) {
node->children[part] = new Trie();
}
// 移动到子节点
node = node->children[part];
// 设置节点名称
node->name = part;
}
}
// 生成子树的表达式字符串,并标记重复的文件夹结构
// expr_to_node: 用于记录已存在的子树表达式及其对应节点
string gen_expr(unordered_map<string, Trie*>& expr_to_node) {
// 叶子节点(无子文件夹)的表达式即为文件夹名称
if (children.empty()) {
return name;
}
// 收集所有子树的表达式
vector<string> expr;
for (auto& [part, child] : children) {
expr.push_back("(" + child->gen_expr(expr_to_node) + ")");
}
// 对子树表达式进行排序,确保同构子树生成相同的表达式
ranges::sort(expr);
// 拼接排序后的子树表达式
string sub_tree_expr;
for (const string& e : expr) {
sub_tree_expr += e;
}
// 检测重复结构:如果该表达式已存在,则标记当前节点和之前节点为删除
if (expr_to_node.count(sub_tree_expr)) {
expr_to_node[sub_tree_expr]->deleted = true;
this->deleted = true;
} else {
// 记录新的子树表达式及其对应节点
expr_to_node[sub_tree_expr] = this;
}
// 返回当前节点的完整表达式:节点名称 + 子树表达式
return name + sub_tree_expr;
}
// 深度优先搜索Trie树,收集所有未被删除的路径
vector<vector<string>> search() {
vector<vector<string>> res; // 结果路径集合
vector<string> path; // 当前遍历路径
// 递归DFS函数
function<void(const Trie*)> dfs = [&](const Trie* node) {
// 如果节点已被删除,则跳过
if (node->deleted) return;
// 如果不是根节点,则将当前节点加入路径
if (!node->name.empty()) {
path.push_back(node->name);
res.push_back(path); // 收集当前路径
}
// 递归处理所有子节点
for (const auto& [part, child] : node->children) {
dfs(child);
}
// 回溯:移除当前节点,以便处理下一个兄弟节点
if (!path.empty()) {
path.pop_back();
}
};
// 从根节点开始DFS
dfs(this);
return res;
}
public:
unordered_map<string, Trie*> children; // 子文件夹映射
bool deleted = false; // 删除标记:true表示该文件夹应被删除
string name; // 文件夹名称
};
class Solution {
public:
// 删除重复文件夹,返回剩余的唯一路径
vector<vector<string>> deleteDuplicateFolder(vector<vector<string>>& paths) {
Trie trie; // 创建Trie树根节点
// 插入所有路径到Trie树
for (const auto& path : paths) {
trie.insert(path);
}
// 生成子树表达式并标记重复节点
unordered_map<string, Trie*> expr_to_node;
for (auto& [part, child] : trie.children) {
// 从根节点的子节点开始生成表达式(跳过根节点,因其无实际名称)
child->gen_expr(expr_to_node);
}
// 收集所有未被删除的路径
vector<vector<string>> result = trie.search();
return result;
}
};
1957. 删除字符使字符串变好 - 力扣(LeetCode)
timer:2025-7-21
class Solution {
public:
string makeFancyString(string s) {
string ans = "";
int count = 1;
for (int i = 0; i < s.size(); i++) {
if (i > 0 && s[i] == s[i-1]) {
count++;
if (count >= 3) {
continue; // 跳过第三个及之后的重复字符
}
} else {
count = 1; // 重置计数
}
ans += s[i];
}
return ans;
}
};
1695. 删除子数组的最大得分 - 力扣(LeetCode)
timer:2025-7-22
// 方法一:滑动窗口 + hash
class Solution {
public:
// 函数功能:找到数组中元素唯一的子数组(子数组中所有元素不重复),并返回其中元素和的最大值
int maximumUniqueSubarray(vector<int>& nums) {
// 用于存储当前子数组中的元素,快速判断元素是否已存在
unordered_set<int> path;
// ans存储最终结果(最大子数组和),tmp存储当前子数组的和,left是当前子数组的左边界索引
int ans = 0, tmp = 0, left = 0;
// 遍历数组中的每个元素,将当前元素作为子数组的右边界
for(int x : nums){
// 如果当前元素已在path中存在,说明出现重复
// 需要移动左边界,直到移除与当前元素相同的元素
while(path.contains(x)){
// 从path中移除左边界元素
path.erase(nums[left]);
// 从当前子数组和中减去左边界元素的值
tmp -= nums[left];
// 左边界右移,缩小子数组范围
left++;
}
// 将当前元素加入path,标记为已在当前子数组中
path.insert(x);
// 将当前元素值加入当前子数组和
tmp += x;
// 更新最大子数组和(取当前最大值和之前记录的最大值中的较大者)
ans = max(ans, tmp);
}
// 返回找到的最大唯一子数组和
return ans;
}
};
// 方法二:滑动窗口 + bool数组
class Solution {
public:
// 函数功能:找到数组中所有元素唯一的子数组,返回这些子数组中元素和的最大值
int maximumUniqueSubarray(vector<int>& nums) {
// ans存储最终结果(最大子数组和),tmp存储当前子数组的和,left是当前子数组的左边界索引
int ans = 0, tmp = 0, left = 0;
// 布尔数组作为哈希表使用,标记元素是否在当前子数组中(假设元素值范围在0~10009之间)
bool path[10010]{}; // 初始化所有元素为false
// 遍历数组,将当前元素x作为子数组的右边界
for(int x : nums){
// 如果当前元素x已在当前子数组中(存在重复)
while(path[x]){
// 移除左边界元素(标记为不在当前子数组中)
path[nums[left]] = false;
// 从当前子数组和中减去左边界元素的值
tmp -= nums[left];
// 左边界右移,缩小子数组范围
left++;
}
// 将当前元素x标记为在当前子数组中
path[x] = true;
// 将当前元素值加入当前子数组和
tmp += x;
// 更新最大子数组和(取当前和与历史最大值中的较大者)
ans = max(ans, tmp);
}
// 返回找到的最大唯一子数组和
return ans;
}
};
1717. 删除子字符串的最大得分 - 力扣(LeetCode)
timer:2025-7-23
class Solution {
public:
int maximumGain(string s, int x, int y) {
int res = 0;
char c1 = 'a', c2 = 'b';
// 确保优先处理收益更高的字符对
if (x < y) {
swap(x, y);
swap(c1, c2);
}
int cnt1 = 0, cnt2 = 0;
for (char ch : s) {
if (ch == c1) {
cnt1++; // 计数第一个字符
} else if (ch == c2) {
if (cnt1 > 0) {
res += x; // 优先匹配高收益组合
cnt1--;
} else {
cnt2++; // 记录无法匹配的第二个字符
}
} else {
// 遇到其他字符,结算当前积累的低收益组合
res += min(cnt1, cnt2) * y;
cnt1 = cnt2 = 0; // 重置计数器
}
}
// 结算字符串末尾剩余的组合
res += min(cnt1, cnt2) * y;
return res;
}
};
2322. 从树中删除边的最小分数 - 力扣(LeetCode)(⭐)
**timer:2025-7-24**
class Solution {
public:
/**
* 问题:将一棵树通过删除两条边分成三个子树,使得三个子树的异或和的最大值与最小值之差最小
* 输入:nums为每个节点的值,edges为树的边集合
* 输出:满足条件的最小差值
*/
int minimumScore(vector<int>& nums, vector<vector<int>>& edges) {
int n = nums.size();
// 构建邻接表表示树结构
vector<vector<int>> g(n);
for (auto& e : edges) {
int x = e[0], y = e[1];
g[x].push_back(y);
g[y].push_back(x);
}
// xr[x]:以x为根的子树的异或和
// in[x]:DFS进入x节点的时间戳(用于判断祖先关系)
// out[x]:DFS离开x节点的时间戳(用于判断祖先关系)
vector<int> xr(n), in(n), out(n);
int clock = 0; // 时间戳计数器
// 深度优先搜索,计算子树异或和与时间戳
auto dfs = [&](this auto&& dfs, int x, int fa) -> void {
in[x] = ++clock; // 记录进入时间
xr[x] = nums[x]; // 初始异或和为节点自身值
// 遍历所有子节点(排除父节点)
for (int y : g[x]) {
if (y != fa) {
dfs(y, x); // 递归处理子节点
xr[x] ^= xr[y]; // 当前节点异或和 += 子节点异或和
}
}
out[x] = clock; // 记录离开时间
};
dfs(0, -1); // 从根节点0开始DFS,父节点设为-1
/**
* 判断x是否为y的祖先节点
* 祖先节点特征:进入时间早于y,且离开时间晚于y
*/
auto is_ancestor = [&](int x, int y) -> bool {
return in[x] < in[y] && in[y] <= out[x];
};
int ans = INT_MAX; // 初始化答案为最大值
/**
* 枚举两条待删除的边:
* 每条边可以表示为"节点与其父节点的连接"
* 这里通过枚举两个节点x和y(x>y,避免重复),代表删除它们与各自父节点的边
*/
for (int x = 2; x < n; x++) {
for (int y = 1; y < x; y++) {
int a, b, c; // 三个子树的异或和
// 情况1:x是y的祖先,删除边后y的子树独立,x的子树减去y的部分也独立
if (is_ancestor(x, y)) {
a = xr[y]; // y为根的子树
b = xr[x] ^ a; // x为根但排除y子树的部分
c = xr[0] ^ xr[x]; // 剩余部分(根节点子树排除x的部分)
}
// 情况2:y是x的祖先,与情况1对称
else if (is_ancestor(y, x)) {
a = xr[x]; // x为根的子树
b = xr[y] ^ a; // y为根但排除x子树的部分
c = xr[0] ^ xr[y]; // 剩余部分
}
// 情况3:x和y分属不同子树,删除后各自独立,剩余部分为总异或减去两者
else {
a = xr[x]; // x为根的子树
b = xr[y]; // y为根的子树
c = xr[0] ^ a ^ b; // 剩余部分
}
// 更新最小差值
ans = min(ans, max({a, b, c}) - min({a, b, c}));
// 提前退出:差值为0是理论最小值,不可能更小
if (ans == 0) {
return 0;
}
}
}
return ans;
}
};
3487. 删除后的最大子数组元素和 - 力扣(LeetCode)
timer:2025-7-24
class Solution {
public:
// 计算不重复非负数的和,若无则返回最大负数
int maxSum(vector<int>& nums) {
unordered_set<int> seen; // 记录已出现的非负数
int sum = 0; // 不重复非负数的总和
int maxNeg = INT_MIN; // 最大的负数
for (int num : nums) {
if (num < 0) {
// 更新最大负数
maxNeg = max(maxNeg, num);
} else {
// 非负数且未出现过,加入总和
if (seen.insert(num).second) {
sum += num;
}
}
}
// 若无可用非负数,返回最大负数,否则返回总和
return seen.empty() ? maxNeg : sum;
}
};
3480. 删除一个冲突对后最大子数组数目 - 力扣(LeetCode) (⭐)
**timer:2025-7-26**
class Solution {
public:
long long maxSubarrays(int n, vector<vector<int>>& pairs) {
// conf[i]存储与i冲突的两个最小位置,初始值n+1表示无冲突
// 因元素编号为1~n,n+1是超出范围的安全值
vector<array<int, 2>> conf(n + 1, {n + 1, n + 1});
// 处理所有冲突对
for (auto& p : pairs) {
int a = p[0], b = p[1];
if (a > b) swap(a, b); // 确保a < b,统一处理顺序
// 仅保留a的两个最小冲突位置(更小的冲突位置限制更强)
if (b < conf[a][0]) {
conf[a][1] = conf[a][0]; // 原最小冲突位置退为第二小
conf[a][0] = b; // 更新最小冲突位置
} else if (b < conf[a][1]) {
conf[a][1] = b; // 更新第二小冲突位置
}
}
long long res = 0; // 最终结果:最大子数组数量
long long max_ext = 0; // 最大额外可分割数
long long ext = 0; // 当前连续区间的额外可分割数
int b0 = n + 1; // 当前最近的冲突位置(限制最严)
int b1 = n + 1; // 当前次近的冲突位置(次要限制)
// 从后往前遍历所有位置(1~n)
for (int i = n; i > 0; i--) {
int prev_b0 = b0; // 保存上一轮的最近冲突位置
// 合并当前位置i的冲突信息,更新b0和b1
for (int c : conf[i]) {
if (c < b0) { // 新冲突位置比当前最近的更近
b1 = b0; // 原最近冲突位置变为次近
b0 = c; // 更新最近冲突位置
} else if (c < b1) { // 新冲突位置介于最近和次近之间
b1 = c; // 更新次近冲突位置
}
}
// 累加基本可分割数:[i, b0)区间内可划分的子数组数量
res += b0 - i;
// 若最近冲突位置发生变化,说明进入新的区间,重置额外计数
if (b0 != prev_b0) {
ext = 0;
}
// 累加额外可分割数:基于次近冲突的额外分割空间
ext += b1 - b0;
// 更新最大额外可分割数
max_ext = max(max_ext, ext);
}
// 总结果 = 基本可分割数 + 最大额外可分割数
return res + max_ext;
}
};
2210. 统计数组中峰和谷的数量 - 力扣(LeetCode)
timer:2027-7-27
// class Solution {
// public:
// int countHillValley(vector<int>& nums) {
// // 步骤1:去重处理,保留相邻不相等的元素
// vector<int> arr;
// for (int num : nums) {
// if (arr.empty() || num != arr.back()) {
// arr.push_back(num);
// }
// }
// // 步骤2:若去重后数组长度小于3,无法形成峰或谷
// int m = arr.size();
// if (m < 3) {
// return 0;
// }
// // 步骤3和4:遍历中间元素,统计峰和谷的数量
// int count = 0;
// for (int i = 1; i < m - 1; ++i) {
// int left = arr[i - 1];
// int curr = arr[i];
// int right = arr[i + 1];
// // 判断是否为峰(大于两侧)或谷(小于两侧)
// if ((curr > left && curr > right) || (curr < left && curr <
// right)) {
// count++;
// }
// }
// return count;
// }
// };
class Solution {
public:
int countHillValley(vector<int>& nums) {
int ans = 0, n = nums.size();
// 用于标记上一个有效位置是否已经被计数
bool counted = false;
for (int i = 1; i < n - 1; i++) {
int l = i - 1, r = i + 1;
// 找到左侧最近的不相等元素
while (l >= 0 && nums[l] == nums[i]) {
l--;
}
// 找到右侧最近的不相等元素
while (r < n && nums[r] == nums[i]) {
r++;
}
// 两侧都需要有不相等的邻居
if (l < 0 || r >= n) {
continue;
}
// 判断是否为峰或谷
bool isHill = (nums[i] > nums[l] && nums[i] > nums[r]);
bool isValley = (nums[i] < nums[l] && nums[i] < nums[r]);
// 如果当前元素与前一个元素相等,且前一个已经被计数,则当前不重复计数
if (i > 1 && nums[i] == nums[i - 1]) {
if (counted) {
continue;
}
}
if (isHill || isValley) {
ans++;
counted = true;
} else {
counted = false;
}
}
return ans;
}
};
2044. 统计按位或能得到最大值的子集数目 - 力扣(LeetCode)
timer:2025-7-28
// 方法一: dfs
class Solution {
public:
int countMaxOrSubsets(vector<int>& nums) {
int count = 0; // 统计符合条件的子集数量
int max_or = 0; // 最大按位或结果
int n = nums.size();
// 计算数组所有元素的按位或,得到最大可能值
for (int num : nums) {
max_or |= num;
}
// 定义DFS lambda函数,使用递归遍历所有子集
// i: 当前处理的元素索引,cur_or: 当前子集的按位或结果
auto dfs = [&](this auto&& dfs, int i, int cur_or) {
// 递归终止条件:处理完所有元素
if (i == n) {
// 如果当前子集的按位或等于最大值,计数加1
if (cur_or == max_or) {
count++;
}
return;
}
// 不选当前元素,直接处理下一个
dfs(i + 1, cur_or);
// 选择当前元素,更新按位或结果后处理下一个
dfs(i + 1, cur_or | nums[i]);
};
// 从第0个元素开始,初始按位或为0(空集)
dfs(0, 0);
return count;
}
};
// 方法二: 二进制枚举
// class Solution {
// public:
// int countMaxOrSubsets(vector<int>& nums) {
// int count = 0; // 统计符合条件的子集数量
// int max_or = 0; // 最大按位或结果
// int n = nums.size();
// // 枚举所有非空子集(mask从1到2^n-1)
// // 每个mask的二进制表示对应一个子集,第i位为1表示选择nums[i]
// for (int mask = 1; mask < (1 << n); mask++) {
// int cur_or = 0; // 当前子集的按位或结果
// // 计算当前子集的按位或
// for (int i = 0; i < n; i++) {
// // 检查mask的第i位是否为1,若是则将nums[i]加入按位或计算
// if ((mask >> i) & 1) {
// cur_or |= nums[i];
// }
// }
// // 更新最大值和计数
// if (cur_or > max_or) {
// // 找到更大的按位或结果,更新最大值并重置计数
// max_or = cur_or;
// count = 1;
// } else if (cur_or == max_or) {
// // 与当前最大值相等,计数加1
// count++;
// }
// }
// return count;
// }
// };
2411. 按位或最大的最小子数组长度 - 力扣(LeetCode)
timer:2025-7-29
// 按位或操作具有单调性,即对一个数进行按位或操作后,结果不会变小
// 当我们从右向左更新数组时,每个位置j存储的是从j到当前i的按位或结果
// 当某个位置j的按位或结果不再变化时,说明再向左扩展子数组也不会增加按位或的值,因此可以停止
class Solution {
public:
vector<int> smallestSubarrays(vector<int>& nums) {
int n = nums.size();
// 结果数组,存储每个位置对应的最小子数组长度
// 初始化为1,因为每个元素自身至少构成一个长度为1的子数组
vector<int> ans(n, 1);
// 遍历数组,以当前索引i作为子数组的右端点
for(int i = 0; i < n; i++){
// x存储当前元素的值,用于后续按位或运算
int x = nums[i];
// 从i的左侧相邻元素开始,向左遍历
// 条件:j >= 0(不越界)且 (nums[j] | x) != nums[j](按位或运算能改变值)
for(int j = i - 1; j >= 0 && (nums[j] | x) != nums[j]; j--){
// 更新nums[j]为原nums[j]与x的按位或结果
// 此时nums[j]表示从j到i的所有元素的按位或结果
nums[j] |= x;
// 更新位置j对应的最小子数组长度
// 子数组为从j到i,长度为i-j+1
ans[j] = i - j + 1;
}
}
// 返回结果数组
return ans;
}
};
2419. 按位与最大的最长子数组 - 力扣(LeetCode)
timer:2025-7-30
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int longestSubarray(vector<int>& nums) {
int maxVal = 0; // 记录数组中的最大值
int maxLen = 0; // 记录最长连续最大值子数组的长度
int currLen = 0; // 记录当前连续最大值的长度
for (int num : nums) {
if (num > maxVal) {
// 遇到更大的元素,更新最大值
maxVal = num;
// 重置当前连续长度为1(当前元素本身)
currLen = 1;
// 最长长度也更新为1
maxLen = 1;
}
else if (num == maxVal) {
// 遇到相同的最大值,当前连续长度加1
currLen++;
// 更新最长长度(取当前最长和历史最长的较大值)
maxLen = max(maxLen, currLen);
}
else {
// 遇到较小值,重置当前连续长度
currLen = 0;
}
}
return maxLen;
}
};
2683. 相邻值的按位异或 - 力扣(LeetCode)
timer:2025-7-31
// #include <numeric> // 包含reduce函数
class Solution {
public:
/**
* 判断是否存在一个原始数组original,使得其相邻元素异或结果等于derived数组
* 已知条件:对于原始数组original和派生数组derived,满足derived[i] =
* original[i] ⊕ original[i+1] 其中original[n] = original[0](数组首尾相连)
*
* 核心逻辑推导:
* 1. 根据异或性质:a ⊕ a = 0,a ⊕ 0 = a,a ⊕ b ⊕ b = a
* 2. 对所有derived元素进行异或:
* derived[0] ⊕ derived[1] ⊕ ... ⊕ derived[n-1]
* = (original[0]⊕original[1]) ⊕ (original[1]⊕original[2]) ⊕ ... ⊕
* (original[n-1]⊕original[0])
* 3.
* 异或运算满足交换律和结合律,中间项original[1]到original[n-1]均出现两次,相互抵消(结果为0)
* 4. 最终简化为:original[0] ⊕ original[0] = 0
* 5. 因此,当且仅当derived数组所有元素异或和为0时,存在有效的original数组
*/
bool doesValidArrayExist(vector<int>& derived) {
// 使用reduce函数计算所有元素的异或和
// bit_xor()是异或运算的函数对象,初始值为0
return reduce(derived.begin(), derived.end(), 0, bit_xor<int>()) == 0;
}
};
2025-8:
118. 杨辉三角 - 力扣(LeetCode)
class Solution {
public:
// 函数功能:生成包含n行的杨辉三角(帕斯卡三角)
// 参数n:表示需要生成的杨辉三角的行数
// 返回值:二维向量,其中每个子向量代表杨辉三角的一行
vector<vector<int>> generate(int n) {
// 初始化结果二维向量,包含n个空的子向量(对应n行)
vector<vector<int>> ans(n);
// 遍历每一行,生成该行的元素(i为行索引,从0到n-1)
for(int i = 0; i < n; i++){
// 调整当前行(第i行)的大小为i+1(第0行1个元素,第1行2个元素...第i行i+1个元素)
// 并将所有元素初始化为1(杨辉三角每行首尾元素均为1)
ans[i].resize(i + 1, 1);
// 计算当前行中除首尾外的中间元素(j为列索引,从1到i-1)
// 因为首尾元素已经初始化为1,无需重复计算
for(int j = 1; j < i; j++){
// 杨辉三角性质:每个中间元素等于上一行中相邻两个元素之和
// 即当前行第j个元素 = 上一行第j-1个元素 + 上一行第j个元素
ans[i][j] = ans[i - 1][j - 1] + ans[i - 1][j];
}
}
// 返回生成的杨辉三角
return ans;
}
};
2561. 重排水果 - 力扣(LeetCode)
timer:2025-8-2
// class Solution {
// public:
// long long minCost(vector<int>& b1, vector<int>& b2) {
// unordered_map<int, int> cnt; // 统计元素数量差异:b1中+1,b2中-1
// for (int i = 0; i < b1.size(); ++i) {
// cnt[b1[i]]++;
// cnt[b2[i]]--;
// }
// vector<int> a, b; // a: b1需移走的元素;b: b2需移走的元素
// int minV = INT_MAX; // 全局最小值,用于中转交换
// for (auto& [v, c] : cnt) {
// if (c % 2 != 0) return -1; // 数量差为奇数,无法平分
// minV = min(minV, v);
// c /= 2; // 实际需移动的数量(正负表方向)
// if (c > 0) {
// for (int i = 0; i < c; ++i) a.push_back(v); // b1多的元素
// } else if (c < 0) {
// for (int i = 0; i < -c; ++i) b.push_back(v); // b2多的元素
// }
// }
// // 排序优化配对成本:a升序,b降序
// ranges::sort(a);
// ranges::sort(b, greater<int>());
// long long res = 0; // 总最小成本
// for (int i = 0; i < a.size(); ++i) {
// // 取直接交换(a[i]或b[i])与中转交换(2*minV)的最小值
// res += min({a[i], b[i], 2 * minV});
// }
// return res;
// }
// };
class Solution {
public:
long long minCost(vector<int>& b1, vector<int>& b2) {
// 统计元素数量差异:b1中出现+1,b2中出现-1
unordered_map<int, int> cnt;
for (int i = 0; i < b1.size(); ++i) {
cnt[b1[i]]++;
cnt[b2[i]]--;
}
vector<int> a; // 存储所有需要参与交换的元素(每个元素需交换的次数=其在差异中的占比)
int minV = INT_MAX; // 全局最小值,用于计算"中转交换"的成本(2*minV)
for (auto& [v, c] : cnt) {
// 若元素数量差为奇数,无法平分到两个篮子,直接返回-1
if (c % 2 != 0)
return -1;
minV = min(minV, v); // 更新全局最小值
c /= 2; // 实际需要交换的次数(原差异的一半)
// 将需要交换的元素加入数组a(数量为差异的绝对值)
// 例如:c=2表示b1多2个v,需移到b2;c=-3表示b2多3个v,需移到b1
int need = abs(c);
for (int i = 0; i < need; ++i) {
a.push_back(v);
}
}
// 核心优化:使用nth_element将数组a分为两部分
// 前半部分为较小的元素,后半部分为较大的元素(无需完全排序,比sort更高效)
int m = a.size() / 2; // 总交换对数(a的长度必为偶数)
ranges::nth_element(a, a.begin() + m);
long long res = 0; // 总最小成本
// 每对交换的最小成本:取"当前元素"与"2*minV(中转交换成本)"的较小值
// 只需要遍历前半部分(较小的元素),因为每对中较小的元素决定直接交换成本
for (int i = 0; i < m; ++i) {
res += min(a[i], 2 * minV);
}
return res;
}
};
2106. 摘水果 - 力扣(LeetCode)
timer:2025-8-3
// 1、向左走、向右走、先向左走再向右走、先向右走再向左走
// 2、计算出向左侧能走的最远位置 left >= startPos - k
// 3、先向左侧走计算出sum —— 初始化窗口
// 4、再向右侧走计算sum —— 进入窗口
// 5、若窗口大于k,最左侧就出窗口 —— 维护窗口大小
// ① bool checkLeft = ((startPos - fruits[left][0]) + (fruits[right][0] - fruits[left][0])) > k ? true : false;
// ② bool checkRight = (fruits[right][0] - startPos + (fruits[right][0] - fruits[left][0])) > k ? true : false;
// 6、更新答案
class Solution {
public:
int maxTotalFruits(vector<vector<int>>& fruits, int startPos, int k) {
int n = fruits.size();
int left;
// 2、计算出向左侧能走的最远位置 left >= startPos - k
left = ranges::lower_bound(fruits, startPos - k, {}, [](auto& f) { return f[0]; }) - fruits.begin();
int sum = 0, ans = 0;
// 3、先向左侧走计算出sum —— 初始化窗口 + 4、再向右侧走计算sum —— 进入窗口
for (int right = left; right < n && fruits[right][0] <= startPos + k; right++) {
sum += fruits[right][1];
// 5、若窗口大于k,最左侧就出窗口 —— 维护窗口大小
while (check(fruits[left][0], fruits[right][0], startPos, k)){
sum -= fruits[left][1];
left++;
}
// 6、更新答案
ans = max(ans, sum);
}
return ans;
}
// 封装判断函数:判断当前窗口是否超过最大步数k
// 参数:左边界位置、右边界位置、起始位置、最大步数
bool check(int leftPos, int rightPos, int startPos, int k) {
// 计算两种路径的步数:先左后右 与 先右后左
int checkLeft = (startPos - leftPos) + (rightPos - leftPos);
int checkRight = (rightPos - startPos) + (rightPos - leftPos);
// 两种路径都超过界时返回true
return checkLeft > k && checkRight > k;
}
};
904. 水果成篮 - 力扣(LeetCode)
timer:2025-8-4
class Solution {
public:
int totalFruit(vector<int>& fruits) {
// 滑动窗口
int n = fruits.size();
int ans = 0;
// 哈希表统计种类数
unordered_map<int, int> hash;
for (int left = 0, right = 0; right < n; right++) {
hash[fruits[right]]++; // 进窗口
while (hash.size() > 2) { // 判断
hash[fruits[left]]--; // 种类数--
if (hash[fruits[left]] == 0) // 该种类个数 == 0
hash.erase(fruits[left]); // 删除该种类
left++; // 出窗口
}
ans = max(ans, right - left + 1); // 更新结果
}
return ans;
}
};
3477. 水果成篮 II - 力扣(LeetCode)
timer:2025-8-5
// 方法一:简单暴力
// class Solution {
// public:
// int numOfUnplacedFruits(vector<int>& fruits, vector<int>& baskets) {
// int n = fruits.size();
// int ans = 0;
// for (int i = 0; i < n; i++) {
// int x = fruits[i];
// for (int j = 0; j < n; j++) {
// if (x <= baskets[j]) {
// baskets[j] = 0;
// x = 0;
// break;
// }
// }
// if (x != 0)
// ans++;
// }
// return ans;
// }
// };
// 方法二:线段树 —— 无序二分
// 线段树类:用于维护区间最大值,并支持查找第一个满足条件的元素并更新
class SegmentTree {
private:
vector<int> mmax; // 存储线段树各节点对应的区间最大值
// 维护节点值:当前节点的最大值 = 左子节点最大值 和 右子节点最大值 中的较大者
void maintain(int i) {
mmax[i] = max(mmax[2 * i], mmax[2 * i + 1]);
}
// 构建线段树(递归)
// 参数:tree-原始数组,i-当前节点索引,l-当前节点覆盖的区间左边界,r-当前节点覆盖的区间右边界
void build(const vector<int>& tree, int i, int l, int r) {
if (l == r) { // 叶子节点:区间只包含一个元素,最大值就是该元素
mmax[i] = tree[l];
return;
}
// 计算区间中点,避免(l + r)溢出,改用 l + (r - l)/2(右移1位等价于除以2)
int m = l + ((r - l) >> 1);
// 递归构建左子树(覆盖[l, m],节点索引为2*i)
build(tree, 2 * i, l, m);
// 递归构建右子树(覆盖[m+1, r],节点索引为2*i+1)
build(tree, 2 * i + 1, m + 1, r);
// 构建完成后维护当前节点的最大值
maintain(i);
}
public:
// 构造函数:初始化线段树
// 参数:tree-原始数组(此处为篮子容量数组)
SegmentTree(const vector<int>& tree) {
size_t n = tree.size();
mmax.resize(4 * n); // 线段树空间通常分配为原始数组的4倍,确保足够存储所有节点
build(tree, 1, 0, n - 1); // 从根节点(索引1)开始构建,覆盖区间[0, n-1]
}
// 查找区间内第一个 >= x 的元素,找到后将其更新为-1并返回索引;未找到返回-1
// 参数:i-当前节点索引,l-当前节点覆盖的区间左边界,r-当前节点覆盖的区间右边界,x-目标值(水果大小)
int findFirst(int i, int l, int r, int x) {
if (mmax[i] < x) // 当前区间的最大值小于x,整个区间都没有符合条件的元素
return -1;
if (l == r) { // 找到叶子节点(符合条件的元素)
mmax[i] = -1; // 更新为-1,表示该位置已使用(不能再放水果)
return l; // 返回该元素的索引
}
// 计算区间中点
int m = l + ((r - l) >> 1);
// 先在左子树中查找(优先找左侧的元素,保证"第一个"的逻辑)
int check = findFirst(2 * i, l, m, x);
if (check < 0) // 左子树未找到,再在右子树中查找
check = findFirst(2 * i + 1, m + 1, r, x);
// 查找后更新当前节点的最大值(因为子节点可能被修改)
maintain(i);
return check; // 返回找到的索引(或-1)
}
};
class Solution {
public:
// 计算未放置的水果数量
// 参数:fruits-水果大小数组,baskets-篮子容量数组
// 逻辑:每个水果需要放入一个容量>=自身大小的篮子,每个篮子只能用一次,返回无法放置的水果数
int numOfUnplacedFruits(vector<int>& fruits, vector<int>& baskets) {
SegmentTree t(baskets); // 用篮子容量数组构建线段树
int n = baskets.size(), ans = 0; // ans记录未放置的水果数
for (int x : fruits) { // 遍历每个水果
// 查找能放置当前水果的第一个篮子(容量>=x)
if (t.findFirst(1, 0, n - 1, x) < 0) {
ans++; // 找不到合适的篮子,未放置数量+1
}
}
return ans;
}
};
3479. 水果成篮 III - 力扣(LeetCode)
timer:2025-8-6
// 线段树类:用于维护区间最大值,并支持查找第一个满足条件的元素并更新
class SegmentTree {
private:
vector<int> mmax; // 存储线段树各节点对应的区间最大值
// 维护节点值:当前节点的最大值 = 左子节点最大值 和 右子节点最大值 中的较大者
void maintain(int i) {
mmax[i] = max(mmax[2 * i], mmax[2 * i + 1]);
}
// 构建线段树(递归)
// 参数:tree-原始数组,i-当前节点索引,l-当前节点覆盖的区间左边界,r-当前节点覆盖的区间右边界
void build(const vector<int>& tree, int i, int l, int r) {
if (l == r) { // 叶子节点:区间只包含一个元素,最大值就是该元素
mmax[i] = tree[l];
return;
}
// 计算区间中点,避免(l + r)溢出,改用 l + (r - l)/2(右移1位等价于除以2)
int m = l + ((r - l) >> 1);
// 递归构建左子树(覆盖[l, m],节点索引为2*i)
build(tree, 2 * i, l, m);
// 递归构建右子树(覆盖[m+1, r],节点索引为2*i+1)
build(tree, 2 * i + 1, m + 1, r);
// 构建完成后维护当前节点的最大值
maintain(i);
}
public:
// 构造函数:初始化线段树
// 参数:tree-原始数组(此处为篮子容量数组)
SegmentTree(const vector<int>& tree) {
size_t n = tree.size();
mmax.resize(4 * n); // 线段树空间通常分配为原始数组的4倍,确保足够存储所有节点
build(tree, 1, 0, n - 1); // 从根节点(索引1)开始构建,覆盖区间[0, n-1]
}
// 查找区间内第一个 >= x 的元素,找到后将其更新为-1并返回索引;未找到返回-1
// 参数:i-当前节点索引,l-当前节点覆盖的区间左边界,r-当前节点覆盖的区间右边界,x-目标值(水果大小)
int findFirst(int i, int l, int r, int x) {
if (mmax[i] < x) // 当前区间的最大值小于x,整个区间都没有符合条件的元素
return -1;
if (l == r) { // 找到叶子节点(符合条件的元素)
mmax[i] = -1; // 更新为-1,表示该位置已使用(不能再放水果)
return l; // 返回该元素的索引
}
// 计算区间中点
int m = l + ((r - l) >> 1);
// 先在左子树中查找(优先找左侧的元素,保证"第一个"的逻辑)
int check = findFirst(2 * i, l, m, x);
if (check < 0) // 左子树未找到,再在右子树中查找
check = findFirst(2 * i + 1, m + 1, r, x);
// 查找后更新当前节点的最大值(因为子节点可能被修改)
maintain(i);
return check; // 返回找到的索引(或-1)
}
};
class Solution {
public:
// 计算未放置的水果数量
// 参数:fruits-水果大小数组,baskets-篮子容量数组
// 逻辑:每个水果需要放入一个容量>=自身大小的篮子,每个篮子只能用一次,返回无法放置的水果数
int numOfUnplacedFruits(vector<int>& fruits, vector<int>& baskets) {
SegmentTree t(baskets); // 用篮子容量数组构建线段树
int n = baskets.size(), ans = 0; // ans记录未放置的水果数
for (int x : fruits) { // 遍历每个水果
// 查找能放置当前水果的第一个篮子(容量>=x)
if (t.findFirst(1, 0, n - 1, x) < 0) {
ans++; // 找不到合适的篮子,未放置数量+1
}
}
return ans;
}
};
3363. 最多可收集的水果数目 - 力扣(LeetCode)
timer:2025-8-7
// class Solution {
// public:
// int maxCollectedFruits(vector<vector<int>>& fruits) {
// int n = fruits.size(); // 获取网格大小(n×n的方阵)
// // 记忆化数组:target[i][j]存储从(i,j)出发能收集的最大水果数,-1表示未计算
// vector<vector<int>> target(n, vector<int>(n, -1));
// // 定义递归DFS函数(使用C++20特性实现lambda递归)
// // 参数i,j:当前所在的行和列
// // 返回值:从(i,j)出发按规则移动能收集的最大水果数
// auto dfs = [&](this auto&& dfs, int i, int j) -> int {
// // 边界条件1:超出有效区域
// // j < n-1-i:位于对角线i+j = n-1的左下方(不属于当前路径区域)
// // j >= n:列索引越界(超出网格右边界)
// if (j < n - 1 - i || j >= n) {
// return 0; // 无效区域,收集0个水果
// }
// // 边界条件2:到达第一行(i=0)
// // 此时j必为n-1(因j >= n-1-i且j < n),即路径起点(0, n-1)
// if (i == 0) {
// return fruits[i][j]; // 返回起点的水果数(路径起点必须收集)
// }
// int& res = target[i][j]; // 引用记忆化数组,结果会直接存入数组
// if (res != -1) { // 若已计算过该位置的结果,直接返回(避免重复计算)
// return res;
// }
// // 递归计算:当前位置的水果数 + 上一行可到达的3个位置(j-1/j/j+1)的最大值
// // 上一行的最优解决定当前行的最优解,符合动态规划思想
// return res = max({dfs(i - 1, j - 1), dfs(i - 1, j), dfs(i - 1, j + 1)}) + fruits[i][j];
// };
// int ans = 0;
// // 1. 计算【主对角线路径】的水果和(从(0,0)到(n-1,n-1),每个(i,i)位置)
// for(int i = 0; i < n; i++) {
// ans += fruits[i][i];
// }
// // 2. 计算【从(0, n-1)出发的路径】的水果和
// // 从倒数第二行(n-2)的最后一列(n-1)开始递归(最后一行n-1无"上一行")
// ans += dfs(n - 2, n - 1);
// // 3. 处理【从(n-1, 0)出发的路径】:通过矩阵转置复用DFS逻辑
// // 转置操作(只交换上三角与下三角元素,避免重复覆盖)
// for(int i = 0; i < n; i++) {
// for(int j = i + 1; j < n; j++) { // j从i+1开始,确保每个元素只交换一次
// swap(fruits[i][j], fruits[j][i]); // 交换(i,j)和(j,i),实现矩阵转置
// }
// }
// // 重置记忆化数组(转置后网格内容变化,旧缓存无效)
// ranges::fill(target, vector(n, -1));
// // 复用DFS计算转置后的右上角路径(等价于原左下角路径)
// return ans += dfs(n - 2, n - 1);
// }
// };
class Solution {
public:
int maxCollectedFruits(vector<vector<int>>& fruits) {
int n = fruits.size();
if (n == 0) return 0;
// 用滚动数组优化的DP函数,计算从(0, n-1)出发的路径水果数
auto dp = [&] -> int {
if (n == 1) return 0; // 若网格只有1行,无第二路径
// 滚动数组:prev存储上一行的状态,curr存储当前行的状态
vector<int> prev(n + 1, 0); // 上一行(i-1行)
vector<int> curr(n + 1, 0); // 当前行(i行)
// 初始化第0行:起点(0, n-1)的水果数
prev[n - 1] = fruits[0][n - 1];
// 从第1行遍历到第n-2行(与原逻辑一致)
for (int i = 1; i < n - 1; ++i) {
// 重置当前行(避免残留上一行数据)
fill(curr.begin(), curr.end(), 0);
// 计算当前行的有效列j
for (int j = max(n - 1 - i, i + 1); j < n; ++j) {
// 当前j的状态依赖上一行的j-1、j、j+1
int maxPrev = 0;
if (j - 1 >= 0) maxPrev = max(maxPrev, prev[j - 1]); // 左上方
maxPrev = max(maxPrev, prev[j]); // 正上方
if (j + 1 < n) maxPrev = max(maxPrev, prev[j + 1]); // 右上方
curr[j] = maxPrev + fruits[i][j]; // 更新当前行状态
}
// 滚动:当前行变为下一行的"上一行"
prev.swap(curr);
}
// 最终结果是倒数第二行(n-2)最后一列(n-1)的值
return prev[n - 1];
};
int ans = 0;
// 1. 主对角线路径总和
for (int i = 0; i < n; ++i) {
ans += fruits[i][i];
}
// 2. 右上角路径总和
ans += dp();
// 3. 转置后计算左下角路径(复用dp逻辑)
for (int i = 0; i < n; ++i) {
for (int j = 0; j < i; ++j) {
swap(fruits[i][j], fruits[j][i]);
}
}
ans += dp();
return ans;
}
};
808. 分汤 - 力扣(LeetCode)
timer:2025-8-8
class Solution {
public:
// 计算汤A和汤B按特定规则分配时,A先分配完的概率(或A和B同时分配完的概率的一半)
double soupServings(int n) {
// 当n足够大时,A先分配完的概率趋近于1,这里是经验值优化,减少计算量
if(n >= 4451) return 1;
// 将汤的份数按25为单位简化(题目中每次分配量都是25的倍数:100=4*25,75=3*25等)
// +24是为了向上取整,等价于ceil(n/25)
n = (n + 24) / 25;
// 记忆化数组:memo[a][b]存储当A剩余a份、B剩余b份时的概率结果
// 避免递归中重复计算相同状态
vector memo(n + 1, vector<double>(n + 1));
// 递归lambda函数,计算(a,b)状态下的概率
// this auto&& 用于支持lambda的自递归调用
auto dfs = [&](this auto&& dfs, int a, int b) -> double{
// 终止条件1:A和B都已分配完,此时概率各占一半(0.5)
if(a <= 0 && b <= 0) return 0.5;
// 终止条件2:A已分配完但B未分配完,A先用完的概率为1
else if (a <= 0) return 1;
// 终止条件3:B已分配完但A未分配完,A先用完的概率为0
else if (b <= 0) return 0;
// 引用memo[a][b],后续赋值会直接更新memo,实现记忆化
double& res = memo[a][b];
// 如果当前状态未计算过(初始值为0),则递归计算
// 四种分配方式的概率平均:
// 1. 给A倒100ml(4份),B不倒
// 2. 给A倒75ml(3份),给B倒25ml(1份)
// 3. 给A倒50ml(2份),给B倒50ml(2份)
// 4. 给A倒25ml(1份),给B倒75ml(3份)
if(res == 0) res = (dfs(a - 4, b) + dfs(a - 3,b - 1) + dfs(a - 2, b - 2) + dfs(a - 1,b - 3)) / 4;
// 返回当前状态的计算结果(已缓存或新计算)
return res;
};
// 从初始状态(n,n)开始计算,即A和B初始量相同(都为n份25ml)
return dfs(n, n);
}
};
231. 2 的幂 - 力扣(LeetCode)
timer:2025-8-9
class Solution {
public:
/**
* 判断一个整数是否为2的幂次方
* 2的幂次方在数学上是指可以表示为2^k(k为非负整数)的数,如1(2^0)、2(2^1)、4(2^2)等
* @param n 待判断的整数
* @return 若n是2的幂次方则返回true,否则返回false
*/
bool isPowerOfTwo(int n) {
// 两个条件的结合:
// 1. n > 0:因为2的幂次方结果一定是正数,排除0和负数
// 2. (n & (n - 1)) == 0:利用二进制特性判断
// - 2的幂次方的二进制表示只有一个1(如2是10,4是100,8是1000)
// - n-1的二进制会将这个1变为0,后面的所有0变为1(如4-1=3是011,8-1=7是0111)
// - 两者按位与(&)的结果必然是0
return n > 0 && (n & (n - 1)) == 0;
}
};
869. 重新排序得到 2 的幂 - 力扣(LeetCode)
timer:2025-8-10
#include <string>
#include <unordered_set>
#include <ranges>
using namespace std;
/**
* 将整数转为字符串并排序,得到数字特征串
*/
string toSortedStr(int n) {
string s = to_string(n); // 转为字符串
ranges::sort(s); // 排序字符
return s; // 返回特征串
}
// 存储所有2的幂的特征串
unordered_set<string> pow2Set;
/**
* 预计算所有小于1e9的2的幂的特征串
* 程序启动时自动执行,仅计算一次
*/
int init = []() {
const int MAX = 1000000000; // 最大范围
// 遍历所有2的幂:i = 1,2,4,8,...
for (int i = 1; i < MAX; i <<= 1) {
string s = toSortedStr(i); // 计算当前2的幂的特征串
pow2Set.insert(s); // 存入集合
}
return 0;
}();
class Solution {
public:
/**
* 判断n重排后是否为2的幂
*/
bool reorderedPowerOf2(int n) {
string s = toSortedStr(n); // 计算n的特征串
return pow2Set.count(s) > 0; // 检查是否在2的幂特征集合中
}
};
2438. 二的幂数组中查询范围内的乘积 - 力扣(LeetCode)
timer:2025-8-11
class Solution {
public:
vector<int> productQueries(int n, vector<vector<int>>& queries) {
const int MOD = 1e9 + 7;
// 分解 n:获取所有二进制位为 1 的幂值(从小到大)
vector<int> powers;
while (n) {
int lowbit = -n & n; // 获取最低位的 1 对应的值(如 12 的 1100 得 4)
powers.push_back(lowbit);
n ^= lowbit; // 移除最低位的 1
}
vector<int> ans;
ans.reserve(queries.size()); // 预分配空间(不初始化元素)
// 处理每个查询
for (auto& q : queries) {
long long tmp = 1;
// 遍历查询区间 [q[0], q[1]],累乘幂值
for (int i = q[0]; i <= q[1]; i++) {
tmp = tmp * powers[i] % MOD; // 边乘边取模
}
ans.push_back(tmp);
}
return ans;
}
};
class Solution {
public:
vector<int> productQueries(int n, vector<vector<int>>& queries) {
const int MOD = 1e9 + 7;
// 分解 n:获取所有二进制位为 1 的幂值(从小到大)
vector<int> powers;
while (n) {
int lowbit = -n & n;
powers.push_back(lowbit);
n ^= lowbit;
}
int m = powers.size();
// 预处理:构建二维 DP 表 res[i][j] 存储区间 [i,j] 的乘积
vector<vector<int>> res(m, vector<int>(m));
for (int i = 0; i < m; i++) {
res[i][i] = powers[i]; // 单个元素区间
for (int j = i + 1; j < m; j++) {
// 递推:区间 [i,j] = [i,j-1] * powers[j]
res[i][j] = 1LL * res[i][j - 1] * powers[j] % MOD;
}
}
vector<int> ans;
ans.reserve(queries.size()); // 预分配空间
// 处理查询:直接查表获取结果
for (auto& q : queries) {
ans.push_back(res[q[0]][q[1]]);
}
return ans;
}
};
2787. 将一个数字表示成幂的和的方案数 - 力扣(LeetCode)
timer:2025-8-12
class Solution {
public:
/**
* 快速幂算法计算 i 的 x 次方
* 快速幂通过将指数x二进制分解,减少乘法运算次数,提高效率
* @param i 底数(正整数)
* @param x 指数(正整数)
* @return i^x 的计算结果(int类型,注意:当结果过大时可能溢出)
*/
int MyPow(int i, int x) {
int ans = 1; // 存储最终结果,初始为1(任何数的0次方为1)
while (x) { // 当指数x不为0时继续循环
if (x & 1) { // 判断x的二进制最低位是否为1(即x是否为奇数)
ans *= i; // 若为奇数,将当前底数i乘入结果
}
i *= i; // 底数自乘(对应指数二进制右移一位,底数平方)
x >>= 1; // 指数右移一位(相当于x = x / 2,向下取整)
}
return ans;
}
/**
* 计算将正整数n表示为若干个不同正整数的x次方之和的方式数
* 结果对 1e9+7 取模
* @param n 目标和(正整数)
* @param x 次方数(正整数)
* @return 满足条件的方式数
*/
int numberOfWays(int n, int x) {
const int MOD = 1e9 + 7; // 取模常数,防止结果溢出
vector<long long> dp(n + 1); // dp[j]表示将j表示为不同x次方数之和的方式数
dp[0] = 1; // 基准情况:和为0只有1种方式(不选任何数)
// 遍历所有可能的底数i,计算i^x并判断是否可参与组成n
for (int i = 1; MyPow(i, x) <= n; i++) {
int v = MyPow(i, x); // 当前底数i的x次方值
// 从后往前更新dp数组(0-1背包思想,避免同一i被重复使用)
for (int j = n; j >= v; j--) {
// 若选择v,则方式数累加dp[j - v](即j - v的组成方式数)
dp[j] += dp[j - v];
}
}
return dp[n] % MOD; // 返回n的组成方式数,结果取模
}
};
326. 3 的幂 - 力扣(LeetCode)
timer:2025-8-13
class Solution {
public:
// 计算不超过 INT_MAX 的最大3的幂(即3^19 = 1162261467)
int myPow() {
long power = 1; // 使用long防止计算过程中溢出
int i = 0; // 指数计数器(实际未使用)
// 循环计算3的幂,直到下一次乘法会超过INT_MAX
while (power <= INT_MAX) {
i++; // 指数增加(未实际使用)
power *= 3; // 计算下一个3的幂
// 当power超过INT_MAX时循环结束
// 此时power是第一个超过INT_MAX的3的幂
}
// 返回最后一个不超过INT_MAX的3的幂(即power/3)
// 实际值 = 3^19 = 1162261467
return power / 3;
}
// 判断整数n是否是3的幂
bool isPowerOfThree(int n) {
// 方法1:使用myPow()动态计算最大3的幂
// 原理:3^19能被所有3^k (0≤k≤19) 整除
// return n > 0 && myPow() % n == 0;
// 方法2:直接使用预计算的3^19(更高效)
// 原理同上,但避免了每次调用时的重复计算
return n > 0 && 1162261467 % n == 0;
}
};
1780. 判断一个数字是否可以表示成三的幂的和 - 力扣(LeetCode)
timer:2025-8-14
class Solution {
public:
/**
* 检查一个整数 n 是否可以表示为若干个不同的 3 的幂之和
* 原理:3 的幂在三进制表示中为 1 后面跟若干个 0(如 3^0=1 是 1,3^1=3 是 10,3^2=9 是 100 等)
* 不同 3 的幂之和的三进制表示中,每个数位只能是 0 或 1(不会出现 2)
* 因此只需判断 n 的三进制表示中是否存在数字 2 即可
* @param n 待检查的整数
* @return 若能表示为不同 3 的幂之和则返回 true,否则返回 false
*/
bool checkPowersOfThree(int n) {
// 循环处理 n,相当于不断获取 n 三进制表示的每一位
while(n) {
// 若当前位(n%3)为 2,说明不符合条件,直接返回 false
if(n % 3 == 2) return false;
// 去掉已处理的最低位,继续检查更高位
n /= 3;
}
// 所有位都不是 2,符合条件
return true;
}
};
342. 4的幂 - 力扣(LeetCode)
timer:2025-8-15
class Solution {
public:
/**
* 判断一个整数是否是4的幂
* @param n 待判断的整数
* @return 若是4的幂返回true,否则返回false
*/
bool isPowerOfFour(int n) {
// 4的幂需要满足三个条件:
// 1. n > 0:4的幂都是正整数,排除负数和0
// 2. (n & (n - 1)) == 0:4的幂一定是2的幂,该条件用于判断n是否为2的幂
// (2的幂二进制表示为1后面跟若干个0,n-1则为0后面跟若干个1,两者按位与结果为0)
// 3. n % 3 == 1:在2的幂中,只有4的幂满足模3余1(4^k = (3+1)^k,展开后除1外均为3的倍数)
return n > 0 && (n & (n - 1)) == 0 && n % 3 == 1;
}
};
1323. 6 和 9 组成的最大数字 - 力扣(LeetCode)
timer:2025-8-16
// class Solution {
// public:
// // 函数:将数字中第一个出现的6改为9,返回得到的最大数
// int maximum69Number (int num) {
// // 将整数转换为字符串,方便操作单个数字字符
// string s = to_string(num);
// // 查找字符串中第一个'6'的位置
// int i = s.find('6');
// // 如果没有找到'6'(即原数全是9),直接返回原数
// if(i == string::npos) return num;
// // 将找到的第一个'6'替换为'9'(最左侧的6改为9能得到最大数)
// s[i] = '9';
// // 将修改后的字符串转回整数并返回
// return stoi(s);
// }
// };
class Solution {
public:
// 函数:将数字中第一个出现的6改为9,返回得到的最大数(数学方法实现)
int maximum69Number (int num) {
// 记录最高位的6所在的位数(如百位的6对应100)
int max_base = 0;
// 用于计算当前位的基数(1、10、100...)
int base = 1;
// 循环遍历数字的每一位(从个位开始)
for(int x = num; x > 0; x /= 10){
// 如果当前位是6,更新max_base为当前位的基数(保证记录的是最高位的6)
if(x % 10 == 6) max_base = base;
// 基数扩大10倍,准备检查下一位(十位、百位...)
base *= 10;
}
// 原数加上3*max_base:因为6变9差值为3,乘以对应基数即完成修改
return num + max_base * 3;
}
};
837. 新 21 点 - 力扣(LeetCode)
timer:2025-8-17
class Solution {
public:
double new21Game(int n, int k, int maxPts) {
// 创建 DP 数组,大小为 n+1,初始化为 0。dp[i] 表示当前分数为 i 时的获胜概率。
vector<double> dp(n + 1);
double sum = 0; // 滑动窗口和,用于维护后续状态的概率和
// 从高分到低分遍历(i 从 n 递减到 0)
for (int i = n; i >= 0; i--) {
// 情况1:当前分数 i >= k(玩家停止抽卡)
if (i >= k) dp[i] = 1.0; // 停止时,分数 i <= n(因为 i <= n),获胜概率为 1
// 情况2:当前分数 i < k(玩家继续抽卡)
else dp[i] = sum / maxPts; // 获胜概率 = 后续 maxPts 个状态的平均值
// 更新滑动窗口和:将当前状态 dp[i] 加入窗口
sum += dp[i];
// 维护窗口大小不超过 maxPts:如果 i + maxPts 在有效范围内,移除最旧状态(最右边)
if (i + maxPts <= n) sum -= dp[i + maxPts]; // 移除超出窗口的状态
}
return dp[0]; // 返回起始分数 0 的获胜概率
}
};
679. 24 点游戏 - 力扣(LeetCode)
timer:2025-8-18
// 误差: 1e-6
// 实数除法: 整数转浮点型 static_cast<double>()
// DFS:
// 1、出DFS: 只剩一个数 判断是不是24
// 2、选出两个数: 放入一个数组
// 3、把剩余数放入数组
// 4、枚举所有计算方法(+ - * /)
// 5、弹入计算后的数: 进行DFS()
// 6、弹出计算后的数: 回溯
class Solution {
public:
// 判断给定的4个整数能否通过加减乘除运算得到24
bool judgePoint24(vector<int>& cards) {
// 将整数转换为double类型,便于后续除法运算(避免整数除法精度丢失)
vector<double> nums;
for (int num : cards) {
nums.push_back(static_cast<double>(num));
}
// 调用深度优先搜索函数进行判断
return dfs(nums);
}
private:
// 浮点数误差范围,用于判断两个浮点数是否相等(因计算机浮点运算存在精度误差)
const double ep = 1e-6;
// 存储四种运算符:加、减、乘、除
const char ops[4] = {'+', '-', '*', '/'};
// 深度优先搜索函数:判断当前数字集合能否通过运算得到24
// 参数nums:当前参与运算的数字集合
// 返回值:能得到24则返回true,否则返回false
bool dfs(vector<double>& nums) {
int n = nums.size();
// 递归终止条件:当集合中只剩一个数时,判断该数是否接近24(在误差范围内)
if (n == 1) {
return abs(24 - nums[0]) < ep;
}
// 从当前数字集合中选择两个不同的数(i和j为索引)
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (i == j) continue; // 跳过相同索引(不能选同一个数)
// 收集剩余的数字(除了i和j对应的数之外的所有数)
vector<double> tmp;
for (int k = 0; k < n; k++) {
if (k != i && k != j) {
tmp.push_back(nums[k]);
}
}
// 枚举四种运算符对选中的两个数进行运算
for (char op : ops) {
// 加、乘具有交换率
if((op == '*' || op == '+') && i < j) continue;
// 除法运算需避免除数为0(在误差范围内视为0)
if (op == '/' && abs(nums[j]) < ep) {
continue;
}
// 根据运算符计算结果,并加入到剩余数字集合中
switch (op) {
case '+': tmp.push_back(nums[i] + nums[j]); break;
case '-': tmp.push_back(nums[i] - nums[j]); break;
case '*': tmp.push_back(nums[i] * nums[j]); break;
case '/': tmp.push_back(nums[i] / nums[j]); break;
}
// 递归判断新的数字集合能否得到24,若能则返回true
if (dfs(tmp)) {
return true;
}
// 回溯:移除当前运算结果,尝试下一种运算符
tmp.pop_back();
}
}
}
// 所有可能的运算组合都无法得到24,返回false
return false;
}
};
2348. 全 0 子数组的数目 - 力扣(LeetCode)
timer:2025-8-19
// 方法一: 暴力模拟
// 核心思路:遍历数组,遇到连续的0序列时,计算该序列包含的所有子数组数量并累加
// 连续k个0的子数组数量 = k*(k+1)/2(数学公式:1+2+...+k)
class Solution {
public:
long long zeroFilledSubarray(vector<int>& nums) {
int n = nums.size(); // 获取数组长度
int i = 0; // 遍历数组的指针
long long ans = 0; // 存储最终结果(子数组总数)
while(i < n) {
// 若当前元素不是0,直接移动指针跳过
if(nums[i] != 0) {
i++;
continue;
}
// 遇到0时,统计连续0的个数
long long count = 0;
while(i < n && nums[i] == 0) { // 循环计数连续的0
count++;
i++; // 移动指针到下一个元素
}
// 累加当前连续0序列的子数组数量(公式:k*(k+1)/2)
ans += count * (count + 1) / 2;
}
return ans;
}
};
// 方法二: 滑动窗口
// 核心思路:用last记录最后一个非0元素的位置,对于每个0元素,当前位置到last之间的所有子数组都以当前0为结尾
// 例如:last=-1(初始无元素),当前i=0(0元素),则子数组数量为0-(-1)=1(即[0])
class Solution {
public:
long long zeroFilledSubarray(vector<int>& nums) {
int n = nums.size(); // 数组长度
int last = -1; // 记录最后一个非0元素的索引(初始化为-1,代表数组开始前)
long long ans = 0; // 结果累计
for(int i = 0; i < n; i++) {
if(nums[i] != 0) { // 若当前元素非0,更新last为当前索引
last = i;
} else { // 若当前元素是0,新增的子数组数量为:当前索引 - 最后一个非0索引
ans += i - last;
}
}
return ans;
}
};
// 方法三: 增量法
// 核心思路:用count记录当前连续0的长度,每遇到一个新的0,新增的子数组数量等于当前连续长度(即包含这个0的所有子数组)
// 例如:连续3个0,过程为:
// 第一个0:count=1,新增1个子数组([0])
// 第二个0:count=2,新增2个子数组([0,0], [0])
// 第三个0:count=3,新增3个子数组([0,0,0], [0,0], [0])
// 累计:1+2+3=6,与公式结果一致
class Solution {
public:
long long zeroFilledSubarray(vector<int>& nums) {
int n = nums.size(); // 数组长度
long long ans = 0; // 结果累计
long long count = 0; // 记录当前连续0的长度
for(int x : nums) { // 遍历每个元素
if(x != 0) { // 遇到非0元素,重置连续0长度为0
count = 0;
} else { // 遇到0元素,连续长度+1,并累加当前长度到结果
count++;
ans += count;
}
}
return ans;
}
};
1277. 统计全为 1 的正方形子矩阵 - 力扣(LeetCode)
timer:2025-8-20
class Solution {
public:
// 计算二进制矩阵中所有由1组成的正方形的数量
int countSquares(vector<vector<int>>& matrix) {
int ans = 0; // 用于累加所有正方形的数量
// 遍历矩阵的每一行
for (int i = 0; i < matrix.size(); i++) {
auto& row = matrix[i]; // 引用当前行,简化代码书写
// 遍历当前行的每一列
for (int j = 0; j < row.size(); j++) {
// 若当前位置不是第一行/第一列,且当前元素为1(可能构成正方形)
// 则更新当前位置的值:以(i,j)为右下角的最大正方形边长
if (i && j && row[j]) {
// 状态转移:当前最大边长 = 左上、上方、左方三个位置的最小边长 + 1
// 保证能与这三个方向的正方形组成更大的正方形
row[j] += min({matrix[i - 1][j - 1], matrix[i - 1][j], matrix[i][j - 1]});
}
// 累加当前位置的贡献:其值即为以该位置为右下角的正方形数量
// (例如值为3表示存在1x1、2x2、3x3共3个正方形)
ans += row[j];
}
}
return ans;
}
};
1504. 统计全 1 子矩形 - 力扣(LeetCode)
timer:2025-8-21
class Solution {
public:
int numSubmat(vector<vector<int>>& mat) {
int m = mat.size(), n = mat[0].size();
int ans = 0; // 存储最终结果
// 枚举所有可能的上边界
for (int top = 0; top < m; top++) {
vector<int> a(n + 1, 0); // 用于存储每列从top到bottom的1的累计数量
// 枚举所有可能的下边界(从当前上边界开始)
for (int bottom = top; bottom < m; bottom++) {
int h = bottom - top + 1; // 当前矩形的高度
int count = 0; // 记录连续满足条件的列数
// 遍历每一列
for (int i = 0; i < n; i++) {
// 将当前下边界行的值加到列累计数组中
a[i] += mat[bottom][i];
// 检查当前列是否满足全1条件
if (a[i] != h) {
count = 0; // 不满足条件,重置连续计数
} else {
count++; // 满足条件,增加连续计数
ans += count; // 将当前连续计数加到结果中
}
}
}
}
return ans;
}
};
3195. 包含所有 1 的最小矩形面积 I - 力扣(LeetCode)
timer:2025-8-22
class Solution {
public:
// 计算包含网格中所有1的最小矩形面积
int minimumArea(vector<vector<int>>& grid) {
// m为网格的行数,n为网格的列数
int m = grid.size(), n = grid[0].size();
// 初始化边界变量:
// l表示包含1的最左列(初始化为最大整数,方便后续取最小值)
// r表示包含1的最右列(初始化为0,方便后续取最大值)
// t表示包含1的最上行(初始化为最大整数,方便后续取最小值)
// b表示包含1的最下行(初始化为0,方便后续取最大值)
int l = INT_MAX, r = 0, t = INT_MAX, b = 0;
// 遍历网格中的每个元素
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 当遇到值为1的元素时,更新边界
if (grid[i][j]) {
l = min(j, l); // 更新最左列(取当前列与已有最左列的较小值)
r = max(j, r); // 更新最右列(取当前列与已有最右列的较大值)
t = min(i, t); // 更新最上行(取当前行与已有最上行的较小值)
b = max(i, b); // 更新最下行(取当前行与已有最下行的较大值)
}
}
}
// 最小矩形的宽度为(最右列 - 最左列 + 1),高度为(最下行 - 最上行 + 1)
// 面积 = 宽度 * 高度
return (r - l + 1) * (b - t + 1);
}
};
3197. 包含所有 1 的最小矩形面积 II - 力扣(LeetCode)
timer:2025-8-23
class Solution {
/**
* 将矩阵顺时针旋转90度
* @param a 输入矩阵
* @return 旋转后的矩阵
* 旋转逻辑:原矩阵a的[i][j]元素会被放置到新矩阵b的[j][m-1-i]位置
* 其中m是原矩阵的行数,旋转后矩阵的行数为原列数n,列数为原行数m
*/
vector<vector<int>> rotate(const vector<vector<int>>& a){
int m = a.size(); // 原矩阵行数
int n = a[0].size(); // 原矩阵列数
vector b(n, vector<int>(m)); // 旋转后矩阵:n行m列
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
// 顺时针旋转90度的坐标映射
b[j][m - 1 - i] = a[i][j];
}
}
return b;
}
/**
* 计算子区域内包含所有1的最小矩形面积
* @param a 输入网格
* @param u 子区域上边界(包含,行索引)
* @param d 子区域下边界(不包含,行索引)
* @param l 子区域左边界(包含,列索引)
* @param r 子区域右边界(不包含,列索引)
* @return 最小矩形面积(若子区域无1,可能返回0或其他值,取决于业务逻辑)
*/
int minimumArea(const vector<vector<int>>& a, int u, int d, int l, int r) {
// 初始化边界变量:记录子区域内1的最左、最右、最上、最下位置
int left = r; // 最左列(初始化为右边界,确保后续min能取到有效值)
int right = 0; // 最右列(初始化为0,确保后续max能取到有效值)
int top = d; // 最上行(初始化为下边界,确保后续min能取到有效值)
int bottom = 0; // 最下行(初始化为0,确保后续max能取到有效值)
// 遍历子区域内的所有元素(行:u到d-1,列:l到r-1)
for (int i = u; i < d; i++)
for (int j = l; j < r; j++)
if (a[i][j] == 1) { // 只关注值为1的元素
left = min(j, left); // 更新最左列
right = max(j, right); // 更新最右列
top = min(i, top); // 更新最上行
bottom = i; // 更新最下行(直接取当前行,因i递增遍历)
}
// 计算最小矩形面积:宽*(右-左+1) * 高*(下-上+1)
return (right - left + 1) * (bottom - top + 1);
}
/**
* 计算当前网格下的最小面积和(分割为3个区域,包含所有1)
* @param a 输入网格
* @return 最小面积和
*/
int solve(const vector<vector<int>>& a) {
int m = a.size(); // 网格行数
int n = a[0].size(); // 网格列数
int ans = INT_MAX; // 初始化结果为最大整数,用于后续取最小值
// 情况1:将网格垂直分割为上、中、下三部分(要求行数至少为3)
if (m >= 3) {
// i是上与中的分割线(上部分:0~i-1行)
// j是中与下的分割线(中部分:i~j-1行,下部分:j~m-1行)
for (int i = 1; i < m; i++) {
for (int j = i + 1; j < m; j++) {
// 计算三部分的最小面积之和
int s = minimumArea(a, 0, i, 0, n); // 上部分面积
s += minimumArea(a, i, j, 0, n); // 中部分面积
s += minimumArea(a, j, m, 0, n); // 下部分面积
ans = min(ans, s); // 更新最小值
}
}
}
// 情况2:混合分割(要求行数至少2,列数至少2)
if (m >= 2 && n >= 2) {
// i是上下分割线(上部分:0~i-1行,下部分:i~m-1行)
// j是左右分割线(左部分:0~j-1列,右部分:j~n-1列)
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
// 子情况1:上部分整体 + 下部分左右分割
int s = minimumArea(a, 0, i, 0, n); // 上部分面积
s += minimumArea(a, i, m, 0, j); // 下左部分面积
s += minimumArea(a, i, m, j, n); // 下右部分面积
ans = min(ans, s);
// 子情况2:上部分左右分割 + 下部分整体
s = minimumArea(a, 0, i, 0, j); // 上左部分面积
s += minimumArea(a, 0, i, j, n); // 上右部分面积
s += minimumArea(a, i, m, 0, n); // 下部分面积
ans = min(ans, s);
}
}
}
return ans;
}
public:
/**
* 计算网格的最小面积和(主函数)
* 思路:考虑原网格和旋转90度后的网格,取两种情况下的最小值
* (旋转是为了处理水平/垂直方向的对称分割情况)
* @param grid 输入网格
* @return 最小面积和
*/
int minimumSum(vector<vector<int>>& grid) {
// 原网格的解 与 旋转后网格的解 取最小值
return min(solve(grid), solve(rotate(grid)));
}
};
1493. 删掉一个元素以后全为 1 的最长子数组 - 力扣(LeetCode)
timer:2025-8-24
class Solution {
public:
int longestSubarray(vector<int>& nums) {
int n = nums.size();
int left = 0; // 窗口左边界
int count0 = 0; // 窗口内0的数量
int maxLen = 0; // 最长有效子数组长度
for (int right = 0; right < n; right++) {
// 1.进入窗口 —— 遇到0则计数
if (nums[right] == 0)
count0++;
// 2.出窗口 —— 当0的数量超过1时,移动左指针缩小窗口
while (count0 > 1) {
if (nums[left] == 0)
count0--; // 左指针移除的是0,计数减1
left++; // 左指针右移
}
// 3.更新答案:
// 计算当前窗口的有效长度(减去0的数量,因为要删除一个0)
maxLen = max(maxLen, right - left + 1 - count0);
}
// 特殊情况:全是1时必须删除一个,所以结果减1
return maxLen == n ? maxLen - 1 : maxLen;
}
};
498. 对角线遍历 - 力扣(LeetCode)
timer:2025-8-25
class Solution {
public:
vector<int> findDiagonalOrder(vector<vector<int>>& mat) {
int m = mat.size(), n = mat[0].size();
vector<int> ans;
// 预分配m*n大小的空间,避免动态扩容带来的性能开销
ans.reserve(m * n);
// k表示对角线的"和值"(即当前对角线上所有元素的行号+列号 = k)
// 对角线的和值范围从0到m+n-2(最大和为最后一行+最后一列:(m-1)+(n-1) = m+n-2)
for(int k = 0; k < m + n - 1; k++){
// 计算当前对角线中列号j的最大值:
// - 不能超过矩阵最大列号n-1
// - 不能超过k(因为j = k - i,i≥0 → j≤k)
int max_j = min(k, n - 1);
// 计算当前对角线中列号j的最小值:
// - 不能小于0
// - 不能小于k - m + 1(因为i = k - j < m → j > k - m → j≥k - m + 1)
int min_j = max(k - m + 1, 0);
// 根据k的奇偶性决定遍历方向:
if(k % 2 == 0){
// k为偶数:从左下到右上遍历(列号j从小到大)
for(int j = min_j; j <= max_j; j++)
ans.push_back(mat[k - j][j]);
} else {
// k为奇数:从右上到左下遍历(列号j从大到小)
for(int j = max_j; j >= min_j; j--){
ans.push_back(mat[k - j][j]);
}
}
}
return ans;
}
};
3000. 对角线最长的矩形的面积 - 力扣(LeetCode)
timer:2025-8-26
// class Solution {
// public:
// int areaOfMaxDiagonal(vector<vector<int>>& dimensions) {
// // 存储当前找到的最大对角线的平方和(使用平方和避免浮点数运算,提高精度)
// int max_dui = 0;
// // 存储最大对角线对应的矩形的最大面积
// int max_s = 0;
// // 遍历每个矩形的尺寸
// for(auto& x : dimensions){
// // 计算当前矩形的对角线平方和(长² + 宽²)
// int cur_dui = x[0] * x[0] + x[1] * x[1];
// // 若当前矩形的对角线平方和大于已知的最大对角线平方和
// if(cur_dui > max_dui){
// // 更新最大对角线平方和
// max_dui = cur_dui;
// // 更新对应的面积为当前矩形的面积(长×宽)
// max_s = x[0] * x[1];
// }
// // 若当前矩形的对角线平方和与已知的最大对角线平方和相等
// else if(cur_dui == max_dui){
// // 取当前矩形面积与已知最大面积中的较大值,更新最大面积
// max_s = max(max_s, x[0] * x[1]);
// }
// }
// return max_s;
// }
// };
class Solution {
public:
int areaOfMaxDiagonal(vector<vector<int>>& dimensions) {
// first成员存储当前最大的对角线平方和(避免浮点数运算,提高精度)
// second成员存储对应矩形的面积
pair<int, int> ans{};
// 遍历每个矩形的尺寸
for(auto& x : dimensions){
int a = x[0], b = x[1];
// 利用pair的比较特性:先比较first(对角线平方和),若相等则比较second(面积)
// max函数会选择对角线平方和更大的pair;若平方和相等,则选择面积更大的pair
ans = max(ans, pair(a * a + b * b, a * b));
}
return ans.second;
}
};
3459. 最长 V 形对角线段的长度 - 力扣(LeetCode)
timer:2025-8-27
class Solution {
// 定义四个对角线方向: 右下、左下、左上、右上
static constexpr int DIRS[4][2] = {{1, 1}, {1, -1}, {-1, -1}, {-1, 1}};
public:
int lenOfVDiagonal(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
// 记忆化数组: m x n x 4方向 x 2种转弯状态
// 存储从每个位置、每个方向、每种转弯状态出发能得到的最长路径长度
vector memo(m, vector<array<array<int, 2>, 4>>(n));
// DFS lambda函数 - 使用递归计算最长路径
// i, j: 当前单元格位置
// k: 当前移动方向(0-3对应DIRS中的四个方向)
// can_turn: 是否还可以右转弯
// target: 下一个单元格期望的值(0或2)
auto dfs = [&](this auto&& dfs, int i, int j, int k, bool can_turn, int target) -> int {
// 沿着方向k移动一步
i += DIRS[k][0];
j += DIRS[k][1];
// 检查是否越界或值不匹配
if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] != target) {
return 0; // 无效移动,返回0
}
// 获取记忆化结果(引用)
// 如果这个状态已经计算过,直接返回结果
int& res = memo[i][j][k][can_turn];
if (res) { // 如果已计算过,直接返回
return res;
}
// 继续直行的情况
// 保持当前方向,目标值交替(1变2,2变1)
res = dfs(i, j, k, can_turn, 2 - target) + 1;
// 如果可以转弯,尝试右转
if (can_turn) {
// 计算当前方向最大可能长度(优化:提前剪枝)
int mmax[4] = {m - i, j + 1, i + 1, n - j};
k = (k + 1) % 4; // 右转(方向索引加1取模4)
if (mmax[k] > res) // 只有可能得到更优解时才继续
res = max(res, dfs(i, j, k, false, 2 - target) + 1);
}
return res;
};
int ans = 0;
// 遍历所有可能的起点
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) { // 只从值为1的单元格开始
for (int k = 0; k < 4; k++) { // 尝试四个方向
// 计算当前方向最大可能长度(优化:提前剪枝)
int mmax[4] = {m - i, j + 1, i + 1, n - j};
if (mmax[k] > ans) // 只有可能得到更优解时才继续
ans = max(ans, dfs(i, j, k, true, 2) + 1);
}
}
}
}
return ans;
}
};
3446. 按对角线进行矩阵排序 - 力扣(LeetCode)
timer:2025-8-28
class Solution {
public:
vector<vector<int>> sortMatrix(vector<vector<int>>& grid) {
int m = grid.size(); // 获取矩阵的行数
int n = grid[0].size(); // 获取矩阵的列数(假设矩阵非空)
// 对角线可以用 k = i - j + n 来表示,k的取值范围是 1 到 m + n - 1
for(int k = 1; k < m + n; k++){
// 计算当前对角线上列索引j的最小值
// max(n - k, 0)确保j不会小于0
int min_j = max(n - k, 0);
// 计算当前对角线上列索引j的最大值
// min(m + n - 1 - k, n - 1)确保j不会超过列数范围
int max_j = min(m + n - 1 - k, n - 1);
// 收集当前对角线上的所有元素
vector<int> a;
for(int j = min_j; j <= max_j; j++){
// 由 i = k + j - n,将元素加入临时数组
a.push_back(grid[k + j - n][j]);
}
// 根据对角线位置决定排序方式:
// - 当min_j > 0时,是主对角线下方的对角线,按升序排序
// - 当min_j = 0时,是主对角线及其上方的对角线,按降序排序
if(min_j > 0) {
ranges::sort(a); // 升序排序
} else {
ranges::sort(a, greater<int>()); // 降序排序
}
// 将排序后的元素放回原矩阵的对应对角线位置
for(int j = min_j; j <= max_j; j++){
grid[k + j - n][j] = a[j - min_j];
}
}
return grid;
}
};
3021. Alice 和 Bob 玩鲜花游戏 - 力扣(LeetCode)
timer:2025-8-29
class Solution {
public:
/**
* 计算花朵游戏中先手获胜的总情况数
* 获胜条件:选择的格子坐标(i,j)满足以下两种情况之一
* 1. i为奇数且j为偶数
* 2. i为偶数且j为奇数
*
* 推导过程:
* 1. 定义范围:i的取值范围是[1, n],j的取值范围是[1, m]
* 2. 计算各类型数字数量:
* - i的奇数个数:(n + 1) / 2 (例如n=5时,1,3,5共3个)
* - i的偶数个数:n / 2 (例如n=5时,2,4共2个)
* - j的奇数个数:(m + 1) / 2
* - j的偶数个数:m / 2
* 3. 获胜情况总数 = (i奇 × j偶) + (i偶 × j奇)
* 4. 数学简化:上述表达式等价于 n×m / 2(整数除法,自动取整)
*
*/
long long flowerGame(int n, int m) {
return 1LL * n * m / 2;
}
};
36. 有效的数独 - 力扣(LeetCode)
timer:2025-8-30
class Solution {
public:
bool isValidSudoku(vector<vector<char>>& board) {
// 记录每行中数字的出现情况:row_cnt[i][x]表示第i行中数字x+1是否已出现
bool row_cnt[9][9] = {};
// 记录每列中数字的出现情况:col_cnt[j][x]表示第j列中数字x+1是否已出现
bool col_cnt[9][9] = {};
// 记录每个3x3子格中数字的出现情况:box_cnt[a][b][x]表示第(a,b)个子格中数字x+1是否已出现
// 其中a = i/3, b = j/3,对应9个3x3子格(0<=a,b<3)
bool box_cnt[3][3][9] = {};
// 遍历数独棋盘的每个单元格(i为行索引,j为列索引)
for(int i = 0; i < 9; i++){
for(int j = 0; j < 9; j++){
char ch = board[i][j]; // 获取当前单元格的字符
if(ch == '.') continue; // 若为'.'(空单元格),跳过当前循环
// 将字符转换为0-8的索引('1'->0, '2'->1, ..., '9'->8)
int x = ch - '1';
// 检查当前数字在当前行、列、子格中是否已出现
// 若任一位置已出现,则数独无效,返回false
if(row_cnt[i][x] || col_cnt[j][x] || box_cnt[i / 3][j / 3][x])
return false;
// 标记当前数字在当前行、列、子格中已出现
row_cnt[i][x] = col_cnt[j][x] = box_cnt[i / 3][j / 3][x] = true;
}
}
// 遍历完所有单元格未发现重复,数独有效
return true;
}
};
37. 解数独 - 力扣(LeetCode)(⭐)
timer:2025-8-31
class Solution {
public:
void solveSudoku(vector<vector<char>>& board) {
// 使用三个数组来跟踪每行、每列和每个3x3子网格中已存在的数字
bool row_has[9][9]{}; // row_has[i][x] 表示第i行是否有数字x+1
bool col_has[9][9]{}; // col_has[j][x] 表示第j列是否有数字x+1
bool sub_box_has[3][3][9]{}; // sub_box_has[i'][j'][x] 表示(i',j')宫格是否有数字x+1
// 初始化标记数组,记录初始数独中已存在的数字
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
char b = board[i][j];
if (b != '.') {
int x = b - '1'; // 将字符'1'-'9'转换为数字0-8
row_has[i][x] = col_has[j][x] = sub_box_has[i / 3][j / 3][x] = true;
}
}
}
// 定义DFS回溯函数
function<bool()> dfs = [&]() -> bool {
int best_i = -1, best_j = -1, min_candidates = 10;
// 寻找候选数字最少的空格
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] != '.') continue; // 跳过非空格
// 计算当前空格的候选数字数量
int count = 0;
for (int x = 0; x < 9; x++) {
if (row_has[i][x] || col_has[j][x] || sub_box_has[i / 3][j / 3][x]) {
continue; // 如果数字x+1已存在,跳过
}
count++;
}
if (count == 0) return false; // 如果没有候选数字,当前路径无效
// 更新候选数字最少的空格
if (count < min_candidates) {
min_candidates = count;
best_i = i;
best_j = j;
}
}
}
// 如果没有找到空格,说明数独已解决
if (best_i == -1) return true;
// 收集候选数字
vector<int> candidates;
for (int x = 0; x < 9; x++) {
if (row_has[best_i][x] || col_has[best_j][x] || sub_box_has[best_i / 3][best_j / 3][x]) {
continue; // 跳过已存在的数字
}
candidates.push_back(x);
}
// 尝试所有候选数字
for (int x : candidates) {
// 填入数字
board[best_i][best_j] = '1' + x;
// 更新标记数组
row_has[best_i][x] = col_has[best_j][x] = sub_box_has[best_i / 3][best_j / 3][x] = true;
// 递归尝试解决剩余空格
if (dfs()) {
return true; // 如果成功,返回true
}
// 回溯:撤销标记
row_has[best_i][x] = col_has[best_j][x] = sub_box_has[best_i / 3][best_j / 3][x] = false;
}
// 恢复空格状态
board[best_i][best_j] = '.';
return false; // 所有候选数字都尝试失败
};
// 开始求解
dfs();
}
};
377

被折叠的 条评论
为什么被折叠?



