最新文章

这里会显示最新的几篇文章摘要。

社交网络(并查集)

问题 C: 社交网络

题目描述

在一个社交网络服务(SNS)中,有N个用户,分别用从1到N的数字标记。

在这个SNS中,两个用户可以成为朋友。友谊是双向的;如果用户X是用户Y的朋友,那么用户Y总是用户X的朋友。

目前,在SNS上有M对朋友关系,第i对由用户Ai和Bi组成。

确定以下操作可以执行的最大次数:

操作:选择三个不同的用户 X 、Y和 Z ,使得 X 和 Y 是朋友,Y 和 Z 是朋友,但 X 和 Z不是朋友。让 X 和 Z 成为朋友。

输入

第一行包含两个正整数 N 和 M,表示有 N 个用户,M 对朋友关系

接下来 M 行,每行描述第 i 对朋友关系。

输出

一行一个整数,表示答案。

样例输入

【样例1】
4 3
1 2
2 3
1 4
【样例2】
3 0
【样例3】
10 8
1 2
2 3
3 4
4 5
6 7
7 8
8 9
9 10

样例输出

【样例1】3
【样例2】0
【样例3】12

提示

样例1解释:
三个新的友谊可以如下产生:

  1. 用户1与用户3成为朋友,用户3是他们的朋友(用户2)的朋友。
  2. 用户3与用户4成为朋友,用户4是他们的朋友(用户1)的朋友。
  3. 用户2与用户4成为朋友,用户4是他们的朋友(用户1)的朋友。

不会有四个或更多的新友谊产生。

  • 对于 40% 的数据,1 ≤ N ≤ 1000
  • 对于 100% 的数据,1 ≤ N ≤ 2*10, 0 ≤ M ≤ 2*10, 1 ≤ Ai < Bi ≤ N 数据保证没有自环。注意,重复的边算一条边。

分析

我们可以证明:对每个连通块,当它不是团(完全图)时,总能找到满足条件的三个点进行操作,并且不断执行操作直到这个连通块变成一个团为止。对于一个有 (k) 个节点的连通块,其完全图边数为
\( \frac{k(k-1)}{2} \)
假设该连通块初始时有 (m) 条边,则最多可以“补全”
\( \frac{k(k-1)}{2} - m \)
条边,也就是操作的最大次数。由于不同连通块之间没有路径,操作只能在各自连通块内进行,所以整个图中可以执行的最大操作次数为所有连通块中“缺失边数”的和,即
\( \text{答案} = \sum_{\text{连通块}} \left(\frac{k(k-1)}{2} - m\right) \)

代码思路就是在并查集的基础上,再维护两个数组,一个cnt[N]储存节点个数,一个e[N]储存边的个数,都储存在根节点位置,再维护一个领接表储存边,用来确保边不重复。

代码

#define int long long
const int N = 2e5 + 5;
int fa[N],e[N],cnt[N];        //分别为并查集根节点,边数,节点数
vector<int> ed[N];             //领接表存边
inline void init(int n) {     //初始化
    for (int i = 1;i <= n;++i) {
        fa[i] = i;
        cnt[i] = 1;
    }
}

int find_fa(int x) {                 //查找,路径压缩
    if (fa[x] != x) fa[x] = find_fa(fa[x]);
    return fa[x];
}

inline void merge(int a,int b) {       //合并集合
    int t = find_fa(a),q = find_fa(b);  
    if (t == q) {e[t]++;return;}      //注意,如果两元素在同一个集合,但是边是不重复的,那么边数加一
    cnt[t] +=  cnt[q];                  //把b集合的个数加到a集合上
    e[t] += e[q]+1;                    //把b集合的边数加到a集合上
    fa[q] = t;                          //合并

}


signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    //
    // freopen("E:/Code/C++/untitled1/input.txt","r",stdin);
    // freopen("output.txt","w",stdout);

    int n,m;
    cin >> n >> m;
    init(n);                     
    for (int i = 0;i < m;++i) { 
        int a,b;     
        cin >> a >> b;              //读入边
        bool f = false;             //标记
        for (auto c : ed[a]) if (c == b) {f = true;break;} //检查是否是重边
        if (f) continue;               //重边跳过
        ed[a].emplace_back(b);         //记录边
        ed[b].emplace_back(a);
        merge(a,b);
    }
    int ans = 0;
    for (int i = 1;i <= n;++i) {          
        if (i == fa[i]) {             //检查有哪些连通块
            ans += 1LL*cnt[i]*(cnt[i]-1)/2 - e[i];     //计算结果
        }
    }

    cout << ans;
}
posted @ 2025-03-01 23:55  bakul  阅读(146)  评论(0)    收藏  举报