2025.11.15 力扣每日一题
3234.统计1显著的字符串的数量
(题解来自题解库作者:灵茶山艾府)
class Solution {
public:int numberOfSubstrings(string s) {vector<int> pos0 = {-1}; // 加个-1哨兵,方便处理cnt0达到最大时的计数int total1 = 0; // 统计[0,r]中的1的总数量int ans = 0; // 最终要返回的“1显著子串”总数// 遍历字符串,r作为子串右边界,逐步统计以r结尾的1显著子串数量。for (int r = 0; r < s.size(); r++) {if (s[r] == '0') {pos0.push_back(r); // 记录0的下标} else {total1++; // 更新区间 [0, r] 内 '1' 的总数。ans += r -pos0.back(); // 统计不含 '0' 的子串。pos0.back() 是// “最靠近 r 的前一个 '0' 的下标”,r -// pos0.back() 表示 “以 r 为右端点、且不含// '0' 的子串数量”(这些子串的 0 数量为// 0,因此必然满足 “1 的数量 ≥ 0 的平方”)。}// 统计包含 '0' 的 1 显著子串int m = pos0.size(); // pos0 的长度(包含哨兵 -1)。// 倒着遍历pos0,那么cnt0 = m-1for (int i = m - 1; i > 0 && (m - i) * (m - i) <= total1;i--) { // m - i 是当前子串中 '0' 的数量 cnt0,此条件保证 “1// 的数量 ≥ 0 的数量的平方”(即 cnt1 ≥ cnt0²)。int p = pos0[i - 1],q = pos0[i]; // 子串的左边界范围是 (p, q](即左端点在 p+1 到// q 之间)。int cnt0 = m - i; // 当前子串中 '0' 的数量。int cnt1 =r - q + 1 - cnt0; //[q,r]中的1的个数 =[q,r]的长度-cnt0ans += max(q - max(cnt0 * cnt0 - cnt1, 0) - p, 0);}}return ans;}
};
// ans += max(q - max(cnt0 * cnt0 - cnt1, 0) - p, 0):
//cnt0* cnt0 -cnt1:需要补充的 '1' 数量(使 cnt1 ≥ cnt0²)。
//max(cnt0* cnt0 - cnt1, 0):若已满足 cnt1 ≥ cnt0²,则补充量为 0。
//q- max(...) -p:可选择的左端点数量(即符合条件的子串数量)。
//max(..., 0):确保结果非负。
代码通过按右边界 r 逐步遍历,分两部分统计 1 显著子串:
先统计不含 '0' 的子串(通过 ans += r - pos0.back() 实现)。
再统计包含 '0' 的子串(通过遍历 pos0 并计算 “满足 cnt1 ≥ cnt0² 的左端点数量” 实现)。
这种方法利用 '0' 的位置和数量关系,高效筛选符合条件的子串,时间复杂度为 O(n)(n 是字符串长度)。
理解 pos0 中 “哨兵 -1” 的作用:
1. 哨兵的本质:解决 “无前置 0” 的边界问题
假设字符串 s = "111"(没有 '0'),如果 pos0 初始为空,当处理第一个 '1' 时,pos0.back() 会因为数组为空而报错。
而初始化为 pos0 = {-1} 后:
- 当没有
'0'时,pos0.back()始终是-1。 - 计算 “不含 0 的子串数量” 时,
r - pos0.back()就会变成r - (-1) = r + 1,正好是 “以r为右端点、且不含 0 的子串数量”(比如r=0时,子串是"1",数量为 1;r=1时,子串是"1"、"11",数量为 2,以此类推)。
2. 结合 “含 0 子串” 的场景理解
假设字符串 s = "0110",pos0 的变化过程是:
- 初始:
pos0 = {-1} - 处理
s[0] = '0':pos0.push_back(0)→pos0 = {-1, 0} - 处理
s[1] = '1':不改变pos0 - 处理
s[2] = '1':不改变pos0 - 处理
s[3] = '0':pos0.push_back(3)→pos0 = {-1, 0, 3}
当处理 r=3(字符 '0')时,若要统计 “以 r=3 为右端点、且包含 '0' 的子串”,pos0 中的 -1, 0, 3 可以帮我们快速划分区间:
- 区间
(-1, 0]:对应子串s[0..3](但实际左端点不能小于 0,所以需要结合逻辑过滤)。 - 区间
(0, 3]:对应子串s[1..3]、s[2..3]等。
哨兵 -1 在这里的作用是统一 “无前置 0” 和 “有前置 0” 的区间计算逻辑,避免因边界情况写额外的条件判断。
3. 总结:哨兵的核心价值
pos0 中的 -1 是一个虚拟的 “0 的位置”,它的存在让我们可以用统一的公式(如 r - pos0.back())处理所有场景(无论前面有没有真实的 '0'),从而简化代码逻辑,避免边界错误。
