2073.买票需要的时间

目标

有 n 个人前来排队买票,其中第 0 人站在队伍 最前方 ,第 (n - 1) 人站在队伍 最后方 。

给你一个下标从 0 开始的整数数组 tickets ,数组长度为 n ,其中第 i 人想要购买的票数为 tickets[i] 。

每个人买票都需要用掉 恰好 1 秒 。一个人 一次只能买一张票 ,如果需要购买更多票,他必须走到 队尾 重新排队(瞬间 发生,不计时间)。如果一个人没有剩下需要买的票,那他将会 离开 队伍。

返回位于位置 k(下标从 0 开始)的人完成买票需要的时间(以秒为单位)。

示例 1:

输入:tickets = [2,3,2], k = 2
输出:6
解释:

队伍一开始为 [2,3,2],第 k 个人以下划线标识。
在最前面的人买完票后,队伍在第 1 秒变成 [3,2,1]。
继续这个过程,队伍在第 2 秒变为[2,1,2]。
继续这个过程,队伍在第 3 秒变为[1,2,1]。
继续这个过程,队伍在第 4 秒变为[2,1]。
继续这个过程,队伍在第 5 秒变为[1,1]。
继续这个过程,队伍在第 6 秒变为[1]。第 k 个人完成买票,所以返回 6。

示例 2:

输入:tickets = [5,1,1,1], k = 0
输出:8
解释:
队伍一开始为 [5,1,1,1],第 k 个人以下划线标识。
在最前面的人买完票后,队伍在第 1 秒变成 [1,1,1,4]。
继续这个过程 3 秒,队伍在第 4 秒变为[4]。
继续这个过程 4 秒,队伍在第 8 秒变为[]。第 k 个人完成买票,所以返回 8。

说明:

  • n == tickets.length
  • 1 <= n <= 100
  • 1 <= tickets[i] <= 100
  • 0 <= k < n

思路

n 个人在排队买票,每人每次限买一张,每买一张票耗时 1 秒,如果要买多张需要重新排队。现在已知 tickets 数组,表示第 i 个人想买票的数量,问第 k 个人完成买票需要多长时间(仅记录买票时间)。

由于数据量不大,直接的解法是用队列模拟排队。假设所有人都要买 m 张票,kn - 1,那么时间复杂度为 O(mn)

考虑整体计算,当第 k 个人排到队尾时,将其前面所有人剩余要买的 tickets[i] 减去 第 k 个人剩余需要买的票数 r = tickets[k] - 1,那么剩余的等待时间为 n * r + (tickets[i] 为负的和)。时间复杂度为 O(n)

网友有一个解法是客户不动,让售票员从头到尾卖票,如果顾客已经完成买票则跳过,这也算是模拟的另一种思路。

代码


/**
 * @date 2024-09-29 8:45
 */
public class TimeRequiredToBuy2073 {

    /**
     * 整体计算
     */
    public int timeRequiredToBuy_v1(int[] tickets, int k) {
        int n = tickets.length;
        int res = 0;
        for (int i = 0; i <= k; i++) {
            tickets[i]--;
            res++;
        }
        int r = tickets[k];
        res += r * n;
        for (int ticket : tickets) {
            int i = ticket - r;
            if (i < 0) {
                res += i;
            }
        }
        return res;
    }

    /**
     * 模拟
     */
    public int timeRequiredToBuy(int[] tickets, int k) {
        ArrayDeque<Integer> q = new ArrayDeque<>();
        int n = tickets.length;
        for (int i = 0; i < n; i++) {
            q.offer(i);
        }
        int res = 0;
        while (!q.isEmpty()) {
            int i = q.poll();
            int remainder = --tickets[i];
            res++;
            if (remainder > 0) {
                q.offer(i);
            } else if (i == k) {
                break;
            }
        }
        return res;
    }
}

性能

2535.数组元素和与数字和的绝对差

目标

给你一个正整数数组 nums 。

  • 元素和 是 nums 中的所有元素相加求和。
  • 数字和 是 nums 中每一个元素的每一数位(重复数位需多次求和)相加求和。

