LeetCode 87 双周赛

88 阅读6分钟

2409. 统计共同度过的日子

题目描述

Alice 和 Bob 计划分别去罗马开会。

给你四个字符串 arriveAliceleaveAlicearriveBobleaveBob 。Alice 会在日期 arriveAliceleaveAlice 之间在城市里(日期为闭区间),而 Bob 在日期 arriveBobleaveBob 之间在城市里(日期为闭区间)。每个字符串都包含 5 个字符,格式为 "MM-DD" ,对应着一个日期的月和日。

请你返回 Alice和 Bob 同时在罗马的天数。

你可以假设所有日期都在 同一个 自然年,而且 不是 闰年。每个月份的天数分别为:[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

示例

输入:arriveAlice = "08-15", leaveAlice = "08-18", arriveBob = "08-16", leaveBob = "08-19"
输出:3
解释:Alice 从 8 月 15 号到 8 月 18 号在罗马。Bob 从 8 月 16 号到 8 月 19 号在罗马,他们同时在罗马的日期为 8 月 16、17 和 18 号。所以答案为 3 。

思路

归一化

实际就是给定2个区间,求2个区间的交集。题目给出的区间的两个端点是日期,包含了月和日两个维度。我们需要做一下归一化,或者扁平化。把月-日这样的点,转化为一维的一个数字。具体的操作就是,把月-日转化为当年的第x天。转化为一维后,就可以将其看成简单的两个区间。

求区间的交集就非常简单了,重叠部分d = min(R1, R2) - max(L1, L2),当不存在交集时,计算出的d会为负数,所以要将结果和0再取一个max

class Solution {
    
    private int[] days = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    
    public int countDaysTogether(String arriveAlice, String leaveAlice, String arriveBob, String leaveBob) {
        // 前缀和
        for (int i = 1; i <= 12; i++) days[i] += days[i - 1];
        // 归一化
        int L1 = normalize(arriveAlice), R1 = normalize(leaveAlice);
        int L2 = normalize(arriveBob), R2 = normalize(leaveBob);
        int d = Math.min(R1, R2) - Math.max(L1, L2) + 1;
        return Math.max(d, 0);
    }
    
    private int normalize(String date) {
        String[] ss = date.split("-");
        int month = Integer.parseInt(ss[0]);
        int day = Integer.parseInt(ss[1]);
        return days[month - 1] + day;
    }
}

2410. 运动员和训练师的最大匹配数

题目描述

给你一个下标从 0 开始的整数数组 players ,其中 players[i] 表示第 i 名运动员的 能力 值,同时给你一个下标从 0 开始的整数数组 trainers ,其中 trainers[j] 表示第 j 名训练师的 训练能力值

如果第 i 名运动员的能力值 小于等于j 名训练师的能力值,那么第 i 名运动员可以 匹配j 名训练师。除此以外,每名运动员至多可以匹配一位训练师,每位训练师最多可以匹配一位运动员。

请你返回满足上述要求 playerstrainers最大 匹配数。

示例

输入:players = [4,7,9], trainers = [8,2,5,8]
输出:2
解释:
得到两个匹配的一种方案是:
- players[0] 与 trainers[0] 匹配,因为 4 <= 8 。
- players[1] 与 trainers[3] 匹配,因为 7 <= 8 。
可以证明 2 是可以形成的最大匹配数。

思路

贪心(实际做时采用排序+双指针)

求最大匹配,映入脑子里的是匈牙利算法。但这道题条件比较特殊,可以用更简单的贪心来做。因为运动员和训练师最多是一一匹配的关系。然后条件是运动员的能力值要小于等于训练师的能力值。那么很直观的贪心策略是,对于一个能力值为x的运动员,我们尝试为他匹配一个训练师时,尽可能找能力值小的。即,在能力值>= x的训练师中,选择能力值最小的那个。

这样做的原因是,可以将能力值更大的训练师,留给其他运动员,增加其他运动员也能被匹配上的可能性。

我们只需要将运动员和训练师两个数组,从小到大排序,然后用双指针进行挨个比较,然后统计计数即可

能够使用双指针的原因是:两个指针移动的方向具有单调性。

i表示当前运动员,j表示当前训练师。若j能力值小于i,那么j不可能匹配i之后的运动员,所以j需要右移。

class Solution {
	public int matchPlayersAndTrainers(int[] players, int[] trainers) {
		Arrays.sort(players);
		Arrays.sort(trainers);
		int i = 0, j = 0, n = players.length, m = trainers.length;
		int ans = 0;
		while (i < n && j < m) {
			if (players[i] <= trainers[j]) {
				ans++;
				i++;
				j++;
			} else {
				j++;
			}
		}
		return ans;
	}
}

----更一下y总的思路,我觉得更加清晰。

这道题,假设我们的最大匹配数是k,那么一定是匹配了能力值最小的前k个y运动员。为什么呢?

现证明一下:若某个最优解下,没有选择前k个玩家中的某一个(假设为i),而是选择了k之后的某个玩家(假设为j)。(假设已经按照能力值从小到大排好了序)。

j这个玩家一定与某一个训练师p匹配了,能力值我们用c来表示(capability),那么一定有c[j] <= c[p]。而由于ij前面,有c[i] <= c[j],那么一定有c[i] <= c[p]。也就是说,我们可以把j替换成i,同样也算作是一个最优解。所以,假设最大匹配数是k,那么选择能力值前k小个运动员的方案,一定是一个最优解。

那么只需要对运动员排个序,然后从小到大依次枚举每个运动员,看该运动员能否被匹配上。假设一个运动员能够被匹配上,那么可能有多个训练师满足条件,那么此时应该给该运动员分配哪个训练师呢?直觉告诉我们应该分配能力值最小的那个训练师。这里做一下证明。

假设运动员i,能够匹配两个训练师pq,其中pq能力值小。只需要证明一下,选择p不会比选择q更差。

当选q时,

  • p没有被使用,则可以将iq的匹配关系,换成ip的匹配关系,匹配个数不会减少。
  • p已经被使用过了,则p一定与另一个运动员j匹配了,并且c[j] <= c[p];那么此时有c[i], c[j] <= c[p] <= c[q],那么我们可以将这两组匹配关系交换一下,即ip匹配,jq匹配,那么匹配个数仍然不会减少。

所以,可以证明,最优解一定是按能力值从小到大,从运动员中进行选取,对每个运动员,选一个能力值最小的训练师与之匹配即可。

2411. 按位或最大的最小子数组长度

题目描述

给你一个长度为 n 下标从 0 开始的数组 nums ,数组中所有数字均为非负整数。对于 0n - 1 之间的每一个下标 i ,你需要找出 nums 中一个 最小 非空子数组,它的起始位置为 i (包含这个位置),同时有 最大按位或运算值

  • 换言之,令 Bij 表示子数组 nums[i...j] 的按位或运算的结果,你需要找到一个起始位置为 i 的最小子数组,这个子数组的按位或运算的结果等于 max(Bik) ,其中 i <= k <= n - 1

一个数组的按位或运算值是这个数组里所有数字按位或运算的结果。

请你返回一个大小为 n 的整数数组 answer,其中 answer[i]是开始位置为 i ,按位或运算结果最大,且 最短 子数组的长度。

子数组 是数组里一段连续非空元素组成的序列。

  • n == nums.length

  • 1 <= n <= 10^5

  • 0 <= nums[i] <= 10^9

示例

输入:nums = [1,0,2,1,3]
输出:[3,3,2,2,1]
解释:
任何位置开始,最大按位或运算的结果都是 3 。
- 下标 0 处,能得到结果 3 的最短子数组是 [1,0,2] 。
- 下标 1 处,能得到结果 3 的最短子数组是 [0,2,1] 。
- 下标 2 处,能得到结果 3 的最短子数组是 [2,1] 。
- 下标 3 处,能得到结果 3 的最短子数组是 [1,3] 。
- 下标 4 处,能得到结果 3 的最短子数组是 [3] 。
所以我们返回 [3,3,2,2,1]

思路

按位统计+前缀和+二分

这道题目和之前某次周赛的题目有点像,位运算相关。

由于位运算的特性,多个数做位运算,二进制位为1的那些位置的数量,是只增不减的。比如下面3个数字(二进制表示)

11001
00110
10011

这三个数字做或运算的结果是11111。而实际上只需要对前两个数做或运算,即可得到这个最大的结果。

每多纳入一个数进来做或运算,1的数量要么不变,要么变多。

题目的要求是:对于每个位置i,区间[i, n - 1]的所有数做或运算,能够得到一个最大的值max。但实际上,可能在某个位置kk < n - 1,区间[i, k] 上所有数做或运算,结果就是max。而要求解的是满足这样条件的最小的k

[i, k]的或运算结果为max,而[i, k - 1]的运算结果小于max。对于每个位置i,都找到这样一个k,即完成解答。

题目的数据范围是:数组中每个数都<= 10^9,那么10^9大概是2^30,实际10^9 < 2^30,那么对于数组中的每个数,其最多有30个二进制位。我们可以对每个位置上的1进行计数。

然后对于任意的i,我们通过前缀和,能够快速算出[i, n - 1]区间内所有数做或运算的结果中,每个二进制位上分别出现了多少个1,进而求得这个最大的结果中有多少个1,假设为m

根据前面的分析,从i往右不断做或运算时,1的个数是只增不减的。并且我们需要找到一个k,满足[i, k]的运算结果中1的个数为m,而[i, k - 1]的运算结果中1的个数不足m。这个过程便可以用二分来做。因为实际上,我们要在[i, n - 1)上找到k,这个k是个分界点,k的左侧都满足或运算结果中1的个数< mk的右侧都满足或运算结果中1的个数=m。这种在具有二段性的区间内,找分界点,就是经典的二分。

至于为什么要统计每一个二进制位上1的个数,这是因为或运算无法记录某个位置上出现了多少个1,无法进行类似前缀和的运算,而通过前缀和可以在 O(1)O(1) 的复杂度内求出[i, n - 1]的或运算结果。

故我们采用对每一个位进行计数这种方式。时间复杂度为 O(nlogn)O(nlogn),数据范围是10^5,故不会超时。

class Solution {
	public int[] smallestSubarrays(int[] nums) {
		int n = nums.length;
		// 求个前缀和
        // 第二维可以把整个数组看成是一个状态, 是增强版的或运算, 会额外记录每一个二进制位上的1的数量
		int[][] cnt = new int[n + 1][30]; 
		for (int i = 1; i <= n; i++) {
			int x = nums[i - 1]; // 求这个数每一个二进制位上的1
			for (int j = 0; j < 30; j++) {
				cnt[i][j] = cnt[i - 1][j];
				if ((x >> j & 1) == 1) cnt[i][j]++; // 计数+1
			}
		}
		int[] ans = new int[n];
		for (int i = 1; i <= n; i++) {
			int targetCnt = howMany(cnt, i, n); // 从i到n的或运算结果中, 二进制位为1的位置有多少个
			// 二分终点
			int l = i, r = n;
			while (l < r) {
				int mid = l + r >> 1;
				if (howMany(cnt, i, mid) < targetCnt) l = mid + 1;
				else r = mid;
			}
			int len = l - i + 1;
			ans[i - 1] = len;
		}
		return ans;
	}

	private int howMany(int[][] cnt, int i, int j) {
        // 前缀和求解
		int[] bitsJ = cnt[j];
		int[] bitsI = cnt[i - 1];
		int ret = 0;
		for (int k = 0; k < 30; k++) {
			if (bitsJ[k] - bitsI[k] > 0) ret++; //这一位上还有1
		}
		return ret;
	}
}

------记录一下y总的思路,太强了。

逆序遍历+贪心

一共30个二进制位,只需要记录一下每一个二进制位上,从左往右第一次出现1时的位置。对[i, n - 1]区间所有数的或运算的结果,对于所有出现过1的二进制位,取其第一次出现时的位置,再取个max。

只需要从右往左遍历一次数组,每次处理30个二进制位,时间复杂度30n。这是一种类似贪心的思路

class Solution {
    public int[] smallestSubarrays(int[] nums) {
        int n = nums.length;
        // 一共30个二进制位, p数组记录某个二进制位上, 第一次出现1时的那个数对应的下标
        int[] p = new int[30];
        Arrays.fill(p, -1);
        int[] ans = new int[n];
        for (int i = n - 1; i >= 0; i--) {
            int t = i; // 终点最少为i
            for (int j = 0; j < 30; j++) {
                // j这个位置, 更新第一次出现1时的下标
                if ((nums[i] >> j & 1) == 1) p[j] = i;
                // j这个位置出现过1, 则更新; 若j这个位置没有出现过1, 则不用管
                else if(p[j] != -1) t = Math.max(t, p[j]);
            }
            ans[i] = t - i + 1;
        }
        return ans;
    }
}

2412. 完成所有交易的初始最少钱数

题目描述

给你一个下标从 0 开始的二维整数数组 transactions,其中transactions[i] = [costi, cashbacki]

数组描述了若干笔交易。其中每笔交易必须以 某种顺序 恰好完成一次。在任意一个时刻,你有一定数目的钱 money ,为了完成交易 imoney >= costi 这个条件必须为真。执行交易后,你的钱数 money 变成 money - costi + cashbacki

请你返回 任意一种 交易顺序下,你都能完成所有交易的最少钱数 money 是多少。

示例

输入:transactions = [[2,1],[5,0],[4,2]]
输出:10
解释:
刚开始 money = 10 ,交易可以以任意顺序进行。
可以证明如果 money < 10 ,那么某些交易无法进行。

思路

周赛当天,前3题我半小时做完了。无比激动的来到第4题,读完题后更兴奋了。感觉题意非常简单,就是n次操作,每次会先减一个数字A,随后再加一个数字B。要求当前数字cur必须大于等于A,该次操作才能进行。

我们可以按任意的顺序执行所有操作,每种顺序可以称之为一个方案。对于每个方案,我们能得出一个初始数字。要求解调的就是所有方案中,最大的初始数字是多少。

又是求什么最大最小,那我又要开始贪了!

可是当我稍加尝试后,发现并没有那么简单。尝试了好几种贪心策略,都被一些错误样例当头一棒。于是这道题又坐牢了1个小时。

我的第一种非常天真非常naive的贪心策略是,最后一次交易过后,没有后续交易了,那么最后一次获得的利润就无法使用,那么将获得利润最大的那笔交易放在最后即可。

class Solution {
	public long minimumMoney(int[][] transactions) {
		int maxGain = 0;
		long netProfit = 0;
		for (int i = 0; i < transactions.length; i++) {
			int[] t = transactions[i];
			maxGain = Math.max(maxGain, t[1]);
			netProfit += t[0];
			netProfit -= t[1];
		}
		return netProfit + maxGain;
	}
}

结果发现不对。随后想了各种贪心策略,都有错误的样例,都不是很正确,然后就绕进去了。这里仅简单记录下当晚都有哪些想法:

  • 模拟,每次选cost最大的,并且要该次操作后,剩余资金尽可能少
  • cashBack最大的放最后;对于cashBack - cost > 0的,从小到大排序,放在靠后的位置;对于cashBack - cost = 0的,将cost高的排在前面,放在靠后;对于cashBack - cost < 0的,按照cashBack从小到大排,若cashBack相等,把cost高的排前面。
  • ....

下面看下y总的思路:

Acwing上类似的题目:耍杂技的牛,国王游戏。其中耍杂技的牛,和这题比较像。这题有点迷惑性。我一开始想的就是按照某种策略,先把所有操作排个序,然后做一次模拟即可求出答案。实际好像不是这样做。

假设我们按照某种顺序执行操作,执行到第i笔操作时,需要满足下面这个不等式(假设第j笔操作的costaja_j 来表示,cashBackbjb_j 来表示)

money(a1b1)(a2b2)....(ai1bi1)>=aimoney - (a_1 - b_1) - (a_2 - b_2) - .... - (a_{i-1} - b_{i-1}) >= a_i

称每种操作顺序为一种方案,则对于全部方案的每一笔操作,都需要满足上面这个不等式。

将上面的不等式进行一下移位变形,转化为如下的不等式 : money>=(a1b1)+(a2b2)+....+(ai1bi1)+aimoney >= (a_1 - b_1) + (a_2 - b_2) + .... + (a_{i-1} - b_{i-1}) + a_i

要求money的最小值,那么只要对不等式右边求一下最大值即可。

我们先来看 aia_i,一共有n种取法。我们枚举 aia_i 的全部情况,再来看前面的 (a1b1)+(a2b2)+....+(ai1bi1)(a_1 - b_1) + (a_2 - b_2) + .... + (a_{i-1} - b_{i-1}),这一坨式子什么时候能取最大值呢?那就是把所有 a>ba > b 的那些操作全部给加起来。

那么我们预处理出一个sum,然后枚举 aia_i,更新一下答案即可。

class Solution {
    public long minimumMoney(int[][] transactions) {
        long sum = 0;
        int n = transactions.length;
        for (int[] t : transactions) {
            int a = t[0], b = t[1];
            if (a > b) sum += a - b;
        }

        long ans = 0;
        for (int i = 0; i < n; i++) {
            int[] t = transactions[i];
            int a = t[0], b = t[1];
            long k = sum + a;
            if (a > b) k -= a - b;
            ans = Math.max(ans, k);
        }
        return ans;
    }
}

总结

这次周赛,考了好几道贪心。对于贪心题目,考察的比较多的是思维。平时训练时,要注重对贪心策略正确性的证明。这次的第4题是属于思维能力还不够。还要继续加油啦~