题目
2612. 最少翻转操作数
给你一个整数 n
和一个在范围 [0, n - 1]
以内的整数 p
,它们表示一个长度为 n
且下标从 0 开始的数组 arr
,数组中除了下标为 p
处是 1
以外,其他所有数都是 0
。
同时给你一个整数数组 banned
,它包含数组中的一些位置。banned
中第 i 个位置表示 arr[banned[i]] = 0
,题目保证 banned[i] != p
。
你可以对 arr
进行 若干次 操作。一次操作中,你选择大小为 k
的一个 子数组 ,并将它 翻转 。在任何一次翻转操作后,你都需要确保 arr
中唯一的 1
不会到达任何 banned
中的位置。换句话说,arr[banned[i]]
始终 保持 0
。
请你返回一个数组 ans
,对于 [0, n - 1]
之间的任意下标 i
,ans[i]
是将 1
放到位置 i
处的 最少 翻转操作次数,如果无法放到位置 i
处,此数为 -1
。
- 子数组 指的是一个数组里一段连续 非空 的元素序列。
- 对于所有的
i
,ans[i]
相互之间独立计算。 - 将一个数组中的元素 翻转 指的是将数组中的值变成 相反顺序 。
示例 1:
输入:n = 4, p = 0, banned = [1,2], k = 4
输出:[0,-1,-1,1]
解释:k = 4,所以只有一种可行的翻转操作,就是将整个数组翻转。一开始 1 在位置 0 处,所以将它翻转到位置 0 处需要的操作数为 0 。
我们不能将 1 翻转到 banned 中的位置,所以位置 1 和 2 处的答案都是 -1 。
通过一次翻转操作,可以将 1 放到位置 3 处,所以位置 3 的答案是 1 。
示例 2:
输入:n = 5, p = 0, banned = [2,4], k = 3
输出:[0,-1,-1,-1,-1]
解释:这个例子中 1 一开始在位置 0 处,所以此下标的答案为 0 。
翻转的子数组长度为 k = 3 ,1 此时在位置 0 处,所以我们可以翻转子数组 [0, 2],但翻转后的下标 2 在 banned 中,所以不能执行此操作。
由于 1 没法离开位置 0 ,所以其他位置的答案都是 -1 。
示例 3:
输入:n = 4, p = 2, banned = [0,1,3], k = 1
输出:[-1,-1,0,-1]
解释:这个例子中,我们只能对长度为 1 的子数组执行翻转操作,所以 1 无法离开初始位置。
提示:
1 <= n <= 105
0 <= p <= n - 1
0 <= banned.length <= n - 1
0 <= banned[i] <= n - 1
1 <= k <= n
banned[i] != p
banned
中的值 互不相同 。
思路
先读懂题目,题目比较长,这里翻转的意思,就是倒序。初始情况下,数组中只有1个值是1。我们先看下下标p为1,经过1次翻转,可能到达哪些位置:
- 如果p作为子数组的右侧端点,那么翻转后,可以达到最小值 :
p - k + 1
- 如果p作为子数组的左侧端点,那么翻转后,可以达到最大值 :
p + k - 1
当然,上述只是最理想的情况,由于我们子数组的长度固定为k,所以当前节点p可能无法作为子数组的左侧端点或者右侧端点,否则就会越界,对于这2种情况,我们再次分类讨论:
- 如果
p < k - 1
,此时p无法作为子数组右侧端点,可以让p移动到最左侧的子数组是[0, k-1]
,此时p移动到的位置是k - 1 - p
- 如果
p > n - k
,此时p无法作为子数组的左侧端点,可以让p移动到最右侧的子数组是[n-k, n-1]
,此时p移动到的位置是2 * n - p - 1 - k
这样,我们已经获取了p经过1次翻转后,可能到达的位置范围,不过这里需要注意的是,并不是范围内每个位置都可以达到,实际上,当我们用滑动窗口滑动子数组的时候,每滑动1个位置,p的翻转位置其实移动了2。
另外,我们还要去掉被ban的位置,这些位置永远不可达。
那我们知道了经过1次移动,可以达到的全部位置后,我们可以使用BFS的思路,继续对上一次达到的新位置去继续遍历。注意,这里只要每次遍历新达到的位置即可,之前已经达到过的位置,既是再次达到,可以确定之前已经使用更少的翻转次数达到过。
图解
代码
优化
我们在过了给出的示例提交后,发现TLE了。
仔细分析一下,问题的原因是,我们每次遍历的时候,都是在[min, max]
之间间隔2每次遍历全部的位置,如果k比较大,这中间位置很多,但是被ban掉的位置和之前已经达到过的位置,是不需要再次遍历的,虽然我们这里使用continue
跳过了,但是还是有很大的浪费,最好可以只精确遍历未达到过的位置。
由于每一次[min, max]
之间达到的位置内部,奇偶性都是一致的(2次[min, max]
之间奇偶性可能不同),所以,我们可以把未达到过的位置,区分奇偶性放在2个TreeSet中,然后从TreeSet中获取到[min, max]
的可能值。
优化后代码
public int[] minReverseOperations(int n, int p, int[] banned, int k) {Set<Integer> banSet = new HashSet<>();for (int ban : banned) {banSet.add(ban);}// 未决策的位置,分开奇偶,因为每次广度优先遍历,要么全是奇数、要么全是偶数TreeSet[] oddEvenArr = new TreeSet[]{new TreeSet<Integer>(), new TreeSet<Integer>()};for (int i = 0 ; i < n; i++) {if (banSet.contains(i) || i == p) {continue;}oddEvenArr[i&1].add(i);}int[] ans = new int[n];Arrays.fill(ans, -1);ans[p] = 0;Queue<Integer> queue = new LinkedList<>();queue.add(p);while (!queue.isEmpty()) {int cur = queue.poll();int min, max;if (cur < k - 1) {min = k - 1 - cur;} else {min = cur - k + 1;}if (cur > n - k) {max = 2 * n - k - 1 - cur;} else {max = cur + k - 1;}TreeSet<Integer> set = oddEvenArr[min&1];Iterator<Integer> it = set.tailSet(min).iterator();while (it.hasNext()) {int val = it.next();if (val > max) {break;}ans[val] = ans[cur] + 1;queue.add(val);it.remove();}}return ans;
}