返回 元素和 与 数字和 的绝对差。

注意:两个整数 x 和 y 的绝对差定义为 |x - y| 。

示例 1:

输入:nums = [1,15,6,3]
输出:9
解释:
nums 的元素和是 1 + 15 + 6 + 3 = 25 。
nums 的数字和是 1 + 1 + 5 + 6 + 3 = 16 。
元素和与数字和的绝对差是 |25 - 16| = 9 。

示例 2:

输入:nums = [1,2,3,4]
输出:0
解释:
nums 的元素和是 1 + 2 + 3 + 4 = 10 。
nums 的数字和是 1 + 2 + 3 + 4 = 10 。
元素和与数字和的绝对差是 |10 - 10| = 0 。

说明:

  • 1 <= nums.length <= 2000
  • 1 <= nums[i] <= 2000

思路

直接根据题意计算即可。由于元素和一定大于数字和,不用分开计算。

代码


/**
 * @date 2024-09-26 8:45
 */
public class DifferenceOfSum2535 {

    public int differenceOfSum(int[] nums) {
        int res = 0;
        for (int num : nums) {
            res += num;
            while (num > 0) {
                res -= num % 10;
                num /= 10;
            }
        }
        return res;
    }

}

性能

997.找到小镇的法官

目标

小镇里有 n 个人,按从 1 到 n 的顺序编号。传言称,这些人中有一个暗地里是小镇法官。

如果小镇法官真的存在,那么:

  1. 小镇法官不会信任任何人。
  2. 每个人(除了小镇法官)都信任这位小镇法官。
  3. 只有一个人同时满足属性 1 和属性 2 。

给你一个数组 trust ,其中 trust[i] = [ai, bi] 表示编号为 ai 的人信任编号为 bi 的人。

如果小镇法官存在并且可以确定他的身份,请返回该法官的编号;否则,返回 -1 。

示例 1:

输入:n = 2, trust = [[1,2]]
输出:2

示例 2:

输入:n = 3, trust = [[1,3],[2,3]]
输出:3

示例 3:

输入:n = 3, trust = [[1,3],[2,3],[3,1]]
输出:-1

说明:

  • 1 <= n <= 1000
  • 0 <= trust.length <= 10^4
  • trust[i].length == 2
  • trust 中的所有trust[i] = [ai, bi] 互不相同
  • ai != bi
  • 1 <= ai, bi <= n

思路

小镇至多有一个法官,他不信任任何人却被所有人信任,二维数组 trust 的元素 [a, b] 表示 a 信任 b,如果小镇法官存在返回其编号,否则返回 -1。

使用集合 trusted 表示被信任的人,trusts 表示愿意相信他人的人。我们需要计算 trusted - (trusted ∩ trusts),还必须判断这个人是否被所有人所信任。另外,有一种特殊情况,小镇只有法官一个人,trust 数组为空,我们应该返回编号 1,而不是找不到。

代码


/**
 * @date 2024-09-22 15:31
 */
public class FindJudge997 {

    public int findJudge(int n, int[][] trust) {
        if (trust.length == 0) {
            return n == 1 ? 1 : -1;
        }
        int[] trustedCnt = new int[n + 1];
        int res = -1;
        for (int[] relation : trust) {
            if (++trustedCnt[relation[1]] == n - 1) {
                res = relation[1];
                break;
            }
        }
        for (int[] relation : trust) {
            if (res == relation[0]) {
                return -1;
            }
        }
        return res;
    }

}

性能

1184.公交站间的距离

目标

环形公交路线上有 n 个站,按次序从 0 到 n - 1 进行编号。我们已知每一对相邻公交站之间的距离,distance[i] 表示编号为 i 的车站和编号为 (i + 1) % n 的车站之间的距离。

环线上的公交车都可以按顺时针和逆时针的方向行驶。

返回乘客从出发点 start 到目的地 destination 之间的最短距离。

示例 1:

