洛谷 P3225 [HNOI2012] 矿场搭建 题解

题面

题面提取

给定一个无向图可能不连通),需要在某些节点设置出口,使得无论图中哪个节点坍塌被删除),其他所有节点都能到达至少一个出口。要求求出最少需要设置多少个出口,以及在最少出口情况下的方案数

解题思路

这是一道典型的图论问题,需要用到割点点双连通分量的概念。

  1. 割点:如果一个节点被删除后,图的连通分量数量增加,那么这个节点就是割点。
  2. 点双连通分量:不包含割点的极大连通子图。

解题步骤:

  1. 使用 Tarjan 算法找出所有割点。
  2. 求出所有的点双连通分量
  3. 对于每个点双连通分量
  • 如果没有割点,需要设置2个出口(防止其中一个出口坍塌),方案数为 C(size,2)
  • 如果有 1 个割点,需要设置 1 个出口(如果割点坍塌,可以通过这个出口逃生),方案数为 size-1
  • 如果有 2 个及以上割点,不需要设置出口(可以通过其他分量的出口逃生)。
  1. 将所有分量的结果相乘得到总方案数。

代码实现(含注释):

/*信友队保佑AC!!*/
#include <bits/stdc++.h>
using namespace std;
const int N = 505; // 最大节点数
vector<int> G[N];  // 图的邻接表表示
int dfn[N];        // DFS访问顺序编号
int low[N];        // 通过回边能到达的最小编号
int cut[N];        // 标记节点是否为割点
int n, m;          // n:实际节点数,m:边数
int idx;           // DFS时间戳
int cnt;           // 点双连通分量计数器
stack<int> st;     // 用于Tarjan算法的栈
vector<int> dcc[N];// 存储每个点双连通分量中的节点
/* Tarjan算法求割点和点双连通分量
 u:当前节点, root:DFS树的根节点 */
void tarjan(int u, int root)
{
	dfn[u] = low[u] = ++idx; // 初始化dfn和low
	st.push(u); // 节点入栈
	int child = 0; // 记录DFS树中子节点数量
	for (int v : G[u]) // 遍历所有邻接节点
	{
		if (!dfn[v]) // 如果v未被访问过
		{
			tarjan(v, root); // 递归访问v
			low[u] = min(low[u], low[v]); // 更新low值
			// 判断是否为割点
			if (low[v] >= dfn[u])
			{
				child++;
				// 如果不是根节点或者是根节点但有多个子树
				if (u != root || child > 1)
				{
					cut[u] = 1; // 标记为割点
				}
				// 找到一个点双连通分量
				cnt++;
				dcc[cnt].clear(); // 清空当前分量
				// 弹出栈中节点直到v,这些节点构成一个点双连通分量
				while (true)
				{
					int x = st.top();
					st.pop();
					dcc[cnt].push_back(x);
					if (x == v)
					{
						break;
					}
				}
				dcc[cnt].push_back(u); // 把u也加入分量
			}
		}
		else // 如果v已被访问过,是回边
		{
			low[u] = min(low[u], dfn[v]); // 更新low值
		}
	}
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	int T = 0; // 测试用例编号
	while (cin >> m, m) // 循环处理每个测试用例
	{
		// 初始化
		for (int i = 0; i < N; i++)
		{
			G[i].clear();
		}
		memset(dfn, 0, sizeof(dfn));
		memset(low, 0, sizeof(low));
		memset(cut, 0, sizeof(cut));
		n = idx = cnt = 0;
		// 读入图
		for (int i = 0; i < m; i++)
		{
			int u, v;
			cin >> u >> v;
			G[u].push_back(v);
			G[v].push_back(u);
			n = max(n, max(u, v)); // 更新实际节点数
		}
		// 寻找所有点双连通分量
		for (int i = 1; i <= n; i++)
		{
			if (!dfn[i]) // 如果未访问过
			{
				tarjan(i, i); // 以i为根进行Tarjan算法
			}
		}
		// 计算结果
		unsigned long long ans1 = 0, ans2 = 1; // ans1:最少出口数,ans2:方案数
		for (int i = 1; i <= cnt; i++) // 遍历每个点双连通分量
		{
			int num = 0; // 该分量中割点数量
			int sz = dcc[i].size(); // 该分量中节点数
			// 统计该分量中的割点数量
			for (int u : dcc[i])
			{
				if (cut[u])
				{
					num++;
				}
			}
			// 根据割点数量决定出口设置方案
			if (num == 0) // 无割点,需要设置2个出口
			{
				ans1 += 2;
				ans2 *= sz * (sz - 1) / 2; // C(sz,2)种选择
			}
			else if (num == 1) // 1个割点,需要设置1个出口
			{
				ans1 += 1;
				ans2 *= sz - 1; // 不能选割点,所以有(sz-1)种选择
			}
			// 有2个及以上割点不需要设置出口
		}
		// 输出结果
		cout << "Case " << ++T << ": " << ans1 << " " << ans2 << endl;
	}
	return 0;
}
// 完结,散花 ^_^ ^_^

