LeetCode 309 周赛

2399. 检查相同字母间的距离

题目描述

给你一个下标从 0 开始的字符串 s ,该字符串仅由小写英文字母组成,s 中的每个字母都 恰好 出现 两次 。另给你一个下标从 0 开始、长度为 26 的的整数数组 distance

字母表中的每个字母按从 025 依次编号(即,'a' -> 0, 'b' -> 1, 'c' -> 2, … , 'z' -> 25)。

在一个 匀整 字符串中,第 i 个字母的两次出现之间的字母数量是 distance[i] 。如果第 i 个字母没有在 s 中出现,那么 distance[i] 可以 忽略

如果 s 是一个 匀整 字符串,返回 true ;否则,返回 false

示例

输入:s = "abaccb", distance = [1,3,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
输出:true
解释:
- 'a' 在下标 0 和下标 2 处出现,所以满足 distance[0] = 1 。
- 'b' 在下标 1 和下标 5 处出现,所以满足 distance[1] = 3 。
- 'c' 在下标 3 和下标 4 处出现,所以满足 distance[2] = 0 。
注意 distance[3] = 5 ,但是由于 'd' 没有在 s 中出现,可以忽略。
因为 s 是一个匀整字符串,返回 true 。

思路

认真读题,简单模拟即可。使用一个Map进行存储,从左到右依次遍历每个字符,第一次遇见某个字符时,直接插入当前位置,第二次遇见时,计算中间间隔的字符数量即可。

class Solution {
    public boolean checkDistances(String s, int[] distance) {
        int[] d = new int[26];
        Arrays.fill(d, -1);
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (d[u] == -1) d[u] = i; // 该字符第一次出现, 记录出现的下标
            else d[u] = i - 1 - d[u]; // 该字符第二次出现, 计算中间间隔的距离
        }
        
        for (int i = 0; i < distance.length; i++) {
            if (d[i] == -1) continue;
            if (d[i] != distance[i]) return false;
        }
        return true;
    }
}

2400. 恰好移动 k 步到达某一位置的方法数目

题目描述

给你两个 整数 startPosendPos 。最初,你站在 无限 数轴上位置 startPos 处。在一步移动中,你可以向左或者向右移动一个位置。

给你一个正整数 k ,返回从 startPos 出发、恰好 移动 k 步并到达 endPos不同 方法数目。由于答案可能会很大,返回对 109 + 7 取余 的结果。

如果所执行移动的顺序不完全相同,则认为两种方法不同。

注意:数轴包含负整数

  • 1 <= startPos, endPos, k <= 1000

示例

输入:startPos = 1, endPos = 2, k = 3
输出:3
解释:存在 3 种从 1 到 2 且恰好移动 3 步的方法:
- 1 -> 2 -> 3 -> 2.
- 1 -> 2 -> 1 -> 2.
- 1 -> 0 -> 1 -> 2.
可以证明不存在其他方法,所以返回 3 。

思路

因为向左移动一次,可以和向右移动一次抵消。所以我们可以先算出startPosendPos的绝对距离,

d = abs(startPos - endPos),我们可以先直接往一个方向移动d步,到达endPos,随后把剩余的步数平均分成往左移动一半的步数,和往右移动一半的步数。故,只有当k等于d加上一个偶数时,才能到达endPos

我们不妨设endPos = startPos + d,即endPosstartPos右侧,距离为d

我们设k = d + 2n,那么,我们需要往右移动d + n次,往左移动n次。问有多少种方案,其实就是个排列组合问题。

我们将往右移动一次看成字符r,往左移动一次看成字符l。那么问题就变成了,给定d + n个字符r,以及n个字符l,问能够组成多少种不同的字符。由于不同的移动顺序会导致不同的方案,所以这其实是个排列问题。

周赛时我采用的是DFS暴力,加上了记忆化的方式,来搜索方案数量。

记忆化搜索

class Solution {
    long ans = 0;
    int MOD = 1_000_000_000 + 7;
    long[][] dp; // 记忆化
    public int numberOfWays(int startPos, int endPos, int k) {
        int d = Math.abs(endPos - startPos); // 净距离
        if (k < d) return 0; // 不足d, 则肯定走不到
        k -= d;
        if ((k & 1) == 1) return 0; // 剩余的步数是奇数步, 则走不到
        int a = d + k / 2, b = k / 2;
        dp = new long[a + 1][b + 1];
        // 边界状态
        for (int i = 1; i <= b; i++) dp[0][i] = 1;
        for (int i = 1; i <= a; i++) dp[i][0] = 1;
        // 暴力搜索
        dfs(d + k / 2, k / 2);
        return (int) dp[a][b];
    }
    
