LeetCode 第30场双周赛

LeetCode 第30场双周赛

大佬们的手速场,本菜鸡手速拼不过,不过本次双周赛依然是我做过最简单的一次了,福利场涨涨自信好了。

1·转变日期格式

给你一个字符串 date ,它的格式为 Day Month Year ,其中:

Day 是集合 {"1st", "2nd", "3rd", "4th", ..., "30th", "31st"} 中的一个元素。
Month 是集合 {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} 中的一个元素。
Year 的范围在 ​[1900, 2100] 之间。

请你将字符串转变为 YYYY-MM-DD 的格式,其中:

YYYY 表示 4 位的年份。
MM 表示 2 位的月份。
DD 表示 2 位的天数。

示例 1:
输入:date = “20th Oct 2052”
输出:“2052-10-20”

解析

本题纯业务题,就是代码量比后面的题还要大,服了。
首先注意到 年份没有其他表示方法,就四个数字的形式,所以照搬就行;
月份是缩写与阿拉伯数字对应关系,写个表,查表就好;
日期显然无论是1st 2nd 3rd 4th 31th都是去掉最后两位字符,同时前面保持两个字符,不足补零的形式。
本题简化了验证日期合法性问题,如果要验证合法性,就需要:
年份范围(是否闰年);
月份范围;
月份对应的总天数;
三者一一验证。
当然本题不做要求,只要把年月日按照上述方法整出来,拼接起来就好。
除此之外输入的date是空格分隔的一个字符串,切分年月日也是要考虑的点,我用的C++,习惯stringstream的方式(主要是懒);
C++代码如下:

string reformatDate(string date) {
    stringstream ss(date);
    unordered_map<string, string>mm{ {"Jan","01"}, {"Feb","02" }, {"Mar","03"}, {"Apr","04"}, {"May","05"}, {"Jun","06"}, {"Jul","07"}, {"Aug","08"}, {"Sep","09"}, {"Oct","10"}, {"Nov","11"}, {"Dec","12"} };
    string tmp, ans;
    vector<string>s;
    while (ss >> tmp) {
      s.push_back(tmp);
    }
    s[0].pop_back();
    s[0].pop_back();
    string dd = s[0].size() == 2 ? s[0] : "0" + s[0];
    return s[2] + "-" + mm[s[1]] + "-" + dd;
  }

代码思路很直接,建立英文简写月份与题目要求mm格式的映射表,将输入通过stringstream输入到vector,对日期弹出后两个字符并酌情补0,最后返回年月日拼接结果即可。

2·子数组和排序后的区间和

给你一个数组 nums ,它包含 n 个正整数。你需要计算所有非空连续子数组的和,并将它们按升序排序,得到一个新的包含 n * (n + 1) / 2 个数字的数组。

请你返回在新数组中下标为 left 到 right (下标从 1 开始)的所有数字和(包括左右端点)。由于答案可能很大,请你将它对 10^9 + 7 取模后返回。

示例 1:
输入:nums = [1,2,3,4], n = 4, left = 1, right = 5
输出:13
解释:所有的子数组和为 1, 3, 6, 10, 2, 5, 9, 3, 7, 4 。将它们升序排序后,我们得到新的数组 [1, 2, 3, 3, 4, 5, 6, 7, 9, 10] 。下标从 le = 1 到 ri = 5 的和为 1 + 2 + 3 + 3 + 4 = 13 。
提示:

1 <= nums.length <= 10^3
nums.length == n
1 <= nums[i] <= 100
1 <= left <= right <= n * (n + 1) / 2

解析

本题要求一个数组所有非空子数组和在排序后的一个区间内的和,理论上讲这个题可以根据给定的范围和原数组排序得到要求和的那几个子数组和,没必要全算,但是很麻烦并且本题数组长度只有 1 0 3 10^3 103即使求出所有子数组排序也不超时,所以就根据题意求所有子数组和就行了。所有的子数组之和排序,在根据给定的范围选择求和即可。
C++代码如下:

