Living-Dream 系列笔记 第84期

连通性问题

  • 点双连通:在无向图中,删除一个点(不是 \(x\) 或者 \(y\))后,点 \(x\) 和点 \(y\) 仍然能够彼此到达,那么称 \(x\)\(y\) 是点双连通的。

  • 边双连通:在无向图中,删除一条边后,点 \(x\) 和点 \(y\) 仍然能够彼此到达,那么称 \(x\)\(y\) 是边双连通的。

性质

  • 点双连通不具有传递性,但边双连通具有。

    image

    如图,但 \(y\) 被删去之后,虽然 \(x,y\) 以及 \(y,z\) 都是点双连通的,但是 \(x,z\) 却不是点双连通的了。

    边双连通具有传递性是因为无论删哪条边 \(x,y\)\(y,z\) 都是边双连通的(由定义易知),这也就是说 \(x,z\) 是边双连通的。

割点

性质:

  • 至少三个点的图中才有割点。(易证)

    image

    如图,\(x\) 即为该图的割点。

判定:

若搜索树中,有从 \(x\)\(y\) 的连边,当 \(low_y \ge dfn_x\) 时(定义同 tarjan 算法),说明 \(y\) 能到达的最小时间戳是 \(x\) 的时间戳,即 \(y\)\(x\)\(x\) 之前的结点「隔开」, 因此 \(x\) 可能是割点。

只要 \(x\) 不是搜索树的根结点,或者 \(x\) 是根结点 且 \(x\) 的子结点大于 \(1\) 个,那么 \(x\) 就是割点。

P3388 & UVA10199

板子。

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

const int N=1e5+5;
int n,m,cnt,rt;
vector<int> G[N],ans;
int dfn[N],low[N];
bool cut[N];

void tarjan(int cur){
	dfn[cur]=low[cur]=++cnt;
	int num=0;
	for(int i:G[cur]){
		if(!dfn[i]){
			tarjan(i);
			low[cur]=min(low[cur],low[i]);
			++num;
			if(low[i]>=dfn[cur]&&(cur!=rt||num>1)){
				cut[cur]=1;
			}
		}
		else{
			low[cur]=min(low[cur],dfn[i]);
		}
	}
}

int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n>>m;
	for(int i=1,u,v;i<=m;i++){
		cin>>u>>v;
		G[u].push_back(v);
		G[v].push_back(u);
	} 
	for(int i=1;i<=n;i++)
		if(!dfn[i])
			rt=i,tarjan(i);
	for(int i=1;i<=n;i++)
		if(cut[i])
			ans.push_back(i);
	cout<<ans.size()<<'\n';
	for(int i:ans)
		cout<<i<<' ';
	return 0;
} 

CF22C

构造题,从简单情况开始考虑

首先 \(m\) 必须 \(\ge n-1\),否则图不可能联通。

\(m=n-1\) 时,显然让 \(v\) 作为根,其他点直接向 \(v\) 连边是最简单的方案,其他方案都可以通过改变树的形态的方式使得其与此方案等价。

接着,当 \(m>n-1\) 时,为了使添加的边尽可能多,我们只能孤立出一个子节点,其他的 \(n-2\) 个子节点连出完全图即可。

于是 \(m\) 的上界即为 \(n-1+\frac{(n-2) \times (n-3)}{2}\),做完了。

code
//
//  CF22C.cpp
//
//
//  Created by _XOFqwq on 2024/11/7.
//

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

int n,m,v;

void print(int x){
    for (int i=1; i<=n; i++) {
        if (!m) {
            return;
        }
        if (i!=v&&i!=x) {
            for (int j=i+1; j<=n; j++) {
                if (!m) {
                    return;
                }
                if (j!=v&&j!=x) {
                    cout<<i<<' '<<j<<'\n',m--;
                }
            }
        }
    }
}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>m>>v;
    if (m<n-1||m>n-1+(n-2)*(n-3)/2) {
        cout<<-1;
    } else {
        for (int i=1; i<=n; i++) {
            if (i!=v) {
                cout<<v<<' '<<i<<'\n';
            }
        }
        m-=n-1;
        if (v==n) {
            print(n-1);
        } else {
            print(n);
        }
    }
    return 0;
}

P3469

容易发现每个点都有一个基础贡献 \(2 \times (n-1)\),只有割点才会产生额外贡献。

对于割点,将它提起来作为根,于是有子树内的联通块和子树外的联通块。子树内的贡献算两次,单点算两次,然后子树外的再算两次即可。具体见代码实现。

code
//
//  P3469.cpp
//
//
//  Created by _XOFqwq on 2024/11/7.
//

#include <bits/stdc++.h>
#define int long long
using namespace std;

const int N=5e5+5;
int n,m,cnt,rt;
int siz[N],ans[N];
bool cut[N];
int dfn[N],low[N];
vector<int> G[N];

void tarjan(int cur){
    dfn[cur]=low[cur]=++cnt;
    siz[cur]=1;
    int num=0,sum=0;
    for (int i : G[cur]) {
        if (!dfn[i]) {
            tarjan(i);
            siz[cur]+=siz[i];
            low[cur]=min(low[cur],low[i]);
            num++;
            if (low[i]>=dfn[cur]&&(cur!=rt||num>1)) {
                sum+=siz[i];
                ans[cur]+=siz[i]*(n-siz[i]); //子树内两次 + 子树外一次 + 子树内与单点
                cut[cur]=1;
            }
        } else {
            low[cur]=min(low[cur],dfn[i]);
        }
    }
    if (cut[cur]) {
        ans[cur]+=(n-sum-1)*(sum+1)+n-1; //子树外一次 + 子树外与单点 + 一次单点
    } else {
        ans[cur]=2ll*(n-1);
    }
}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>m;
    for (int i=1,u,v; i<=m; i++) {
        cin>>u>>v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    rt=1,tarjan(1);
    for (int i=1; i<=n; i++) {
        cout<<ans[i]<<'\n';
    }
    return 0;
}

posted @ 2024-11-09 17:59  _XOFqwq  阅读(6)  评论(0编辑  收藏  举报