【题解】P3623 [APIO2008]免费道路
题意
给出一个包含 \(n\) 个顶点和 \(m\) 条无向边的无向图。已知边有 \(0\) 和 \(1\) 两种类型,试求原图的一棵生成树,使得其恰好包含 \(k\) 条类型 \(0\) 的边。若不存在满足条件的生成树则输出 no solution
\(1 \leq n \leq 2 \times 10^4, 1 \leq m \leq 10^5, 0 \leq k \leq n - 1\)
思路
最小生成树。
对于连通的图,可以考虑从其中删去若干条边,构造出一棵符合条件的生成树。
假设图中仅剩类型为 \(1\) 的边,则此时图可能不连通。因此我们发现如果一条类型为 \(0\) 的边连接了两个当前尚未连通的顶点,说明当前边应该被加入图中。
当然,加入图中的边集 \(A\) 不唯一。为了使加入图中的边数量最少,这里可以用类似 kruskal 的方法求这些边。
图中仅剩类型为 \(0\) 的边的情况同理,令此时加入图中的边集为 \(B\)。
求一棵生成树,实际上等价于希望图中仅剩 \(A\) 或 \(B\) 时令图连通需要加入的边数最小。不妨令图中只剩下 \(A\) 和 \(B\) 中的边,然后对原图跑一遍类似 kruskal 的算法,求出令图连通需要加入的边。
但是我们需要满足类型 \(0\) 的数量限制。因此我们可以考虑先对于类型 \(0\) 中不属于 \(A\) 的边求一部分最小生成树,把 \(k\) 条类型 \(0\) 的边加满;然后对于类型 \(1\) 中不属于 \(B\) 的边求剩下部分的最小生成树,加满类型 \(1\) 的 \(n - 1 - k\) 条边。
容易看出跑最小生成树时遍历边的顺序不影响图的连通性,或者可以看作是给每条边赋上一个附加的权值,使得按照附加权值排序以后边按照我们遍历的顺序排列。
无解的情况为:
-
图不连通
-
\(|A| > k\) 或 \(|B| > n - 1 - k\)
原因:上面令 \(A, B\) 的大小取到了可能的最小值,即 \(|A|, |B|\) 分别小于等于图中类型 \(0\) 和类型 \(1\) 的边的数量下限。如果仍然存在大于的情况说明无解。 -
最终加不满 \(k\) 条类型 \(0\) 的边或加不满 \(n - 1 - k\) 条类型 \(1\) 的边。
原因:若加不满 \(k\) 条类型 \(0\) 的边,此时有两种可能:
1. 图中类型 \(0\) 的边数量不足 \(k\) 条。
2. 未加入图的边连接的两个顶点已经被 \(A, B\) 以及之后加入的类型 \(0\) 的边连通了。这意味着对于任意一种 \(A, B\) 的选取方式,类型 \(0\) 中存在恒定数量的“废边”。
换言之,加入这些废边会导致出现若干环。此时在环中删去一条边 \((u, v)\),得到新的图 \(G^{\prime}\),此时可以看作是向 \(G^{\prime}\) 中加入边 \((u, v)\)。又因为加入 \((u, v)\) 会出现环,所以 \((u, v)\) 可以看作是该局面下的废边,即废边数量恒定。在此局面无法加满 \(k\) 条边,意味着在所有可能的局面下都无法加满 \(k\) 条边。
不能加入废边的原因是生成树只有 \(n - 1\) 条边,加入废边会导致无法加入原本对连通性有贡献的边。若加不满 \(n - 1 - k\) 条类型 \(1\) 的边,此时又有两种可能:
1. 图中类型 \(1\) 的边不足 \(n - 1 - k\) 条
2. 原图不连通
加满 \(k\) 条类型 \(0\) 的边时,假设此时使用了 \(x\) 条边,因为图中无环(不存在一条边连接两个已经连通的顶点),所以有 \(x + 1\) 个顶点被连通。不妨令所有连通块缩成一个点,构造出新图 \(G^{\prime}\),则我们需要在该图中用 \(n - 1 - x\) 条边连通 \(n - (x + 1) = n - x\) 个顶点(每加入一条边会合并两个连通块,即减少一个,共减少 \(x\) 个)。若新图连通,则显然有解。新图不连通的情况可以对应到原图不连通的情况。
综上,跑三遍 kruskal 即可。
时间复杂度 \(O(m \log n)\)
代码
#include <cstdio>
#include <vector>
using namespace std;
const int maxn = 2e4 + 5;
const int maxm = 1e5 + 5;
int n, m, k;
int fa[maxn];
int u[maxm], v[maxm], c[maxm];
vector<int> g[2];
bool vis[maxm];
void init()
{
for (int i = 1; i <= n; i++) fa[i] = i;
}
int get(int x)
{
if (fa[x] == x) return x;
return fa[x] = get(fa[x]);
}
void merge(int x, int y)
{
x = get(x);
y = get(y);
if (x != y) fa[y] = x;
}
bool check()
{
int cnt = 0;
for (int i = 1; i <= n; i++)
if (get(i) == i) cnt++;
return (cnt == 1);
}
int main()
{
int cnt0, cnt1;
cnt0 = cnt1 = 0;
scanf("%d%d%d", &n, &m, &k);
init();
for (int i = 1; i <= m; i++)
{
scanf("%d%d%d", &u[i], &v[i], &c[i]);
g[c[i]].push_back(i);
merge(u[i], v[i]);
}
if (!check())
{
puts("no solution");
return 0;
}
init();
for (int i = 0; i < g[1].size(); i++) merge(u[g[1][i]], v[g[1][i]]);
for (int i = 0; i < g[0].size(); i++)
{
if (get(u[g[0][i]]) != get(v[g[0][i]]))
{
merge(u[g[0][i]], v[g[0][i]]);
vis[g[0][i]] = true;
cnt0++;
}
}
if (cnt0 > k)
{
puts("no solution");
return 0;
}
init();
for (int i = 0; i < g[0].size(); i++) merge(u[g[0][i]], v[g[0][i]]);
for (int i = 0; i < g[1].size(); i++)
{
if (get(u[g[1][i]]) != get(v[g[1][i]]))
{
merge(u[g[1][i]], v[g[1][i]]);
vis[g[1][i]] = true;
cnt1++;
}
}
if (cnt1 > n - 1 - k)
{
puts("no solution");
return 0;
}
init();
for (int i = 1; i <= m; i++)
if (vis[i]) merge(u[i], v[i]);
for (int i = 0; i < g[0].size(); i++)
{
if (cnt0 == k) break;
if (get(u[g[0][i]]) != get(v[g[0][i]]))
{
merge(u[g[0][i]], v[g[0][i]]);
vis[g[0][i]] = true;
cnt0++;
}
}
if (cnt0 < k)
{
puts("no solution");
return 0;
}
for (int i = 0; i < g[1].size(); i++)
{
if (cnt1 == n - 1 - k) break;
if (get(u[g[1][i]]) != get(v[g[1][i]]))
{
merge(u[g[1][i]], v[g[1][i]]);
vis[g[1][i]] = true;
cnt1++;
}
}
for (int i = 1; i <= m; i++)
if (vis[i]) printf("%d %d %d\n", u[i], v[i], c[i]);
return 0;
}