LOJ6807 「THUPC 2022 初赛」最小公倍树

LOJ6807 「THUPC 2022 初赛」最小公倍树

题目大意

题目链接

给定两个正整数 \(L, R​\)。考虑一张 \(R - L + 1​\) 个节点的无向图,节点编号分别为 \(L, L + 1, \dots, R​\),任意两个节点之间都有连边,边权为两点点权的最小公倍数(\(\mathrm{lcm}​\))。求这张图的最小生成树的边权和。

数据范围:\(1\leq L\leq R\leq 10^6\)\(R - L \leq 10^5\)

本题题解

考虑用 Kruskal 算法求最小生成树,也就是贪心,每次取出两个端点不在一个连通块的、边权最小的边。问题是,这张图里的边实在是太多了,所以我们要考虑,什么样的边有可能成为被选中的边。从最小公倍数的特殊设定入手。

最小”公倍数,这个要求太严格了,有点烦人。不妨这样重建一张新图:对于每个正整数 \(k\),在所有点权为 \(k\) 的倍数的点里,两两连边,边权为点权之积除以 \(k\)。也就是说,我们现在只关心“公倍数”,而不关心“最小”的要求。所以,这张新图里会包含原图所有的边,同时多出一些边。不过,多出的边边权一定大于原有的边,所以不会对答案产生影响。接下来我们只需要在这张新图上求最小生成树。

考虑点权为 \(k\) 的倍数的点所产生的边,哪些有可能被当前 Kruskal 的贪心选中呢?一定是点权最小的两个不在同一连通块里的点之间的边。具体来说,是大于等于 \(L\) 的第一个 \(k\) 的倍数,和最小的与它不在同一连通块里的 \(k\) 的倍数,之间的边。这是因为所有边边权都是点权之积除以 \(k\),在 \(k\) 固定的情况下,一定选点权之积最小的边。

当然,上段讨论的是假设 Kruskal 已经在执行的过程中,我们已经知道哪些点在同一个连通块的情况。那么在所有过程开始之前,我们如何知道哪些边有可能被选中呢?仔细看上段的结论,我们发现一个惊人的性质:对于 \(k\)不管怎么选,选出来的边一定有一个端点是大于等于 \(L\) 的第一个 \(k\) 的倍数。也就是说,在开始时,对于每个 \(k\),我们只需要保留以这个点为端点的边就可以了,这样的边大约有 \(\frac{R - L + 1}{k} - 1\) 条,而其他的边可以全部扔掉,不会影响答案!设 \(n = R - L + 1\),那么我们需要的总边数是 \(\mathcal{O}\left(\sum_{k = 1}^{n} \frac{n}{k}\right) = \mathcal{O}(n\log n)\) 级别的(调和级数的结论)。

至此,我们已经可以得到一个简单的做法:把这 \(\mathcal{O}(n\log n)\) 条边拿出来,排序,然后执行 Kruskal 算法。这样做所需的空间复杂度是 \(\mathcal{O}(n\log n)\) 的。还有一种更省空间的写法是,用 \(\texttt{std::priority_queue}\),每次弹出一条边(记下这条边对应的 \(k\)),如果两个端点在同一连通块内,就把右端点挪到该 \(k\) 对应的下一个右端点,得到一条新的边,加入队列。因为同一个 \(k\) 对应的边权是随着右端点的递增而递增的,所以它本质上就是给这 \(n\) 个有序序列做归并排序。

时间复杂度 \(\mathcal{O}(n\log^2 n)\),空间复杂度 \(\mathcal{O}(n)\)

参考代码

// problem: LOJ6807
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;

template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }

const int MAXN = 1e5 + 1;

int L, R;
int f[MAXN + 5], g[MAXN + 5];

int fa[MAXN + 5], sz[MAXN + 5];
int get_fa(int u) {
	return (fa[u] == u) ? u : (fa[u] = get_fa(fa[u]));
}
void unite(int u, int v) {
	int uu = get_fa(u);
	int vv = get_fa(v);
	if (uu != vv) {
		if (sz[uu] > sz[vv])
			swap(uu, vv);
		fa[uu] = vv;
		sz[vv] += sz[uu];
	}
}

int main() {
	cin >> L >> R;
	priority_queue<pair<ll, int> > que;
	for (int i = 1; i <= R - L; ++i) {
		f[i] = ((L - 1) / i + 1) * i; // >= L 的第一个 i 的倍数
		if (f[i] + i <= R) {
			g[i] = f[i] + i;
			que.push(mk(-(ll)f[i] / i * g[i], i));
		}
	}
	for (int i = 1; i <= R - L + 1; ++i) {
		fa[i] = i;
		sz[i] = 1;
	}
	ll ans = 0;
	for (int i = 1; i <= R - L; ++i) {
		pair<ll, int> t = que.top();
		que.pop();
		while (get_fa(f[t.se] - L + 1) == get_fa(g[t.se] - L + 1)) {
			g[t.se] += t.se;
			if (g[t.se] <= R) {
				que.push(mk(-(ll)f[t.se] / t.se * g[t.se], t.se));
			}
			t = que.top();
			que.pop();
		}
		ans -= t.fi; // 根据默认大根堆的特点,t.fi 是负数
		unite(f[t.se] - L + 1, g[t.se] - L + 1);
		g[t.se] += t.se;
		if (g[t.se] <= R) {
			que.push(mk(-(ll)f[t.se] / t.se * g[t.se], t.se));
		}
		// cerr << "add " << f[t.se] << " " << g[t.se] << endl;
	}
	cout << ans << endl;
	return 0;
}
posted @ 2022-04-10 18:15  duyiblue  阅读(237)  评论(2编辑  收藏  举报