反转硬币
思路
拿到这道题,我们先搞清楚规则:桌上有 个硬币,
0 表示正面,1 表示反面。每一步,数一下当前有多少个反面朝上的硬币,假设是 个,就把第
个位置的硬币翻转。重复这个过程,直到所有硬币都变成正面(全
0)。
直接模拟的话,每一步 要么加 1 要么减 1,总步数可能达到
级别,对于
接近
的数据会超时。
那怎么优化呢?我们换个视角来理解这个过程。
把 值的变化想象成一个"行走"的过程:从初始的
出发,目标是走到
。每到一个位置
,看一眼 coins[
]:
- 如果是
1(反面),就能"通过"这一层,往下走到,同时把它翻成
0; - 如果是
0(正面),就会被"弹回去",往上走到,同时把它翻成
1。
对于被弹回的情况,我们需要在上面转一圈再回来。回来之后,这个位置已经变成 1 了,就能顺利通过。
关键观察来了:当位置 是
0 时,我们要往上找到第一个 1 的位置 (从位置
开始往上扫)。中间经过的都是
0,一路弹上去再弹回来,总代价是 。而这一过程结束后,位置
的
1 被消耗掉了(变成 0),其余中间位置恢复原状。
于是算法就清晰了:用一个有序集合维护所有 1 的位置。从 开始逐层下降:
- 如果
在集合中(是
1),直接移除,代价,
减
;
- 否则,在集合中查找
的最小元素
,代价
,移除
,
减
。
每层只做一次集合查询和删除,总时间 。
另外可以证明, 的值永远不会超过
(因为最多
个硬币),且当 coins[
] 为
0 时, 以上必定存在
1(否则 个
1 无法放进 个位置里),所以过程一定能终止,不存在"不可能"的情况。
代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
scanf("%d", &n);
char s[100005];
scanf("%s", s);
set<int> ones;
for (int i = 0; i < n; i++) {
if (s[i] == '1') ones.insert(i);
}
int k = (int)ones.size();
long long total = 0;
while (k > 0) {
auto it = ones.find(k - 1);
if (it != ones.end()) {
ones.erase(it);
total += 1;
k--;
} else {
auto it2 = ones.lower_bound(k);
int j = *it2;
total += 2LL * (j - k) + 3;
ones.erase(it2);
k--;
}
}
printf("%lld\n", total);
return 0;
}
import java.util.*;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine().trim());
String s = br.readLine().trim();
TreeSet<Integer> ones = new TreeSet<>();
for (int i = 0; i < n; i++) {
if (s.charAt(i) == '1') ones.add(i);
}
int k = ones.size();
long total = 0;
while (k > 0) {
if (ones.contains(k - 1)) {
ones.remove(k - 1);
total += 1;
k--;
} else {
int j = ones.ceiling(k);
total += 2L * (j - k) + 3;
ones.remove(j);
k--;
}
}
System.out.println(total);
}
}
复杂度分析
- 时间复杂度:
。初始化有序集合
,之后每层下降做一次集合查询和删除,共
层,每次
。
- 空间复杂度:
,存储有序集合。

京公网安备 11010502036488号