int rangeSum(vector<int>& nums, int n, int left, int right) {
    vector<uint64_t>subsum;
    for (int i = 0; i < n; ++i) {
      uint64_t tmp = (uint64_t)nums[i];
      subsum.push_back(tmp);
      for (int j = i + 1; j < n; ++j) {
        tmp += (uint64_t)nums[j];
        subsum.push_back(tmp);
      }
    }
    sort(subsum.begin(), subsum.end());
    uint64_t ans=0;
    for (int i = left - 1; i <= right - 1; ++i) {
      ans += subsum[i];
    }
    return (int)ans;
  }

思路很直接,算所有的子数组和,放在数组里,排序,选择要求范围求和。这里为了防溢出子数组和用了uint64_t,不过按数据范围应该int就够了。

3· 三次操作后最大值与最小值的最小差

给你一个数组 nums ,每次操作你可以选择 nums 中的任意一个数字并将它改成任意值。
请你返回三次操作后, nums 中最大值与最小值的差的最小值。

示例 1:
输入:nums = [5,3,2,4]
输出:0
解释:将数组 [5,3,2,4] 变成 [2,2,2,2].
最大值与最小值的差为 2-2 = 0 。

示例 2:
输入:nums = [1,5,0,10,14]
输出:1
解释:将数组 [1,5,0,10,14] 变成 [1,1,0,1,1] 。
最大值与最小值的差为 1-0 = 1 。

解析

本题要求一个数组可以有3次机会修改任意数到任意值,问修改完之后剩余的最大最小差值最小是多少。
本题的“修改”实际上等同于“去除”,因为可以先去掉三个数,把他们修改为去掉后剩下的最大值或最小值,这样不影响最后差值的取值,二者是一样的。
因此我们考虑“去掉”三个数剩下数的最大值和最小值。
首先,这种修改或去除,只能用于数组原来最大的几个数或最小的几个数,修改中间数是没有意义的,取差值也不会考虑它们。因此本题要想得到最小的差值,就要考虑去掉最大的或最小的,总之相当于排序后从一头一尾开始,一共去掉3个数,可以在头尾任意分配(0,3)(1,2)(2,1)(3,0)这四种情况,四种中最小的即为所求。
当然如果数组长度小于等于4意味着所有数都能改成1个相同的数字,一定返回0.
C++代码如下:

 int minDifference(vector<int>& nums) {
    int len = nums.size();
    if (len <= 4) return 0;
    sort(nums.begin(), nums.end());
    int minD = INT_MAX;
    for (int i = 0; i <= 3; ++i) {
      minD = min(minD, nums[len - 1 - i] - nums[3 - i]);
    }
    return minD;
}

这里简单说一下为什么只要考虑4种情况。我们可以认为去掉3个数,剩下的数中找一个最大值和最小值,让他们最接近。数组是先排序的,是递增有序的。如果我让第0个数是修改后最小值,那么有3次机会将最大的三个数改掉,这样数组最大就是nums[len-4]这个数,此时的差值就是 n u m s [ l e n − 4 ] − n u m s [ 0 ] nums[len-4]-nums[0] nums[len4]nums[0]
同理,如果想让第1个是最小值,首先改掉第0个数,现在还有2次机会,改掉最大的两个,结果是 n u m s [ l e n − 3 ] − n u m s [ 1 ] nums[len-3]-nums[1] nums[len3]nums[1]
接下来还会有 n u m s [ l e n − 2 ] − n u m s [ 2 ] nums[len-2]-nums[2] nums[len2]nums[2] n u m s [ l e n − 1 ] − n u m s [ 3 ] nums[len-1]-nums[3] nums[len1]nums[3]
那么如果我们想让nums[4]最小呢,很遗憾,做不到,因为三次机会只够修改3个数,而比nums[4]小的有4个,他永远不可能是最小的,以后的数也是一样,所以后面的情况是做不到的,只要考虑上述4种情况里最小的就够了。

4· 石子游戏 IV