    // a 表示还剩多少个字符a可以用
    // b 表示还剩多少个字符b可以用
    private long dfs(int a, int b) {
        // 其中一个字符用完了, 那么方案数为1
        if (a == 0 || b == 0) return 1;
        // 记忆化
        if (dp[a][b] != 0) return dp[a][b];
        // 具有对称性, 若dp[b][a] 求解出来了, 那么dp[a][b]也求出来了
        if (b < dp.length && a < dp[0].length && dp[b][a] != 0) {
            return dp[a][b] = dp[b][a];
        }
        // 当前这个位置取字符a或者取字符b
        dp[a][b] = dfs(a - 1, b) + dfs(a, b - 1);
        dp[a][b] = dp[a][b] % MOD;
        return dp[a][b];
    }

}

二维DP(from yxc)

这道题也可以用动态规划来做,一看数据范围1000,那么n^2的算法不会超时,可以考虑用n^2的算法来做。用二维DP,第一维存移动的步数,第二维存到达的位置的下标。这种想法不太好控制下标(因为会向左或向右移动,下标可能越界,实际编码时,可以将startPosendPos往右平移500,这样就算取到最值的情况下,也不会走到越界(往左能走到的最远距离,就是当startPos = endPos = 1, k = 1000,最多往左走到-500;往右能走到的最远,当startPos = endPos = 1000, k = 1000,最多往右走到1500,整个下标平移后,范围大概在[0, 2000]))。

class Solution {

    int N = 2010, MOD = 1_000_000_000 + 7;

    // 第一维表示走了多少步, 第二维表示当前所在的下标
    // f表示方案数
    int[][] f = new int[1010][N];

    public int numberOfWays(int startPos, int endPos, int k) {
        startPos += 500;
        endPos += 500;
        f[0][startPos] = 1;
        for (int i = 1; i <= k; i++) {
            for (int j = 0; j < N; j++) {
                // 能不能从j - 1转移过来
                if (j > 0) f[i][j] = f[i - 1][j - 1];
                // 能不能从j + 1转移过来
                if (j + 1 < N) f[i][j] = (f[i][j] + f[i - 1][j + 1]) % MOD;
            }
        }
        return f[k][endPos];
    }
}

进阶:用乘法逆元来求组合数(from yxc)

根据上面的分析,我们最终是求解这样一个问题,给定n个字符a,和m个字符b,将所有组合排列起来,问能组合成多少种不同的字符串。其实可以这样考虑这个问题,一共有m + n个位置,我们需要从中挑选出n个位置来放字符a,那剩余的位置就自动放了b,所以其实求解一个组合数 C m + n n C_{m+n}^n Cm+nn,对于组合数,我们可以用组合数公式来求解。但是这个过程可能会溢出,由于该题的模数 1 0 9 + 7 10^9 + 7 109+7 是个质数,故我们可以使用乘法逆元,将除法转换为乘法。

根据欧拉定理的特例,费马小定理,当 p p p 是一个质数时,有 a p − 1 m o d    p = 1 a^{p-1} \mod p = 1 ap1modp=1

a a a m o d    p \mod p modp 下的乘法逆元 a − 1 a^{-1} a1 ,满足 a × a − 1 m o d    p = 1 a × a^{-1} \mod p = 1 a×a1modp=1,容易推出 a − 1 = a p − 2 m o d    p a^{-1} = a^{p - 2} \mod p a1=ap2modp

对任意的数 b b b,若 b b b 能整除 a a a,则有 b a m o d    p = b × a − 1 m o d    p \frac{b}{a} \mod p = b × a^{-1} \mod p abmodp=b×a1modp

则我们对于组合数的分母上的每个数,求一下其逆元 a p − 2 m o d    p a^{p-2} \mod p ap2modp,将除法转化为乘法,而求解逆元的过程实际是个幂运算,故可以用快速幂算法。由于 p p p 的级别在 1 0 9 10^9 109 ,幂的次数是固定的 p − 2 p - 2 p2 ,我们近似地将其看成 p p p,则快速幂的运算次数( l o g p log{p} logp)大概在30次左右( 2 30 ≈ 1 0 9 2^{30} ≈ 10^9 230109),总的运算次数大概是 31 m 31m 31m(分子的运算次数 m m m,分母的运算次数 30 m 30m 30m),大概是线性复杂度了,非常快。

