题目
题目大意 对于一张有 n n n 条边无向图 G G G,选出 k k k 个点,使得封锁任何一个节点后,其他节点都可以到达你选出的 k k k 个点的其中一个点。输出最小的 k k k 和方案数量。
数据范围 包含多组数据,对于 100 % 100\% 100% 的数据,满足 1 ⩽ n ⩽ 500 1\leqslant n\leqslant 500 1⩽n⩽500,且答案不超过 2 64 2^{64} 264
题解
定义
- 点-双连通 对于一个连通图,如果任意两点之间至少存在两条“点不重复”的路径,则说这个图是点-双连通的(简称双连通, b i c o n n e c t e d biconnected biconnected)。
- 边-双连通 类似的,如果任意两点之间至少存在两条“边不重复”的路径,则说这个图是边-双连通的( e d g e − b i c o n n e c t e d edge-biconnected edge−biconnected)。
- 点双连通分量 对于一张无向图,点-双连通的极大子图称为点双连通分量( p o i n t − b i c o n n e c t e d point-biconnected point−biconnected c o m p o n e n t component component),简称双连通分量(只有点双连通分量才可以省略“点”)或块( b l o c k block block)
- 边双连通分量 类似的,边-双连通的极大子图称为边双连通分量( e d g e − b i c o n n e c t e d edge-biconnected edge−biconnected c o m p o n e n t component component)
% 显然地,我们可以得到以下几个结论:
- 点-双连通的图中任意两条边都在同一个简单环中,即内部无割顶。
- 边-双连通的图中任意每条边都至少在一个简单环中,即所有的边都不是桥。
- 除了桥不属于任何边双连通分量外,每条边恰好属于一个点双连通分量。
- 不同的点双连通分量有且仅有一个公共点,且它一定是割顶。
- 任意割顶都是至少两个不同点双连通分量的公共点。
- 把所有桥删除后,每个连通分量对应原图中的一个边双连通分量。
证明 (全都是反证)
-
假设一个点-双连通图中存在一个割顶,则删除这个点之后图不再连通,与点-双连通图的定义矛盾,得证。
-
假设一个边-双连通图中存在桥,则删除桥后,图不再连通,与边-双连通图的定义矛盾,得证。
-
设无向图 G G G 中存在一条不是桥的边属于两个边双连通分量,则这两个双联通分量互相连通,因此这两个边-双联通分量都不满足“极大”的定义,因而除了桥外的每条边至少属于一个边-双联通分量。设另一条边 ( u , v ) (u,v) (u,v) 不存在于任何一个边双连通分量中且 ( u , v ) (u,v) (u,v) 不是桥,则拆除边 ( u , v ) (u,v) (u,v) 后不会导致连通分量数量的增加,故则 u u u 和 v v v 至少存在两条间接路径,因而存在边双连通分量包含了 u u u 和 v v v,则 ( u , v ) (u,v) (u,v) 在边双连通分量中。
-
假设有两个不同的点双连通分量有多个公共点,则通过这些公共点,两个点双连通分量之间可以互相到达,与“极大子图”的定义矛盾。设这个点不是割顶,则删除这个点之后,两个双联通分量仍然连通,与“极大子图”矛盾,得证。
-
设一个割顶不是任何两个点双连通分量的公共点,根据割顶的定义,删除这个点之后,原图不连通,与点双连通分量的定义矛盾,得证。
-
设原结论不成立,即删除桥后连通分量中不存在至少两条“边不重复”的路径,则连通分量中存在桥,因此桥没有删完,与结论中删除所有桥的操作矛盾,得证。
%
我们已经知道如何求解割顶了,很容易可以发现,当我们找到割顶的时候,就已经完成了一次对某个极大点双连通子图的访问,那么我们如果在进行DFS的过程中将遍历过的点保存起来,是不是就可以得到点双连通分量了?
根据第
3
3
3 个结论,如果依然采用原来强联通分量的方法,会导致一些点双连通分量不能找到正确的点(因为一个点不止在一个点双连通分量中出现),而且根据结论
4
4
4,这些少了的点一定是割点。因此,我们必须把栈中存储的点换成边。然后类似强联通分量的依次出栈并把边的两个端点加入点双连通分量中。
其实还可以优化,割点的
l
o
w
low
low 数组根本没必要存储,因为深搜的过程中每次只需要访问所有儿子的
l
o
w
low
low 值,因此可以写成返回值的形式。
接下来是代码
class BCC:public form{
struct Edge {
int u,v;
};stack<Edge> s;
int pre[maxn]; //第一次访问的时间(戳)
int dfs_clock; //时间(戳)
bool cut[maxn]; //标记节点是否为割顶
public:
int bcc_cnt; //点双连通分量的数目
int bccno[maxn]; //节点属于的点双连通分量的编号
vector<int> bcc[maxn]; // 点双连通分量中的点
int size[maxn]; //点双连通分量中的点数
int cuts[maxn]; //表示点双连通分量里有多少个割点
int dfs(int u,int fa) {
int low=pre[u]=++dfs_clock;
int child=0;
for(int i=head[u]; i; i=edges[i].next) {
int v=edges[i].v;
Edge len=(Edge) {u,v};
if(!pre[v]) {
s.push(len);
child++;
int lowv=dfs(v,u);
low=min(low,lowv); //用后代更新low
if(lowv>=pre[u]) { //找到了一个子树满足割顶的条件
cut[u]=1;
bcc_cnt++;
size[bcc_cnt]=0;
bcc[bcc_cnt].clear();
while(true){ //保存bcc信息
Edge x=s.top();
s.pop();
if(bccno[x.u]!=bcc_cnt) {
size[bcc_cnt]++;
bcc[bcc_cnt].push_back(x.u);
bccno[x.u]=bcc_cnt;
}
if(bccno[x.v]!=bcc_cnt) {
size[bcc_cnt]++;
bcc[bcc_cnt].push_back(x.v);
bccno[x.v]=bcc_cnt;
}
if(x.u==u && x.v==v)
break;
}
}
} else if(pre[v]<pre[u]&&v!=fa) { //用反向边更新low
s.push(len);
low=min(low,pre[v]);
}
}
if(fa<0&&child==1)
cut[u]=0; //对于根节点若只有一个子树则不是割顶
return low;
}
void Tarjan(int n) {
memset(pre,0,sizeof(pre));
memset(cut,0,sizeof(cut));
memset(bccno,0,sizeof(bccno));
memset(cuts,0,sizeof(cuts));
dfs_clock=0;
bcc_cnt=0;
for(int i=1;i<=n;i++)
if(!pre[i])
dfs(i,-1);
for(int i=1;i<=bcc_cnt;i++){
int ll=bcc[i].size();
for(int j=0;j<ll;j++)
cuts[i]+=cut[bcc[i][j]];
}
}
}sol;
% 回到题目上,可以发现并证明:
-
若点双连通分量中存在 1 1 1 个以上的割点,则这个点双连通分量是安全的,即不需要选择任何一个点,因为根据结论 4 4 4,可以得到:存在 1 1 1 个以上的割点就说明和至少 2 2 2 个点双连通分量直接相连,当一个割点崩了,还可以通过其他割点跑到另一个双联通分量中。
-
若点双连通分量中存在 1 1 1 个割点,则这个点双连通分量需要选择一个点。
-
若双联通分量中不存在割点,则说明没有和其他双联通分量相连,如果只选择一个点,万一这个点崩了,那就玩完了,因此要选择两个点。
% 由此可以解决第一个任务。对于第二个任务,用排列组合计算即可,这不是本文的重点。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1000;
class form{
protected:
struct edge {
int v,next;
} edges[maxn];
int len,head[maxn];
public:
void clear(){memset(head,0,sizeof(head));len=1;}
form():len(1){memset(head,0,sizeof(head));}
void addedges(int u,int v) { //加边
edges[++len].v=v;
edges[len].next=head[u];
head[u]=len;
edges[++len].v=u;
edges[len].next=head[v];
head[v]=len;
}
};
class BCC:public form{
struct Edge {
int u,v;
};stack<Edge> s;
int pre[maxn]; //第一次访问的时间(戳)
int dfs_clock; //时间(戳)
bool cut[maxn]; //标记节点是否为割顶
public:
int bcc_cnt; //点双连通分量的数目
int bccno[maxn]; //节点属于的点双连通分量的编号
vector<int> bcc[maxn]; // 点双连通分量中点
int size[maxn]; //点双连通分量中的点数
int cuts[maxn]; //表示点双连通分量里有多少个割点
int dfs(int u,int fa) {
int low=pre[u]=++dfs_clock;
int child=0;
for(int i=head[u]; i; i=edges[i].next) {
int v=edges[i].v;
Edge len=(Edge) {u,v};
if(!pre[v]) {
s.push(len);
child++;
int lowv=dfs(v,u);
low=min(low,lowv); //用后代更新low
if(lowv>=pre[u]) { //找到了一个子树满足割顶的条件
cut[u]=1;
bcc_cnt++;
size[bcc_cnt]=0;
bcc[bcc_cnt].clear();
while(true){ //保存bcc信息
Edge x=s.top();
s.pop();
if(bccno[x.u]!=bcc_cnt) {
size[bcc_cnt]++;
bcc[bcc_cnt].push_back(x.u);
bccno[x.u]=bcc_cnt;
}
if(bccno[x.v]!=bcc_cnt) {
size[bcc_cnt]++;
bcc[bcc_cnt].push_back(x.v);
bccno[x.v]=bcc_cnt;
}
if(x.u==u && x.v==v)
break;
}
}
} else if(pre[v]<pre[u]&&v!=fa) { //用反向边更新low
s.push(len);
low=min(low,pre[v]);
}
}
if(fa<0&&child==1)
cut[u]=0; //对于根节点若只有一个子树则不是割顶
return low;
}
void Tarjan(int n) {
memset(pre,0,sizeof(pre));
memset(cut,0,sizeof(cut));
memset(bccno,0,sizeof(bccno));
memset(cuts,0,sizeof(cuts));
dfs_clock=0;
bcc_cnt=0;
for(int i=1;i<=n;i++)
if(!pre[i])
dfs(i,-1);
for(int i=1;i<=bcc_cnt;i++){
int ll=bcc[i].size();
for(int j=0;j<ll;j++)
cuts[i]+=cut[bcc[i][j]];
}
}
}sol;
int main() {
int u,v,n,m,cases=0;
while(~scanf("%d",&m)&&m) {
n=0;
unsigned long long ans1=0,ans2=1;
sol.clear();
for(int i=1; i<=m; i++) {
scanf("%d%d",&u,&v);
sol.addedges(u,v);
n=max(n,u);
n=max(n,v);
}
sol.Tarjan(n);
for(int i=1;i<=sol.bcc_cnt;i++){
if(sol.cuts[i]==0){
ans1+=2;
ans2*=sol.size[i]*(sol.size[i]-1)/2;
}else if(sol.cuts[i]==1){
ans1++;
ans2*=sol.size[i]-1;
}
}
printf("Case %d: %llu %llu\n",++cases,ans1,ans2);
}
return 0;
}
% 可以发现,一些割点是在返回父亲的时候才被标记的,因此统计 BCC \text{BCC} BCC中有多少个割点只能在搜索完成后统计。