LeetCode 第200场周赛
本期周赛依旧不算太难,不过需要比较仔细,不然容易被罚时。
1. 统计好三元组
给你一个整数数组 arr ,以及 a、b 、c 三个整数。请你统计其中好三元组的数量。
如果三元组 (arr[i], arr[j], arr[k]) 满足下列全部条件,则认为它是一个 好三元组 。
0 <= i < j < k < arr.length
|arr[i] - arr[j]| <= a
|arr[j] - arr[k]| <= b
|arr[i] - arr[k]| <= c
其中 |x| 表示 x 的绝对值。
返回 好三元组的数量 。
示例 1:
输入:arr = [3,0,1,1,9,7], a = 7, b = 2, c = 3
输出:4
解释:一共有 4 个好三元组:[(3,0,1), (3,0,1), (3,1,1), (0,1,1)] 。
示例 2:
输入:arr = [1,1,2,2,3], a = 0, b = 0, c = 1
输出:0
解释:不存在满足所有条件的三元组。
解析
本题给一个数组和三个整形变量,定义了“好的三元组”,也即三个递增的坐标i,j,k,满足位置i和j的元素差的绝对值小于等于a,位置j和k元素差的绝对值小于等于b,i和k位置元素差的绝对值小于等于c。求满足条件的三元组(i, j, k)的数量
本题数据规模比较小,不需要考虑其他的,按照要求去找
O
(
N
3
)
O(N^3)
O(N3)解决不会超时。
按照定义,三重循环即可。
C++代码如下:
//No 1
int countGoodTriplets(vector<int>& arr, int a, int b, int c) {
int ans = 0,len=arr.size();
if (len<3) return 0;
for (int i = 0; i < len - 2; ++i) {
for (int j = i + 1; j < len - 1; ++j) {
if (abs(arr[i] - arr[j]) <= a) {
for (int k = j + 1; k < len; ++k) {
if (abs(arr[j] - arr[k]) <= b && abs(arr[i] - arr[k]) <= c) ++ans;
}
}
}
}
return ans;
}
2. 找出数组游戏的赢家 显示英文描述
给你一个由 不同 整数组成的整数数组 arr 和一个整数 k 。
每回合游戏都在数组的前两个元素(即 arr[0] 和 arr[1] )之间进行。比较 arr[0] 与 arr[1] 的大小,较大的整数将会取得这一回合的胜利并保留在位置 0 ,较小的整数移至数组的末尾。当一个整数赢得 k 个连续回合时,游戏结束,该整数就是比赛的 赢家 。
返回赢得比赛的整数。
题目数据 保证 游戏存在赢家。
示例 1:
输入:arr = [2,1,3,5,4,6,7], k = 2
输出:5
将进行 4 回合比赛,其中 5 是赢家,因为它连胜 2 回合。
示例 2:
输入:arr = [3,2,1], k = 10
输出:3
解释:3 将会在前 10 个回合中连续获胜。
提示:
2 <= arr.length <= 10^5
1 <= arr[i] <= 10^6
arr 所含的整数 各不相同 。
1 <= k <= 10^9
解析
本题给定一个数组,要求每回合在数组下边0,1两个位置比较大小,大的数获胜并留在nums[0],小的数排到数组尾部,其他数依次向前补位。一个数连续获胜k次即为胜者,求出胜者的数字。
本题看一下数据范围,就知道如果真的按照游戏过程模拟肯定超时,我们要从数组本身出发找到第一个能连胜k次的数。
首先,我们知道,数组中必然存在一个最大值maxV,一旦这个最大值站上擂台(下标为0或1)时,其他数就没机会了,怎么比都是maxV获胜,他可以无限连胜下去。所以,实际上我们要找排在最大值之前的(下表小于maxV下标)的数有没有能连胜k次的,如果没有,结果必为maxV。
那么怎么知道一个数能胜几轮呢?
首先,对于不是最左端(下标0)的数,如果这个数左边没有比他大的,那么当他移动到nums[1]处时,第一次上擂台,左边没有比它大的数,那此时的nums[0]一定小于nums[1],该数获胜一次。而最左端的数,或者左边存在比自身大的值的数字,就没有这个机会。我们用canWinOne数组记录每个位置是否存在这种第一次上擂台就赢的机会。
同时,根据前文分析,一旦排在最大值之前的数都不能完成k连胜,那就是最大值maxV最后获胜。因此我们遍历一次数组,找到最大值maxV及其坐标maxI,并获取canWinOne数组。显然canWinOne[0]一定是0.
另外,一个数能坐多久的擂主,实际上取决于右边第一个比他大的数在什么位置。这是经典单调栈的应用。用一个栈记录元素下表,遍历数组,若当前数字大于栈顶坐标对应的数字,说明该数字找到了第一个右边最大值,将坐标记下来保存在rightGreater中;否则将新的坐标压入栈,继续遍历。
这样一个数如果不是第一个、或者不是比左边的数都大,那么它一次都赢不了,不可能胜,无需考虑;如果是第一个数或者比左边都大,则需要考虑他何时被挤下去,对于一个数下标为i,右侧第一个比他大的数在下标k处,则这个数能连胜k-i-1轮,另外,如果他比左边都大还需要再加1,也就是第一次上擂台的胜场;如果是第一个数则没有这个胜场。由于我们设定了canWinOne数组里如果比左边数都大就记为1,第一个数和坐标有更大数的记为0,所以对于满足i==0 || canWinOne[i]==1,也就是有机会获胜的数字,其胜场数为rightGreater[i] - i-1+ canWinOne[i]。
我们遍历最大数字之前的所有数,计算可能的连胜数,如果满足大于等于k的条件,就返回这个数。如果没有满足的,那最后肯定是最大的数获胜,返回maxV即可。
C++代码如下:
//No 2
int getWinner(vector<int>& arr, int k) {
int maxV = INT_MIN, maxI = -1;
int len = arr.size();
vector<int>canWinOne(len, 0);
vector<int>rightGreater(len, 0);
for (int i = 0; i < len; ++i) {
if (arr[i] > maxV) {
canWinOne[i] = 1;
maxV = arr[i];
maxI = i;
}
}
canWinOne[0] = 0;
stack<int>s;
s.push(0);
int i = 1;
while (i < len) {
if (!s.empty() && arr[i] > arr[s.top()]) {
rightGreater[s.top()] = i;
s.pop();
}
else {
s.push(i);
++i;
}
}
if (!s.empty()) {
rightGreater[s.top()] = -1;
s.pop();
}
for (int i = 0; i < maxI; ++i) {
if (i==0 || canWinOne[i]==1) {
int tmp = rightGreater[i] - i-1+ canWinOne[i];
if (tmp >= k) return arr[i];
}
}
return maxV;
}
3. 排布二进制网格的最少交换次数
给你一个 n x n 的二进制网格 grid,每一次操作中,你可以选择网格的 相邻两行 进行交换。
一个符合要求的网格需要满足主对角线以上的格子全部都是 0 。
请你返回使网格满足要求的最少操作次数,如果无法使网格符合要求,请你返回 -1 。
主对角线指的是从 (1, 1) 到 (n, n) 的这些格子。
示例 1:
输入:grid = [[0,0,1],[1,1,0],[1,0,0]]
输出:3
示例 2:
输入:grid = [[0,1,1,0],[0,1,1,0],[0,1,1,0],[0,1,1,0]]
输出:-1
解释:所有行都是一样的,交换相邻行无法使网格符合要求。
解析
本题采取贪心的策略。题目要求主对角线右上方的格子里全是0,也就是要求第i行最右侧的1的位置必须小于等于i。第0行最多只能在位置0有一个1,最后一行也就是n-1行可以全是1,也就是最右端1可以再n-1位置上。
那么我们首先遍历每一行,找到最右端的1的坐标,存放在数组里。接下来我们分别遍历每一行,采取贪心的策略,如果这一行满足条件,也就是最右1的下标小于等于行号,也就是cur[i]<=i的情况,说明此时这一行符合条件,又因为越靠上的行对1的要求“越高”(要求尽量靠左),因此即使这一行“浪费”了,也无需跟交换。如果遇到后面某一行不满足要求,只能向下寻找,因为连i行都不满足更不可能满足i之前要求更高的行了。
对于符合要求的行,直接跳过即可,对于不满足要求的,也就是cur[i]>i的,我们可以向下寻找第一个满足这一行要求的,按照规则相邻两两交换,换上来,并累计交换的次数;如果寻找完最后一行都找不到符合条件的,那就意味着怎样交换都得不到结果,返回-1即可。
如果遍历完都能找到符合各行要求的,就返回累积的总次数即可。
C++代码如下:
//No 3
int minSwaps(vector<vector<int>>& grid) {
int n = grid.size();
vector<int>cur(n);
for (int i=0; i < n; ++i) {
int k = n - 1;
while (k >= 0 && grid[i][k] == 0)--k;
cur[i] = k;
}
int ans = 0;
for (int i = 0; i < n; ++i) {
if (cur[i] > i) {
int j = i + 1;
while (j<n && cur[j]>i)++j;
if (j == n) return -1;
else {
for (int k = j; k > i; --k) {
swap(cur[k], cur[k - 1]);
++ans;
}
}
}
}
return ans;
}
4. 最大得分
你有两个 有序 且数组内元素互不相同的数组 nums1 和 nums2 。
一条 合法路径 定义如下:
选择数组 nums1 或者 nums2 开始遍历(从下标 0 处开始)。
从左到右遍历当前数组。
如果你遇到了 nums1 和 nums2 中都存在的值,那么你可以切换路径到另一个数组对应数字处继续遍历(但在合法路径中重复数字只会被统计一次)。
得分定义为合法路径中不同数字的和。
请你返回所有可能合法路径中的最大得分。
由于答案可能很大,请你将它对 10^9 + 7 取余后返回。
示例 1:
输入:nums1 = [2,4,5,8,10], nums2 = [4,6,8,9]
输出:30
解释:合法路径包括:
[2,4,5,8,10], [2,4,5,8,9], [2,4,6,8,9], [2,4,6,8,10],(从 nums1 开始遍历)
[4,6,8,9], [4,5,8,10], [4,5,8,9], [4,6,8,10] (从 nums2 开始遍历)
最大得分为上图中的绿色路径 [2,4,6,8,10] 。
示例 2:
输入:nums1 = [1,3,5,7,9], nums2 = [3,5,100]
输出:109
解释:最大得分由路径 [1,3,5,100] 得到。
解析
本题给定两个数组,数组内无重复元素且严格递增,寻找累计和最大的合法路径。合法路径必须从2个数组之一开始,到二者中一个的结尾为止,可以在两个数组相同元素位置跳转到另一个数组,而不能在不同的元素处跳转。
本题是动态规划,感觉比第三题更容易想一点。
首先对于两个严格递增的数组,寻找其中相同的元素可以采用双指针的方法。我们就按照双指针法遍历,并定义两个dp数组,dp1[i] 表示走到第一个数组nums1的位置i处的最大累计和,dp2[j] 表示走到nums2的j位置的最大的累积和。为了便于计算我们在dp数组最开始位置加一个0. 这样dp[i]对应的是走到nums[i-1]位置的值。
首先,如果nums1[i-1]和nums2[j-1]相等,意味着可以在这里左右横跳。那么能取得的最大值,实际上就是dp1[i-1]和dp2[j-1]的较大值,加上nums1[i-1]的值。因为这个点可以通过两条路径都能到达,所以当然取其中更大的那个。计算完成后两个指针都可以向前移动。
如果二者不等,实际上只能从本数组进行累加。考虑到使用双指针查找相等的元素,我们每次都选取较小的那个元素,计算后将其向前移动。避免错过相同数值的位置。
C++代码如下:
//No 4
int maxSum(vector<int>& nums1, vector<int>& nums2) {
int M = 1e9 + 7;
int l1 = nums1.size(), l2 = nums2.size(), i = 1, j = 1;
vector<int64_t>dp1(l1 + 1, 0), dp2(l2 + 1, 0);
while (i <= l1 && j <= l2) {
if (nums1[i - 1] == nums2[j - 1]) {
dp1[i] = max(dp1[i - 1], dp2[j - 1]) + (int64_t)nums1[i - 1];
dp2[j] = max(dp1[i - 1], dp2[j - 1]) + (int64_t)nums1[i - 1];
++i;
++j;
}
else if (nums1[i - 1] < nums2[j - 1]) {
dp1[i] = dp1[i - 1] + (int64_t)nums1[i - 1];
++i;
}
else {
dp2[j] = dp2[j - 1] + (int64_t)nums2[j - 1];
++j;
}
}
while (i <= l1) {
dp1[i] = dp1[i - 1] + (int64_t)nums1[i - 1];
++i;
}
while (j <= l2) {
dp2[j] = dp2[j - 1] + (int64_t)nums2[j - 1];
++j;
}
return (max(dp1[l1],dp2[l2]))%M;
}
注意如果一个数组遍历完了,意味着另一条没遍历完的实际上也没有可以跳转的位置了,所以依次累加即可。