Minimum Reverse Operations

发布时间 2023-04-15 15:08:13作者: onlyblues

Minimum Reverse Operations

You are given an integer n and an integer p in the range [0, n - 1]. Representing a 0-indexed array arr of length n where all positions are set to $0$'s, except position p which is set to 1 .

You are also given an integer array banned containing some positions from the array. For the ith position in banned`arr[banned[i]] = 0`, and banned[i] != p.

You can perform multiple operations on `arr`. In an operation, you can choose a subarray with size k and reverse the subarray. However, the 1 in arr should never go to any of the positions in banned. In other words, after each operation arr[banned[i]] remains 0 .

Return an array ans where for each i from [0, n - 1]ans[i] is the minimum number of reverse operations needed to bring the 1 to position i in arr, or -1 if it is impossible.

  • A subarray is a contiguous non-empty sequence of elements within an array.
  • The values of ans[i] are independent for all i's.
  • The reverse of an array is an array containing the values in reverse order.

Example 1:

Input: n = 4, p = 0, banned = [1,2], k = 4
Output: [0,-1,-1,1]
Explanation: In this case k = 4 so there is only one possible reverse operation we can perform, which is reversing the whole array. Initially, 1 is placed at position 0 so the amount of operations we need for position 0 is 0. We can never place a 1 on the banned positions, so the answer for positions 1 and 2 is -1. Finally, with one reverse operation we can bring the 1 to index 3, so the answer for position 3 is 1. 

Example 2:

Input: n = 5, p = 0, banned = [2,4], k = 3
Output: [0,-1,-1,-1,-1]
Explanation: In this case the 1 is initially at position 0, so the answer for that position is 0. We can perform reverse operations of size 3. The 1 is currently located at position 0, so we need to reverse the subarray [0, 2] for it to leave that position, but reversing that subarray makes position 2 have a 1, which shouldn't happen. So, we can't move the 1 from position 0, making the result for all the other positions -1. 

 Example 3:

Input: n = 4, p = 2, banned = [0,1,3], k = 1
Output: [-1,-1,0,-1]
Explanation: In this case we can only perform reverse operations of size 1. So the 1 never changes its position.

Constraints:

  • $1 \leq n \leq {10}^5$
  • $0 \leq p \leq n - 1$
  • $0 \leq \text{banned.length} \leq n - 1$
  • $0 \leq \text{banned}[i] \leq n - 1$
  • $1 \leq k \leq n$
  • $\text{banned}[i] \ne p$
  • all values in banned are unique

 

