leetcode 第 235 场算法比赛

作者: | 更新日期:

前三题二十分钟搞定,第四题没做出来。

本文首发于公众号:天空的代码世界,微信号:tiankonguse

零、背景

这次比赛前三题太水了,第四题自己的思路对了,但是实现较复杂,被卡超时了,最后 STL vector 改成数组形式就过了。

比赛代码:https://github.com/tiankonguse/leetcode-solutions/tree/master/contest/2/235

一、截断句子

题意:若干个单词组成一个句子字符串,单词之间使用一个空格分隔。
求输出前 k 个单词组成的句子。

思路:最笨的方法是先把句子转化为单词列表,取前 k 个,再拼接为句子字符串即可。

小技巧:直接查空格的个数,第 k 个空格设置为字符串结束符 \0 即可。
当然,其他语言,在要设置 \0 的位置,求前缀子串即可。

string truncateSentence(string s, int k) {
    int word_num = 0;
    for(auto& c: s){
        if(c == ' '){
            word_num++;
        }
        if(word_num == k){
            c = '\0';
        }
    }
    return s.c_str();
}

二、查找用户活跃分钟数

题意:给出每个用户 ID 的活跃时间,可以统计出每个用户的活跃次数(相同用户相同时间只记一次活跃)。
求活跃次数分别是 1 到 k 的用户个数。

思路:先正向统计每个用户的活跃次数,然后相反统计活跃次数对应的用户数。

vector<int> findingUsersActiveMinutes(vector<vector<int>>& logs, int k) {
    unordered_map<int, unordered_set<int>> m;
    for(auto& v: logs){
        m[v[0]].insert(v[1]);
    }
    vector<int> ans(k, 0);
    for(auto& p: m){
        int num = p.second.size();
        if(num > k) continue;
        ans[num-1]++;
    }
    return ans;        
}

三、绝对差值和

题意:给两个长度相同的数组 nums1 和 nums2。
定义绝对差值和:两个数组所有位置对应的两个数字的差绝对值之和。
现在增加一个选择:可以用 nums1 的任意一个元素,最多替换一次 nums1 的其他位置的元素。
求多了一个选择后,最小的绝对差值之和。

思路:由于是最多替换一次,可以理解为不替换和替换一次。

不替换,就是原数组计算出的答案。

替换一次,就需要考虑替换哪个位置,有 n 个位置。
确定位置后,还需要考虑替换哪个元素,又有 n 个元素。

总复杂度:O(n^2)

优化:确定位置后,替换元素的最优解是确定的。
如果先对 nums1 排序,在坐标轴上画出绝对值差,会发现这个曲线是类似于抛物线的 V 型曲线。

所以我们可以三份做这道题找到最优解。

当然,进一步分析这个绝对值差,会发现我们的目标是在数组中找一个离指定数字最近的元素。

那最近的元素的位置有啥特征呢?
显然,最近的是相等的元素,其次是第一个大于或第一个小于的元素。

所以我们可以直接使用二分查找找到大于等于和小于等于的元素,取最优值即可。

三分和二分的复杂度都是O(n log(n))

int minAbsoluteSumDiff(vector<int>& nums1, vector<int>& nums2) {
    int n = nums1.size();
    set<int> s; // 类似于排序,并去重了

    ll base = 0;
    for(int i=0;i<n;i++){
        base += dis(nums1[i], nums2[i]);
        s.insert(nums1[i]);
    }


    ll ans = base;
    for(int i=0;i<n;i++){
        auto it = s.lower_bound(nums2[i]); // 第一个大于等于的
        if(it == s.end()) it--; // 没找到,取最后一个


        ll tmp = base - dis(nums1[i], nums2[i]) + dis(*it, nums2[i]) ;
        if(tmp < ans) ans = tmp;


        if(it != s.begin()) it--;  //第一个小于等于的
        tmp = base - dis(nums1[i], nums2[i]) + dis(*it, nums2[i]) ;
        if(tmp < ans) ans = tmp;
    }

    return ans % 1000000007;
}

四、序列中不同最大公约数的数目

题意:给 n 个数字,每个子序列可以求出一个最大公约数。
求所有子序列可以得到的不同的最大公约数个数。

思路:

方法一:排列组合

最简单的方法是排列组合求出所有子序列,即选与不选。

复杂度O(2^N)
通过测试用例:16 / 39
我做了不少剪枝优化,但还是超时了,必须的。

方法二:枚举最大公约数。

