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\)
當然,上段討論的是假設 Kruskal 已經在執行的過程中,我們已經知道哪些點在同一個連通塊的情況。那麼在所有過程開始之前,我們如何知道哪些邊有可能被選中呢?仔細看上段的結論,我們發現一個驚人的性質:對於 \(k\),不管怎麼選,選出來的邊一定有一個端點是大於等於 \(L\) 的第一個 \(k\) 的倍數。也就是說,在開始時,對於每個 \(k\),我們只需要保留以這個點為端點的邊就可以了,這樣的邊大約有 \(\frac{R - L + 1}{k} - 1\) 條,而其他的邊可以全部扔掉,不會影響答案!設 \(n = R - L + 1\)
至此,我們已經可以得到一個簡單的做法:把這 \(\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;
}