输入:distance = [1,2,3,4], start = 0, destination = 1
输出:1
解释:公交站 0 和 1 之间的距离是 1 或 9,最小值是 1。

示例 2:

输入:distance = [1,2,3,4], start = 0, destination = 2
输出:3
解释:公交站 0 和 2 之间的距离是 3 或 7,最小值是 3。

示例 3:

输入:distance = [1,2,3,4], start = 0, destination = 3
输出:4
解释:公交站 0 和 3 之间的距离是 6 或 4,最小值是 4。

说明:

  • 1 <= n <= 10^4
  • distance.length == n
  • 0 <= start, destination < n
  • 0 <= distance[i] <= 10^4

思路

有一个数组 distance,元素 distance[i] 表示车站 i 到 车站 i + 1 的距离。环线上的车可以顺时针或逆时针行驶,求 startdestination 的最短距离。

假设,start <= destination。实际上是比较子数组 [start, destination - 1] 的元素和 与 子数组 [0, start - 1] [destination, n - 1] 的元素和,取其中的较小值。

由于是环形的,因此有可能 start > destination。注意处理初始条件,否则子数组 [start, destination - 1] 不合法,而 [0, start - 1] [destination, n - 1] 则覆盖了所有站点。

代码


/**
 * @date 2024-09-16 20:08
 */
public class DistanceBetweenBusStops1184 {

    public int distanceBetweenBusStops(int[] distance, int start, int destination) {
        int n = distance.length;
        int s = Math.min(start, destination);
        int e = Math.max(start, destination);
        int d1 = 0, d2 = 0;
        for (int i = 0; i < n; i++) {
            if (i < s || i >= e) {
                d1 += distance[i];
            } else {
                d2 += distance[i];
            }
        }
        return Math.min(d1, d2);
    }
}

性能

2848.与车相交的点

目标

给你一个下标从 0 开始的二维整数数组 nums 表示汽车停放在数轴上的坐标。对于任意下标 i,nums[i] = [starti, endi] ,其中 starti 是第 i 辆车的起点,endi 是第 i 辆车的终点。

返回数轴上被车 任意部分 覆盖的整数点的数目。

示例 1:

输入:nums = [[3,6],[1,5],[4,7]]
输出:7
解释:从 1 到 7 的所有点都至少与一辆车相交,因此答案为 7 。

示例 2:

输入:nums = [[1,3],[5,8]]
输出:7
解释:1、2、3、5、6、7、8 共计 7 个点满足至少与一辆车相交,因此答案为 7 。

说明:

  • 1 <= nums.length <= 100
  • nums[i].length == 2
  • 1 <= starti <= endi <= 100

思路

用一个二维数组表示 n 个线段,每一行表示线段的两个端点,问这些线段覆盖的整数点个数。

简单的想法是将线段覆盖的每个整数加入 HashSet,最后返回集合大小即可。线段个数最多 100 个,端点的范围在 1 ~ 100 最多10000 次循环,可行。

更好的做法是将相交的线段合并,最终仅计算相交线段的长度之和即可。先将线段按照起点排序,如果后面线段的起点小于前面的终点,取当前线段的终点与前面合并线段的终点中的较大值,否则计算长度并累加。

还想到了之前有一道题使用了 差分思想,那个是给定一个整数,判断覆盖该整数的线段有多少个。它是将 cnt[start]++; cnt[end + 1]--,直接累加 [0, query] 内的 cnt[i] 即为答案。

差分数组 diff 的核心思想是记录相邻元素的差,当对连续区间 [i, j] 同时增加 k,可以简化为 diff[i] + k; diff[j + 1] - k。原数组的第 i 个元素可以通过累加差分数组得到。

刚开始理解差分数组可能会想不明白,为什么只修改差分数组两个端点的值就可以将区间内的值同时加 k。核心点是理解当前值是由差分数组累加得来的,从 i 开始,当前值加了 k,后面的差分值为0,因此值与 i 位置相同。当到达 j + 1 减去了 k,后面再累加就不会受到前面的影响。