解题思路

  比赛的时候想到用bfs求最短路,结果发现每个状态的转移次数太多了,要bfs的话时间复杂度差不多是$O(nk)$,肯定超时的。

  可以根据$1$所在的不同位置把局面定义为对应的状态(即节点),那么一共有$n$种状态。如果两个状态间能相互转换,那么就在这两个状态之间连一条权值为$1$的边。因此发现可以从状态$p$开始bfs,求到其他状态的最小距离。

  可以发现边的数量非常多,假设当前$1$的所在位置是$x$,要对长度为$k$的区间$[l,r]$进行翻转($l \leq x \leq r$),等价于把$x$变到$r - x + l$。而对于固定长度为$k$的区间翻转,$x$最多变换到$k$个不同的位置,如下图:

  其中区间的左端点$l \in [x - k + 1, x]$,对应的右端点$r \in [x, x + k - 1]$,当然还要保证不能越界。因此可以发现如果在bfs时直接这样枚举每一条边,那么时间复杂度就达到了$O(nk)$了。

  可以发现$x$能变换到的位置的奇偶性都是一样的。假设当前的左端点为$l=i$,那么右端点就是$r=i+k-1$,对区间$[l,r]$翻转那么$x$就会变到$r-x+l = 2i-x+k-1$。当把区间右移一个单位,即$l=i+1$,$r=i+k$,那么$x$就会变到$r-x+l = 2(i+1)-x+k-1$,可以发现变化后的位置的奇偶性都是一样的。

  为此,如果$1$位于$x$处,那么经过一次区间翻转,$x$能够变化到的位置就是$[x-k-1, x-k+1, x-k+3, \ldots, x+k-1]$,当然还要保证不能越界。而事实上根据bfs的性质,如果一个节点已经被更新了,那么到这个节点的最小距离就已经确定了,之后不会再被更新。因此在枚举$x$能够变化到的位置时实际上很多状态都是不会再被更新的了。那么有没有什么做法可以避免枚举到已经不会再更新的状态呢?

  这里提供一个做法,就是开两个平衡树分别存所有奇数的状态和偶数的状态。当要枚举$x$能变化到的状态时,先根据$x-k-1$的奇偶性来判断转移到的状态是奇数还是偶数,然后在对应的平衡树中对区间$[x-k-1, x+k-1]$内还存在的的状态进行更新,并从平衡树中删除。这样就能保证每次枚举到的状态都是还未被更新的状态。查找区间端点以及删除都是$O(\log{n})$的时间复杂度,因此整个bfs的时间复杂度就是$O(n \log{n})$。

  AC代码如下:

 1 class Solution {
 2 public:
 3     vector<int> minReverseOperations(int n, int p, vector<int>& banned, int k) {
 4         vector<bool> vis(n);
 5         for (auto &x : banned) {
 6             vis[x] = true;
 7         }
 8         set<int> st[2];
 9         for (int i = 0; i < n; i++) {
10             if (!vis[i] && i != p) st[i & 1].insert(i);    // 根据奇偶性把数放到对应的平衡树中 
11         }
12         st[0].insert({-0x3f3f3f3f, 0x3f3f3f3f});    // 插入哨兵,保证在二分时有结果 
13         st[1].insert({-0x3f3f3f3f, 0x3f3f3f3f});
14         vector<int> dist(n, -1);
15         dist[p] = 0; 
16         queue<int> q({p});
17         while (!q.empty()) {
18             int t = q.front();
19             q.pop();
20             int x = k - 1 - t & 1;    // 判断x能转移到的状态的奇偶性 
21             auto l = st[x].lower_bound(2 * max(0, t - k + 1) + k - 1 - t);    // 找到最左边能变化到的位置 
22             auto r = st[x].upper_bound(2 * min(n - 1, t + k - 1) - k + 1 - t);    // 找到最右边能变化到的位置 
23             while (l != r) {
24                 dist[*l] = dist[t] + 1;
25                 q.push(*l);
26                 l = st[x].erase(l);    // 从平衡树中删除已被更新的状态 
27             }
28         }
29         return dist;
30     }
31 };

  还可以用并查集来实现上面所说的删除操作。本质上就是找到某个区间内还没有被删除的位置,因此可以开个并查集来维护删除位置的连通性。当要删除一个位置$x$时,那么就将$x$所在的集合的代表元素(其实就是$x$)指向$x+2$所在集合的代表元素(注意要保证奇偶性相同,因此加$2$)。每个集合的代表元素的含义就是当前集合中除了代表元素外,每个元素向右找的第一个还没被删除的位置。

  AC代码如下,时间复杂度为$O(n)$:

 1 class Solution {
 2 public:
 3     vector<int> minReverseOperations(int n, int p, vector<int>& banned, int k) {
 4         vector<int> fa(n + 2);
 5         for (int i = 0; i < n + 2; i++) {
 6             fa[i] = i;
 7         }
 8         function<int(int)> find = [&](int x) {
 9             return fa[x] == x ? fa[x] : fa[x] = find(fa[x]);
10         };
11         for (auto &x : banned) {
12             fa[x] = find(x + 2);
13         }
14         fa[p] = find(p + 2);
15         vector<int> dist(n, -1);
16         dist[p] = 0;
17         queue<int> q({p});
18         while (!q.empty()) {
19             int t = q.front();
20             q.pop();
21             int l = find(2 * max(0, t - k + 1) + k - 1 - t);
22             int r = 2 * min(n - 1, t + k - 1) - k + 1 - t;
23             while (l <= r) {
24                 dist[l] = dist[t] + 1;
25                 q.push(l);
26                 fa[l] = find(l + 2);
27                 l = find(l);
28             }
29         }
30         return dist;
31     }
32 };

 

参考资料

  最少翻转操作数 BFS+平衡树【力扣周赛 339】:https://www.bilibili.com/video/BV1va4y1M7Fr/