[国家集训队] 小 Z 的袜子

GitHub跳转原题关系图返回列表

用莫队维护当前区间内同色袜子对数量,再与总二元组数量约分得到概率。

OJ: luogu

题目 ID: P1494

难度:普及+/提高

标签:莫队离线数据结构

日期: 2026-06-22 23:14

题意

给定一排袜子的颜色。每个询问 [l,r] 中随机选两只袜子,求两只颜色相同的概率,输出约分后的分数。

思路

如果已知区间内每种颜色的出现次数 cnt[c],那么同色选择方案数是:

text
sum cnt[c] * (cnt[c] - 1) / 2

总选择方案数是:

text
len * (len - 1) / 2

朴素做法是每个询问重新扫描区间统计颜色。

先看一个可以直接验证想法的朴素解:

cpp
#include <bits/stdc++.h>
using namespace std;

// brute.cpp:每个询问直接统计颜色次数并计算概率,只适合小数据。

const int MAXN = 505;

int n, m;
int color[MAXN];
long long cnt[MAXN];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> color[i];
    }
    for (int i = 1; i <= m; i++) {
        int l, r;
        cin >> l >> r;
        for (int c = 0; c <= n; c++) {
            cnt[c] = 0;
        }
        for (int j = l; j <= r; j++) {
            cnt[color[j]]++;
        }
        long long same = 0;
        long long len = r - l + 1;
        for (int c = 1; c <= n; c++) {
            same += cnt[c] * (cnt[c] - 1) / 2;
        }
        if (len == 1) {
            cout << "0/1\n";
        } else {
            long long total = len * (len - 1) / 2;
            long long g = gcd(same, total);
            cout << same / g << '/' << total / g << '\n';
        }
    }

    return 0;
}

要处理很多区间询问,可以用莫队。维护当前区间 [cur_l, cur_r],以及当前区间内同色袜子对数量 same_pair_count

移动端点时只会加入或删除一只袜子:

  • 加入颜色 c:它能和已有 cnt[c] 只同色袜子组成新同色对,所以 same_pair_count += cnt[c]
  • 删除颜色 c:删除后剩余 cnt[c] 只同色袜子,少掉这些配对,所以 same_pair_count -= cnt[c]

莫队排序后,左右端点移动总量较小。每个询问调整完区间后,用 same_pair_count / C(len,2) 约分输出即可。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 50005;

struct Query {
    int l, r, id;
};

int n, m;
int block_size;
int color[MAXN];
long long color_count[MAXN]; // 当前区间内每种颜色出现次数。
long long same_pair_count;   // 当前区间内同色无序对数量之和。
long long answer_num[MAXN], answer_den[MAXN];
Query queries[MAXN];

bool cmp_query(const Query &a, const Query &b) {
    int block_a = a.l / block_size;
    int block_b = b.l / block_size;
    if (block_a != block_b) {
        return block_a < block_b;
    }
    if (block_a & 1) {
        return a.r > b.r;
    }
    return a.r < b.r;
}

void add_pos(int pos) {
    int c = color[pos];
    // 新加入一个颜色 c,会和已有 color_count[c] 只同色袜子配成同色对。
    same_pair_count += color_count[c];
    color_count[c]++;
}

void remove_pos(int pos) {
    int c = color[pos];
    color_count[c]--;
    same_pair_count -= color_count[c];
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> color[i];
    }
    for (int i = 1; i <= m; i++) {
        cin >> queries[i].l >> queries[i].r;
        queries[i].id = i;
    }

    block_size = max(1, (int)sqrt(n));
    sort(queries + 1, queries + m + 1, cmp_query);

    int cur_l = 1, cur_r = 0;
    for (int i = 1; i <= m; i++) {
        int l = queries[i].l;
        int r = queries[i].r;
        while (cur_l > l) {
            cur_l--;
            add_pos(cur_l);
        }
        while (cur_r < r) {
            cur_r++;
            add_pos(cur_r);
        }
        while (cur_l < l) {
            remove_pos(cur_l);
            cur_l++;
        }
        while (cur_r > r) {
            remove_pos(cur_r);
            cur_r--;
        }

        long long len = r - l + 1;
        if (len == 1) {
            answer_num[queries[i].id] = 0;
            answer_den[queries[i].id] = 1;
        } else {
            long long total_pair = len * (len - 1) / 2;
            long long g = gcd(same_pair_count, total_pair);
            answer_num[queries[i].id] = same_pair_count / g;
            answer_den[queries[i].id] = total_pair / g;
        }
    }

    for (int i = 1; i <= m; i++) {
        cout << answer_num[i] << '/' << answer_den[i] << '\n';
    }

    return 0;
}

复杂度

时间复杂度约为 O((n+m)n)O((n+m)\sqrt{n})

空间复杂度 O(n+m)O(n+m)

总结

莫队题的关键是设计好“加入一个点”和“删除一个点”对答案的影响。本题维护的是同色二元组数量,而不是直接维护概率。