针对这道题,将原数组视为每个整数点被覆盖的次数,开始时均为 0,差分数组也均为 0。最后只需统计覆盖次数大于 0 的整数个数即可。

代码


/**
 * @date 2024-09-15 21:53
 */
public class NumberOfPoints2848 {

    /**
     * 差分数组 1ms  O(n)
     */
    public int numberOfPoints_v2(List<List<Integer>> nums) {
        // 数据范围为 1~100,0~100 共101个数,但是由于需要访问 end+1,因此初始化为102个
        int[] diff = new int[102];
        for (List<Integer> segment : nums) {
            diff[segment.get(0)]++;
            diff[segment.get(1) + 1]--;
        }
        int res = 0;
        int cnt = 0;
        for (int i : diff) {
            cnt += i;
            if (cnt > 0){
                res++;
            }
        }
        return res;
    }

    /**
     * 合并区间 排序 O(nlogn) 3ms
     */
    public int numberOfPoints_v1(List<List<Integer>> nums) {
        PriorityQueue<int[]> q = new PriorityQueue<>((a, b) -> a[0] - b[0]);
        for (List<Integer> num : nums) {
            q.offer(new int[]{num.get(0), num.get(1)});
        }
        // 初始化为-1,是因为循环中第一次 res += maxEnd - start + 1; start maxEnd并不是实际的线段数据
        int res = -1;
        int start = 0;
        int maxEnd = 0;
        while (!q.isEmpty()) {
            int[] segment = q.poll();
            int s = segment[0];
            int e = segment[1];
            if (s > maxEnd) {
                res += maxEnd - start + 1;
                start = s;
                maxEnd = e;
            } else {
                maxEnd = Math.max(maxEnd, e);
            }
        }
        // 如果最后一个线段无需合并,那么需要单独将其长度加进来
        // 如果最后一个线段需要合并,由于队列中没有数据了,也需要将最后合并的线段长度加进来
        res += maxEnd - start + 1;
        return res;
    }

    /**
     * 5ms O(c*n) c为区间最大长度
     */
    public int numberOfPoints(List<List<Integer>> nums) {
        Set<Integer> s = new HashSet<>(nums.size());
        for (List<Integer> num : nums) {
            for (int i = num.get(0); i <= num.get(1); i++) {
                s.add(i);
            }
        }
        return s.size();
    }
}

性能

977.有序数组的平方

目标

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

示例 1:

输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]

示例 2:

输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]

说明:

  • 1 <= nums.length <= 10^4
  • -10^4 <= nums[i] <= 10^4
  • nums 已按 非递减顺序 排序

进阶:

  • 请你设计时间复杂度为 O(n) 的算法解决本问题

思路

有一个非递减顺序排列的数组(数组中存在负数),求其各元素平方组成的数组,要求也按非递减顺序排列。

将负数的绝对值压入栈中直到遇到正数,然后比较当前正数与栈顶元素的大小,取其最小值计算平方即可。这里使用了指针模拟 栈 的操作。

代码


/**
 * @date 2024-09-08 20:40
 */
public class SortedSquares977 {

    public int[] sortedSquares(int[] nums) {
        int n = nums.length;
        int[] res = new int[n];
        int top = -1;
        int j = 0;
        for (int i = 0; i < n; i++) {
            if (nums[i] <= 0) {
                nums[i] = - nums[i];
                top++;
            } else {
                while (top >= 0 && nums[i] >= nums[top]) {
                    res[j++] = nums[top] * nums[top];
                    top--;
                }
                res[j++] = nums[i] * nums[i];
            }
        }
        // 如果没有正数,循环中的else分支不会执行,这里判断一下
        while (top >= 0) {
            res[j++] = nums[top] * nums[top];
            top--;
        }
        return res;
    }

}

性能

3174.清除数字 – 双端队列

目标

给你一个字符串 s 。

你的任务是重复以下操作删除 所有 数字字符:

  • 删除 第一个数字字符 以及它左边 最近 的 非数字 字符。

请你返回删除所有数字字符以后剩下的字符串。

