80岁老爷爷学算法之枚举

8 阅读5分钟

80岁老爷爷学算法之枚举

学算法第二天上午

老爷爷回想起自己高中的时候学英语,第一个单词是abandon,于是他准备尝试一下leetcode1. 两数之和

爷爷看到标题,心想有什么难的,不就是在一串数字里找两个数,加起来刚好等于目标值嘛?他慢悠悠地戴上老花镜,从抽屉里摸出泛黄的草稿纸和磨得光滑的铅笔,先把题目仔仔细细看了一遍:“给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。”

爷爷看这道题无时间复杂度限制,于是直接两层for循环遍历(换网友说法,可以直接回去等通知了),于是爷爷换了一种方法——枚举,只遍历一次,然后去找是否存在target-nums[i],这里还有一个小细节,为了从逻辑上杜绝了使用同一个元素的风险,爷爷通过 “只在已遍历过的元素中查找” 的方式,让代码更安全、简洁,核心代码如下:

for(int i = 0;i<nums.size();i++){
    int complete = target - nums[i];
    if(num_map.find(complete)!=num_map.end())  
         return {num_map[complete],i};
    num_map[nums[i]] = i;
}

完整代码:

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        int n = nums.size();
        unordered_map<int, int> num_map;
        for(int i = 0;i<nums.size();i++){
            int complete = target - nums[i];
            if(num_map.find(complete)!=num_map.end())  return {num_map[complete],i};
            num_map[nums[i]] = i;
        }
        return {};
    }
};

这道题非常适合枚举入门,通常最简单的方法也是枚举右,寻找左

下午

爷爷刚吃完饭,想巩固一下上午学的枚举,于是找到了他的儿子为他推荐一道题,2441. 与对应负数同时存在的最大正整数,爷爷一看,这和两数之和一样,不就是target=0吗?两数之和是求3个下标,需要用map,而这道题只需要返回一个数,用set就行了,代码如下:

class Solution {
public:
    int findMaxK(vector<int>& nums) {
        unordered_set<int> s;
        int res =-1;
        for(int f:nums){
            if(s.count(-f))  res = max(res,abs(f));
            s.insert(f);
        }
        return res;
    }
};

爷爷直接秒了这道题,于是午睡去了。

晚上

一觉睡到了5点,爷爷把下午秒掉的那道题草稿纸整整齐齐夹进算法笔记本,指尖摸着纸上的 set 和 map,笑得满脸皱纹都舒展了:“原来这枚举藏着个万能小窍门,枚举右边,查找左边,不管是找和为 0,还是找和为 target,根子全是一个道理!”

孙子凑过来扒着桌子看,爷爷指着代码慢悠悠讲:“你记好咯,我先走到第 i 个数(右边这个),回头瞅瞅前面已经走过的数里,有没有我要找的搭档。找到就用,找不到就把自己记下来,等后面的数来找我,比傻乎乎两层循环挨个试,省劲一百倍!”

儿子见老爷子越学越起劲,又翻出一道1512. 好数对的数目,笑着说:“爸,这题还是同一个思路,您再练练手?”

爷爷扶了扶老花镜,一字一句读题:“数组里,下标 i<j、数字还一样的,就是好数对,求一共有多少个。” 刚读完爷爷先乐了:“这还能难住我?照上午的法子来!”他刚想提笔写两层循环,手立马顿住,一拍大腿:“不对!不用笨办法,还是枚举右边,查左边!” 爷爷琢磨得透亮:两数之和是找 “加起来等于目标” 的搭档,这题是找 “和我一模一样” 的搭档。我遍历到当前这个数(右边的 j),先看看左边已经出现过几个相同的数,有几个就多几个好数对,把这些数加起来,就是答案!

想通这点,爷爷提笔就写出代码,和两数之和的逻辑一模一样,只是把 “找配对” 改成了 “数个数”:

class Solution {
public:
    int numIdenticalPairs(vector<int>& nums) {
        unordered_map<int, int> cnt; // 记左边每个数出现过几次
        int ans = 0;
        for(int j = 0; j < nums.size(); j++){ // 枚举右边j
            ans += cnt[nums[j]]; // 左边有几个一样的,就加几个好数对
            cnt[nums[j]]++; // 把自己记下来,留给右边找
        }
        return ans;
    }
};

写完爷爷指着代码给孙子讲:“你看,还是遍历右边的 j,先查左边记了几个同数字,再加到答案里,最后才把自己存进表里。跟两数之和是不是一个模子刻出来的?”

怕孙子听不懂,爷爷又在草稿纸上举例子:“比如数组 [1,2,3,1,1,3],走到第二个 1,左边有 1 个 1,加 1;走到第三个 1,左边有 2 个 1,加 2,加起来就是所有好数对!”

三道题做完,天已经擦黑,爷爷端起茶杯抿了一口,乐呵呵地叹道:“原来算法不是啥高深天书,吃透一个小思路,三道题全拿下!这枚举右,查找左,真是个省心的宝!”他翻开笔记本,在扉页认认真真添了一行歪歪扭扭的字:“学算法,不用急,吃透一个,举一反三。”桌上的台灯暖黄光亮着,老爷爷的算法小课堂,还在慢悠悠地继续着。

情节虚构,切勿当真