leetcode每日一题:最少翻转操作数

image

题目

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] 之间的任意下标 ians[i] 是将 1 放到位置 i 处的 最少 翻转操作次数,如果无法放到位置 i 处,此数为 -1

  • 子数组 指的是一个数组里一段连续 非空 的元素序列。
  • 对于所有的 ians[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的思路,继续对上一次达到的新位置去继续遍历。注意,这里只要每次遍历新达到的位置即可,之前已经达到过的位置,既是再次达到,可以确定之前已经使用更少的翻转次数达到过。

图解

image

代码

image

优化

​ 我们在过了给出的示例提交后,发现TLE了。

image

​ 仔细分析一下,问题的原因是,我们每次遍历的时候,都是在[min, max]之间间隔2每次遍历全部的位置,如果k比较大,这中间位置很多,但是被ban掉的位置和之前已经达到过的位置,是不需要再次遍历的,虽然我们这里使用continue跳过了,但是还是有很大的浪费,最好可以只精确遍历未达到过的位置。

​ 由于每一次[min, max]之间达到的位置内部,奇偶性都是一致的(2次[min, max]之间奇偶性可能不同),所以,我们可以把未达到过的位置,区分奇偶性放在2个TreeSet中,然后从TreeSet中获取到[min, max]的可能值。

优化后代码

image

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;
}

耗时

image

posted @ 2025-03-20 12:34  podongfeng  阅读(71)  评论(0)    收藏  举报