Luogu P8250 交友问题 题解 [ 蓝 ] [ 根号分治 ] [ Bitset ] [ 复杂度均摊 ]

交友问题:很有趣的一道根号分治。

首先对于这种一看就很能按度数暴力枚举的题,想到“利用点的度数进行根号分治”的 trick 是容易的。于是可以先求出每个点的度数,设阈值为 \(B\),有两种做法:

Sol.1 Bitset 做法

因为本题不是 recall,所以开 \(2\times 10^5\) 个 Bitset 显然是不行的。因此我们只对超过阈值的开 Bitset,不超过阈值的就暴力枚举。容易发现一共最多需要开 \(\min(n, \dfrac{2m}{B})\) 个 Bitset。

考虑对两点超过 / 未超过阈值进行分类讨论

  • \(deg_u, deg_v \ge B\)
    • 此时可以直接利用 \(u,v\) 预处理的 Bitset 求答案,将 \(v\) 的 Bitset 取反后和 \(u\) 的 Bitset 按位与,然后利用 count 函数即可。时间复杂度 \(O(\dfrac{n}{w})\)
  • \(deg_u < B, deg_v \ge B\)
    • 显然可以直接暴力枚举 \(u\) 的邻域,判断邻域是否为 \(v\) 的邻域即可。时间复杂度 \(O(B)\)
  • \(deg_v < B, deg_u \ge B\)
    • 显然也是枚举 \(v\) 的邻域,判断一下,用 \(u\) 的度数减掉这个值即可。时间复杂度 \(O(B)\)
  • \(deg_u, deg_v < B\)
    • 暴力枚举 \(u,v\) 的邻域,判个交即可。时间复杂度 \(O(B)\)

复杂度是 \(O(m\min(n, \dfrac{2m}{B})+q(\dfrac{n}{w} + B))\) 的。平衡一下发现 \(B\)\(\sqrt{2m}\) 是比较优秀的。总体时间复杂度 \(O((m+q)\sqrt{m} + \dfrac{qn}{w})\)

#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi = pair<int, int>;
const int N = 200005, M = 700005, B = 1200;
int n, m, q, deg[N], id[N], cnt;
int h[N], idx;
struct Edge{
    int v, ne;
}e[2 * M];
void add(int u, int v)
{
    e[++idx] = {v, h[u]};
    h[u] = idx;
}
bitset<N> vis[B + 10], tmp;
int main()
{
    //freopen("sample.in", "r", stdin);
    //freopen("sample.out", "w", stdout);
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin >> n >> m >> q;
    for(int i = 1; i <= m; i++)
    {
        int u, v;
        cin >> u >> v;
        add(u, v);
        add(v, u);
        deg[u]++; deg[v]++;
    }
    for(int i = 1; i <= n; i++)
    {
        if(deg[i] >= B)
        {
            id[i] = ++cnt;
            for(int j = h[i]; j ; j = e[j].ne)
            {
                int v = e[j].v;
                vis[cnt][v] = 1;
            }
            vis[cnt][i] = 1;
        }
    }
    while(q--)
    {
        int x, y;
        cin >> x >> y;
        if(id[x] && id[y])
        {
            int ans = ((~vis[id[y]]) & (vis[id[x]])).count();
            if(vis[id[y]][x] == 0) ans--;
            cout << ans << '\n';
        }
        else if(id[x])
        {
            int ans = 0;
            for(int i = h[y]; i ; i = e[i].ne)
            {
                int v = e[i].v;
                ans += vis[id[x]][v];
            }
            cout << deg[x] - ans << '\n';
        }
        else if(id[y])
        {
            int ans = 0;
            for(int i = h[x]; i ; i = e[i].ne)
            {
                int v = e[i].v;
                ans += (vis[id[y]][v] ^ 1);
            }
            cout << ans << '\n';
        }
        else
        {
            int ans = 0;
            tmp[y] = 1;
            for(int i = h[y]; i ; i = e[i].ne)
            {
                int v = e[i].v;
                tmp[v] = 1;
            }
            for(int i = h[x]; i ; i = e[i].ne)
            {
                int v = e[i].v;
                ans += (tmp[v] ^ 1);
            }            
            cout << ans << '\n';
            for(int i = h[y]; i ; i = e[i].ne)
            {
                int v = e[i].v;
                tmp[v] = 0;
            }
            tmp[y] = 0;
        }
    }
    return 0;
}

Sol.2 边数均摊做法

口胡一下,没有写代码。

注意到 \(deg\ge B\) 的点最多有 \(\dfrac{2m}{B}\) 个,所以对这些度数大的点进行预处理。具体地,我们可以遍历整张图,固定下这个点作为 \(u,v\) 后枚举 \(v,u\),然后暴力计算答案。因为每条边最多遍历两次,所以均摊复杂度是 \(O(m)\) 的。该部分总体复杂度 \(O(\dfrac{m^2}{B})\)

依然是进行分类讨论

  • \(deg_u, deg_v < B\)
    • 按上文做法直接暴力枚举即可。时间复杂度 \(O(B)\)
  • 否则 \(u,v\) 之中必有一个点的度数大于等于阈值,直接根据预处理的结果输出即可。

复杂度是 \(O(\dfrac{m^2}{B} + qB)\) 的,平衡一下复杂度,发现 \(B = \sqrt{\dfrac{m^2}{q}}\) 是比较优秀的。总体复杂度是 \(O(\sqrt{m^2q})\)

posted @ 2025-08-13 16:30  KS_Fszha  阅读(14)  评论(0)    收藏  举报