力扣3171.找到最近或最接近k的子数组--模板题(从O(n²)到O(nlogU))

题目

上来我首先想的是滑动窗口,然而发现因为或运算不存在逆运算,所以其实是没有办法用常规的滑动窗口来进行计算的。在灵神的题解中,有一个方法是滑动窗口,不过我现在水平不够,暂且搁置

考虑暴力做法:

只有三个元素:

很明显就是遍历所有的子数组,从 [0] 到 [0] ,从 [0] 到 [1] ,从 [0] 到 [2],从 [1] 到 [1] ,从 [1] 到 [2],从[2]到[2],得到的所有子数组的值和k进行或运算,假设数组元素的值分别是:1,3,4

因为常规的遍历方式是左序号增大,然后增大右序号,即只有在左序号固定的情况下把所有的可能的子数组都遍历完毕才进行增加,这种遍历方式得到的子数组的值是:

从 [0] 到 [0] :1

从 [0] 到 [1] :1|3

从 [0] 到 [2]:1|3|4

从 [1] 到 [1] :3

从 [1] 到 [2]:3|4

从 [2] 到 [2]:4

这是没什么规律的

但是换个想法,先固定右序号,再增加左序号,得到的序列值是:

从 [0] 到 [0] :1

从 [0] 到 [1] :1|3

从 [1] 到 [1] :3

从 [2] 到 [2]:4

从 [1] 到 [2]:3|4

从 [0] 到 [2]:1|3|4

发现,其实 1 | 3 | 4 可以从 1 | 3 再or个 4 计算得来

使用一个数组来保存或运算的结果,称为or_num

设立一个变量i来遍历nums的右端点(枚举右,维护左)

在遍历到1的时候,or_num数组:

1 [] []

在遍历到3的时候:

1|3 3 []

在遍历到4的时候:

1|3|4 3|4 4

恰好对应了所有的子数组的按位或的值

可以写出如下代码:

 
class Solution {
public:
    int minimumDifference(vector<int>& nums, int k) {
        int ans=INT_MAX;
        vector<int> or_num(nums.size());
        for(int i=0;i<nums.size();i++){
            for(int j=i;j>=0;j--){
                if(i==j){
                    or_num[j]=nums[j];
                }
                else{
                    or_num[j]=or_num[j] | nums[i];
                }
                ans=min(ans,abs(k-or_num[j]));
            }
        }
        return ans;
    }
};

这个代码可以过大部分的用例了,剩下几个会超时,那么需要优化

(其实仔细一想,这个算法的复杂度还是n²。。)

以下是我本人应该是完全想不到的做法:

因为我们这个做法,是不断按位与,假设有四个数a,b,c,d,我们的计算结果依次是:

a

a|b b

a|b|c b|c c

a|b|c|d b|c|d c|d d

考虑在计算第四行的时候,如果c&d=c,会发生什么?

在这种情况下,我们可以化简为:

a

a|b b

a|b|c b|c c

a|b|c b|c c d

第四行的前三个值是和第三行一样的

所以,为了方便循环的计算,我们在遇到出现c&d=d这种情况,即or_num[i] | nums[j] 的时候,就可以不用再遍历当前这一层了,可以直接break掉,从而节省了运行的时间

AC代码:

class Solution {
public:
    int minimumDifference(vector<int>& nums, int k) {
        int ans=INT_MAX;
        vector<int> or_num(nums.size());
        for(int i=0;i<nums.size();i++){
            int flag=1;
            for(int j=i;j>=0;j--){
                if(i==j){
                    or_num[j]=nums[j];
                }
                else{
                    flag=(or_num[j] | nums[i]) - or_num[j];
                    or_num[j]=or_num[j] | nums[i];
                }
                ans=min(ans,abs(k-or_num[j]));
                if(!flag) break;
            }
        }
        return ans;
    }
};

不妨来考虑一下,为什么在这种情况下,时间复杂度会是O(nlongU)?(U是当前nums数组的最大值)

我们每次更新or_num[i],都必然是让当前的这个or_num[i]变大

那么,这个变大是否是有上限的?

当然有,题目里给出了,1 <= nums[i] <=10^9

10^9大于2^{29},但是小于2^{30},所以,一个数字的二进制表示最多只有30位

我们每次让or_num[i]变大,都是改变某一位的0变成1,这个过程最多可以进行30次

所以,对于or_num数组中的每一个位置,我们最多可以增大29次

这个过程最多进行n次,也就是说我们最多进行n*29次循环就可以完成遍历

所以时间复杂度是O(nlogU).

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值