LeetCode 2055. 蜡烛之间的盘子

143 阅读3分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

给你一个长桌子,桌子上盘子和蜡烛排成一列。给你一个下标从 0 开始的字符串 s ,它只包含字符 '' 和 '|' ,其中 '' 表示一个 盘子 ,'|' 表示一支 蜡烛 。同时给你一个下标从 0 开始的二维整数数组 queries ,其中 queries[i] = [lefti, righti] 表示 子字符串 s[lefti...righti] (包含左右端点的字符)。对于每个查询,你需要找到 子字符串中 在 两支蜡烛之间 的盘子的 数目 。如果一个盘子在 子字符串中 左边和右边 都 至少有一支蜡烛,那么这个盘子满足在 两支蜡烛之间 。比方说,s = "|||||" ,查询 [3, 8] ,表示的是子字符串 "||**|" 。子字符串中在两支蜡烛之间的盘子数目为 2 ,子字符串中右边两个盘子在它们左边和右边 都 至少有一支蜡烛。 请你返回一个整数数组 answer ,其中 answer[i] 是第 i 个查询的答案。

此题亦可以用暴力穷举的方式求解,只需遍历到指定区域的两根蜡烛,得到其中的盘子个数,最后相加得到总和,不过暴力穷举的时间复杂度为O(n^3),所以不考虑。

此题可以用前缀和的方式求解,在这之前,我们先来分析一下这道题,以题目中的示例2为例:

image.png

若是想求得范围[1,17]中有多少个盘子在蜡烛之间,则需要求解的范围如下:

image.png

不难发现,下标[1]、[2]、[17]位置的元素我们压根不用管,因为它们不在两根蜡烛之间,那么问题可以转换为以下思路:

  1. 首先求得左边界距离最近的蜡烛,记为c1
  2. 其次求得右边界最近的蜡烛,记为c2
  3. 最后求解c1与c2之间的盘子数即可

分别来实现这三个过程,首先求解左边界距离最近的蜡烛,我们只需从左往右进行遍历,若是找到蜡烛则记录下标,如:

image.png

对于前3个元素,因为都是盘子,所以全部标记为-1,直至遇到第4个元素为蜡烛,标记为下标3:

image.png

后面的盘子也都标记为3,直至遇到新的蜡烛,就更新标记,以此类推,即可得到任意位置左边界距离最近的蜡烛。

代码如下:

private static int[] getPreCandleIndex(String s) {
    int[] ans = new int[s.length()];
    int temp = -1;
    for (int i = 0; i < s.length(); ++i) {
        // 遇到蜡烛则更新下标
        if (s.charAt(i) == '|') {
            temp = i;
        }
        ans[i] = temp;
    }
    return ans;
}

求解右边界距离最近的蜡烛也是如此,只不过是从右往左遍历,代码如下:

private static int[] getNextCandleIndex(String s) {
    int[] ans = new int[s.length()];
    int temp = -1;
    for (int i = s.length() - 1; i >= 0; --i) {
        if (s.charAt(i) == '|') {
            temp = i;
        }
        ans[i] = temp;
    }
    return ans;
}

最后是第三步,求解c1到c2区间内的盘子总数,这里我们可以使用前缀和,先将每个位置前面有多少个盘子求出来:

private static int[] getPrePlatesSum(String s) {
    int[] ans = new int[s.length()];
    for (int i = 0; i < s.length(); ++i) {
        // 第一个位置前缀和为0
        if (i == 0) {
            ans[i] = 0;
        } else {
            // 首先得到前一个位置的前缀和
            ans[i] = ans[i] + ans[i - 1];
        }
        // 若当前位置为盘子,和加1
        if (s.charAt(i) == '*') {
            ++ans[i];
        }
    }
    return ans;
}

这样就得到了任意位置前有多少个盘子了,现在若是想求某个区间的盘子数,只需让右侧区间位置的前缀和减去左侧区间位置的前缀和即可。

还需要注意一些特殊情况,如:

image.png

对于范围[0,2],它找不到左侧距离最近的蜡烛,又如:

image.png

对于范围[20],它找不到右侧距离最近的蜡烛,还有:

image.png

对于范围[4,5],它左侧距离最近的蜡烛下标为6,右侧距离最近的蜡烛下标为3,此时左侧蜡烛下标大于右侧蜡烛下标。

以上三种特殊情况,其盘子总数均为0。

综上所述,代码如下:

public static int[] platesBetweenCandles(String s, int[][] queries) {
    int[] ans = new int[queries.length];
    // 计算左侧距离最近的蜡烛下标
    int[] preCandleIndex = getPreCandleIndex(s);
    // 计算右侧距离最近的蜡烛下标
    int[] nextCandleIndex = getNextCandleIndex(s);
    // 计算前缀和
    int[] prePlatesSum = getPrePlatesSum(s);

    for (int i = 0; i < queries.length; ++i) {
        // 得到左侧最近的蜡烛下标
        int left = nextCandleIndex[queries[i][0]];
        // 得到右侧最近的蜡烛下标
        int right = preCandleIndex[queries[i][1]];
        // 处理特殊情况
        if (left == -1 || right == -1 || left > right) {
            ans[i] = 0;
        } else {
            // 前缀和相减
            ans[i] = prePlatesSum[right] - prePlatesSum[left];
        }
    }
    return ans;
}

private static int[] getPrePlatesSum(String s) {
    int[] ans = new int[s.length()];
    for (int i = 0; i < s.length(); ++i) {
        // 第一个位置前缀和为0
        if (i == 0) {
            ans[i] = 0;
        } else {
            // 首先得到前一个位置的前缀和
            ans[i] = ans[i] + ans[i - 1];
        }
        // 若当前位置为盘子,和加1
        if (s.charAt(i) == '*') {
            ++ans[i];
        }
    }
    return ans;
}

private static int[] getNextCandleIndex(String s) {
    int[] ans = new int[s.length()];
    int temp = -1;
    for (int i = s.length() - 1; i >= 0; --i) {
        if (s.charAt(i) == '|') {
            temp = i;
        }
        ans[i] = temp;
    }
    return ans;
}

private static int[] getPreCandleIndex(String s) {
    int[] ans = new int[s.length()];
    int temp = -1;
    for (int i = 0; i < s.length(); ++i) {
        // 遇到蜡烛则更新下标
        if (s.charAt(i) == '|') {
            temp = i;
        }
        ans[i] = temp;
    }
    return ans;
}

此题得解。