Day8 双连通分量

版权声明:https://blog.csdn.net/huashuimu2003 https://blog.csdn.net/huashuimu2003/article/details/87536888

A. BZOJ 1718: [Usaco2006 Jan] Redundant Paths 分离的路径

题目

BZOJ 1718
POJ 3177
LUOGU 2860

题解

思路:在同一个边双连通分量中,任意两点都有至少两条独立路可达,所以同一个边双连通分量里的所有点可以看做同一个点。
缩点后,新图是一棵树,树的边就是原无向图的桥。
现在问题转化为:在树中至少添加多少条边能使图变为双连通图。
结论
添加边数=(树中度为1的节点数+1)/2 具体方法为,首先把两个最近公共祖先最远的两个叶节点之间连接一条边,这样可以把这两个点到祖先的路径上所有点收缩到一起,因为一个形成的环一定是双连通的。
然后再找两个最近公共祖先最远的两个叶节点,这样一对一对找完,恰好是(leaf+1)/2次,把所有点收缩到了一起。
点评
边双连通分量缩点后,原图变成一颗真正的树,而树上各种操作可以和其他知识点结合起来。
这种敏感性要有,比如缩点之后就可以快速求必经边,必经点之类的。
比较懒,就把老师ppt上的题解粘过来了。

代码

#include<bits/stdc++.h>
using namespace std;
const int maxn=3e5+10;
template<typename T>inline void read(T &x)
{
    x=0;
    T f=1,ch=getchar();
    while (!isdigit(ch)) ch=getchar();
    if (ch=='-') f=-1, ch=getchar();
    while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar();
    x*=f;
}
int ver[maxn<<1],Next[maxn<<1],head[maxn],len=1;
inline void add(int x,int y)
{
    ver[++len]=y,Next[len]=head[x],head[x]=len;
}
int dfn[maxn],low[maxn],id;
bool bridge[maxn<<1];
inline void tarjan(int x,int inedge)
{
    dfn[x]=low[x]=++id;
    for (int i=head[x];i;i=Next[i])
    {
        int y=ver[i];
        if (!dfn[y])
        {
            tarjan(y,i);
            low[x]=min(low[x],low[y]);
            if (low[y]>dfn[x])
                bridge[i]=bridge[i^1]=1;
        }
        else if (i!=(inedge^1))
            low[x]=min(low[x],dfn[y]);
    }
}
int c[maxn],Out[maxn],dcc;
inline void dfs(int x)
{
    c[x]=dcc;
    for (int i=head[x];i;i=Next[i])
    {
        int y=ver[i];
        if (c[y] || bridge[i]) continue;
        dfs(y);
    }
}
int main()
{
	freopen("rpaths.in","r",stdin);
	freopen("rpaths.out","w",stdout);
    int n,m;read(n);read(m);
    for (int i=1;i<=m;++i)
    {
        int x,y;
        read(x);read(y);
        add(x,y);add(y,x);
    }
    for (int i=1;i<=n;++i)
        if (!dfn[i]) tarjan(i,-1);
    for (int i=1;i<=n;++i)
        if (!c[i]) ++dcc,dfs(i);
    for (int i=2;i<=len;i+=2)
    {
        int x=ver[i^1],y=ver[i];
        if (c[x]!=c[y])
            ++Out[c[x]],++Out[c[y]];
    }
    int ans=0;
    for (int i=1;i<=dcc;++i)
        if (Out[i]==1) ++ans;
    printf("%d\n",(ans+1)>>1);
    return 0;
}

B. 逃不掉的路

题目

题目描述
现代社会,路是必不可少的。任意两个城镇都有路相连,而且往往不止一条。但有些路连年被各种XXOO,走着很不爽。按理说条条大路通罗马,大不了绕行其他路呗——可小撸却发现:从a城到b城不管怎么走,总有一些逃不掉的必经之路。
他想请你计算一下,a到b的所有路径中,有几条路是逃不掉的?
输入格式
第一行是n和m,用空格隔开。
接下来m行,每行两个整数x和y,用空格隔开,表示x城和y城之间有一条长为1的双向路。
第m+2行是q。接下来q行,每行两个整数a和b,用空格隔开,表示一次询问。
输出格式
对于每次询问,输出一个正整数,表示a城到b城必须经过几条路。
样例输入
5 5
1 2
1 3
2 4
3 4
4 5
2
1 4
2 5
样例输出
0
1
样例解释
第1次询问,1到4的路径有 1–2--4 ,还有 1–3--4 。没有逃不掉的道路,所以答案是0。
第2次询问,2到5的路径有 2–4--5 ,还有 2–1--3–4--5 。必须走“4–5”这条路,所以答案是1。
数据约定与范围
共10组数据,每组10分。
有3组数据,n ≤ 100 , n ≤ m ≤ 200 , q ≤ 100。
另有2组数据,n ≤ 103, n ≤ m ≤ 2 x 103 , 100 < q ≤ 105
另有3组数据,103 < n ≤ 105 , m = n-1 , 100 < q ≤ 105
另有2组数据,103 < n ≤ 105 , n ≤ m ≤ 2 x 105 , 100 < q ≤ 105
对于全部的数据,1 ≤ x,y,a,b ≤ n;对于任意的道路,两端的城市编号之差不超过104
任意两个城镇都有路径相连;同一条道路不会出现两次;道路的起终点不会相同;查询的两个城市不会相同。