这种用乘法逆元求解组合数的方法,几乎可以求解任意的组合数(非常快,数据也不会溢出)

可以参考ACWING-算法基础课,第四章数学章节,求解组合数一共有大概4种解法:

  1. 利用递推公式 C n m = C n − 1 m + C n − 1 m − 1 C_n^m = C_{n-1}^m + C_{n-1}^{m-1} Cnm=Cn1m+Cn1m1 ,也就是利用DP
  2. 利用乘法逆元
class Solution {

    int MOD = 1_000_000_000 + 7;

    public int numberOfWays(int startPos, int endPos, int k) {
        int d = Math.abs(startPos - endPos);
        if (d > k) return 0;
        k -= d;
        if ((k & 1) == 1) return 0;
        int a = d + k / 2, b = k / 2;
        int n = a + b, m = a;
        int res = 1;
        // 分子
        for (int i = n; i > n - m; i--) {
            res = (int) ((long) res * i % MOD);
        }

        // 分母
        for (int i = 1; i <= m; i++) {
            res = (int) ((long)res * qmi(i, MOD - 2, MOD) % MOD);
        }
        return res;
    }

    // 快速幂
    private int qmi(int a, int b, int p) {
        int res = 1;
        while (b > 0) {
            if ((b & 1) == 1) res = (int) ((long) res * a % p);
            a = (int) ((long)a * a % p);
            b >>= 1;
        }
        return res;
    }
}

2401. 最长优雅子数组

题目描述

给你一个由 整数组成的数组 nums

如果 nums 的子数组中位于 不同 位置的每对元素按位 **与(AND)**运算的结果等于 0 ,则称该子数组为 优雅 子数组。

返回 最长 的优雅子数组的长度。

子数组 是数组中的一个 连续 部分。

**注意:**长度为 1 的子数组始终视作优雅子数组。

  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^9

示例

输入:nums = [1,3,8,48,10]
输出:3
解释:最长的优雅子数组是 [3,8,48] 。子数组满足题目条件:
- 3 AND 8 = 0
- 3 AND 48 = 0
- 8 AND 48 = 0
可以证明不存在更长的优雅子数组,所以返回 3 。

思路

求最长的长度,想到二分。对优雅子数组的定义:每对元素按位与的结果为0,这里要使用位运算。

什么时候,一个子数组中的所有元素,两两做与,结果都是0呢。我们多举一些样例数据,就能发现。所有元素的1的位置,必须全部错开,才能保证两两相与都是0。

可以先考虑2个数,按位与要得0,那么就不可能在某个位置,2个数都是1。即,一个数为1的位置,另一个数必须为0。

比如,A中有5个位置为0,那么当考虑B时,B中最多只能在这5个位置上为1,;在考虑C时,A中为1和B中为1的位置,C都不能为1。考虑的数越多,能够取1的位置就越少。

最后,所有数为1的位置,都是互相错开的。

