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/
标签:Operations,状态,Reverse,int,banned,leq,Minimum,position,reverse From: https://www.cnblogs.com/onlyblues/p/17320848.html