Avatar
“”

一个兴趣使然的追梦人

▼

【题解】AND, OR, Sort!

题目链接

https://codeforces.com/contest/2266/problem/C

题目大意

给定一个长度为 \(n\) 的二进制字符串 \(s\)(仅包含字符 0 和 1)。你可以执行以下操作任意次(可以为 0 次):

  • 选择一个位置 \(i\)(\(1 \le i \le n\)),并将字符 \(s_i\) 替换为前缀 \(s_1, s_2, \dots, s_i\) 的按位与(bitwise AND)或按位或(bitwise OR)。

求将字符串调整为非递减序(即形式为 \(00\dots0011\dots11\))所需的最少操作次数。


解题思路与技巧

1. 技巧一:目标状态反推(Target State Characterization)

不从繁杂的操作去正向搜索,而是分析合法终态的几何特征。
排好序的 0-1 串必然形如 \(00\dots0011\dots11\),它实质上完全由一个分割点(Split Point)决定:分割点左侧全是 0,右侧全是 1。

2. 技巧二:挖掘关键不变量(Invariants)

  • 首位字符不可变:
    位置 \(1\)(下标 0)的前缀只有其自身,做 AND 或 OR 运算结果依然是它自己。因此,首字符永远无法被修改。
  • 首字符为 1 的情况:
    既然首字符永远无法变成 0,为了让整串非递减,整个字符串最终必须全部变为 1。
    由于首位就是 1,后面的任何 0 都可以直接利用前缀 OR 变成 1。此时的最小操作数,恰好等于整个串中 0 的总个数。

3. 技巧三:两阶段操作保序法(Constructive Order)

当首字符为 0 时,可以在任意位置 \(i\) 划分分界线(左边全变 0,右边全变 1)。
将区间 \([0, i]\) 变为 0、区间 \([i+1, n-1]\) 变为 1 的代价看似是:

\[\text{Cost} = (\text{左侧 } 1 \text{ 的个数}) + (\text{右侧 } 0 \text{ 的个数}) \]

那么这个代价在操作过程中是否会产生依赖冲突导致死锁呢?
答案是不会,只要按照以下两阶段顺序操作即可证明其充分性:

  1. 第一阶段(自左向右 OR 右侧):
    如果右侧有 0 想变成 1,只要左侧还保留着原有的 1,我们先不动左侧的 1,直接自左往右扫描右半段,利用前缀 OR 把所有目标为 1 的位置刷成 1。
  2. 第二阶段(AND 清理左侧):
    右侧处理完毕后,由于首位 \(s_0 = 0\) 始终存在,左半段的所有位置前缀中都含有 0,此时对左侧原本为 1 的位置执行前缀 AND,即可顺利刷为 0。

两步操作互不干扰,因此任意分割点下的代价计算均成立。

4. 技巧四:前缀/后缀计数线性转移

分割点共有 \(O(n)\) 个。不需要在每个分割点耗费 \(O(n)\) 暴力重算,只需先统计全串 0 的总数,然后用一个循环从左向右滑动分割点:

  • 遇到 1:左侧需要变 0 的数量 \(+1\);
  • 遇到 0:右侧需要变 1 的数量 \(-1\);
  • 每次 \(O(1)\) 维护并更新最小值即可。

复杂度分析

  • 时间复杂度:统计字符出现次数需要 \(O(n)\),滑动分界点遍历一次需要 \(O(n)\),单组用例时间复杂度为 \(O(n)\)。总复杂度为 \(O(\sum n) \le O(2 \cdot 10^5)\),效率极高。
  • 空间复杂度:仅使用几个基础计数器,额外辅助空间复杂度为 \(O(1)\)。

C++ 完整代码

#include <iostream>
#include <string>
#include <algorithm>

using namespace std;

void solve() {
    int n;
    cin >> n;
    string s;
    cin >> s;

    // 统计整个字符串中 '0' 的总数量
    int zeros_right = 0;
    for (int i = 0; i < n; i++) {
        if (s[i] == '0') {
            zeros_right++;
        }
    }

    // 若首字符为 '1',由于首位无法改变,最终串必须全为 '1'
    // 代价就是把所有 '0' 变成 '1'
    if (s[0] == '1') {
        cout << zeros_right << "\n";
        return;
    }

    // 首位为 '0':滑动维护分割点
    int ones_left = 0;
    int ans = 1e9;

    // 枚举分割点:区间 [0, i] 置为 0,区间 [i + 1, n - 1] 置为 1
    for (int i = 0; i < n; i++) {
        if (s[i] == '1') {
            ones_left++;
        } else {
            zeros_right--;
        }
        ans = min(ans, ones_left + zeros_right);
    }

    cout << ans << "\n";
}

int main() {
    // 基础输入输出加速
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int t;
    cin >> t;
    while (t--) {
        solve();
    }

    return 0;
}
posted @ 2026-10-07 19:46  hermanO  阅读(11)  评论(0)    收藏  举报