示例 1:

输入:s = "abc"
输出:"abc"
解释:
字符串中没有数字。

示例 2:

输入:s = "cb34"
输出:""
解释:
一开始,我们对 s[2] 执行操作,s 变为 "c4" 。
然后对 s[1] 执行操作,s 变为 "" 。

说明:

  • 1 <= s.length <= 100
  • s 只包含小写英文字母和数字字符。
  • 输入保证所有数字都可以按以上操作被删除。

思路

删除给定字符串中的数字字符,每次删除操作需要同步删除该字符左侧最后一个非数字字符。

遍历的过程中使用栈保存非数字字符,遇到数字字符就弹栈,然后返回栈底到栈顶的字符即可。

知识点:

  • ArrayDeque 双端队列的特性取决于如何放入数据

                    start
             last           first
    offer       4 3 2 1
    push              1 2 3 4
  • offer是向左添加数据

  • push是向右添加数据

  • poll/pop/remove 默认从右向左取数据

  • 如果api中带last,例如pollLast、removeLast则是从左向右取,first则相反

代码


/**
 * @date 2024-09-05 8:47
 */
public class ClearDigits3174 {

    public String clearDigits(String s) {
        int n = s.length();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) {
            char c = s.charAt(i);
            if (c > '9' || c < '0') {
                sb.append(c);
            } else {
                sb.deleteCharAt(sb.length() - 1);
            }
        }
        return sb.toString();
    }

}

性能

1450.在既定时间做作业的学生人数

目标

给你两个整数数组 startTime(开始时间)和 endTime(结束时间),并指定一个整数 queryTime 作为查询时间。

已知,第 i 名学生在 startTime[i] 时开始写作业并于 endTime[i] 时完成作业。

请返回在查询时间 queryTime 时正在做作业的学生人数。形式上,返回能够使 queryTime 处于区间 [startTime[i], endTime[i]](含)的学生人数。

示例 1:

输入:startTime = [1,2,3], endTime = [3,2,7], queryTime = 4
输出:1
解释:一共有 3 名学生。
第一名学生在时间 1 开始写作业,并于时间 3 完成作业,在时间 4 没有处于做作业的状态。
第二名学生在时间 2 开始写作业,并于时间 2 完成作业,在时间 4 没有处于做作业的状态。
第三名学生在时间 3 开始写作业,预计于时间 7 完成作业,这是是唯一一名在时间 4 时正在做作业的学生。

示例 2:

输入:startTime = [4], endTime = [4], queryTime = 4
输出:1
解释:在查询时间只有一名学生在做作业。

示例 3:

输入:startTime = [4], endTime = [4], queryTime = 5
输出:0

示例 4:

输入:startTime = [1,1,1,1], endTime = [1,3,2,4], queryTime = 7
输出:0

示例 5:

输入:startTime = [9,8,7,6,5,4,3,2,1], endTime = [10,10,10,10,10,10,10,10,10], queryTime = 5
输出:5

说明:

  • startTime.length == endTime.length
  • 1 <= startTime.length <= 100
  • 1 <= startTime[i] <= endTime[i] <= 1000
  • 1 <= queryTime <= 1000

思路

当满足 startTime[i] <= queryTime && endTime[i] >= queryTime 时计数即可。

当 queryTime 是一个数组时,可以使用差分数组或者二分查找来解,具体参考官网题解。

代码


/**
 * @date 2024-09-01 14:35
 */
public class BusyStudent1450 {

    public int busyStudent(int[] startTime, int[] endTime, int queryTime) {
        int n = startTime.length;
        int res = 0;
        for (int i = 0; i < n; i++) {
            if (startTime[i] <= queryTime && endTime[i] >= queryTime) {
                res++;
            }
        }
        return res;
    }
}

性能

3127.构造相同颜色的正方形

目标

给你一个二维 3 x 3 的矩阵 grid ,每个格子都是一个字符,要么是 'B' ,要么是 'W' 。字符 'W' 表示白色,字符 'B' 表示黑色。

