[C++&Rust]LeetCode No.1442 形成两个异或相等数组的三元组数目

原贴地址:http://blog.leanote.com/post/dawnmagnet/74b9cba6cc8a

题目

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XGn9tz85-1621307923629)(https://leanote.com/api/file/getImage?fileId=60a31386ab64411fb000251e)]

思路分析

又是一道异或的题目。
我们再强调一下,异或的最重要的性质就是两个相同的数字异或是0
接下来看一下我们的这个数据量,不太适合暴力,如果按照题目的说法对(i, j, k)三元组进行遍历,复杂度就是 O ( n 3 ) O(n^3) O(n3),达到了 30 0 3 = 27000000 300^3=27000000 3003=27000000复杂度,这种已经很大了,很危险,很有可能过不了。这也是对于所有题目的一个小提示:如果复杂度达到 1 0 9 10^9 109,就过不了了。
这个题目既然如此危险,不到万不得已就不写暴力算法。
此处说明一下优化的手段,一般来说O(n^2)的算法只能往O(n)和O(nlgn)进行优化,但这里我们观察了一下也没什么可以二分的办法,所以我们就用O(n)进行思考,既然是O(n)那么每一次查询都应该是O(1)的复杂度,有可能吗?
这里就要介绍一下前缀和的思想。
我们先思考一个跟本问题很像的一个问题,就是我们将本题中的异或换成加法。我们给入一个查询,查询的是数组中间某一段的和。为了在O(1)的时间应对每次查询,我们新建一个前缀和数组,称为prefix

prefix中的下标arr中的下标
0n0
1n0+n1
2n0+n1+n2
3n0+n1+n2+n3
4n0+n1+n2+n3+n4

如果我们获得了前缀和数组,对于每次查询都能在O(1)的时间返回,因为如果我们想要获得1至3之间的值(包含3),我们就用**prefix[3]-prefix[0]**就获得了1至3之间的值的和。

我们再仔细思考一下为什么是加法就可以这么做。以及换成异或之后我们应该如何做。
性质|加法+|异或^
–|--
逆操作|减法-|异或^
一顺一逆的结果|0+n-n=0|0nn=0

现在我们可以知道为什么上述操作中我们会用prefix[3]减去prefix[0],因为减法是加法的逆操作。而正因为一顺一逆的结果是0,这个前缀和才有它存在的意义。

我们仿照着加法设计出来我们的异或前缀和表

prefix中的下标arr中的下标
0n0
1n0^n1
2n0n1n2
3n0n1n2^n3
4n0n1n2n3n4

同样的,我们如果想获得nums中从1异或到3的值,我们只需要获取prefix[3]^prefix[0],也是一个O(1)的操作。
我们再回到题目中的三元组来看一下,如何将前缀和应用到三元组的简化当中。

对于三元组(i, j, k)来说,
a = arr[i] ^ … ^ arr[j - 1]
= prefix[j - 1] ^ prefix[i - 1]
此处使用到了异或的性质

同理b = prefix[k] ^ prefix[j - 1]
所以我们继续推导,当a = b时,我们得出一个惊人的结论,就是prefix[k]=prefix[i-1]
也就是说,当**prefix[k]=prefix[i-1]**时,我们可以获取k和i中包含的任意一点作为j,并且都是成立的!也就是说,我们确定了一个(k,i)对,就可以让答案加上一个k-i。

经过我们这么一番倒腾,题目啪的一下就变成了另外一个等价的题目。

新题目

给你一个整数数组arr
将其处理成异或前缀和prefix
在prefix数组中寻找下标二元组(i, j), 满足 i < j 并 且 p r e f i x [ i ] = = p r e f i x [ j ] i < j 并且 prefix[i] == prefix[j] i<jprefix[i]==prefix[j],每找到一对就让答案加上 j − i − 1 j - i - 1 ji1,返回最终的答案。

新题目思路分析

经过一番倒腾, O ( n 3 ) O(n^3) O(n3)的复杂度一下子变成了 O ( n 2 ) O(n^2) O(n2),很开心啊!但这个题目,似乎还有优化的空间,因为经过我们处理的新题目看起来就是一个寻找相等数字的事情,似乎(如果加个哈希表),能够处理到 O ( n ) O(n) O(n)的复杂度!啪的一下就站起来了,震惊了!这都可以?

Rust代码

use std::collections::HashMap;
impl Solution {
    pub fn count_triplets(arr: Vec<i32>) -> i32 {
        let n = arr.len();
        let mut cnt = HashMap::new();
        let mut total = HashMap::new();
        let mut ans = 0;
        let mut s = 0;
        for k in 0..n {
            let s_nxt = s ^ arr[k];
            if let Some(&cnt_val) = cnt.get(&s_nxt) {
                ans += cnt_val * k - *total.entry(s_nxt).or_insert(0);
            }
            *cnt.entry(s).or_insert(0) += 1;
            *total.entry(s).or_insert(0) += k;
            s = s_nxt;
        }
        ans as i32
    }
}

C++代码

class Solution {
public:
    int countTriplets(vector<int> &arr) {
        int n = arr.size();
        unordered_map<int, int> cnt, total;
        int ans = 0, s = 0;
        for (int k = 0; k < n; ++k) {
            int s_nxt = s ^ arr[k];
            if (cnt.find(s_nxt) != cnt.end()) {
                ans += cnt[s_nxt] * k - total[s_nxt];
            }
            ++cnt[s];
            total[s] += k;
            s = s_nxt;
        }
        return ans;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值