'''
思路:
利用点双连通分量对每一个连通子图进行缩点,如果某个双连通分量对应的点在缩点图后的度是1,说明只有一个割点与其相连,
那这个双连通分量里面必须放一个逃生点,这个双连通分量就有分量中节点数那么多中选择,对于度超过1的
双连通分量,不需要额外在分量中添加逃生点,这中双连通分量中的点一定有两条以上的路可以逃生,对于
特殊情况,如果一个子图没有割点,那子图里面需要放两个逃生点,那就有节点数对2取组合数这么多种选择,
分别用加法原则和乘法原则计算两个答案,注意没有割情况下,子图的点个数如果是1,那只需要放一个逃生点即可,
子图点个数大于等于2时候,都需要在子图放2个逃生点
'''
from typing import List, Tuple
import sys
sys.setrecursionlimit(9999999)
class MergeSetExt:
def __init__(self, max_key_val = 0, trans_key_func=None):
if trans_key_func is not None and max_key_val > 0:
# 如果能够提供转换key的回调,并查集内部用线性表存储
self.trans_key_callback = trans_key_func
self.m = [-1 for _ in range(max_key_val+1)]
self.__root2cluster_size = [0 for _ in range(max_key_val+1)]
else:
# 如果不能提供转换key的回调,并查集内部用hash表存储
self.trans_key_callback = None
self.m = {}
self.__root2cluster_size = {}
self.__root_cnt = 0
def getRoot(self, node):
buf = []
root = self.trans_key_callback(node) if self.trans_key_callback else node
while self.m[root] != root:
buf.append(root)
root = self.m[root]
for key in buf:
self.m[key] = root
return root
def merge(self, a, b):
orig_a, orig_b = a, b
if self.trans_key_callback:
a, b = self.trans_key_callback(a), self.trans_key_callback(b)
for node in [a, b]:
if (self.trans_key_callback is None and node not in self.m) or (self.trans_key_callback is not None and self.m[node] == -1):
self.m[node] = node
self.__root2cluster_size[node] = 1
self.__root_cnt += 1
root1 = self.getRoot(orig_a)
root2 = self.getRoot(orig_b)
if root1 != root2:
self.m[root1] = root2
self.__root2cluster_size[root2] += self.__root2cluster_size[root1]
if self.trans_key_callback:
self.__root2cluster_size[root1] = 0
else:
self.__root2cluster_size.pop(root1)
self.__root_cnt -= 1
# 根据根节点的数值获取其所在的簇的大小
def getClusterSize(self, root):
return self.__root2cluster_size[root]
def isInSameSet(self, a, b):
if a == b:
return True
orig_a, orig_b = a, b
if self.trans_key_callback:
a, b = self.trans_key_callback(a), self.trans_key_callback(b)
for node in [a, b]:
if self.m[node] == -1:
return False
else:
for node in [orig_a, orig_b]:
if node not in self.m:
return False
return self.getRoot(orig_a) == self.getRoot(orig_b)
def getRootNum(self):
return self.__root_cnt
def getClusters(self):
rec = {}
if self.trans_key_callback is None:
for node in self.m:
root = self.getRoot(node)
if root not in rec:
rec[root] = []
rec[root].append(node)
else:
for node in range(len(self.m)):
if self.m[node] == -1:
continue
root = self.getRoot(node)
if root not in rec:
rec[root] = []
rec[root].append(node)
return [nodes for nodes in rec.values()]
class Tarjan:
# 求无向连通图的桥
# 所有节点编号是1, 2, 3, ..... max_node_num
@staticmethod
def getCuttingPointAndCuttingEdge(edges: List[Tuple], max_node_num):
if len(edges) == 0:
return [], []
link = {}
dfn = [0x7fffffff] * (max_node_num + 1)
low = [0x7fffffff] * (max_node_num + 1)
global_time = [0]
for a, b in edges:
if a not in link:
link[a] = []
if b not in link:
link[b] = []
link[a].append(b)
link[b].append(a)
cutting_points, cutting_edges = [], []
def dfs(cur, prev, root):
global_time[0] += 1
dfn[cur], low[cur] = global_time[0], global_time[0]
children_cnt = 0
flag = False
for next in link[cur]:
if next != prev:
if dfn[next] == 0x7fffffff:
children_cnt += 1
dfs(next, cur, root)
# 割点条件1 当前节点不是root, 有儿子,且子节点的low大于等于当前节点的dfn
if cur != root and low[next] >= dfn[cur]:
flag = True
low[cur] = min(low[cur], low[next])
# 割边条件 子节点的low 大于当前节点dfn
if low[next] > dfn[cur]:
cutting_edges.append([cur, next] if cur < next else [next, cur])
else:
low[cur] = min(low[cur], dfn[next])
# 割点条件2 当前节点是root,且有多于2个的子节点
if flag or (cur == root and children_cnt >= 2):
cutting_points.append(cur)
dfs(edges[0][0], None, edges[0][0])
return cutting_points, cutting_edges
# 获取无向图的割边和边双连通分量列表
@staticmethod
def getEdgeDCC(edges, max_node_num):
_, cutting_edges = Tarjan.getCuttingPointAndCuttingEdge(edges, max_node_num)
forbid_e = set()
for a, b in cutting_edges:
forbid_e.add((a, b))
forbid_e.add((b, a))
merge_set = MergeSetExt(max_key_val=max_node_num, trans_key_func=lambda x : x)
for i in range(1, max_node_num+1):
merge_set.merge(i, i)
for a, b in edges:
if (a, b) in forbid_e:
continue
merge_set.merge(a, b)
return cutting_edges, merge_set.getClusters()
# 获取无向图的割点和点双连通分量列表,点双连通分量中不包含割点
@staticmethod
def getVertexDCC(edges, max_node_num):
cutting_point, _ = Tarjan.getCuttingPointAndCuttingEdge(edges, max_node_num)
is_cutting_point = [0] * (max_node_num + 1)
for node in cutting_point:
is_cutting_point[node] = 1
merge_set = MergeSetExt(max_key_val=max_node_num, trans_key_func=lambda x : x)
for i in range(1, max_node_num+1):
if is_cutting_point[i] == 0:
merge_set.merge(i, i)
for a, b in edges:
if is_cutting_point[a] == 1 or is_cutting_point[b] == 1:
continue
merge_set.merge(a, b)
return cutting_point, merge_set.getClusters()
case_cnt = 1
while True:
m = int(input())
if m == 0:
break
edges = []
link = {}
max_node = -1
for i in range(m):
s = sys.stdin.readline()
a, b = map(int, s.split())
edges.append((a, b))
max_node = max(max_node, a)
max_node = max(max_node, b)
if a not in link:
link[a] = []
if b not in link:
link[b] = []
link[a].append(b)
link[b].append(a)
merge_set = MergeSetExt(max_key_val=max_node, trans_key_func=lambda x : x)
for node in range(1, max_node+1):
merge_set.merge(node, node)
for a, b in edges:
merge_set.merge(a, b)
clusters = merge_set.getClusters()
total_cnt = 1
total_exit_vertex_cnt = 0
# 枚举每一个连通的子图
for cluster in clusters:
e = []
for node in cluster:
if node in link:
for ne in link[node]:
if ne > node:
e.append((node, ne))
node2idx = { node: i+1 for i, node in enumerate(cluster) }
e = [ (node2idx[a], node2idx[b]) for a, b in e ]
e_link = {}
for a, b in e:
if a not in e_link:
e_link[a] = []
if b not in e_link:
e_link[b] = []
e_link[a].append(b)
e_link[b].append(a)
max_node_idx = len(cluster)
cutting_point, dcc_list = Tarjan.getVertexDCC(e, max_node_num=max_node_idx)
if len(cutting_point) == 0:
# 没有割点的情况下,必须至少放两个逃生点在子图里面(当然,如果子图就只有一个点,只放一个逃生点即可)
if len(cluster) == 1:
cnt = 1
total_exit_vertex_cnt += 1
else:
cnt = (len(cluster) * (len(cluster) - 1)) // 2 # 对2取组合数
total_exit_vertex_cnt += 2
total_cnt *= cnt
else:
node2dccid = [0] * (max_node_idx + 1)
for dcc_id, dcc in enumerate(dcc_list):
for node in dcc:
node2dccid[node] = dcc_id
for id, node in enumerate(cutting_point):
node2dccid[node] = 10000 + id
visit_e = set()
degree = [0] * len(dcc_list)
for node in cutting_point:
src_dcc_id = node2dccid[node]
for ne in e_link[node]:
dcc_id = node2dccid[ne]
if dcc_id < 10000 and (src_dcc_id, dcc_id) not in visit_e:
visit_e.add((src_dcc_id, dcc_id))
degree[dcc_id] += 1
cnt = 1
for i, val in enumerate(degree):
if val == 1:
# 点双连通分量的度是1,需要在双连通分量里面选一个点作为逃生点
cnt *= len(dcc_list[i])
total_exit_vertex_cnt += 1
total_cnt *= cnt
print(f'Case {case_cnt}: {total_exit_vertex_cnt} {total_cnt}')
case_cnt += 1