题目描述
给你一个下标从 0 开始的字符串 s
,该字符串仅由小写英文字母组成,s
中的每个字母都 恰好 出现 两次 。另给你一个下标从 0 开始、长度为 26
的的整数数组 distance
。
字母表中的每个字母按从 0
到 25
依次编号(即,'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;
}
}
题目描述
给你两个 正 整数 startPos
和 endPos
。最初,你站在 无限 数轴上位置 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 。
思路
因为向左移动一次,可以和向右移动一次抵消。所以我们可以先算出startPos
到endPos
的绝对距离,
即d = abs(startPos - endPos)
,我们可以先直接往一个方向移动d
步,到达endPos
,随后把剩余的步数平均分成往左移动一半的步数,和往右移动一半的步数。故,只有当k
等于d
加上一个偶数时,才能到达endPos
。
我们不妨设endPos = startPos + d
,即endPos
在startPos
右侧,距离为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,第一维存移动的步数,第二维存到达的位置的下标。这种想法不太好控制下标(因为会向左或向右移动,下标可能越界,实际编码时,可以将startPos
和endPos
往右平移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 ap−1modp=1,
则 a a a 在 m o d p \mod p modp 下的乘法逆元 a − 1 a^{-1} a−1 ,满足 a × a − 1 m o d p = 1 a × a^{-1} \mod p = 1 a×a−1modp=1,容易推出 a − 1 = a p − 2 m o d p a^{-1} = a^{p - 2} \mod p a−1=ap−2modp。
对任意的数 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×a−1modp
则我们对于组合数的分母上的每个数,求一下其逆元 a p − 2 m o d p a^{p-2} \mod p ap−2modp,将除法转化为乘法,而求解逆元的过程实际是个幂运算,故可以用快速幂算法。由于 p p p 的级别在 1 0 9 10^9 109 ,幂的次数是固定的 p − 2 p - 2 p−2 ,我们近似地将其看成 p p p,则快速幂的运算次数( l o g p log{p} logp)大概在30次左右( 2 30 ≈ 1 0 9 2^{30} ≈ 10^9 230≈109),总的运算次数大概是 31 m 31m 31m(分子的运算次数 m m m,分母的运算次数 30 m 30m 30m),大概是线性复杂度了,非常快。
这种用乘法逆元求解组合数的方法,几乎可以求解任意的组合数(非常快,数据也不会溢出)
可以参考ACWING-算法基础课,第四章数学章节,求解组合数一共有大概4种解法:
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;
}
}
题目描述
给你一个由 正 整数组成的数组 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的位置,都是互相错开的。
那么能够推出的一个判断条件是:
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;
}
}
题目描述
给你一个整数 n
,共有编号从 0
到 n - 1
的 n
个会议室。
给你一个二维整数数组 meetings
,其中 meetings[i] = [starti, endi]
表示一场会议将会在 半闭 时间区间 [starti, endi)
举办。所有 starti
的值 互不相同 。
会议将会按以下方式分配给会议室:
返回举办最多次会议的房间 编号 。如果存在多个房间满足此条件,则返回编号 最小 的房间。
半闭区间 [a, b)
是 a
和 b
之间的区间,包括 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);
}
}
这次周赛的结果比较满意,第二题和第三题难度都不小,但都做出来了,虽然时间花的比较长。
再接再厉!