1. 程式人生 > 其它 >LOJ6807 「THUPC 2022 初賽」最小公倍樹

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;
}