题解

经过分析后,就可以得出方法:
既然是求必须经过的边,那么边双包含的集合肯定不是必须经过的,这样我们可以先划分出边双连通分量,进行缩点,这样就构建了一棵树,而树上任意两点间逃不掉的路的条数(边权为1)就是他们的距离,求树上两点间的距离即可。
所以,综上所述,这是一道边双+LCA的模板题。

代码

#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+10;
template<typename T>inline void read(T &x)
{
	x=0;
	T f=1,ch=getchar();
	while (!isdigit(ch)) ch=getchar();
	if (ch=='-') f=-1, ch=getchar();
	while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar();
	x*=f;
}
int ver[maxn<<1],Next[maxn<<1],head[maxn],len=1;
inline void add(int x,int y)
{
	ver[++len]=y,Next[len]=head[x],head[x]=len;
}
int dfn[maxn],low[maxn],id;
bool bridge[maxn<<1];
inline void tarjan(int x,int inedge)
{
	dfn[x]=low[x]=++id;
	for (int i=head[x];i;i=Next[i])
	{
		int y=ver[i];
		if (!dfn[y])
		{
			tarjan(y,i);
			low[x]=min(low[x],low[y]);
			if (low[y]>dfn[x])
				bridge[i]=bridge[i^1]=1;
		}
		else if (i!=(inedge^1))
			low[x]=min(low[x],dfn[y]);
	}
}
int c[maxn],dcc;
inline void dfs(int x)
{
	c[x]=dcc;
	for (int i=head[x];i;i=Next[i])
	{
		int y=ver[i];
		if (c[y] || bridge[i]) continue;
		dfs(y);
	}
}
int vc[maxn<<1],Nc[maxn<<1],hc[maxn],lc;
inline void addc(int x,int y)
{
	vc[++lc]=y,Nc[lc]=hc[x],hc[x]=lc;
}
int d[maxn],f[maxn][21],t;
queue<int>q;
inline void bfs(int s)
{
	q.push(s);
	d[s]=1;
	while (!q.empty())
	{
		int x=q.front();
		q.pop();
		for (int i=hc[x];i;i=Nc[i])
		{
			int y=vc[i];//错误原因2:vc打成ver
			if (d[y]) continue;
			d[y]=d[x]+1;
			f[y][0]=x;
			for (int j=1;j<=20;++j)//错误原因3:++j打成++i
				f[y][j]=f[f[y][j-1]][j-1];
			q.push(y);
		}
	}
}
inline int lca(int x,int y)
{
	if (d[x]>d[y]) swap(x,y);
	for (int i=20;i>=0;--i)
		if (d[f[y][i]]>=d[x]) y=f[y][i];//错误原因1:未写‘=’号
	if (x==y) return x;
	for (int i=20;i>=0;--i)
		if (f[x][i]!=f[y][i])
			x=f[x][i],y=f[y][i];
	return f[x][0];
}
int main()
{
	freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);
	int n,m;
	read(n);read(m);
	for (int i=1;i<=m;++i)
	{
		int x,y;
		read(x);read(y);
		add(x,y);add(y,x);
	}
	for (int i=1;i<=n;++i)
		if (!dfn[i]) tarjan(i,-1);
	for (int i=1;i<=n;++i)
		if (!c[i]) ++dcc,dfs(i);
	lc=1;
	for (int i=2;i<=len;++i)
	{
		int x=ver[i^1],y=ver[i];
		if (c[x]==c[y]) continue;
		addc(c[x],c[y]),addc(c[y],c[x]);
	}
	bfs(1);
	int q;read(q);
	for (int i=1;i<=q;++i)
	{
		int x,y;
		read(x);read(y);
		printf("%d\n",d[c[x]]+d[c[y]]-(d[lca(c[x],c[y])]<<1));
	}
	return 0;
}

C.HDU 3394 railway 点双联通+桥

题目p1691

HDU 3394

