396 矿场搭建(点连通分量)

1. 问题描述:

煤矿工地可以看成是由隧道连接挖煤点组成的无向图。为安全起见,希望在工地发生事故时所有挖煤点的工人都能有一条出路逃到救援出口处。于是矿主决定在某些挖煤点设立救援出口,使得无论哪一个挖煤点坍塌之后,其他挖煤点的工人都有一条道路通向救援出口。请写一个程序,用来计算至少需要设置几个救援出口,以及不同最少救援出口的设置方案总数。

输入格式

输入文件有若干组数据,每组数据的第一行是一个正整数 N,表示工地的隧道数。接下来的 N 行每行是用空格隔开的两个整数 S 和 T,表示挖煤点 S 与挖煤点 T 由隧道直接连接。注意,每组数据的挖煤点的编号为 1∼Max,其中 Max 表示由隧道连接的挖煤点中,编号最大的挖煤点的编号,可能存在没有被隧道连接的挖煤点。输入数据以 0 结尾。

输出格式

每组数据输出结果占一行。其中第 i 行以 Case i: 开始(注意大小写,Case 与 i 之间有空格,i 与 : 之间无空格,: 之后有空格)。其后是用空格隔开的两个正整数,第一个正整数表示对于第 i 组输入数据至少需要设置几个救援出口,第二个正整数表示对于第 i 组输入数据不同最少救援出口的设置方案总数。输入数据保证答案小于 264,输出格式参照以下输入输出样例。

数据范围

1 ≤ N ≤ 500,
1 ≤ Max ≤ 1000

输入样例:

9
1  3
4  1
3  5
1  2
2  6
1  5
6  3
1  6
3  2
6
1  2
1  3
2  4
2  5
3  6
3  7
0

输出样例:

Case 1: 2 4
Case 2: 4 1
来源:https://www.acwing.com/problem/content/description/398/

2. 思路分析:

这道题目属于点的双连通分量的题目,因为图不一定是连通的,所以我们可以分别看每一个连通块考虑每一个连通块的情况即可,其中每一个连通块中点的双连通分量主要分为两种情况:

  • 无割点,所以不管坏掉哪一个点都是连通的,我们应该设置两个出口,因为有可能坏掉的是出口,设置两个出口的时候当一个出口坏掉之后可以走到另外一个出口
  • 有割点:分为只有一个割点和大于1个割点的情况,当割点数目为1时,我们可以将点的双连通分量中不是割点的其余点中选择一个位置作为出口即可,如果割点坏掉了那么当前点的双连通分量的其余点可以走到这个出口,当出口坏掉了那么其余点可以走到割点也即可以走到其余的出口,当不是割点的点坏掉了那么也可以走到割点进而走到其余的出口,所以在非割点的位置处设置一个出口即可;当点的双连通分量中割点的数目大于1的时候任何一点坏掉了那么点的双连通分量中的其余点都可以走到另外一个割点的位置进而走到其余的出口,所以不用设置出口

割点相当于是一个出口,无向图中点的双连通分量与有向图的强连通分量和无向图的边双连通分量有所区别,点的双连通分量中的每一个割点至少属于两个点的双连通分量中(这里的割点是从整体看的,某一个割点属于至少属于两个点的双连通分量中),所以我们在求解点的所有双连通分量的过程也有所不同,但是在求解的是和大部分过程是类似的,点的双连通分量也需要借助于栈来记录正在遍历的节点编号,使用数组dfn和low分别记录遍历每一个节点的时间戳和当前节点往下遍历的过程中能够到达的最早时间戳,我们在求解点的双连通分量的过程中标记遍历的点是否是割点:主要判断:dfn[x] <= low[y]是否成立,如果成立说明节点y能够遍历到的最早的时间戳就是x也即不能够到达其余的节点,并且我们在遍历节点x的邻接点的时候使用一个count来记录满足dfn[x] <= low[y]的邻接点数目,如果当前的节点x不是根节点或者是count > 1那么说明当前的节点x是割点,如果当前的节点是根节点并且count = 1说明当前的节点x不是割点,因为去掉节点x还是连通的,我们可以使用一个数组cut来记录对应的节点编号是否是割点,当我们找到一个割点之后那么将割点所在的点的双连通分量找出来,这里在找的时候需要注意循环的判断条件为t != next,其中t为栈stk的栈顶元素,next为当前节点u的下一个邻接点,这里的判断条件与边的双连通分量要区分开,不弹出当前的节点u是因为当前的节点u是另外一个双连通分量的节点,这个节点应该在更早的连通分量中弹出,由于t !=next 所以割点是没有被弹出来的,所以循环结束之后需要往连通分量中加入割点u(割点也是点的双连通分量中的节点),当求解完所有连通块的点的双连通分量之后那么遍历每一个点的双连通分量求解割点的数目,根据割点的数目求解答案,如果当前连通块中割点数目为0,那么根据连通分量中点的数目,如果只有一个点那么需要放置一个出口(特判),大于一个点那么需要在连通分量中任意位置中放置两个出口,方案数目为Cn2,如果只有一个割点那么在不是割点的位置放置一个出口,方案数目为dcc[i].size() - 1,当割点数目大于2之后不用管,也即不用放置出口,因为每一个连通块都是独立的,所以方案数目相乘即可。

