题意
P3623 [APIO2008]免费道路
给出一个包含 n 个顶点和 m 条无向边的无向图。已知边有 0 和 1 两种类型,试求原图的一棵生成树,使得其恰好包含 k 条类型 0 的边。若不存在满足条件的生成树则输出 no solution
1≤n≤2×104,1≤m≤105,0≤k≤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′,此时可以看作是向 G′ 中加入边 (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′,则我们需要在该图中用 n−1−x 条边连通 n−(x+1)=n−x 个顶点(每加入一条边会合并两个连通块,即减少一个,共减少 x 个)。若新图连通,则显然有解。新图不连通的情况可以对应到原图不连通的情况。
综上,跑三遍 kruskal 即可。
时间复杂度 O(mlogn)
代码
#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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现