那么能够推出的一个判断条件是:

  • 把子数组中的每个数依次做按位与,得到的结果是0(我们把这个条件成为A

但是注意,这个条件,只是必要条件,而不是充分条件,举个简单的例子。

A=11001
B=00110
C=00110

很明显,B和C按位与的运算结果不为0,但由于A对应的位置为0,所以三个数做按位与的结果为0。

那么如何表示这种1都是错开的关系呢?上面的数据,在满足A条件,但是不满足优雅子数组的定义的原因在于,有多个数字,在某个位置上为1,而只要有一个数字在这个位置上是0,就能使得整体的运算结果是0。所以,我们需要保证,竖着看时,在某一个位置上,只有一个数能够是1。那么在对于某个位置,把每个数在这个位置上的数字,做一下异或运算,以及或运算。若异或运算的结果和或运算的结果相等,则说明在每个位置上,都只有一个数是1。

----其实这个思路也有问题,再举一个反例,比如一共5个数,5个数在某一位置上的数如下

0
1
0
1
0

那么这一位置上的数做异或得1,做或 得1。然而实际这个位置上出现了多个1。

再来想。我们把或运算换成加法运算。

只有当每个位置上都只存在一个1时,做加法的结果,和做异或运算的结果相等。于是我们就用加法+异或这个条件,来判断优雅子数组。

由于加法会产生进位,那么只有当每个位置上只有1个1时,加法运算的结果才和异或运算的一样。(其实这里也可以把异或运算替换为或运算)。

**但是!**这道题我们要求最大长度,在二分长度的过程中,我们需要利用滑动窗口,窗口在滑动的过程中,当左侧端点滑出窗口,我们需要从运算结果中减去左侧被移出去的元素的值。

这时!使用异或运算就有用武之地了!因为A ^ B ^ A = B,我们可以利用异或运算的这个性质,对窗口左侧移出的元素,的异或运算结果,进行恢复!而用或运算则做不到这一点。

这里使用加法运算(和),以及异或运算,就能充分的判断是否是优雅子数组了。由于用到和,我们预处理一下,得到前缀和数组。

所以最终的思路就是:二分+滑动窗口+前缀和+位运算

class Solution {
    
    long[] preSum;
    
	public int longestNiceSubarray(int[] nums) {
		int ans = 1;
        
        preSum = new long[nums.length + 1];
        for (int i = 1; i <= nums.length; i++) preSum[i] = preSum[i - 1] + nums[i - 1];
        
		// 二分长度
		int l = 1, r = nums.length;
		while (l < r) {
			int mid = l + r + 1>> 1;
			if (check(nums, mid)) l = mid;
			else r = mid - 1;
		}
		return l;
	}

	// 该数组中是否存在长度为len的优雅子数组
	private boolean check(int[] nums, int len) {
        
        // 滑动窗口
        int t1 = -1;
        for (int i = 0, j = 0; i < nums.length; i++) {
            if (i == 0) t1 = nums[i];
            else t1 = t1 ^ nums[i];
            if (i - j + 1 == len) {
                if (t1 == (int) (preSum[i + 1] - preSum[j])) return true;
                // 把 j 去掉
                t1 = t1 ^ nums[j]; // 异或运算能恢复
                j++;
            }
        }
        return false;
	}
}

其实后面发现,不用二分长度,直接一次滑动窗口就行了(汗 -_-||)

滑动窗口1(from yxc)

思路也是类似,只要保证某个子数组中,在每个位置最多中出现一个1即可,这里由于数据范围最大是10^9,即最大有30位左右,我们直接对每个数,统计其每个位置上的1即可。

class Solution {
    public int longestNiceSubarray(int[] nums) {
        int[] cnt = new int[40]; // 每个位置上1的出现次数
        int total = 0; // 出现1的次数超过1的位置的数量
        int ans = 0;
        for (int i = 0, j = 0; i < nums.length; i++) {
            for (int k = 0; k < 31; k++) {
                if ((nums[i] >> k & 1) == 1) {
                    if (cnt[k] == 1) total++; // 这个位置上的1的数量第一次超过1
                    cnt[k]++;
                }
            }
            while (total > 0) {
                // 当存在一个位置上出现多个1时, 右移j
                for (int k = 0; k < 31; k++) {
                    if ((nums[j] >> k & 1) == 1) {
                        cnt[k]--;
                        if (cnt[k] == 1) total--; // 这个位置上的1第一次降为1
                    }
                }
                j++;
            }
            ans = Math.max(ans, i - j + 1);
        }
        return ans;
    }
}

滑动窗口2(位运算)

使用位运算,而不是每个位置依次统计

class Solution {
    public int longestNiceSubarray(int[] nums) {
        int state = 0, ans = 0;
        for (int i = 0, j = 0; i < nums.length; i++) {
            while ((state & nums[i]) != 0) {
                // 当前i无法加入状态, 右移j
                state ^= nums[j];
                j++;
            }
            state ^= nums[i];
            ans = Math.max(ans, i - j + 1);
        }
        return ans;
    }
}

2402. 会议室III

题目描述

给你一个整数 n ,共有编号从 0n - 1n 个会议室。

给你一个二维整数数组 meetings ,其中 meetings[i] = [starti, endi] 表示一场会议将会在 半闭 时间区间 [starti, endi) 举办。所有 starti 的值 互不相同

会议将会按以下方式分配给会议室:

  1. 每场会议都会在未占用且编号 最小 的会议室举办。
  2. 如果没有可用的会议室,会议将会延期,直到存在空闲的会议室。延期会议的持续时间和原会议持续时间 相同
  3. 当会议室处于未占用状态时,将会优先提供给原 开始 时间更早的会议。

返回举办最多次会议的房间 编号 。如果存在多个房间满足此条件,则返回编号 最小 的房间。

半闭区间 [a, b)ab 之间的区间,包括 a不包括 b

示例

输入:n = 2, meetings = [[0,10],[1,5],[2,7],[3,4]]
输出:0
解释:
- 在时间 0 ,两个会议室都未占用,第一场会议在会议室 0 举办。
- 在时间 1 ,只有会议室 1 未占用,第二场会议在会议室 1 举办。
- 在时间 2 ,两个会议室都被占用,第三场会议延期举办。
- 在时间 3 ,两个会议室都被占用,第四场会议延期举办。
- 在时间 5 ,会议室 1 的会议结束。第三场会议在会议室 1 举办,时间周期为 [5,10) 。
- 在时间 10 ,两个会议室的会议都结束。第四场会议在会议室 0 举办,时间周期为 [10,11) 。
会议室 0 和会议室 1 都举办了 2 场会议,所以返回 0 。 

思路

感觉有点像区间合并的问题。就把整个过程模拟一下。我脑子里的思路大概是:对n个会议室,维护一下这个会议室的会议的结束时间。当所有会议室都被占用时,结束时间最早的会议室,应该要安排给下一个等待的会议。同样,对于等待的会议,需要取出开始时间最早的会议。这样,就用两个堆来实现即可。

先将会议按照开始时间从小到大排序。因为我们总是会尽可能安排开始时间早的会议。

一定要一个堆维护当前空闲的会议室,空闲的会议室不需要考虑该会议室的结束时间;另一个堆要维护当前正在使用的会议室,当轮到某个会议召开时,若当前有空闲会议室,则直接从空闲会议室中挑一个编号最小的;若当前无空闲会议室,则需要从已被使用的会议室中,找到一个结束时间最早的,若有多个结束时间最早的会议室,则要选编号最小的(双关键字排序)。

class Solution {

	class Pair {
		long end;
		int no;
		Pair(long end, int no) {
			this.end = end;
			this.no = no;
		}
	}

	public int mostBooked(int n, int[][] meetings) {
		// 先按照区间左端点排序
		quickSort(meetings, 0, meetings.length - 1);
        // 这里排序可以替换为 Arrays.sort(meetings, (a, b) -> a[0] - b[0]);
		int[] cnt = new int[n];
		// 空闲的会议室
		PriorityQueue<Integer> free = new PriorityQueue<>();
        // 已使用的会议室, 按照结束时间, 会议室编号, 进行双关键字排序!注意!
		PriorityQueue<Pair> used = new PriorityQueue<>((o1, o2) -> {
			if (o1.end < o2.end) return -1;
			if (o1.end == o2.end) return o1.no - o2.no;
			return 1;
		});
		// 初始化, 将全部会议室放到空闲当中
		for (int i = 0; i < n; i++) free.offer(i);
		for (int i = 0; i < meetings.length; i++) {
			int start = meetings[i][0], end = meetings[i][1];
			// 如果轮到当前会议时, 有某个会议室已经使用完毕, 则要释放出来
			while (used.size() > 0 && used.peek().end <= start) {
				Pair p = used.poll();
				free.offer(p.no);
			}
			if (free.size() > 0) {
				// 有空闲的会议室, 则直接使用
				int x = free.poll();
				used.offer(new Pair((long) end, x));
				cnt[x]++;
			} else {
				// 没有, 则从被占用的会议室中选出结束时间最早的, 结束时间相同则选编号最小的
				Pair p = used.poll();
				used.offer(new Pair(p.end + end - start, p.no));
				cnt[p.no]++;
			}
		}
		int ans = 0;
		for (int i = 0; i < n; i++) {
			if (cnt[i] > cnt[ans]) {
				ans = i;
			}
		}
		return ans;
	}

	private void quickSort(int[][] arr, int l, int r) {
		if (l >= r) return ;
		int x = arr[l + r >> 1][0], i = l - 1, j = r + 1;
		while (i < j) {
			do i++; while (arr[i][0] < x);
			do j--; while (arr[j][0] > x);
			if (i < j) {
				int[] t = arr[i];
				arr[i] = arr[j];
				arr[j] = t;
			}
		}
		quickSort(arr, l, j);
		quickSort(arr, j + 1, r);
	}
}

总结

这次周赛的结果比较满意,第二题和第三题难度都不小,但都做出来了,虽然时间花的比较长。

再接再厉!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值