LeetCode-形成两个异或相等数组的三元组数目

5.21号LeetCode-形成两个异或相等数组的三元组数目——中等

坚持每日奋斗,加油!

对于这道题,我觉得是很有意思的。所以记录了下来。

题目描述:

给你一个整数数组 arr 。

现需要从数组中取三个下标 i、j 和 k ,其中 (0 <= i < j <= k < arr.length) 。

a 和 b 定义如下:

a = arr[i] ^ arr[i + 1] ^ … ^ arr[j - 1]
b = arr[j] ^ arr[j + 1] ^ … ^ arr[k]
注意:^ 表示 按位异或 操作。

请返回能够令 a == b 成立的三元组 (i, j , k) 的数目。

示例 1:

输入:arr = [2,3,1,6,7]
输出:4
解释:满足题意的三元组分别是 (0,1,2), (0,2,2), (2,3,4) 以及 (2,4,4)
示例 2:

输入:arr = [1,1,1,1,1]
输出:10
示例 3:

输入:arr = [2,3]
输出:0
示例 4:

输入:arr = [1,3,5,7,9]
输出:3
示例 5:

输入:arr = [7,11,12,9,5,2,7,17,22]
输出:8

提示:

1 <= arr.length <= 300
1 <= arr[i] <= 10^8

根据题目给出的a = arr[i] ^ arr[i + 1] ^ … ^ arr[j - 1]和b = arr[j] ^ arr[j + 1] ^ … ^ arr[k]可以推出:

  • a的值是从下标i开始到下标j结束之间的所有元素的异或结果(其中,包含下标为i的元素,但不包含下标为j的元素)

  • b的值是从下标j开始到下标k结束之间的所有元素的异或结果(其中,包含下标为j和k的元素)

这道题的思路为:

定义长度为n的数组arr的异或前缀和:

则Si可等于:0, 【i = 0】或者arr0 ^ arr1 ^ … ^ arr(i - 1),【1 <= i <= n】

由定义的可以得出:

Si可等于:0,【i = 0】;S(i - 1) ^ arr(i - 1),【1 <= i <= n】

这是一个关于Si的递推式,根据该递推式可以用O(n)的时间计算出数组arr的异或前缀和数组。(所以这道题需要使用到前缀和)

假设两个不同的下标i和j(0 < i < j),求出它们的前缀和Si和Sj,有:

Si ^ Sj = (arr0 ^ arr1 ^ … ^ arr(i - 1)) ^ (arr0 ^ arr1 ^ … ^ arr(i - 1) ^ arri ^ … ^ arr(j - 1))

计算得出:Si ^ Sj = arri ^ … ^ arr(j - 1)
所以可推算出:数组arr的子区间[i,j]的元素异或可表示为:Si ^ S(j + 1)
那么根据题目中给出的条件可得出:

a = Si ^ Sj

b = Sj ^ S(k + 1)

又因为a = b,所以可得:

Si = S(k + 1)

注:【】中表示的是需要满足的条件。

分析完以上后,我首先想到的是使用三重循环来实现。因为我们只需要通过三重循环来判断Si是否等于S(k + 1)即可。当然,首先的第一步还是要计算出arr数组的前缀和。

方法一——三重循环:

代码如下:

public static int countTriplets1(int[] arr) {
	int n = arr.length;
	//创建一个数组,用来存储数组arr的前缀和
	int[] S = new int[n + 1];
	for (int i = 0; i < n; i++) {
		S[i + 1] = S[i] ^ arr[i];
	}
		
	//创建一个变量,用来表示最终的结果
	int ans = 0;
	for (int i = 0; i < n; i++) {
		for (int j = i + 1; j < n; j++) {
			for (int k = j; k < n; k++) {
				//这里j和k可以相等,但是i和j是不能相等的
				if(S[i] == S[k + 1]) {
					ans++;
				}
			}
		}
	}
	return ans;
}

使用以上的方法,其时间复杂度和空间复杂度自然是高的。所以为了降低,我们再进行分析。

