硬币翻转

GitHub跳转原题关系图返回列表

从右往左看每个位置的强制翻转决策,可化简为统计相邻字符变化次数,再看末位是否为 0。

OJ: luogu

题目 ID: P2708

难度:普及-

标签:贪心字符串模拟

日期: 2026-06-19 10:26

题意

给出一排 0/1 硬币,1 表示正面,0 表示反面。

一次操作可以把从第一个硬币开始的某个前缀整体翻面。

要求用最少操作次数把所有硬币都变成 1

思路

这题从右往左看最清楚。

最直接的教学版写法如下:

cpp
// brute.cpp:从右往左真正模拟翻前缀,作为教学版和对拍基准程序。
#include <bits/stdc++.h>
using namespace std;

string s;

void flip_prefix(int right_pos) {
    for (int i = 0; i <= right_pos; i++) {
        if (s[i] == '0') {
            s[i] = '1';
        }
        else {
            s[i] = '0';
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> s;

    int ans = 0;
    int n = (int) s.size();

    // 从右往左定死每个位置:如果当前位置是 0,只能翻到这里为止。
    for (int i = n - 1; i >= 0; i--) {
        if (s[i] == '0') {
            flip_prefix(i);
            ans++;
        }
    }

    cout << ans << '\n';
    return 0;
}

如果当前考虑到位置 i

  • 它已经是 1,不用动;
  • 它是 0,那就只能立刻把前缀 [0..i] 翻掉,因为以后再也没机会改它。

这说明最优策略其实是被强制决定的。

再往前推一步,会发现答案可以直接线性统计:

  1. 每当相邻两个字符不同,答案加一;
  2. 如果最后一个字符是 0,答案再加一。

这样就不需要真的翻前缀了。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

string s;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> s;

    int n = (int) s.size();
    int ans = 0;

    // 每次相邻字符变化,说明至少要多做一次翻转来切换当前前缀状态。
    for (int i = 1; i < n; i++) {
        if (s[i] != s[i - 1]) {
            ans++;
        }
    }

    // 最右端如果还是 0,最后还必须再翻一次。
    if (s[n - 1] == '0') {
        ans++;
    }

    cout << ans << '\n';
    return 0;
}

复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(1)O(1)

总结

这题的关键是把“前缀翻转”换成“从右往左定位置”的思考方式。

一旦看到右边位置的决策是强制的,就很容易推到最终的线性统计公式。