枚举每个最大公约数 max_gcd,判断数组中的子序列,是否可以求出最大公约数 max_gcd 即可。

显然可以推导出几个性质。

性质1:如果 max_gcd 是一个答案(是某个子序列的最大公约数),则那些元素的约数中肯定有 max_gcd(废话,是最大公约数了,肯定是约数)。
性质2:max_gcd 如果是某个子序列的答案,那么所有的约数有 max_gcd 的元素,组成子序列后,求出的最大公约数依旧是 max_gcd

性质2 是啥意思呢?
举个例子大家就明白了。

假设数组是 [4, 8, 16]
可以确定 max_gcd=4 是答案,因为子序列[4,8]的最大公约数是 4。 根据性质2,4 的所有倍数元素,即4,8,16 组成的子序列的最大公约数肯定也是 4。

性质2 可以使用反证法证明,这里就不再证明了。

有了性质2,我们就可以做这道题了。

枚举最大公约数,然后找到这个最大公约数的所有倍数元素,判断这些元素能否计算得到当前的最大公约数即可。

有点绕,还是以[4, 8, 16]为例,来解释下最后一句话是啥意思。

例如 2 ,找到的所有倍数元素是 4,8,16,求出的最大公约数是 4。
所以 2 不是答案。

那怎么找到一个数字的所有倍数元素呢?

我比赛的时候,想的是预处理所有数字,计算出每个元素的约数,建立约数到元素的映射关系。

最开始,我使用暴力方法找出所有的元素的所有约数。
复杂度:O(n^2)
通过测试用例:14 / 39
显然超时了。

后来,我预先打素数表,统计素约数的个数,再组合求出所有约数。
复杂度:O(n log(n))
通过测试用例:37 / 39
发现还是超时了,可能申请了大量内存,常数有点高。

赛后,看第一名的代码,思想差不多,但是没想到可以直接可以使用素数筛法来做这道题。

简单来说就是不需要预选统计一个数字的倍数元素列表。
而是实时的判断指定数字的倍数,是不是在元素列表中。
复杂度计算:N/1 + N/2+ ... +N/N
复杂度:O(n log(n))
关于这个公式的具体复杂度,比较难计算,大家当做近似上面的复杂度即可。

int countDifferentSubsequenceGCDs(vector<int>& nums) {
    int max_val = 0;
    for (const auto v : nums) {
        max_val = max(max_val, v);
    }
        
    vector<bool> is;
    is.resize(max_val+1, false);
    for (const auto v : nums) {
        is[v] = true;
    }

    int ans = 0;
    for(int i=1;i<=max_val;i++){
        int gcd_val = 0;
        for(int j = i; j <= max_val; j+=i){
            if(!is[j]) continue;
            
            if(gcd_val == 0){
                gcd_val = j;
            }else{
                gcd_val = gcd(gcd_val,  j);
            }
        }
        if(gcd_val == i){
            ans++;
        }
    }
    return ans;
}

求素约数方法优化:

复杂度:O(n log(n))
通过测试用例:37 / 39
回头看看我的思路,复杂度没问题,就快要过了,有点不甘心。

于是我需要做一些优化,来防止数据量大时,导致在内存上消耗太多无关的时间。
具体来说就是,所有的 vector 换成定长数组,然后一提交,竟然通过了。

代码较长这里就不贴代码了,感兴趣的可以自己去看源码吧(文章头部有源码链接)。

五、最后

这次比赛前三题太水,第四题数据量比较大,卡耗时了,以后不再使用 vector,所有地方都使用定长数组吧。

回头看看最后一题,有点意思。

我的方法是正规思路,统计出每个数字的倍数元素。
方法是求出每个元素的素约数与个数,然后排列组合求出所有约数,约数到元素的反向关系储存下来。

比赛第一名的方法是素数筛法来直接拉求答案。
即对于枚举的每个约数,找到这个约数的所有倍数,对于倍数素数求 gcd,然后判断是否满足性质2。

怎么说呢,比赛第一名的方法虽然简单粗暴,但是确实很巧妙。
关键是这种思路我一般情况下是想不到的,难道这就是差距吗?

思考题:第三题如果是可以交换一次 nums1 数组两个位置的元素,又改怎么做呢?

加油,算法人。

《完》

-EOF-

本文公众号:天空的代码世界
个人微信号:tiankonguse
公众号ID:tiankonguse-code

本文首发于公众号:天空的代码世界,微信号:tiankonguse
如果你想留言,可以在微信里面关注公众号进行留言。

关注公众号,接收最新消息

tiankonguse +
穿越