Alice 和 Bob 两个人轮流玩一个游戏,Alice 先手。
一开始,有 n 个石子堆在一起。每个人轮流操作,正在操作的玩家可以从石子堆里拿走 任意 非零 平方数 个石子。
如果石子堆里没有石子了,则无法操作的玩家输掉游戏。
给你正整数 n ,且已知两个人都采取最优策略。如果 Alice 会赢得比赛,那么返回 True ,否则返回 False 。

示例 1:

输入:n = 1
输出:true
解释:Alice 拿走 1 个石子并赢得胜利,因为 Bob 无法进行任何操作。

示例 2:

输入:n = 2
输出:false
解释:Alice 只能拿走 1 个石子,然后 Bob 拿走最后一个石子并赢得胜利(2 -> 1 -> 0)。

解析

本题看起来就是一道动态规划,直接往这想就对了。
显然,如果有k个石子,第一个人取走了m个,如果按照最优解法,剩余k-m个是先手必胜,而此时Alice 取过了,轮到Bob,Bob是先手,所以此时是Bob胜利。
那么如果对于所有的第一步取法,Alice取完之后剩下的都是Bob胜,那Alice就没有胜算了,他第一次怎么拿都赢不了,那也意味着此时是一个先手必败的局;反之,如果存在一种情况,Alice先拿了m个,而k-m是先手必败,意味着此时Bob必败,那Alice照此法先拿m个,他一定赢,此时的k就是先手必胜局。
那对于输入的n,Alice先手有几种取法呢?题目规定只能拿非零完全平方,所以我们可以先求出一个完全平方数表sqmap。在建一个动规数组dp,数据类型为bool,dp[i]==true意味着i个石子先手必胜,dp[i]==false意味着i个石子先手必败。
首先dp[0]显然为false,没有石子拿就输了;dp[1]显然为true,Alice拿走1个就赢了。
接下来遵循以下状态转移方程:
d p [ i ] = t r u e , ∃ m ∈ s q m a p & & m ≤ i & & d p [ i − m ] = = f a l s e dp[i]=true,{\exist m \in sqmap}\quad \&\&\quad m\le i \quad\&\&\quad dp[i-m]==false dp[i]=true,msqmap&&mi&&dp[im]==false
d p [ i ] = f a l s e , ∀ m ∈ s q m a p & & m ≤ i & & d p [ i − m ] = = t r u e dp[i]=false,\forall m \in sqmap\quad\&\&\quad m\le i \quad\&\&\quad dp[i-m]==true dp[i]=false,msqmap&&mi&&dp[im]==true
也即:
对于总石子数i,我们在完全平方数表中遍历所有小于i的完全平方数m,代表Alice可能先拿的数量,此时剩下的i-m个石子,是Bob先手。如果存在这样的m使得dp[i-m]==false,也就是存在Alice拿走一定数目后先手(此时是Bob,因为Alice拿过一次了)必败的情况,那么聪明的Alice一定会这么拿导致Bob必输,此时意味着i个石子先手必胜,dp[i]是true。反之,如果所有可能的m都导致剩下的i-m先手(Bob)必胜,则i个石子Alice必输,dp[i]为false。
最后返回n个石子的情况即可。
因为不大于n的完全平方数有 n \sqrt{n} n 个,所以此解法时间复杂度 O ( N N ) O(N\sqrt N) O(NN )对于题设的 1 0 5 10^5 105的数据范围不会超时。
C++代码如下:

bool winnerSquareGame(int n) {
    vector<int>sqmap;
    for (int i = 1; i*i <= n; ++i) {
      sqmap.push_back(i*i);
    }
    vector<bool>dp(n + 1);
    dp[0] = false;
    dp[1] = true;
    for (int i = 2; i <= n; ++i) {
      int j = 0;
      bool a = false;
      while (j<sqmap.size() && sqmap[j] <= i) {
        int t = i - sqmap[j];
        if (dp[t] == false) {
          a = true;
          break;
        }
        ++j;
      }
      dp[i] = a;
    }
    return dp[n];
  }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值