You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

求无法获胜的玩家数:O(n²)解法超时后的最优解决方案

如何高效统计无法获胜的玩家数量(n≤1e5)

问题回顾

我们有n名玩家,每名玩家拥有A、B、C三个属性值。如果存在另一名玩家j,满足A[j] > A[i]B[j] > B[i]C[j] > C[i],那么玩家i无法获胜。需要统计所有无法获胜的玩家数量。

暴力解法的局限性

最直接的思路是暴力遍历每个玩家,检查是否存在满足条件的玩家j,代码如下:

int count_players_cannot_win(vector<vector<int>> values) {
    int c = 0;
    int n = values.size();
    for(int i = 0; i < n; i++) {
        for(int j = 0; j!= i && j < n; j++) {
            if(values[i][0] < values[j][0] && values[i][1] < values[j][1] && values[i][2] < values[j][2]) {
                c += 1;
                break;
            }
        }
    }
    return c;
}

但这个解法的时间复杂度是O(n²),当n≤1e5时,1e10级别的操作次数会直接导致超时(TLE),必须用更高效的方法。

最优解法:线段树优化(O(nlogn))

这个问题属于三维偏序问题的简化版,我们可以通过排序+线段树的组合将时间复杂度降到O(nlogn),核心思路如下:

1. 排序预处理

首先对玩家按A属性降序排序,如果A属性相同,则按B属性升序排序。这样做的好处是:

  • 当处理到某个玩家时,已经处理过的所有玩家的A属性都≥当前玩家的A属性。
  • 同A属性的玩家按B升序排序,确保同A的玩家不会互相干扰(因为A相同,不可能满足A[j]>A[i])。

2. B属性的坐标压缩

由于B属性的取值范围可能很大(比如到1e9),直接用B值作为线段树的下标不现实,所以我们需要对B属性做离散化

  • 收集所有玩家的B属性值并排序。
  • 对于每个玩家的B值,用lower_bound找到它在排序后数组中的位置,将B的范围压缩到0~n-1,这样线段树的大小就可控了。

3. 线段树的作用

线段树用来维护当前已处理玩家中,B属性大于某个值的所有玩家的最大C属性值。对于每个玩家,我们只需要查询线段树中B[i]对应位置之后的最大C值:

  • 如果这个最大值大于当前玩家的C值,说明存在一个已处理的玩家(A≥当前,B>当前,C>当前),也就是存在j满足A[j]>A[i]B[j]>B[i]C[j]>C[i],当前玩家无法获胜。
  • 查询完成后,再将当前玩家的C值更新到线段树对应的B位置(保留最大值,因为相同B值的玩家中,最大的C值对后续查询最有意义)。

4. 批量处理同A的玩家

为了避免同A属性的玩家互相影响,我们会批量处理所有A属性相同的玩家:先统一查询这些玩家是否无法获胜,再统一将他们的C值更新到线段树中。这样同A的玩家不会被计入彼此的判断条件。

完整代码实现

#include <vector>
#include <algorithm>
#include <climits>
#include <iostream>
using namespace std;

int n;
const int N = 4e5 + 1;
int tree[N];

// 查询[L, n-1]区间内的最大C值
int get_max(int i, int l, int r, int L) {
    if(r < L || n <= l) return INT_MIN;
    else if(L <= l) return tree[i];
    int m = (l + r)/2;
    return max(get_max(2*i+1, l, m, L), get_max(2*i+2, m+1, r, L));
}

// 更新线段树中pos位置的最大值为v
void update(int i, int l, int r, int pos, int v) {
    if(r < pos || pos < l) return;
    else if(l == r) {
        tree[i] = max(tree[i], v);
        return;
    }
    int m = (l + r)/2;
    update(2*i+1, l, m, pos, v);
    update(2*i+2, m + 1, r, pos, v);
    tree[i] = max(tree[2*i+1], tree[2*i+2]);
}

// 排序比较函数:A降序,A相同则B升序
bool comp(vector<int> a, vector<int> b) {
    return a[0] != b[0] ? a[0] > b[0] : a[1] < b[1];
}

int solve(vector<vector<int>> &v) {
    n = v.size();
    vector<int> b(n);
    // 收集所有B属性用于离散化
    for(int i = 0; i < n; i++) {
        b[i] = v[i][1];
    }
    sort(b.begin(), b.end());
    
    // 按A降序、B升序排序玩家
    sort(v.begin(), v.end(), comp);
    
    int ans = 0;
    for(int i = 0; i < n;) {
        int j = i;
        // 批量查询所有A相同的玩家
        while(j < n && v[j][0] == v[i][0]) {
            int B_val = v[j][1];
            // 找到B_val离散化后的位置
            int pos = lower_bound(b.begin(), b.end(), B_val) - b.begin();
            // 查询B大于当前B_val的最大C值
            int max_c = get_max(0, 0, n - 1, pos + 1);
            if(max_c > v[j][2]) {
                ans++;
            }
            j++;
        }
        // 批量更新线段树,加入所有A相同的玩家
        while(i < j) {
            int B_val = v[i][1];
            int C_val = v[i][2];
            int pos = lower_bound(b.begin(), b.end(), B_val) - b.begin();
            update(0, 0, n - 1, pos, C_val);
            i++;
        }
    }
    return ans;
}

样例验证

拿题目中的样例输入来看:

3
1 4 2
4 3 2
2 5 3

排序后的玩家顺序是:

  1. (4,3,2) → A最大
  2. (2,5,3) → A次之
  3. (1,4,2) → A最小

处理过程:

  • 先处理A=4的玩家:此时线段树为空,查询结果为INT_MIN,不计数;然后将B=3(离散化位置0)的C=2更新到线段树。
  • 处理A=2的玩家:查询B>5的位置(离散化后B=5在排序后的b数组中是最后一位,pos+1=3,超出范围,查询结果INT_MIN),不计数;然后将B=5(位置2)的C=3更新到线段树。
  • 处理A=1的玩家:查询B>4的位置(pos是1,查询[1,2]区间的最大C,结果是3),3>2,所以计数+1;然后更新线段树。

最终答案是1,和样例输出一致。

内容的提问来源于stack exchange,提问作者tusharRawat

火山引擎 最新活动