描述 Description
有一个公园有n个景点,这n个景点由m条无向道路连接而成。公园的管理员准备规划一一些形成回路的参观路线。如果一条道路被多条参观路线公用,那么这条路是冲突的;如果一条道路没在任何一个回路内,那么这条路是多余的道路。
问分别有多少条有冲突的路和多余的路
输入格式 Input Format
包括多组数据
每组数据第一行2个整数n,m
接下来m行,每行2个整数x,y,表示从x到y有一条无向边。
输入数据以n=0,m=0结尾
输出格式 Output Format
一行2个整数,表示你要求的多余的道路和冲突的道路的数量。
样例输入 Sample Input
8 10
0 1
1 2
2 3
3 0
3 4
4 5
5 6
6 7
7 4
5 7
0 0
样例输出 Sample Output
1 5
时间限制 Time Limitation
1s
注释 Hint
【数据范围】
n<=10000
m<=100000
0<=x,y<n
每个测试点有10组数据。
来源 Source
hdoj 3394
题面+数据来自2018级 宋逸群

题解

查了半天,是道桥与点双的板子题(然而我打了边双。。。。。。。。)。。。。。。。
一.解释一下点双:点双连通分量:对于一个连通图,如果任意两点至少存在两条“点不重复”的路径,则说这个图是点双连通的,简单来说就是任意两条边都在同一个简单环中,即内部无割顶。

二.先尝试解释一下要求输出的两个东西究竟是什么:
1.多余边:不在任何环中,一定是桥。
2.冲突边:如果一个环内的边数大于点数,那么这个环内所有边都是“冲突边”。其实就是点双了。

三.怎么判断一个双连通分量中环的个数呢?根据点数跟边数的关系 。
1.当点数=边数,形成一个环 。
2.当点数>边数(一条线段,说明这条边是桥) 。
3.当点数<边数,那么就含1个以上的环了。

四.HDU的数据挺水的。

代码

简陋注释,可能有错,欢迎大家指出!!!!!!

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
const int maxm=1e4+10;
template<typename T>inline void read(T &x)
{
	x=0;
	T f=1,ch=getchar();
	while (!isdigit(ch)) ch=getchar();
	if (ch=='-') f=-1, ch=getchar();
	while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar();
	x*=f;
}
int bridge,vDCC;

int ver[maxn<<1],Next[maxn<<1],head[maxm],len;
inline void add(int x,int y)
{
	ver[++len]=y,Next[len]=head[x],head[x]=len;
}

int belong[maxm],cnt=0;
bool instack[maxm];
void GetvDCC()
{
	int num=0;
	for (int j=1;j<=cnt;++j)
	{
		int x=belong[j];//属于第j个点双的点
		for (int i=head[x];i;i=Next[i])//邻接表的dfs
		{
			int y=ver[i];
			if (instack[y]) ++num;//如果说他被访问过,那他肯定不在栈里,也就说是点双了
		}
	}
	num>>=1;//无向图,所以点会过两次
	if (num>cnt) vDCC+=num;//如果一个环内的边数大于点数,那么这个环内所有边都是“冲突边”
}
int Stack[maxm],top;
int dfn[maxm],low[maxm],low1[maxm],id=0;
void tarjan(int x,int inedge)//点双连通缩点方法:清空路径,枚举ver,Next数组中存储的路径,建立双向边
{
	low[x]=low1[x]=dfn[x]=++id;
	Stack[++top]=x;
	for (int i=head[x];i;i=Next[i])
	{
		int y=ver[i];
		if (!dfn[y])//y结点还未访问过
		{
			tarjan(y,i);//访问y结点
			low[x]=min(low[x],low[y]);//更新low
			low1[x]=min(low1[x],low1[y]);//错误原因,割边与割点的判定是不一样的
			if (low1[y]>dfn[x]) ++bridge;//正如上面所说:当点数>边数(一条线段,说明这条边是桥)
			if (low[y]>=dfn[x])
			{
				int k;
				cnt=0;
				memset(instack,0,sizeof(instack));//注意在进行标记前要把它清空
				do
				{
					k=Stack[top--];//取出栈顶
					belong[++cnt]=k;//k属于第cnt个点双
					instack[k]=1;
				} while (k!=y);
				belong[++cnt]=x;
				instack[x]=1;
				GetvDCC();
			}
		}
		else
		{
			low[x]=min(low[x],dfn[y]);//与有向图区分,此处else不需要判别y节点是否在栈内
			if (i!=(inedge^1)) low1[x]=min(low1[x],dfn[y]);
		}
	}
}
int main()
{
	freopen("way.in","r",stdin);
	freopen("way.out","w",stdout);
	while (1)
	{
		int n,m;read(n);read(m);
		if (!n && !m) break;
		memset(head,0,sizeof(head));
		len=1;
		while (m--)
		{
			int x,y;read(x);read(y);
			++x,++y;//结点是从零开始的
			if (x==y) continue;
			add(x,y),add(y,x);//无向图
		}

		memset(dfn,0,sizeof(dfn));
		bridge = vDCC = id = top = 0;
		for (int i=1;i<=n;++i)
			if (!dfn[i]) tarjan(i,-1);
		printf("%d %d\n",bridge,vDCC);
	}
	return 0;
}

