Search for a command to run...
Progressive hints first, then the full explanation and implementation when you're ready to cash out.
Review status
AI-generated and still unreviewed. Double-check the details before internalizing them.
Hints
Open only as much as you need to keep the solve alive.
With exactly chosen edges, being not a tree is the same as containing a cycle. If it were acyclic, it would be a forest with edges on vertices, so it would have exactly one component: a tree. Tiny fact, huge punch.
Sort all edges by weight. If the first edges already do not form a tree, that sum is instantly optimal, because no other edges can be lighter.
Otherwise, the first edges form a tree . Every candidate answer is made by adding some edges outside and deleting the same number of edges from , while making sure at least one cycle survives.
For one added edge , the cycle is exactly that edge plus the path from to in . So you may delete only a tree edge outside that path. The best deletion is the maximum-weight tree edge outside the path.
You never need to add more than two non-tree edges. After checking all one-edge swaps, the only two-edge swap that can still matter uses the two cheapest non-tree edges; delete the two heaviest tree edges common to both of their tree paths.
Key Reframe
The phrase not a tree is trying to bait you into thinking about disconnected components. Do not take the bait. Since we must choose exactly edges, the chosen graph is not a tree if and only if it contains a cycle.
Why? A graph on vertices with edges and no cycle is a forest. A forest with components has edges, so , hence . That is a tree. So the only way to fail tree-ness is to have a cycle. Much cleaner.
Sort edges by weight. Let be the first edges, and let
.
If these edges already contain a cycle, output . You literally cannot beat the sum of the cheapest edges. Done.
So assume those edges form a tree. From now on, call it . Every other edge is at least as heavy as every edge of .
What An Answer Looks Like
Any valid answer starts from , adds some edges outside , and removes the same number of tree edges. It must leave at least one cycle alive.
Take one cycle that survives in the final set. Suppose uses edges outside . Then we need to include those expensive edges, and delete tree edges that are not on this cycle. The false move is assuming can be large and then trying to optimize all possible cycles. That road leads straight into implementation hell.
The nice fact: an optimal answer exists with .
Here is the proof without magic hand-waving. Suppose some surviving cycle uses non-tree edges. Among those non-tree edges, take the two cheapest, call them . Among the deleted tree edges, take the two heaviest, call them .
If one of does not have one of on its path in , then that single non-tree edge creates its own fundamental cycle, and deleting that missed tree edge keeps the cycle alive. This one-swap costs no more than the original big swap, because every remaining added edge is at least as heavy as every remaining deleted tree edge.
Otherwise, both and contain both and on their tree paths. Then add and delete . The symmetric difference of the two tree paths, plus the two non-tree edges, is still a cycle. Again the cost is no worse than the original. So big swaps are useless. Fancy, but not cursed.
Now we only need one-swap and two-swap cases.
One-Swap Case
Take a non-tree edge . In , the path from to plus is a cycle.
To keep that cycle after deleting one tree edge, the deleted edge must be outside this path. So for this , the best move is
.
We compute this for every non-tree edge.
Use HLD on . A path becomes disjoint index intervals. Its complement is also intervals after sorting/merging, so a segment tree can query the maximum edge weight outside the path in per non-tree edge.
Two-Swap Case
A real two-swap adds two non-tree edges. If deleting one of the two tree edges was already enough with one of those non-tree edges, then the one-swap case already beats or ties it. So the only two-swap that matters deletes two tree edges that lie on both added edges' tree paths.
Even better: after all one-swaps are checked, the only pair of added edges worth checking is the two cheapest non-tree edges.
Suppose some useful two-swap adds edges and deletes tree edges . If a cheaper non-tree edge misses or , then using alone and deleting the missed tree edge is a one-swap no worse than this two-swap. Therefore, for a two-swap to improve on all one-swaps, every non-tree edge cheaper than the more expensive of must contain both and . In particular, the two cheapest non-tree edges contain both , and using them is no worse.
So we take the first two non-tree edges after sorting. Let their tree paths be and . We need the two maximum-weight tree edges in . If there are at least two, the candidate is
.
HLD gives each path as intervals. Intersect the two interval lists, query top two edge weights on each overlap, and merge the answers.
Correctness Sketch
The cheapest edges are an obvious lower bound. If they already contain a cycle, they are optimal.
Otherwise they form a tree . Any valid set must contain some cycle. Looking at that cycle, if it uses at least three non-tree edges, the two-cheapest-added/two-heaviest-deleted argument above converts it into a no-more-expensive one-swap or two-swap. Thus checking swaps of size at most two is enough.
For one-swap, the only cycle created by adding is its fundamental cycle, so the deleted tree edge must be outside the - path. Taking the heaviest such edge is optimal.
For two-swap, any case not deleting two common path edges is dominated by a one-swap. Any useful remaining two-swap can be replaced by one using the two cheapest non-tree edges. Therefore the algorithm checks every possible optimal form.
Complexity
Sorting costs . HLD plus the segment tree costs . Each one-swap query costs , and the single two-swap query costs . Total complexity is , easily fine for .
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = (1LL << 62);
const ll NEG = -(1LL << 60);
void setIO() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
}
struct DSU {
vector<int> p, sz;
DSU(int n = 0) { init(n); }
void init(int n) {
p.resize(n);
sz.assign(n, 1);
iota(p.begin(), p.end(), 0);
}
int find(int x) {
return p[x] == x ? x : p[x] = find(p[x]);
}
bool unite(int a, int b) {
a = find(a);
b = find(b);
if (a == b) return false;
if (sz[a] < sz[b]) swap(a, b);
p[b] = a;
sz[a] += sz[b];
return true;
}
};
struct Edge {
int u, v, id;
ll w;
};
struct Best {
ll a, b;
Best(ll x = NEG, ll y = NEG) : a(x), b(y) {}
};
Best mergeBest(Best x, Best y) {
array<ll, 4> vals = {x.a, x.b, y.a, y.b};
sort(vals.begin(), vals.end(), greater<ll>());
return Best(vals[0], vals[1]);
}
struct SegTree {
int n;
vector<Best> st;
void init(const vector<ll>& base) {
n = 1;
while (n < (int)base.size()) n <<= 1;
st.assign(2 * n, Best());
for (int i = 0; i < (int)base.size(); i++) st[n + i] = Best(base[i], NEG);
for (int i = n - 1; i >= 1; i--) st[i] = mergeBest(st[i << 1], st[i << 1 | 1]);
}
Best query(int l, int r) const {
if (l > r) return Best();
l += n;
r += n;
Best left, right;
while (l <= r) {
if (l & 1) left = mergeBest(left, st[l++]);
if (!(r & 1)) right = mergeBest(st[r--], right);
l >>= 1;
r >>= 1;
}
return mergeBest(left, right);
}
};
struct HLD {
int n, cur;
vector<vector<pair<int, ll>>> adj;
vector<int> parent, depth, heavy, head, pos, sz;
vector<ll> parW;
SegTree seg;
HLD(const vector<vector<pair<int, ll>>>& g) { init(g); }
void init(const vector<vector<pair<int, ll>>>& g) {
adj = g;
n = adj.size();
parent.assign(n, -1);
depth.assign(n, 0);
heavy.assign(n, -1);
head.assign(n, 0);
pos.assign(n, 0);
sz.assign(n, 1);
parW.assign(n, NEG);
vector<int> order;
order.reserve(n);
vector<int> st = {0};
parent[0] = -2;
while (!st.empty()) {
int v = st.back();
st.pop_back();
order.push_back(v);
for (auto [to, w] : adj[v]) {
if (to == parent[v]) continue;
parent[to] = v;
depth[to] = depth[v] + 1;
parW[to] = w;
st.push_back(to);
}
}
for (int i = n - 1; i >= 0; i--) {
int v = order[i];
int bestSize = 0;
sz[v] = 1;
for (auto [to, w] : adj[v]) {
if (parent[to] != v) continue;
sz[v] += sz[to];
if (sz[to] > bestSize) {
bestSize = sz[to];
heavy[v] = to;
}
}
}
vector<ll> base(n, NEG);
cur = 0;
vector<pair<int, int>> starts = {{0, 0}};
while (!starts.empty()) {
auto [v, h] = starts.back();
starts.pop_back();
for (int x = v; x != -1; x = heavy[x]) {
head[x] = h;
pos[x] = cur;
base[cur++] = parW[x];
for (auto [to, w] : adj[x]) {
if (parent[to] == x && to != heavy[x]) starts.push_back({to, to});
}
}
}
seg.init(base);
}
vector<pair<int, int>> pathIntervals(int a, int b) const {
vector<pair<int, int>> res;
while (head[a] != head[b]) {
if (depth[head[a]] < depth[head[b]]) swap(a, b);
res.push_back({pos[head[a]], pos[a]});
a = parent[head[a]];
}
if (depth[a] > depth[b]) swap(a, b);
if (pos[a] + 1 <= pos[b]) res.push_back({pos[a] + 1, pos[b]});
return res;
}
vector<pair<int, int>> normalize(vector<pair<int, int>> v) const {
sort(v.begin(), v.end());
vector<pair<int, int>> res;
for (auto [l, r] : v) {
if (l > r) continue;
if (res.empty() || l > res.back().second + 1) res.push_back({l, r});
else res.back().second = max(res.back().second, r);
}
return res;
}
ll outsidePathMax(int a, int b) const {
auto ints = normalize(pathIntervals(a, b));
Best ans;
int last = 0;
for (auto [l, r] : ints) {
if (last < l) ans = mergeBest(ans, seg.query(last, l - 1));
last = max(last, r + 1);
}
if (last < n) ans = mergeBest(ans, seg.query(last, n - 1));
return ans.a;
}
Best pathIntersectionBest(int a, int b, int c, int d) const {
auto x = normalize(pathIntervals(a, b));
auto y = normalize(pathIntervals(c, d));
Best ans;
int i = 0, j = 0;
while (i < (int)x.size() && j < (int)y.size()) {
int l = max(x[i].first, y[j].first);
int r = min(x[i].second, y[j].second);
if (l <= r) ans = mergeBest(ans, seg.query(l, r));
if (x[i].second < y[j].second) i++;
else j++;
}
return ans;
}
};
int main() {
setIO();
int tc;
cin >> tc;
while (tc--) {
int n, m;
cin >> n >> m;
vector<Edge> edges(m);
for (int i = 0; i < m; i++) {
cin >> edges[i].u >> edges[i].v >> edges[i].w;
--edges[i].u;
--edges[i].v;
edges[i].id = i;
}
sort(edges.begin(), edges.end(), [](const Edge& a, const Edge& b) {
if (a.w != b.w) return a.w < b.w;
return a.id < b.id;
});
DSU dsu(n);
ll baseSum = 0;
bool isTree = true;
for (int i = 0; i < n - 1; i++) {
baseSum += edges[i].w;
if (!dsu.unite(edges[i].u, edges[i].v)) isTree = false;
}
if (!isTree) {
cout << baseSum << '\n';
continue;
}
vector<vector<pair<int, ll>>> adj(n);
for (int i = 0; i < n - 1; i++) {
auto &e = edges[i];
adj[e.u].push_back({e.v, e.w});
adj[e.v].push_back({e.u, e.w});
}
HLD hld(adj);
ll ans = INF;
for (int i = n - 1; i < m; i++) {
auto &e = edges[i];
ll removed = hld.outsidePathMax(e.u, e.v);
if (removed != NEG) ans = min(ans, baseSum + e.w - removed);
}
if (m - (n - 1) >= 2) {
auto &a = edges[n - 1];
auto &b = edges[n];
Best common = hld.pathIntersectionBest(a.u, a.v, b.u, b.v);
if (common.b != NEG) ans = min(ans, baseSum + a.w + b.w - common.a - common.b);
}
cout << (ans == INF ? -1 : ans) << '\n';
}
}#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = (1LL << 62);
const ll NEG = -(1LL << 60);
void setIO() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
}
struct DSU {
vector<int> p, sz;
DSU(int n = 0) { init(n); }
void init(int n) {
p.resize(n);
sz.assign(n, 1);
iota(p.begin(), p.end(), 0);
}
int find(int x) {
return p[x] == x ? x : p[x] = find(p[x]);
}
bool unite(int a, int b) {
a = find(a);
b = find(b);
if (a == b) return false;
if (sz[a] < sz[b]) swap(a, b);
p[b] = a;
sz[a] += sz[b];
return true;
}
};
struct Edge {
int u, v, id;
ll w;
};
struct Best {
ll a, b;
Best(ll x = NEG, ll y = NEG) : a(x), b(y) {}
};
Best mergeBest(Best x, Best y) {
array<ll, 4> vals = {x.a, x.b, y.a, y.b};
sort(vals.begin(), vals.end(), greater<ll>());
return Best(vals[0], vals[1]);
}
struct SegTree {
int n;
vector<Best> st;
void init(const vector<ll>& base) {
n = 1;
while (n < (int)base.size()) n <<= 1;
st.assign(2 * n, Best());
for (int i = 0; i < (int)base.size(); i++) st[n + i] = Best(base[i], NEG);
for (int i = n - 1; i >= 1; i--) st[i] = mergeBest(st[i << 1], st[i << 1 | 1]);
}
Best query(int l, int r) const {
if (l > r) return Best();
l += n;
r += n;
Best left, right;
while (l <= r) {
if (l & 1) left = mergeBest(left, st[l++]);
if (!(r & 1)) right = mergeBest(st[r--], right);
l >>= 1;
r >>= 1;
}
return mergeBest(left, right);
}
};
struct HLD {
int n, cur;
vector<vector<pair<int, ll>>> adj;
vector<int> parent, depth, heavy, head, pos, sz;
vector<ll> parW;
SegTree seg;
HLD(const vector<vector<pair<int, ll>>>& g) { init(g); }
void init(const vector<vector<pair<int, ll>>>& g) {
adj = g;
n = adj.size();
parent.assign(n, -1);
depth.assign(n, 0);
heavy.assign(n, -1);
head.assign(n, 0);
pos.assign(n, 0);
sz.assign(n, 1);
parW.assign(n, NEG);
vector<int> order;
order.reserve(n);
vector<int> st = {0};
parent[0] = -2;
while (!st.empty()) {
int v = st.back();
st.pop_back();
order.push_back(v);
for (auto [to, w] : adj[v]) {
if (to == parent[v]) continue;
parent[to] = v;
depth[to] = depth[v] + 1;
parW[to] = w;
st.push_back(to);
}
}
for (int i = n - 1; i >= 0; i--) {
int v = order[i];
int bestSize = 0;
sz[v] = 1;
for (auto [to, w] : adj[v]) {
if (parent[to] != v) continue;
sz[v] += sz[to];
if (sz[to] > bestSize) {
bestSize = sz[to];
heavy[v] = to;
}
}
}
vector<ll> base(n, NEG);
cur = 0;
vector<pair<int, int>> starts = {{0, 0}};
while (!starts.empty()) {
auto [v, h] = starts.back();
starts.pop_back();
for (int x = v; x != -1; x = heavy[x]) {
head[x] = h;
pos[x] = cur;
base[cur++] = parW[x];
for (auto [to, w] : adj[x]) {
if (parent[to] == x && to != heavy[x]) starts.push_back({to, to});
}
}
}
seg.init(base);
}
vector<pair<int, int>> pathIntervals(int a, int b) const {
vector<pair<int, int>> res;
while (head[a] != head[b]) {
if (depth[head[a]] < depth[head[b]]) swap(a, b);
res.push_back({pos[head[a]], pos[a]});
a = parent[head[a]];
}
if (depth[a] > depth[b]) swap(a, b);
if (pos[a] + 1 <= pos[b]) res.push_back({pos[a] + 1, pos[b]});
return res;
}
vector<pair<int, int>> normalize(vector<pair<int, int>> v) const {
sort(v.begin(), v.end());
vector<pair<int, int>> res;
for (auto [l, r] : v) {
if (l > r) continue;
if (res.empty() || l > res.back().second + 1) res.push_back({l, r});
else res.back().second = max(res.back().second, r);
}
return res;
}
ll outsidePathMax(int a, int b) const {
auto ints = normalize(pathIntervals(a, b));
Best ans;
int last = 0;
for (auto [l, r] : ints) {
if (last < l) ans = mergeBest(ans, seg.query(last, l - 1));
last = max(last, r + 1);
}
if (last < n) ans = mergeBest(ans, seg.query(last, n - 1));
return ans.a;
}
Best pathIntersectionBest(int a, int b, int c, int d) const {
auto x = normalize(pathIntervals(a, b));
auto y = normalize(pathIntervals(c, d));
Best ans;
int i = 0, j = 0;
while (i < (int)x.size() && j < (int)y.size()) {
int l = max(x[i].first, y[j].first);
int r = min(x[i].second, y[j].second);
if (l <= r) ans = mergeBest(ans, seg.query(l, r));
if (x[i].second < y[j].second) i++;
else j++;
}
return ans;
}
};
int main() {
setIO();
int tc;
cin >> tc;
while (tc--) {
int n, m;
cin >> n >> m;
vector<Edge> edges(m);
for (int i = 0; i < m; i++) {
cin >> edges[i].u >> edges[i].v >> edges[i].w;
--edges[i].u;
--edges[i].v;
edges[i].id = i;
}
sort(edges.begin(), edges.end(), [](const Edge& a, const Edge& b) {
if (a.w != b.w) return a.w < b.w;
return a.id < b.id;
});
DSU dsu(n);
ll baseSum = 0;
bool isTree = true;
for (int i = 0; i < n - 1; i++) {
baseSum += edges[i].w;
if (!dsu.unite(edges[i].u, edges[i].v)) isTree = false;
}
if (!isTree) {
cout << baseSum << '\n';
continue;
}
vector<vector<pair<int, ll>>> adj(n);
for (int i = 0; i < n - 1; i++) {
auto &e = edges[i];
adj[e.u].push_back({e.v, e.w});
adj[e.v].push_back({e.u, e.w});
}
HLD hld(adj);
ll ans = INF;
for (int i = n - 1; i < m; i++) {
auto &e = edges[i];
ll removed = hld.outsidePathMax(e.u, e.v);
if (removed != NEG) ans = min(ans, baseSum + e.w - removed);
}
if (m - (n - 1) >= 2) {
auto &a = edges[n - 1];
auto &b = edges[n];
Best common = hld.pathIntersectionBest(a.u, a.v, b.u, b.v);
if (common.b != NEG) ans = min(ans, baseSum + a.w + b.w - common.a - common.b);
}
cout << (ans == INF ? -1 : ans) << '\n';
}
}