LeetCode周赛287场,训练你的逆向思维

1,325 阅读8分钟

大家好,日拱一卒,我是梁唐。

今天是周一,老规矩,我们来看昨天上午的LeetCode周赛。本文始发于公众号:Coder梁,欢迎关注

这次的比赛由地平线公司赞助,似乎是一家小公司,给的比赛奖励和内推机会都比较少。居然只有前5名能获得内推……说实话有点搞笑……

这次的赛题总体偏简单,适合新人练手。

好了,废话不多说了,我们一起来看题吧。

转化时间需要的最少操作数

难度:1星

给你两个字符串 currentcorrect ,表示两个 24 小时制时间

24 小时制时间"HH:MM" 进行格式化,其中 HH0023 之间,而 MM0059 之间。最早的 24 小时制时间为 00:00 ,最晚的是 23:59

在一步操作中,你可以将 current 这个时间增加 151560 分钟。你可以执行这一操作 任意 次数。

返回将 current 转化为 correct 需要的 最少操作数

解法

提示当中有一个信息很关键,current <= correct,也就是说目标时间一定比当前时间大,所以就不存在需要溢出的情况,比如把10点拨到3点。

既然不存在这种特殊情况,就很好办了,想要尽可能少地执行操作,就需要每一次操作的收益尽量大。这其中显然波动小时的收益是最大的,其次是15分钟,再是5分钟,最后是一分钟。

我们可以先从最高的60分钟开始操作,当时间差不足60分钟时,拨动15分钟,以此类推。所以我们需要先把时间转化成分钟格式,然后计算两个时间的差值,再用贪心的思路操作即可。

class Solution {
public:
    int convertTime(string current, string correct) {
        auto conv = [](string s) -> int {
           return (s[0] - '0') * 10 + s[1] - '0';
        };
        int cur_h = conv(current.substr(0, 2)), cur_m = conv(current.substr(3, 2));
        int tar_h = conv(correct.substr(0, 2)), tar_m = conv(correct.substr(3, 2));
        cur_m += cur_h * 60;
        tar_m += tar_h * 60;
        
        int hour = (tar_m - cur_m) / 60;
        cur_m += 60 * hour;
        
        int qua = (tar_m - cur_m) / 15;
        cur_m += 15 * qua;
        
        int fiv = (tar_m - cur_m) / 5;
        cur_m += 5 * fiv;
        
        return hour + qua + fiv + tar_m - cur_m;
    }
};

找出输掉零场或一场比赛的玩家

难度:1星

给你一个整数数组 matches 其中 matches[i] = [winneri, loseri] 表示在一场比赛中 winneri 击败了 loseri

返回一个长度为 2 的列表 answer

  • answer[0] 是所有 没有 输掉任何比赛的玩家列表。
  • answer[1] 是所有恰好输掉 一场 比赛的玩家列表。

两个列表中的值都应该按 递增 顺序返回。

注意:

  • 只考虑那些参与 至少一场 比赛的玩家。
  • 生成的测试用例保证 不存在 两场比赛结果 相同

解法

模拟题,我们直接模拟题目的意思即可。

遍历matches,存储每一个玩家获胜和失利和参加比赛的次数,当获胜次数等于参赛次数时,此玩家是全胜玩家。当失利次数等于1时,此玩家是只输一场的玩家。

就是一个map的简单使用,几乎没有技术含量。

class Solution {
public:
    vector<vector<int>> findWinners(vector<vector<int>>& matches) {
        map<int, int> win, lose, att;
        
        for (auto &vt : matches) {
            win[vt[0]]++;
            lose[vt[1]]++;
            att[vt[0]]++;
            att[vt[1]]++;
        }
        
        vector<int> all_win, lose_one;
        for (auto it = att.begin(); it != att.end(); it++) {
            int i = it->first, n = it->second;
            if (win.count(i) && win[i] == n) {
                all_win.push_back(i);
            }
            if (lose.count(i) && lose[i] == 1) {
                lose_one.push_back(i);
            }
        }
        
        vector<vector<int>> ret{all_win, lose_one};
        return ret;
    }
};