代码实现(无注释):

#include <bits/stdc++.h>
using namespace std;
const int N = 505;
vector<int> G[N];
int dfn[N], low[N], cut[N];
int n, m, idx, cnt;
stack<int> st;
vector<int> dcc[N];
void tarjan(int u, int root)
{
	dfn[u] = low[u] = ++idx;
	st.push(u);
	int child = 0;
	for (int v : G[u])
	{
		if (!dfn[v])
		{
			tarjan(v, root);
			low[u] = min(low[u], low[v]);
			if (low[v] >= dfn[u])
			{
				child++;
				if (u != root || child > 1)
				{
					cut[u] = 1;
				}
				cnt++;
				dcc[cnt].clear();
				while (true)
				{
					int x = st.top();
					st.pop();
					dcc[cnt].push_back(x);
					if (x == v)
					{
						break;
					}
				}
				dcc[cnt].push_back(u);
			}
		}
		else
		{
			low[u] = min(low[u], dfn[v]);
		}
	}
}
int main()
{
//	freopen ( ".in", "r", stdin );
//	freopen ( ".out", "w", stdout );
	ios::sync_with_stdio ( false );
	cin.tie ( 0 );
	cout.tie ( 0 );
	int T = 0;
	while (cin >> m, m)
	{
		for (int i = 0; i < N; i++)
		{
			G[i].clear();
		}
		memset(dfn, 0, sizeof(dfn));
		memset(low, 0, sizeof(low));
		memset(cut, 0, sizeof(cut));
		n = idx = cnt = 0;
		for (int i = 0; i < m; i++)
		{
			int u, v;
			cin >> u >> v;
			G[u].push_back(v);
			G[v].push_back(u);
			n = max(n, max(u, v));
		}
		for (int i = 1; i <= n; i++)
		{
			if (!dfn[i])
			{
				tarjan(i, i);
			}
		}
		unsigned long long ans1 = 0, ans2 = 1;
		for (int i = 1; i <= cnt; i++)
		{
			int num = 0;
			int sz = dcc[i].size();
			for (int u : dcc[i])
			{
				if (cut[u])
				{
					num++;
				}
			}
			if (num == 0)
			{
				ans1 += 2;
				ans2 *= sz * (sz - 1) / 2;
			}
			else if (num == 1)
			{
				ans1 += 1;
				ans2 *= sz - 1;
			}
		}
		cout << "Case " << ++T << ": " << ans1 << " " << ans2 << endl;
	}
	return 0;
}

样例测试:

image

AC记录:

image

感受:

这道题目综合考察了图论中的割点和双连通分量知识,难度适中但需要细心。

最后
不要打开
不要打开
不要打开
不要打开
不要打开
不要打开
不要打开
不要打开
不要打开
不要打开
不要打开
不要打开
不要打开
不要打开

你还是打开了

祝大家都能AK IOI

1 个赞

@王天皓 建议不要法AC代码

1 个赞

六,真“无注释”

@CZF2919 不要在意这些细节(

1 个赞

问一问,最后的那个格式咋搞

image
image
image

1 个赞

@CZF2919 这样

1 个赞