ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

环形字符串子串权值求和的动态规划与滑动窗口解法

环形字符串子串权值求和的动态规划与滑动窗口解法 1. 题目解析与思路拆解这道题目考察的是环形字符串中子串权值求和的问题属于动态规划与滑动窗口结合的高级应用。我们先来拆解题目要求给定一个由0和1组成的环形字符串需要计算所有长度≥2的子串中01子序列的数量之和。这里的01子序列指的是在原字符串中删除任意字符后剩下的恰好是01的序列。1.1 关键概念理解环形字符串处理由于字符串是环形的常规的线性处理方法不再适用。常见的处理技巧是将原字符串复制一份接在自己后面这样就能用线性方法模拟环形特性。子序列与子串的区别子串必须连续选取的字符序列子序列可以不连续选取的字符序列权值计算每个子串的权值是其包含01子序列的数量。例如001有2个01子序列删除第二个0和删除第一个0。1.2 暴力解法分析最直观的解法是枚举所有长度≥2的子串对每个子串统计其中01子序列的数量。对于长度为n的字符串子串数量O(n²)每个子串统计01子序列O(m²)m为子串长度 总时间复杂度为O(n⁴)对于n1e5显然不可行。1.3 优化思路我们需要找到一种能在线性时间内计算所有子串权值之和的方法。观察发现每个01子序列的贡献可以拆解为对于每个1它前面有多少个0在环形情况下滑动窗口可以高效维护0和1的数量关系因此我们可以采用滑动窗口前缀和的方法将时间复杂度优化到O(n)。2. 算法设计与实现细节2.1 滑动窗口设计为了处理环形字符串我们将原字符串s复制一份得到ss这样任何环形子串都可以表示为这个扩展字符串中的一个线性子串。定义滑动窗口大小为n原字符串长度窗口从左向右滑动每次移动一个位置。我们需要维护以下变量num0窗口内0的数量sum0窗口内所有0的位置和相对于窗口起始位置num1窗口内1的数量sum辅助变量记录当前窗口内所有0对后续1的贡献sum1当前窗口内01子序列的总数2.2 核心算法流程初始化窗口处理前n个字符统计初始的num0、sum0、num1、sum和sum1滑动窗口移除最左边字符的影响加入右边新字符的影响累加当前窗口的sum1到最终答案模运算处理由于结果可能很大每次累加后取模2.3 关键操作解释加入0时的处理if (s[i] 0) { num0; sum0 i; // 记录0的位置 }加入1时的处理else { // s[i] 1 num1; sum1 sum0; // 新1与前面所有0形成新子序列 sum num0; // 记录这些0对后续的贡献 }移除字符时的处理// 移除窗口左端点元素 i - n 的影响 sum1 - sum; // 去掉以移除元素为起点产生的贡献 sum0 - num0; // 更新所有0下标和 if (s[i - n] 0) { num0--; // 0数量减少 sum - num1; // 0被移除减少对后续1的贡献 } else { num1--; // 1数量减少 }3. 代码实现与注释以下是完整实现代码附详细注释#include iostream #include string using namespace std; #define rep(i, a, b) for (int i (a), _##i (b); i _##i; i) using ll long long; const int N 1e5 5; const int mod 1e9 7; // 全局变量 ll ans 0; // 最终答案 ll sum 0; // 当前窗口内0对后续1的贡献 ll num0 0; // 当前窗口内0的数量 ll sum0 0; // 当前窗口内所有0的下标之和 ll num1 0; // 当前窗口内1的数量 ll sum1 0; // 当前窗口内01子序列数量 void solve() { int n; cin n; string s; cin s; // 为了模拟环形把s复制一份自己接到自己后面并在前面加空格使下标从1开始 s s s; // 初始化滑动窗口处理前n个字符 rep(i, 1, n) { if (s[i] 0) { num0; sum0 i; // 记录0的位置 } else { // s[i] 1 num1; sum1 sum0; // 新1与前面所有0形成新子序列 sum num0; // 记录这些0对后续的贡献 } } // 滑动窗口处理后续n个字符 rep(i, n 1, 2 * n) { // 移除窗口左端点元素 i - n 的影响 sum1 - sum; // 去掉以移除元素为起点产生的贡献 sum0 - num0; // 更新所有0下标和 if (s[i - n] 0) { num0--; // 0数量减少 sum - num1; // 0被移除减少对后续1的贡献 } else { num1--; // 1数量减少 } // 加入新的元素 s[i] if (s[i] 0) { num0; sum0 i; // 加入0记录位置 } else { // s[i] 1 num1; sum1 sum0; // 新加入的1与已有0形成新子序列 sum num0; // 0的数量影响sum } // 累加当前窗口的贡献 ans (ans sum1) % mod; } cout ans \n; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); int t 1; // cin t; // 如果有多组测试数据可以打开 while (t--) { solve(); } return 0; }4. 算法复杂度分析时间复杂度O(n)初始化窗口O(n)滑动窗口处理O(n)每个字符最多被处理两次加入和移除空间复杂度O(n)主要空间消耗来自字符串的扩展存储5. 常见问题与调试技巧5.1 边界情况处理全0或全1字符串权值总和应为0需要确保算法在这种情况下能正确输出0最小长度n2只有1个子串即整个字符串需要单独验证这种情况模运算处理确保每次累加后都取模特别注意减法操作后可能出现负数需要加mod再取模5.2 调试技巧小规模测试先用手算验证小例子如n3的001确保窗口滑动时各变量的更新逻辑正确变量跟踪打印窗口滑动过程中各变量的值特别关注sum1的增减是否符合预期环形特性验证构造一个明显需要环形处理的案例如011验证算法是否能正确处理跨越首尾的子串5.3 性能优化输入输出加速ios::sync_with_stdio(false); cin.tie(nullptr);这可以显著提高C的I/O速度对于大规模输入很重要。变量复用复用全局变量减少内存分配但要注意每次solve()前是否需要重置全局变量避免不必要计算在滑动窗口时只更新受影响的变量例如移除1时不会影响sum变量6. 算法扩展与变种6.1 类似问题线性字符串版本去掉环形处理问题会更简单可以用类似的双指针/滑动窗口方法统计10子序列逻辑类似但需要反向处理记录每个1后面有多少个0多字符统计如统计001、110等更复杂的子序列需要维护更多状态变量6.2 其他解法思路前缀和优化预处理0和1的前缀数量可以快速计算任意区间内的01子序列数分治法将环形字符串拆分为线性段处理合并时需要特殊处理跨越分割点的子串动态规划定义dp[i][j]表示处理到第i个字符时的某种状态适用于更复杂的子序列统计问题在实际编码竞赛中滑动窗口方法通常是这类问题的最优解因为它既高效又易于实现。理解并掌握这种方法的思维模式可以解决许多类似的子串统计问题。
返回列表