方法二——二重循环:

**分析:**由以上的推演,可知,当Si = S(k + 1)成立时,那么在[i + 1,k]这个区间中任意的一个元素都可以满足题目所给出的条件。所以我们只需要计算出这个区间的长度即可,即k - i。

代码如下:

public static int countTriplets2(int[] arr) {
	int n = arr.length;
	int[] S = new int[n + 1];
	for (int i = 0; i < n; i++) {
		S[i + 1] = S[i] ^ arr[i];
	}
		
	int ans = 0;
	for (int i = 0; i < n; i++) {
		for (int k = i + 1; k < n; k++) {
			if(S[i] == S[k + 1]) {
				ans += (k - i);
			}
		}
	}
	return ans;
}

通过测试,得出这个方法大大减少了时间复杂度和空间复杂度。这是个很不错的方法。

还有方法可以实现这道题吗?这自然是还有的。就是使用哈希表来实现,也就是一重循环。一开始,我也想到了使用哈希表,但是由于我对这个还不熟(根本还没有学,只是因为刷题目的时候看到过,然后去稍微了解了一下),所以靠自己没有做出来,最后还是借助了官方的解答理解了。

方法三——一重循环(哈希表):

分析:

对于下标k,若下标i = i1,i2, … , im时均满足Si = S(k + 1),根据方法二的思路,可得:
这些二元组(i1,k),(i2,k), … ,(im,k)对答案的贡献之后为:
(k - i1) + (k - i2) + … + (k - im) = m * k - (i1 + i2 + … + im)

也就是说,当遍历下标k时,我们需要知道所以满足Si = S(k + 1)的

1.下标i的出现次数
2.下标i之后

这些都可以借助哈希表来实现,

创建两个哈希表。一个哈希表用来统计Sk的出现次数,另一个哈希表统计值为Sk的下标之后

代码如下:

import java.util.HashMap;
import java.util.Map;

public static int countTriplets3(int[] arr) {
	int n = arr.length;
	int[] S = new int[n + 1];
	for (int i = 0; i < n; i++) {
		S[i + 1] = S[i] ^ arr[i];
	}
		
	Map<Integer, Integer> cnt = new HashMap<Integer, Integer>();
	Map<Integer, Integer> total = new HashMap<Integer, Integer>();
	int ans = 0;
	for (int k = 0; k < n; k++) {
		if(cnt.containsKey(S[k + 1])) {
			ans += cnt.get(S[k + 1]) * k - total.get(S[k + 1]);
		}
		cnt.put(S[k], cnt.getOrDefault(S[k], 0) + 1);
		total.put(S[k], total.getOrDefault(S[k], 0) + k);
	}
	return ans;
}

对于这个方法,我觉的这个方法没有第二个方法好。因为经过测试,它的时间复杂度和空间复杂度的降低效果没有第二种方法好。

对于这个方法是有优化的。

方法三优化后的代码如下:

import java.util.HashMap;
import java.util.Map;

public static int countTriplets(int[] arr) {
	int n = arr.length;
	Map<Integer, Integer> cnt = new HashMap<Integer, Integer>();
	Map<Integer, Integer> total = new HashMap<Integer, Integer>();
	int ans = 0, s = 0;
	for (int k = 0; k < n; k++) {
		int val = arr[k];
		if(cnt.containsKey(s ^ val)) {
			ans += cnt.get(s ^ val) * k - total.get(s ^ val);
		}
		cnt.put(s, cnt.getOrDefault(s, 0) + 1);
		total.put(s, total.getOrDefault(s, 0) + k);
		s ^= val;
	}
	return ans;
}

这个优化后的效果自然是比没有优化前的好。但是与方法二相比还是差了一点。

总结:

对于这道题,难度是有一点的。主要是在如何能更好的减少时间复杂度和空间复杂度方面上。还有就是对前缀和的疑问,由于我没怎么做过与前缀和有关的题目,所以还是有点模糊的。多刷题目吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值