最小树形图
最小树形图
简介:
在一个有向图中构造一颗最小生成树
(有根树)
解法:
朱刘算法:
- 判断图的连通性:如果所有点不联通,无解
- 除根节点外寻找每个点的最小入边,记pre[v]为点v的入边顶点,in[v]为最小入边的边权
- 判断是否图中是否存在环,如果无环则ans += in[v],输出答案,否则进行4
- 缩点:将成环的点v,v,v,....,v缩为一个点,ans +=in[v~v]
更新环外的点到环的距离:w -= in[v]
不断重复3,4直到没有环输出ans,算法复杂度O(n*m)
借用别人的图来解释一下缩点:
在第2步出现了环2-3-4,ans += in[2-3-4],之后更新点1到环的距离,因为对于同一个点,只有一个最小入边,此时对于点4最小入边存在两个(1,3),而边3-4已经加入到了ans之中,所以对于边1-4要消去这个影响,故而此时边1-4的边权变为了(4-2 =)2,减去的2就是边3-4的影响,我们可以理解为此时我们断开了边3-4,连上了边1-4【依据:除根节点外,每个节点都有且只有一个最小入边】
无根树的处理:虚根
增加一个超级起点,连接所有顶点,边权为sum
sum = 前m条边的边权和+1
此时无解的条件为ans == -1||ans>=2*sum
因为我们只会选一个点为真实的根,所以sum只会被选一次,其余边加起来也不会超过sum,所以如果ans>=2*sum,说明超级起点至少选择了两个顶点作为根
借用其他人的图解释一下虚根:
此时顶点4为虚根,连接了其他所有顶点,边权为sum
绿色的边为实际选择的最小树形图,所以真实的根节点为0
而答案为ans-sum
如果我们想要求得真实的根节点,只需要在求最小树形图的时候,如果选择的边的顶点为超级起点,则更新pos为i,因为总的边数为m+n,而只有后n条边为新增的超级起点边,所以只要pos-m+1即为真实的根节点(节点从0开始)
代码实现:
- 有根树
#include<iostream> # include<cmath> # include<cstring> # include<algorithm> using namespace std; # define int long long # define endl "\n" const int N = 1e3 + 10, M = 4e4 + 10, inf = 0x3f3f3f3f; int n, m; struct edg { int a, b; int w; } e[M]; int in[N];//最小入边 int pre[N];//最小入边前驱 int id[N], vis[N];//id:缩点后的编号 int root = 1;//根 int ans = 0;//答案 int getans() { int cnt = 0, u, v; int laz; while (1) { /*初始化所有最小入边为inf*/ for (int i = 1; i <= n; ++i) { in[i] = inf; id[i] = vis[i] = 0; } //1.寻找除根节点外所有点的最小入边 for (int i = 1; i <= m; ++i) { /*如果a,b不同并且当前边权小于点b的最小入边则更新*/ if (e[i].a ^ e[i].b && e[i].w < in[e[i].b]) { pre[e[i].b] = e[i].a; in[e[i].b] = e[i].w; } } in[root] = 0; for (int i = 1; i <= n; ++i) { //2.判断连通性,如果某个点的最小入边为inf则说明其无入边,不联通 if (in[i] == inf) return (ans = -1); ans += in[i]; u = i; //3.判断是否成环 while (u ^ root && vis[u]^i && !id[u]) { //若当前节点不为根并且非入点(非环)并且尚未编号就不断向前遍历 vis[u] = i; u = pre[u]; } /*如果当前节点不为根节点并且没有编号,说明成环*/ if (u ^ root && !id[u]) { /*缩点*/ id[u] = ++cnt; for (v = pre[u]; v ^ u; v = pre[v]) { id[v] = cnt; } } } /*如果没有缩点说明无环返回ans*/ if (!cnt) return ans; //更新所有节点编号 for (int i = 1; i <= n; ++i) { if (!id[i]) id[i] = ++cnt; } //4.更新环外点到环的距离 for (int i = 1; i <= m; ++i) { laz = in[e[i].b];//最小入边 /*如果a,b不属于同一个id说明a,b不为同一个环则更新距离*/ if ((e[i].a = id[e[i].a]) != (e[i].b = id[e[i].b])) { e[i].w -= laz; } } n = cnt;//更新节点数目(缩点后) root = id[root];//更新根节点 cnt = 0; } } int ll = 0; void solve() { ll++; cin >> n >> m; ans = 0; root = 1; for (int i = 1; i <= m; ++i) { int a, b, w; cin >> a >> b >> w; e[i] = {a + 1, b + 1, w}; } getans();//朱刘算法 printf("Case #%d: ", ll); if (ans != -1) printf("%d\n", ans); else printf("Possums!\n"); } int tt; signed main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); tt = 1; cin >> tt; while (tt--) solve(); return 0; }
- 无根树-虚根
#include<iostream> # include<cmath> # include<cstring> # include<algorithm> using namespace std; # define int long long # define endl "\n" const int N = 1e3 + 10, M = 4e4 + 10, inf = 0x3f3f3f3f; int n, m; struct edg { int a, b; int w; } e[M]; int in[N];//最小入边 int pre[N];//最小入边前驱 int id[N], vis[N];//id:缩点后的编号 int root = 1;//根 int ans = 0;//答案 int pos = 0; int getans() { int cnt = 0, u, v; int laz; while (1) { /*初始化所有最小入边为inf*/ for (int i = 1; i <= n; ++i) { in[i] = inf; id[i] = vis[i] = 0; } //1.寻找除根节点外所有点的最小入边 for (int i = 1; i <= m; ++i) { /*如果a,b不同并且当前边权小于点b的最小入边则更新*/ if (e[i].a ^ e[i].b && e[i].w < in[e[i].b]) { pre[e[i].b] = e[i].a; in[e[i].b] = e[i].w; if (e[i].a == root) pos = i; } } in[root] = 0; for (int i = 1; i <= n; ++i) { //2.判断连通性,如果某个点的最小入边为inf则说明其无入边,不联通 if (in[i] == inf) return (ans = -1); ans += in[i]; u = i; //3.判断是否成环 while (u ^ root && vis[u]^i && !id[u]) { //若当前节点不为根并且非入点(非环)并且尚未编号就不断向前遍历 vis[u] = i; u = pre[u]; } /*如果当前节点不为根节点并且没有编号,说明成环*/ if (u ^ root && !id[u]) { /*缩点*/ id[u] = ++cnt; for (v = pre[u]; v ^ u; v = pre[v]) { id[v] = cnt; } } } /*如果没有缩点说明无环返回ans*/ if (!cnt) return ans; //更新所有节点编号 for (int i = 1; i <= n; ++i) { if (!id[i]) id[i] = ++cnt; } //4.更新环外点到环的距离 for (int i = 1; i <= m; ++i) { laz = in[e[i].b];//最小入边 /*如果a,b不属于同一个id说明a,b不为同一个环则更新距离*/ if ((e[i].a = id[e[i].a]) != (e[i].b = id[e[i].b])) { e[i].w -= laz; } } n = cnt;//更新节点数目(缩点后) root = id[root];//更新根节点 cnt = 0; } } int ll = 0; void solve() { while (cin >> n >> m) { ans = 0; int sum = 0; int nowm = m; pos = 0; for (int i = 1; i <= m; ++i) { int a, b, w; cin >> a >> b >> w; e[i] = (edg) {a + 1, b + 1, w}; sum += w; } sum++; n++; root = n; /*新增超级起点并连接其余所有顶点,边权为sum*/ for (int i = 1; i < n; ++i) { e[++m] = (edg) {n, i, sum}; } getans();//朱刘算法 if (ans == -1 || ans >= sum * 2) puts("impossible"); else printf("%lld %lld\n", ans - sum, pos - nowm - 1); puts(""); } } int tt; signed main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); tt = 1; // cin >> tt; while (tt--) solve(); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效