1 qoj14549 造桥与砍树 题解
造桥与砍树
题面
给定 \(n\) 个点,每个点有权值 \(t[i]\) ,任意两个点 \(x, y\) 之间的边权为 \((t[x] + t[y]) \bmod k\),求这张图的最小生成树的边权和。
\(1 \le n \le 10^5\)
\(0 \le t[i] \le 10^9, 1 \le k \le 10^9\)
题解
这道题求最小生成树,如果点数或者边数少的话,我们还可以考虑 kruskal 或者 prim。
不过这题显然是不想让我们这么做的,因为这是个稠密图,我们可以尝试将 prim 算法进行一些改动。
考虑 prim 算法是怎么求最小生成树的?
维护一个最小生成树的集合,然后每次找到集合中的点到集合外权值最小的边,然后将这条边归入最小生成树。
这道题我们可以用类似的过程。
首先,我们肯定会将 \(t[i] \bmod k\) ,所以我们考虑对于一个 \(x\) ,使 \((t[x] + t[y]) \bmod k\) 最小的 \(y\) 一定是满足 \(t[y] \ge k - t[x]\) 的最小的 \(t[y]\),或者是 \(t[y]\) 最小的。
所以我们维护一个按照 \(t[x]\) 排序的未加入最小生成树的点集,每次我们只需要在点集中二分出符合条件的点即可。
然后再维护一个边权从小到大排序的边集,每次从边集中选出边权最小的一条边加入最小生成树。
每次取出一条边,一定有一个点属于最小生成树,一个点不属于,那么我们将两个点都找一次最小边加入边集,这样能够保证时刻边集中都保存着生成树中所有点到点集外点的最小边。保证正确性。
时间复杂度 \(O(n \log n)\)
code
#include <iostream>
#include <cstring>
#include <cstdio>
#include <set>
#include <queue>
using namespace std;
namespace michaele {
const int N = 1e5 + 10;
int n, k;
int t[N];
struct node {
int z, x, y;
bool operator < (const node &t) const {
return t.z < z;
}
};
set <pair <int, int> > s;
priority_queue <node> q;
bool vis[N];
void add (int x) {
if (!s.size ()) return;
auto it = s.lower_bound ({k -t[x], 0});
if (it == s.end ()) {
q.push ({(t[x] + s.begin ()->first) % k, x, s.begin ()->second});
} else {
q.push ({(t[x] + it->first) % k, x, it->second});
}
}
void solve () {
set <pair <int, int> > es;
priority_queue <node> eq;
s.swap (es);
q.swap (eq);
ios :: sync_with_stdio (0);
cin.tie (0);
cout.tie (0);
cin >> n >> k;
for (int i = 1; i <= n; i ++) {
vis[i] = 0;
}
for (int i = 1; i <= n; i ++) {
cin >> t[i];
t[i] %= k;
s.insert ({t[i], i});
}
long long ans = 0;
int st = s.begin ()->second;
s.erase (s.begin ());
add (st);
while (q.size ()) {
auto tmp = q.top ();
q.pop ();
if (!s.count ({t[tmp.y], tmp.y})) continue;
ans += tmp.z;
s.erase (s.lower_bound({t[tmp.y], tmp.y}));
if (s.empty ()) break;
add (tmp.y);
add (tmp.x);
}
cout << ans << endl;
}
void Main () {
int T;
cin >> T;
while (T --) {
solve ();
}
}
}
int main () {
michaele :: Main ();
return 0;
}

浙公网安备 33010602011771号