LeetCode 力扣双周赛 63

143 阅读2分钟

周末最快乐的事,就是补题和写题解给老婆做饭。

周赛传送门

2037. 使每位学生都有座位的最少移动次数

思路:贪心

时间复杂度O(nlgn)O(n*\lg n)

一种贪心策略是:在未匹配的学生和座位中,总是让最左边的学生选择最左边的座位。

尝试证明一下。试想最左边的学生 AA 选择了较右的一个座位 rr,则必然有另一个学生 BB 选择最左边的座位 ll

设四者的位置分别为 PAP_APrP_rPBP_BPlP_l。则上述匹配方案的移动次数为 PAPr+PBPl|P_A-P_r| + |P_B-P_l|

根据定义易得:

  • PAPBP_A \le P_B
  • PlPrP_l \le P_r

因此交换 AABB 的座位后的移动次数 PAPl+PBPr|P_A-P_l| + |P_B-P_r| 肯定不会超过前者。因此,一定存在一种「最左边学生选择了最左边座位」的匹配方案。

当我们把最左边的学生和座位移除后,剩下的 n1n-1 个学生和座位组成一个新的子问题,而且上述贪心策略同样适用~

class Solution {
public:
    int minMovesToSeat(vector<int>& seats, vector<int>& students) {
        sort(seats.begin(), seats.end());
        sort(students.begin(), students.end());

        int anw = 0;
        for (int i = 0; i < seats.size(); i++) {
            anw += abs(seats[i] - students[i]);
        }

        return anw;
    }
};

2038. 如果相邻两个颜色均相同则删除当前颜色

思路:比较可删除字符的数量

时间复杂度O(n)O(n)

根据游戏规则可知,每次操作后可删除字符只会减少一个,不会有其他变化。因此直接统计可删除的 'A' 和 'B' 的数量即可。当且仅当前者大于后者时 Alice 才能胜出。

下面的实现中,每个位置最多需要三次比较,老板们还有更简练的实现吗?

class Solution {
public:
    bool winnerOfGame(string colors) {
        int A = 0, B = 0;
        for (int i = 2; i < colors.size(); i++) {
            // 判断连续的三个字符是否相同,相同则意味着中间字符可被删除。
            if (colors[i] == colors[i-1] && colors[i] == colors[i-2]) {
                (colors[i] == 'A') ? ++A : ++B;
            }
        }
        return A > B;
    }
};

2039. 网络空闲的时刻

思路:BFS求最短路

时间复杂度O(n+e)O(n+e)

先用 BFS 求出节点 00 到其他所有节点的最短路。节点 ii 的往返时间记为 TiT_i。结合题意,易得节点 ii 最后一次发送消息的时间点为:

lasti=Ti1patienceipatienceilast_i = \frac{T_i - 1}{patience_i}*patience_i

因此最终答案为:

maxi=1n(lasti+Ti)\max_{i=1}^{n}(last_i + T_i)
class Solution {
public:
    vector<int> edge[100001];
    int len[100001];
    void bfs() {
        queue<int> q;
        q.push(0);
        memset(len, -1, sizeof(len));
        len[0] = 0;
        while(!q.empty()) {
            auto f = q.front();
            q.pop();
            for (auto next : edge[f]) {
                if (len[next] == -1) {
                    len[next] = len[f]+1;
                    q.push(next);
                }
            }
        }
    }
    int networkBecomesIdle(vector<vector<int>>& edges, vector<int>& patience) {
        // 构建边表
        for (const auto &e : edges) {
            edge[e[0]].emplace_back(e[1]);
            edge[e[1]].emplace_back(e[0]);   
        }
        // 求解 0 节点到其他节点的最短路
        bfs();
        
        // 找到最大的 $T[i] + last[i]$
        int anw = 0;
        for (int i = 1; i < patience.size(); i++) {
            int T = len[i]*2;
            T += (T-1)/patience[i]*patience[i];
            anw = max(anw, T);
        }
        return anw+1;
    }
};

2040. 两个有序数组的第 K 小乘积

思路:二分

时间复杂度O(nlg(m+n))O(n*\lg (m+n))nn 为数组长度,mm 为乘积的取值范围的大小。

设有整数 xx,如果小于 xx 的乘积小于 kk,则说明答案不小于 xx,反之答案小于 xx

显然,可以通过二分 xx 的值在 O(lgm)O(\lg m) 的时间复杂度下找到答案。

最后一个问题,如何确定小于 xx 的乘积的数量呢?可先将 nums2nums2 排序,然后枚举 nums1nums1 中的元素,不难发现对于确定的 nums1inums1_inums1inumsjnums1_i*nums_j 是关于 jj 单调变化的。因此可以借助二分快速定位满足 nums1inumsj<xnums1_i*nums_j\lt xjj的范围。因此,该过程的时间复杂度为 O(nlgn)O(n*\lg n)

综上,可通过两次二分在 O(nlg(m+n))O(n*\lg (m+n)) 的时间复杂度下求得答案。

class Solution {
  public:
    int64_t cal(std::vector<int> &pos, std::vector<int> &neg, std::vector<int> &B, int64_t goal) {
      // 正数时,乘积是关于 j 单调递增的
      int64_t cnt = 0;
      for (int i = 0 ; i < pos.size(); i++) {
        int l = 0, r = B.size()-1;
        while (l <= r) {
          int mid = (l+r)>>1;
          if (pos[i]*1L*B[mid] >= goal) {
            r = mid-1;
          } else {
            l = mid+1;
          }
        }
        cnt += l;
      }

      // 负数时,乘积是关于 j 单调递减的
      for (int i = 0, j = 0; i < neg.size(); i++) {
        int l = 0, r = B.size()-1;
        while (l <= r) {
          int mid = (l+r)>>1;
          if (neg[i]*1L*B[mid] >= goal) {
            l = mid+1;
          } else {
            r = mid-1;
          }
        }
        cnt += (B.size()-r-1);
      }

      return cnt;
    }

    long long kthSmallestProduct(vector<int>& A, vector<int>& B, long long k) {
      // 先把正负数区分开,便于处理
      vector<int> pos, neg;
      for (auto a : A) {
        if (a < 0) { neg.emplace_back(a); }
        else if (a > 0) { pos.emplace_back(a); }
      }

      // l 和 r 为第一层二分的边界
      int64_t l = -1 * 1e11;
      int64_t r = 1e11;
      while (l <= r) {
        int64_t x = (l+r)>>1;
        // 统计小于 x 的乘积的数量
        int64_t cnt = cal(pos, neg, B, x);

        // 注意,cal 中未统计 A[i] == 0 的情况,在这里补上。
        if (0 < x) {
          cnt += (A.size() - pos.size() - neg.size())*B.size();
        }
        // cnt < k,答案可能在 [x+1, r] 中,也可能是 x。
        if (cnt < k) {
          l = x+1;
        } else {
          // 答案一定在 [l, x-1] 中。
          r = x-1;
        }
      }
      // 不难发现,最后一次移动 l 时,x 即为答案,因此 l-1 就是答案。
      return l-1;
    }
};