第218场LeetCode周赛

本文介绍了四道LeetCode周赛题目,包括设计字符串Goal解析器、求解最大数对操作次数、连接连续二进制数字以及最小不兼容性问题。涉及字符串处理、排序、动态规划等算法,详细解析了每道题目的解题思路和C++实现。
摘要由CSDN通过智能技术生成

第218场LeetCode周赛

No 1. 设计 Goal 解析器

请你设计一个可以解释字符串 command 的 Goal 解析器 。command 由 “G”、"()" 和/或 “(al)” 按某种顺序组成。Goal 解析器会将 “G” 解释为字符串 “G”、"()" 解释为字符串 “o” ,"(al)" 解释为字符串 “al” 。然后,按原顺序将经解释得到的字符串连接成一个字符串。

给你字符串 command ,返回 Goal 解析器 对 command 的解释结果。

示例 1:
输入:command = “G()(al)”
输出:“Goal”
解释:Goal 解析器解释命令的步骤如下所示:
G -> G
() -> o
(al) -> al
最后连接得到的结果是 “Goal”

提示:
1 <= command.length <= 100
command 由 “G”、"()" 和/或 “(al)” 按某种顺序组成

解析

本题例行签到题,字符串一定是合法的,也即要么是单独的G,要么是空括号对,要么是(al),所以只要顺序遍历字符串command,遇到G保持,遇到左括号(,判断一下他右边是右括号还是字母a,转换成不同的字符。
C++代码如下:

//No 1
  string interpret(string command) {
    string ans;
    int n = command.size(),i=0;
    while (i < n) {
      if (command[i] == 'G') {
        ans += 'G';
        ++i;
      }
      else {
        if (command[i] == '(' && command[i + 1] == ')') {
          ans += 'o';
          i += 2;
        }
        else {
          ans += "al";
          i += 4;
        }
      }
    }
    return ans;
  }

No 2. K 和数对的最大数目

给你一个整数数组 nums 和一个整数 k 。

每一步操作中,你需要从数组中选出和为 k 的两个整数,并将它们移出数组。

返回你可以对数组执行的最大操作数。

示例 1:
输入:nums = [1,2,3,4], k = 5
输出:2
解释:开始时 nums = [1,2,3,4]:

  • 移出 1 和 4 ,之后 nums = [2,3]
  • 移出 2 和 3 ,之后 nums = []
    不再有和为 5 的数对,因此最多执行 2 次操作。

示例 2:
输入:nums = [3,1,3,4,3], k = 6
输出:1
解释:开始时 nums = [3,1,3,4,3]:

  • 移出前两个 3 ,之后nums = [1,4,3]
    不再有和为 6 的数对,因此最多执行 1 次操作。

提示:

1 < = n u m s . l e n g t h < = 1 0 5 1 <= nums.length <= 10^5 1<=nums.length<=105
1 < = n u m s [ i ] < = 1 0 9 1 <= nums[i] <= 10^9 1<=nums[i]<=109
1 < = k < = 1 0 9 1 <= k <= 10^9 1<=k<=109

解析

本题也比较简单,其实就是类似two sum这题,只不过这里每个元素只能使用一次(不是数值而是元素),所以其实只要排序+双指针就可以了。数组长度 1 0 5 10^5 105也不会超时。

//No 2
  int maxOperations(vector<int>& nums, int k) {
    sort(nums.begin(), nums.end());
    int n = nums.size(), i = 0, j = n - 1,ans=0;
    while (i < j) {
      if (nums[i] + nums[j] == k) {
        ++ans;
        ++i;
        --j;
      }
      else if (nums[i] + nums[j] < k) {
        ++i;
      }
      else --j;
    }
    return ans;
  }

No 3. 连接连续二进制数字

给你一个整数 n ,请你将 1 到 n 的二进制表示连接起来,并返回连接结果对应的 十进制 数字对 10^9 + 7 取余的结果。

示例 1:
输入:n = 1
输出:1
解释:二进制的 “1” 对应着十进制的 1 。

示例 2:
输入:n = 3
输出:27
解释:二进制下,1,2 和 3 分别对应 “1” ,“10” 和 “11” 。
将它们依次连接,我们得到 “11011” ,对应着十进制的 27 。

提示:

1 <= n <= 10^5

解析

本题要求我们将1至n的数字的二进制连接起来,再转化为十进制,并输出这个数对1e9+7的余数。实际上本题并不是把每个数字(32位)全部二进制表示都拼接,看样例可以知道,先导0是不考虑的,也即数字3只拼接11,5只考虑101这样的。

二进制的拼接,实际上等价于移位再相加。例如将数字a和b的二进制数字拼接起来,实际上相当于a左移t位,再加b,这个t就是数字b去掉先导0,也就是最左边1所在的位。而左移,显然又对应了十进制下乘 2 t 2^t 2t运算。

总结来说,如果我们要知道1到i进行二进制拼接后十进制数 f [ i ] f[i] f[i]是多少,首先我们将这一问题分解为在已知1到i-1结果 f [ i − 1 ] f[i-1] f[i1],然后在此基础上:
求解数字i有几位(从最左边1开始): t = l o g 2 i + 1 = l o g 10 i / l o g 10 2 + 1 t=log_{2}i+1=log_{10}i/log_{10}2+1 t=log2i+1=log10i/log102+1
然后计算: f [ i ] = f [ i − 1 ] ∗ 2 t + i f[i]=f[i-1]*2^t+i f[i]=f[i1]2t+i
显然这是一个动态规划的过程。接下来就是一般动规的步骤了。
这里要注意,因为C++没有2为底的对数函数,就采用10为底的对数加换底公式计算;动规计算过程要每一步都取余防止溢出。
C++代码如下。

 //No 3
  int concatenatedBinary(int n) {
    if (n == 1) return 1;
    int M = 1e9 + 7;
    vector<long long>dp(n + 1, 0);
    dp[1] = 1;
    for (int i = 2; i <= n; ++i) {
      int t = (int)(log10((double)i) / log10(2.0)) + 1;
      dp[i] = ((dp[i - 1] * (long long)pow(2,t)) % M + i) % M;
    }
    return dp[n];
  }

No 4. 最小不兼容性

给你一个整数数组 nums​​​ 和一个整数 k 。你需要将这个数组划分到 k 个相同大小的子集中,使得同一个子集里面没有两个相同的元素。

一个子集的 不兼容性 是该子集里面最大值和最小值的差。

请你返回将数组分成 k 个子集后,各子集 不兼容性 的 和 的 最小值 ,如果无法分成分成 k 个子集,返回 -1 。

子集的定义是数组中一些数字的集合,对数字顺序没有要求。

示例 1:
输入:nums = [1,2,1,4], k = 2
输出:4
解释:最优的分配是 [1,2] 和 [1,4] 。
不兼容性和为 (2-1) + (4-1) = 4 。
注意到 [1,1] 和 [2,4] 可以得到更小的和,但是第一个集合有 2 个相同的元素,所以不可行。

示例 2:
输入:nums = [6,3,8,1,3,1,2,2], k = 4
输出:6
解释:最优的子集分配为 [1,2],[2,3],[6,8] 和 [1,3] 。
不兼容性和为 (2-1) + (3-2) + (8-6) + (3-1) = 6 。

示例 3:
输入:nums = [5,3,3,6,3,3], k = 3
输出:-1
解释:没办法将这些数字分配到 3 个子集且满足每个子集里没有相同数字。

提示:
1 <= k <= nums.length <= 16
nums.length 能被 k 整除。
1 <= nums[i] <= nums.length

解析

本题给定一个数组和数字k,要求我们将数组分为k组,每组元素数量相同,且不含相同元素(k个子集),同时定义每个子集中最大最小元素的差叫做不兼容性。如果能够划分,就返回能做到的最小的不兼容性和,否则返回-1。

本题我一开始没有太好的思路,想了一个贪心+搜索的憨憨方法,我甚至不能证明这个贪心是对的,然而就过了,我服了。简单说一下吧,真要做题还得采取正统状压DP的路子。
这个憨憨方法是这样的。
首先统计数组元素的值与其个数,放入一个数组,数组存储{value, num}数对,然后按照value大小排序。
下面把分割子集的过程想象成如下的流程,首先分为k组,我们假设每组有ng个不同元素。接下来先划分第一组,我们考虑使不兼容性最小的方法,那就是要么选最小的几个不同数,要么选最大的几个。我处于以下的原因这么考虑:

  1. 最小值无论分到哪个子集都是最小的,要想使不兼容性更小就要在这个子集中尽可能放比较小的数。最大值也是如此。
  2. 如果我们从靠中间部分取数字,就会导致后续的子集能选的数字的数值差异进一步扩大。

出于这个直觉,每次都分两种情况,要么从小到大选不同的元素构成一个子集,要么从大到小选。然后递归去求解剩下的元素构造k-1个子集得到的最小不兼容性和。搜索过程中每一步判断有没有足够的不同元素,如果没有就说明这条路分下去会导致某个子集元素重复,所以直接返回-1即可。由于元素数量最大16,每划分一次将产生2条路,最后得到最多 2 16 2^{16} 216种情况,不会超时。

  //No 4
  int dfs(int k, int ng, vector<pair<int, int>>vm) {
    if (k == 1) {
      for (auto p : vm) {
        if (p.second > 1) return -1;
      }
      int ans = vm.back().first - vm[0].first;
      return ans;
    }
    else {
      if (vm.size() < ng) return -1;
      //front
      int ans1 = vm[ng-1].first-vm[0].first;
      vector<pair<int, int>>tfront = vm;
      for (int i = 0; i < ng; ++i) {
        tfront[i].second--;
      }
      for (auto it = tfront.begin(); it != tfront.end();) {
        if (it->second == 0) {
          it = tfront.erase(it);
        }
        else ++it;
      }
      int t1 = dfs(k - 1, ng, tfront);
      if (t1 == -1) ans1 = -1;
      else ans1 += t1;

      //back
      int ans2 = vm[vm.size()-1].first - vm[vm.size()-ng].first;
      vector<pair<int, int>>tfback = vm;
      for (int i = tfback.size()-1; i > tfback.size()-1-ng; --i) {
        tfback[i].second--;
      }
      for (auto it = tfback.begin(); it != tfback.end();) {
        if (it->second == 0) {
          it = tfback.erase(it);
        }
        else ++it;
      }
      int t2 = dfs(k - 1, ng, tfback);
      if (t2 == -1) ans2 = -1;
      else ans2 += t2;
      if (ans1 == -1 && ans2 == -1) return -1;
      else if (ans1 == -1) return ans2;
      else if (ans2 == -1) return ans1;
      else return min(ans1, ans2);
    }
  }
  int minimumIncompatibility(vector<int>& nums, int k) {
    map<int, int>m;
    int len = nums.size(),ng=len/k;
    vector<int>v(len+1, 0);
    vector<pair<int, int>>vm;
    for (auto n : nums) {
      m[n]++;
      v[n]++;
    }
    int maxN = 0;
    for (auto it = m.begin(); it != m.end(); ++it) {
      maxN = max(maxN, it->second);
      vm.push_back(*it);
    }
    if (maxN > k) return -1;
    sort(vm.begin(), vm.end(), [](pair<int, int>& p1, pair<int, int>& p2) {return p1.first < p2.first; });
    int ans = dfs(k, ng, vm);
    return ans;
  }

上述这个憨憨做法只是通过了本题的实例,但我不能保证理论上是对的,也不知道能不能找到反例,大家看个乐就好。

下面介绍一下正经做本题的思路吧。状态压缩动态规划。
这个可以参考大佬的题解
这种方法使用二进制方式表示数组中的数字是否选过,以及表示待选择的子集。当我们选择mask数据组成若干子集的最小不兼容性和就等于,除掉当前选入的某个子集剩下的最小和,加上这个子集的不兼容性。也就是对于当前要选择的元素,遍历所有可能的候选子集,选取一种不兼容性和最小的作为达到当前元素选择的最小取值。

这里标记候选、子集等都采用二进制位操作进行。具体可查看上边大佬的题解。虽然靠直觉蒙了一种解法,而且感觉这个递归能改动规,不过还是太菜了,先去消化一下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值