D.BZOJ 2730: [HNOI2012]矿场搭建

题目

BZOJ 2730
LUOGU 3225

题解

首先我们知道,对于这张图,我们可以枚举坍塌的是哪个点,对于每个坍塌的点,最多可以将图分成若干个不连通的块,这样每个块我们可能需要一个出口才能满足题目的要求,枚举每个坍塌的点显然是没有意义的,我们只需要每个图的若干个割点,这样除去割点的图有若干个块,我们可以求出只与一个割点相连的块,这些块必须要一个出口才能满足题目的要求,每个块内有块内个数种选法,然后将所有满足一个割点相连的块的点数连乘就行了。

对于每个与一个割点相连的块必须建出口可以换一种方式理解,我们将每个块看做一个点,那么算上割点之后,这张图就变成了一颗树,只有叶子节点我们需要建立出口,因为对于非叶子节点我们不论断掉哪个点我们都有另一种方式相连,这里的叶子节点就是与一个割点相连的块。
最后还有个特判,就是对于一个双连通图,我们至少需要选取两个点作为出口,因为如果就选一个,可能该点为坍塌点,这时我们就任选两个点就行了,方案数为点数x(点数-1)>>1

代码该如何写?
先tarjan求一下所有的点双。
然后对于每一个点双,分类讨论:
1、只有一个割点,必须选一个非割点。
2、有>=2个割点,不用选
3、有0个割点,必须选俩。

比较懒,就把老师ppt上的题解粘过来了。

代码

#include<bits/stdc++.h>
using namespace std;
const int maxn=510;
template<typename T>inline void read(T &x)
{
    x=0;
    T f=1,ch=getchar();
    while (!isdigit(ch)) ch=getchar();
    if (ch=='-') f=-1, ch=getchar();
    while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar();
    x*=f;
}
long long vDCC,ans=1;//错误原因:未开long long

int ver[maxn<<1],Next[maxn<<1],head[maxn],len;
inline void add(int x,int y)
{
    ver[++len]=y,Next[len]=head[x],head[x]=len;
}
int dfn[maxn],low[maxn],id,root;
bool cut[maxn];
inline void tarjan(int x,int fa)
{
    dfn[x]=low[x]=++id;
    int tot=0;
    for (int i=head[x];i;i=Next[i])
    {
        int y=ver[i];
        if (!dfn[y])
        {
            tarjan(y,x);
            low[x]=min(low[x],low[y]);
            if (low[y]>=dfn[x])
            {
                ++tot;
                if (x^root || tot>1) cut[x]=1;
            }
        }
        else if (y!=fa)
            low[x]=min(low[x],dfn[y]);
    }
}
int c[maxn],dcc,num,cnt;
inline void dfs(int x)
{
    c[x]=dcc;
    ++cnt;
    for (int i=head[x];i;i=Next[i])
    {
        int y=ver[i];
        if (c[y]!=dcc && cut[y]) ++num,c[y]=dcc;
        if (!c[y])
        	dfs(y);
    }
}
int main()
{
	freopen("input.in","r",stdin);
	freopen("output.out","w",stdout);
    for (int Case=1;;++Case)
    {
        int n=0,m;
        read(m);
        if (!m) exit(0);
        memset(dfn,0,sizeof(dfn));
        memset(c,0,sizeof(c));
        memset(cut,0,sizeof(cut));
        memset(head,0,sizeof(head));
        vDCC=dcc=id=len=0;
        ans=1;
        for (int i=1;i<=m;++i)
        {
            int x,y;read(x);read(y);
            n=max(n,max(x,y));
            add(x,y);add(y,x);
        }
        for (int i=1;i<=n;++i)
            if (!dfn[i]) root=i,tarjan(i,0);
        for (int i=1;i<=n;++i)
            if (!c[i] && !cut[i])
            {
                ++dcc,cnt=num=0;
                dfs(i);
                if (!num) vDCC+=2,ans*=cnt*(cnt-1)/2;
                if (num==1) ++vDCC,ans*=cnt;
            }
        printf("Case %d: %lld %lld\n",Case,vDCC,ans);
    }
    return 0;
}

刷题是一种出路,枚举是一种思想,打表是一种勇气,搜索是一种信仰,剪枝是一种精神,骗分是一种日常。——melody

猜你喜欢

转载自blog.csdn.net/huashuimu2003/article/details/87536888