你的任务是改变 至多一个 格子的颜色,使得矩阵中存在一个 2 x 2 颜色完全相同的正方形。

如果可以得到一个相同颜色的 2 x 2 正方形,那么返回 true ,否则返回 false 。

示例 1:

输入:grid = [["B","W","B"],["B","W","W"],["B","W","B"]]
输出:true
解释:
修改 grid[0][2] 的颜色,可以满足要求。

示例 2:

输入:grid = [["B","W","B"],["W","B","W"],["B","W","B"]]
输出:false
解释:
只改变一个格子颜色无法满足要求。

示例 3:

输入:grid = [["B","W","B"],["B","W","W"],["B","W","W"]]
输出:true
解释:
grid 已经包含一个 2 x 2 颜色相同的正方形了。

说明:

  • grid.length == 3
  • grid[i].length == 3
  • grid[i][j] 要么是 'W' ,要么是 'B' 。

思路

有一个 3 x 3 矩阵,每一个格子有黑白两色,分别用 B W 表示。问能否修改至多一个格子的颜色使矩阵中存在一个 2 x 2 的纯色(颜色相同)矩阵。

可能的 2 x 2 矩阵只有4个,它们有一个公共格子,以它为中心向左上、右上、左下、右下方向判断即可。可以分情况讨论,修改颜色的格子是中间的格子,那么要求其余的三个格子颜色相同。否则,其余格子与中间格子颜色不同的个数最多只能有一个。综上,与中心格子不同的颜色数只能为0、1、3,只需判断不等于2即可。

代码

/**
 * @date 2024-08-31 21:28
 */
public class CanMakeSquare3127 {
    static private final int[][][] directions = new int[][][]
    {
            {{-1, 0}, {-1, -1}, {0, -1}},
            {{1, 0}, {1, -1}, {0, -1}},
            {{-1, 0}, {-1, 1}, {0, 1}},
            {{1, 0}, {1, 1}, {0, 1}}
    };

    public boolean canMakeSquare(char[][] grid) {
        char mid = grid[1][1];
        for (int[][] direction : directions) {
            int cnt = 0;
            for (int[] cor : direction) {
                if (mid != grid[1 + cor[0]][1 + cor[1]]) {
                    cnt++;
                }
            }
            if (cnt != 2) {
                return true;
            }
        }
        return false;
    }

}

性能

3142.判断矩阵是否满足条件

目标

给你一个大小为 m x n 的二维矩阵 grid 。你需要判断每一个格子 grid[i][j] 是否满足:

  • 如果它下面的格子存在,那么它需要等于它下面的格子,也就是 grid[i][j] == grid[i + 1][j]
  • 如果它右边的格子存在,那么它需要不等于它右边的格子,也就是 grid[i][j] != grid[i][j + 1]

如果 所有 格子都满足以上条件,那么返回 true ,否则返回 false 。

示例 1:

输入:grid = [[1,0,2],[1,0,2]]
输出:true
解释:
网格图中所有格子都符合条件。

示例 2:

输入:grid = [[1,1,1],[0,0,0]]
输出:false
解释:
同一行中的格子值都相等。

示例 3:

输入:grid = [[1],[2],[3]]
输出:false
解释:
同一列中的格子值不相等。

说明:

  • 1 <= n, m <= 10
  • 0 <= grid[i][j] <= 9

思路

判断矩阵是否满足纵向元素都相等,横向相邻元素各不同。

代码


/**
 * @date 2024-08-29 0:07
 */
public class SatisfiesConditions3142 {

    public boolean satisfiesConditions(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        for (int i = 1; i < m; i++) {
            if (grid[i][0] != grid[i - 1][0]) {
                return false;
            }
        }
        for (int j = 1; j < n; j++) {
            if (grid[0][j] == grid[0][j - 1]) {
                return false;
            }
        }
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (grid[i][j] == grid[i][j - 1] || grid[i][j] != grid[i - 1][j]) {
                    return false;
                }
            }
        }
        return true;
    }
}

性能