求解点的双连通分量其实是一个缩点的过程,将每一个点的双连通分量缩成一个点,画画图更容易理解其中的求解过程。

3. 代码如下:

from typing import List


class Solution:
    # dcc_cnt表示点连通分量的个数, dcc记录每一个点连通分量对应的节点编号, stk存储当前正在遍历的节点编号, timestamp表示遍历节点的时间戳, top表示栈顶元素位置, root表示根节点
    dcc_cnt, dcc, stk, cut, timestamp, top, root = None, None, None, None, None, None, None

    def tarjan(self, u: int, dfn: List[int], low: List[int], g: List[List[int]]):
        dfn[u] = low[u] = self.timestamp + 1
        self.timestamp += 1
        self.stk[self.top + 1] = u
        self.top += 1
        # 当前的节点是点的双连通分量
        if u == self.root and not g[u]:
            self.dcc_cnt += 1
            self.dcc[self.dcc_cnt].append(u)
            return
        count = 0
        for next in g[u]:
            if dfn[next] == 0:
                self.tarjan(next, dfn, low, g)
                low[u] = min(low[u], low[next])
                if dfn[u] <= low[next]:
                    # 计算当前dfn[u] <= low[next]的数量
                    count += 1
                    # 当前的点是割点
                    if u != self.root or count > 1: self.cut[u] = 1
                    self.dcc_cnt += 1
                    # 求解当前割点所在点的双连通分量中的所有节点
                    while True:
                        t = self.stk[self.top]
                        self.top -= 1
                        self.dcc[self.dcc_cnt].append(t)
                        # 注意等于的是next而不是等于u, 因为循环结束之后还会加入u这个割点, 不能够弹出这个割点, 这个割点还属于另外一个连通分量需要在另外一个连通分量中弹出
                        if t == next: break
                    self.dcc[self.dcc_cnt].append(u)
            else:
                low[u] = min(low[u], dfn[next])

    def process(self):
        # 矿井的最大编号为1000
        T = 0
        while True:
            g = [list() for i in range(1010)]
            m = int(input())
            if m == 0: break
            n = 0
            for i in range(m):
                a, b = map(int, input().split())
                # 无向边
                g[a].append(b)
                g[b].append(a)
                # 求解矿井的最大编号
                n = max(n, a, b)
            dfn, low = [0] * (n + 10), [0] * (n + 10)
            # cut用来标记哪些点是割点
            self.stk, self.dcc, self.cut = [0] * (n + 10), [list() for i in range(n + 10)], [0] * (n + 10)
            self.timestamp = self.dcc_cnt = self.top = 0
            for i in range(1, n + 1):
                # 只有当没有被访问过的时候才调用tarjan算法
                if dfn[i] == 0:
                    self.root = i
                    self.tarjan(i, dfn, low, g)
            # res记录需要的救援出口的数量, num表示可以选择的救援出口的方案数目
            res, num = 0, 1
            for i in range(1, self.dcc_cnt + 1):
                count = 0
                for next in self.dcc[i]:
                    if self.cut[next] == 1:
                        count += 1
                t = len(self.dcc[i])
                if count == 0:
                    if t > 1:
                        res += 2
                        num *= t * (t - 1) // 2
                    else:
                        res += 1
                elif count == 1:
                    res += 1
                    num *= (t - 1)
            T += 1
            print("Case {}: {} {}".format(T, res, num))


if __name__ == "__main__":
    Solution().process()

猜你喜欢

转载自blog.csdn.net/qq_39445165/article/details/121406088