BFS & 有序集合
解法:BFS & 有序集合
因为每次操作不关心字符的具体下标,所以我们只关心当前还剩几个 0 要变。
首先容易想到 BFS 的解法:设现在有 $c$ 个 0,如果操作后变成了 $c'$ 个 0,那么从节点 $c$ 向 $c'$ 连一条边。我们要求的就是从初始节点到节点 $0$ 的最短路。
但直接 BFS 的复杂度是 $\mathcal{O}(n^2)$ 的,因为我要枚举一次操作选几个 0。对于给定的 $c$,它能到达的节点之间有没有什么关联呢?
先来看个例子,假设一共有 $n = 7$ 个字符,现在还有 $c = 4$ 个 0 要变,每次要变 $k = 5$ 个下标我们能做哪些变换?
- 选 $4$ 个
0以及 $1$ 个1,变化后还有 $c - 4 + 1 = c - 3$ 个0要变。 - 选 $3$ 个
0以及 $2$ 个1,变化后还有 $c - 3 + 2 = c - 1$ 个0要变。 - 选 $2$ 个
0以及 $3$ 个1,变化后还有 $c - 2 + 3 = c + 1$ 个0要变。
可以发现,对于给定的 $c$,变化量的奇偶性总是相同的,而且变化范围(在相同奇偶性的条件下)是连续的。
可到达的节点是连续的,就能使用 leetcode 2612. 最少翻转操作数 里的套路,不熟悉的读者可以首先学习这道题。简单来说,我们可以用一个有序集合(c++ 里的 set)维护还没有到达过的节点,这样就能用 $\mathcal{O}(\log n)$ 的复杂度找出范围内还没有访问过的下一个节点,并把该节点从有序集合里移除。BFS 复杂度降为 $\mathcal{O}(n\log n)$。
参考代码(c++)
class Solution {
public:
int minOperations(string s, int K) {
int n = s.size();
int dis[n + 1];
memset(dis, -1, sizeof(dis));
// 按奇偶性把所有节点加入有序集合
set<int> st[2];
for (int i = 0; i <= n; i++) st[i & 1].insert(i);
// 计算当前字符串有几个 0,这是我们的出发节点
int S = 0;
for (char c : s) if (c == '0') S++;
queue<int> q;
q.push(S); dis[S] = 0; st[S & 1].erase(S);
while (!q.empty()) {
int sn = q.front(); q.pop();
// 计算变化的范围
// 最多选 min(K, sn) 个 0,以及最多选 min(K, n - sn) 个 1
int l = min(K, sn);
l = (K - l) - l;
int r = min(K, n - sn);
r = r - (K - r);
// 将指定范围里还未访问过的节点取出来,然后删除
auto &tmp = st[(sn + l) & 1];
auto it = tmp.lower_bound(sn + l);
while (it != tmp.end()) {
if (*it > sn + r) break;
q.push(*it);
dis[*it] = dis[sn] + 1;
tmp.erase(it++);
}
}
return dis[0];
}
};