每个小孩最多能分到多少糖果

难度:2.5星

给你一个 下标从 0 开始 的整数数组 candies 。数组中的每个元素表示大小为 candies[i] 的一堆糖果。你可以将每堆糖果分成任意数量的 子堆 ,但 无法 再将两堆合并到一起。

另给你一个整数 k 。你需要将这些糖果分配给 k 个小孩,使每个小孩分到 相同 数量的糖果。每个小孩可以拿走 至多一堆 糖果,有些糖果可能会不被分配。

返回每个小孩可以拿走的 最大糖果数目

解法

还是先看数据范围,可以发现数据范围不小,尤其是K范围最大有1e12,初始糖果的堆数也有1e5,基本排除了暴力求解的方法。

很多同学拿到这道题会觉得很棘手,因为找不到突破口,不知道从哪里入手可以找到答案。

的确,因为糖果堆数是变量,每一堆糖果的数量也是变量,这些值都是不确定的,我们想要从这些信息当中找到答案好像没有什么很好的途径。

所以本题的关键就是及时发现这一点:正向思维不太可行,及时从反方向入手思考。

正向思维是通过题目信息想办法找到答案,反向思维是先假设一个答案,然后通过题目信息判断这个假设是否正确。一旦我们转换过思维之后,就会发现所有挡在我们面前的大山都不见了。

假设最后的答案是x,我们怎么判断它是否成立呢?很简单,我们遍历每一堆糖果,把它拆成每堆x的小堆。最后拆出来的结果就是nums[i] / x,也就是能够分给这么多小朋友。我们只要把所有的结果相加,和K比较一下大小,看看是否能够满足所有小朋友都能获得一堆,就可以确定x是否正确。

假设x成立,根据题意,显然所有小于x的数也成立。如果我们把是否成立看成是函数f,那么f(x)就是一个二值函数。在某个点之前全为1,到达某一个点之后变成0。我们要做的就是找到这个临界点,怎么找?二分。

所以只要能转换过思路,这道题就没有难度了,代码实现也很简单。

class Solution {
public:
    int maximumCandies(vector<int>& cand, long long k) {
        long long tot = 0;
        int n = cand.size();
        for (int i = 0; i < n; i++) {
            tot += cand[i];
        }
        long long avg = tot / k;
      	// 左闭右开区间
        long long l = 0, r = avg+1;
        while (l + 1 < r) {
            long long m = (l + r) >> 1;
            long long s = 0;
            for (int i = 0; i < n; i++) {
                s += cand[i] / m;
            }
          	// 计算每堆m,最多能划分的堆数,如果大于等于K,说明满足条件
            if (s >= k) l = m;
            else r = m;
        }
        return l;
    }
};

加密解密字符串

难度:3星

给你一个字符数组 keys ,由若干 互不相同 的字符组成。还有一个字符串数组 values ,内含若干长度为 2 的字符串。另给你一个字符串数组 dictionary ,包含解密后所有允许的原字符串。请你设计并实现一个支持加密及解密下标从 0 开始字符串的数据结构。

字符串 加密 按下述步骤进行:

  1. 对字符串中的每个字符 c ,先从 keys 中找出满足 keys[i] == c 的下标 i
  2. 在字符串中,用 values[i] 替换字符 c

字符串 解密 按下述步骤进行:

  1. 将字符串每相邻 2 个字符划分为一个子字符串,对于每个子字符串 s ,找出满足 values[i] == s 的一个下标 i 。如果存在多个有效的 i ,从中选择 任意 一个。这意味着一个字符串解密可能得到多个解密字符串。
  2. 在字符串中,用 keys[i] 替换 s

