20250902 Codeforces 1101C

Codeforces 1101C

题目链接:https://codeforces.com/problemset/problem/1101/C

题目描述

There are \(n\) segments \([l_i, r_i]\) for \(1 \le i \le n\). You should divide all segments into two non-empty groups in such way that there is no pair of segments from different groups which have at least one common point, or say that it's impossible to do it. Each segment should belong to exactly one group.

To optimize testing process you will be given multitest.

题目标签:1400排序


题目解析

要把所有的小区间分两组,要求两组的任两个区间无交集。第一反应是经典的“区间选点”/“区间合并”,但是这两道题其实是不同的思路;本题的类题应该是Leetcode 56. 合并区间

先说一下这道题目的做法。

首先,我们枚举所有的区间。可以想象一个口袋,我们往里不断装入不同的区间,装入的每一个区间都必须要能够与口袋里合成出来的最大的大区间合并。

举个例子:[1, 3], [2, 4], [5, 6],那么这个答案显然是1, 1, 2(或者反过来2, 1, 1)。为什么我们知道这么分?我们从前往后遍历所有区间,发现只要后一个区间的左端点在前一个区间的右端点前,这两个区间一定要在一个组。我们不妨直接把他合成为一个大区间[1, 4],然后装入口袋。

什么时候能够结束遍历?只要我们找到了第一个放不进去口袋的元素,我们就找到了答案——所有口袋里的是一组,剩下的区间另一组——如果没找到答案,那么就无解(说明所有的都可以加入口袋)。

但是这样还有一个小问题:我们的区间不一定是有序的。如果数组不是有序的,那么我们就有可能会错过本可能装入口袋的元素。

举个例子:[1, 3], [2, 4], [5, 6], [1, 1],这个样例导致我们错过了[1, 1]这个点,而错误地在[5, 6]划开了。

怎么避免这个问题?稍微想想,我们只需要让区间按照左端点从小到大排序就好了。这样思考的本质是贪心地选择所有可能装入口袋的元素

AC代码
#include <bits/stdc++.h>

using u32 = uint32_t;

using i64 = int64_t;
using u64 = uint64_t;

using i128 = __int128;
using u128 = unsigned __int128;

using namespace std;

#define F(i, a, b) for (int i = (a); i <= (b); i++)
#define Fd(i, a, b) for (int i = (a); i >= (b); i--)

#ifndef DEBUG
struct __X {
  __X& operator<<(const auto& str) { return *this; }
  void sp(const std::string& str = "") {}
} dout;
#define debug(x) ;
#endif

constexpr int mod = 998244353;
constexpr int MOD = 1E9 + 7;
constexpr int INF = 1E9;
constexpr int N = 2E5 + 10;

void sol() {
  int n;
  cin >> n;
  // 为了原序输出,需要存下标
  vector<tuple<int, int, int>> a(n);
  for (int i = 0; i < n; i ++) {
    auto& [l, r, idx] = a[i];
    cin >> l >> r;
    idx = i;
  }
  ranges::sort(a);

  int maxR = get<1>(a[0]);
  for (int i = 1; i < n; i ++) {
    auto& [l, r, idx] = a[i];
    // 先看能不能进行划分
    if (l > maxR) {
      vector<int> ans(n, 1);
      for (int j = 0; j < i; j ++) {
        auto& [_, __, index] = a[j];
        ans[index] = 2;
      }
      for (auto& v : ans) {
        cout << v << ' ';
      }
      cout << '\n';
      return;
    }
    // 如果不能,加入口袋,合成大区间
    maxR = max(maxR, r);
  }

  cout << -1 << '\n';
}

signed main() {
  std::ios::sync_with_stdio(false);
  std::cin.tie(nullptr);
  int T;
  cin >> T;
  while (T --) sol();
}

/* By Tangzy
⠄⠄⠄⠄⢠⣿⣿⣿⣿⣿⢻⣿⣿⣿⣿⣿⣿⣿⣿⣯⢻⣿⣿⣿⣿⣆⠄⠄⠄
⠄⠄⣼⢀⣿⣿⣿⣿⣏⡏⠄⠹⣿⣿⣿⣿⣿⣿⣿⣿⣧⢻⣿⣿⣿⣿⡆⠄⠄
⠄⠄⡟⣼⣿⣿⣿⣿⣿⠄⠄⠄⠈⠻⣿⣿⣿⣿⣿⣿⣿⣇⢻⣿⣿⣿⣿⠄⠄
⠄⢰⠃⣿⣿⠿⣿⣿⣿⠄⠄⠄⠄⠄⠄⠙⠿⣿⣿⣿⣿⣿⠄⢿⣿⣿⣿⡄⠄
⠄⢸⢠⣿⣿⣧⡙⣿⣿⡆⠄⠄⠄⠄⠄⠄⠄⠈⠛⢿⣿⣿⡇⠸⣿⡿⣸⡇⠄
⠄⠈⡆⣿⣿⣿⣿⣦⡙⠳⠄⠄⠄⠄⠄⠄⢀⣠⣤⣀⣈⠙⠃⠄⠿⢇⣿⡇⠄
⠄⠄⡇⢿⣿⣿⣿⣿⡇⠄⠄⠄⠄⠄⣠⣶⣿⣿⣿⣿⣿⣿⣷⣆⡀⣼⣿⡇⠄
⠄⠄⢹⡘⣿⣿⣿⢿⣷⡀⠄⢀⣴⣾⣟⠉⠉⠉⠉⣽⣿⣿⣿⣿⠇⢹⣿⠃⠄
⠄⠄⠄⢷⡘⢿⣿⣎⢻⣷⠰⣿⣿⣿⣿⣦⣀⣀⣴⣿⣿⣿⠟⢫⡾⢸⡟⠄.
⠄⠄⠄⠄⠻⣦⡙⠿⣧⠙⢷⠙⠻⠿⢿⡿⠿⠿⠛⠋⠉⠄⠂⠘⠁⠞⠄⠄⠄
⠄⠄⠄⠄⠄⠈⠙⠑⣠⣤⣴⡖⠄⠿⣋⣉⣉⡁⠄⢾⣦⠄⠄⠄⠄⠄⠄⠄⠄
*/

关于为什么要进行排序,这里引用一个解释。

两个区间 [a, b] 和 [c, d] 重叠的完整数学条件是:
\(a ≤ d\)(第一个区间的起点 ≤ 第二个区间的终点)
\(c ≤ b\)(第二个区间的起点 ≤ 第一个区间的终点)
而排序的核心作用就是通过调整区间顺序,消除对 \(a ≤ d\) 的显式判断,使得我们只需关注 \(c ≤ b\) 这一个条件。

这样,我们就避免了很繁琐的条件判断,降低了时间复杂度。

顺推的思路如下:

如果是暴力,那么我去枚举所有的区间,然后再去枚举剩下的区间,看能否合并成一个更大的区间。如果找到了,说明我们可以继续找,枚举剩下的区间,看能否合并;反之,说明一开始枚举的区间是不行的,那么我们就下一个。这个可以用两个for循环搞定,时间复杂度是\(O(n^3)\)的。这显然是不能够接受的。

所以我们考虑优化时间复杂度。注意到区间合并的条件较为繁琐,所以我们设法优化。因为选择与顺序无关,那么就可以考虑排序了。至于左端点排还是右端点排,都跑跑然后手玩一下就行了。

posted @ 2025-09-02 19:51  Tangzy0121  阅读(13)  评论(0)    收藏  举报