实现 Encrypter 类:

  • Encrypter(char[] keys, String[] values, String[] dictionary)keysvaluesdictionary 初始化 Encrypter 类。
  • String encrypt(String word1) 按上述加密过程完成对 word1 的加密,并返回加密后的字符串。
  • int decrypt(String word2) 统计并返回可以由 word2 解密得到且出现在 dictionary 中的字符串数目。

解法

这题的题目比较长,简单来说就是让我们实现一个类,可以进行编码和解码。

本题大概可以分为三个部分,第一个部分是构造函数,第二个部分是编码,第三个部分是解码。其中编码最简单,我们根据题意,对每一个字符进行映射转换即可。要做这样的映射转换,需要我们先存储下字母keys和字符串values之间的映射关系。

相对复杂的是解码的操作,因为在解码时字符串可能有多个匹配的结果。比如样例中的"ei"可以匹配a也可以匹配c。解码之后我们还要统计在dictionary存在的数量。

这里有一个陷阱,常规思路可能是先对字符串进行解码,枚举所有解码的组合,然后再判断每一个结果是否出现在了dictionary当中。但分析一下就会发现,在解码的时候可能性可能很多。尤其是values当中有大量重复的时候。选择的可能性将会以指数增长,那么导致的必然结果就是超时。

要解决超时的问题也需要我们反向思考,关键点在于我们遍历所有可能的解码组合可能很多,但我们要匹配的dictionary其实并不大。所以我们可以反向操作,枚举dictionary中所有的字符串,判断每一个字符串是否可能通过解码得到。字典中最多只有100个字符串,这样一来匹配的空间就大大减小了。

反向匹配的操作也不难,我们在解码的时候,把每一位可以映射的多个字符存在set当中,每一位对应一个set,整体对应一个set数组。在匹配dictionary中字符串的时候,只需要根据序号找到对应的set,判断一下当前字符是否在set中存在。只要有一位不存在,就说明不能构成匹配,如此即可通过。

更多细节查看代码:

class Encrypter {
public:
    map<char, string> mapping;
  	// 反向map,存储value到key的映射,由于可能一对多,所以需要用set来存储
    map<string, set<char>> refmap;
    vector<string> dict;
    
    Encrypter(vector<char>& keys, vector<string>& values, vector<string>& dictionary) {
        dict = dictionary;
        int n = keys.size();
        for (int i = 0; i < n; i++) {
            mapping[keys[i]] = values[i];
            if (refmap.count(values[i]) == 0) {
                set<char> st;
                refmap[values[i]] = st;
            }
            refmap[values[i]].insert(keys[i]);
        }
    }
    
    string encrypt(string word1) {
        string ret = "";
        for (auto c: word1) {
            ret += mapping[c];
        }
        return ret;
    }
    
    int decrypt(string word2) {
        int ret = 0;
      	// 存储每一位解码之后的set
        vector<set<char>> chars;
        for (int i = 0; i < word2.length(); i += 2) {
            string sub = word2.substr(i, 2);
          	// 无法解码,直接返回0
            if (refmap.count(sub) == 0) return 0;
            chars.push_back(refmap[sub]);
        }
        
        for (int i = 0; i < dict.size(); i++) {
            string &s = dict[i];
            if (s.length() != chars.size()) continue;
            bool flag = true;
            for (int j = 0; j < s.length(); j++) {
                char c = s[j];
              	// 判断s的每一位是否都能在set中找到
                if (chars[j].count(c) == 0) {
                    flag = false;
                    break;
                }
            }
            ret += flag;
        }
        return ret;
    }
};

/**
 * Your Encrypter object will be instantiated and called as such:
 * Encrypter* obj = new Encrypter(keys, values, dictionary);
 * string param_1 = obj->encrypt(word1);
 * int param_2 = obj->decrypt(word2);
 */

这次比赛的最后两题套路都是一样的,都是正面解题很难,需要反向思考,核心来说题目的难度并不非常大,非常适合拿来锻炼一下思维能力。

好了,关于这次的周赛就聊到这里,感谢大家的阅读。