3312. Sorted GCD Pair Queries
題目 / Problem
中文: 給你一個長度為 n 的整數陣列 nums 和一個查詢陣列 queries。考慮所有滿足 0 <= i < j < n 的數對 (nums[i], nums[j]),計算每一對的最大公因數(GCD),把這些 GCD 值收集起來並由小到大排序,得到陣列 gcdPairs。對於每個查詢 queries[i],回傳 gcdPairs[queries[i]]。
English: You are given an integer array nums of length n and an array queries. For every pair (nums[i], nums[j]) with 0 <= i < j < n, compute its GCD. Collect all these GCD values and sort them ascending to form gcdPairs. For each query queries[i], return the element gcdPairs[queries[i]].
Constraints:
- 2 <= n == nums.length <= 10^5
- 1 <= nums[i] <= 5 * 10^4
- 1 <= queries.length <= 10^5
- 0 <= queries[i] < n * (n - 1) / 2
Worked example: nums = [2,3,4], queries = [0,2,2].
The three pairs give gcd(2,3)=1, gcd(2,4)=2, gcd(3,4)=1. Sorted: gcdPairs = [1,1,2]. So the answers are gcdPairs[0]=1, gcdPairs[2]=2, gcdPairs[2]=2 → [1,2,2].
名詞解釋 / Glossary
- GCD(最大公因數)/ greatest common divisor:能同時整除兩個數的最大正整數,例如
gcd(4,6)=2。/ The largest positive integer that divides both numbers. - 數對 / pair:從陣列中選兩個位置
i < j。總共有n*(n-1)/2個數對。/ Two chosen indicesi < j; there aren*(n-1)/2of them. - inclusion–exclusion / 容斥原理:一種計數技巧。要算「GCD 恰好等於
g」的數量,先算「GCD 是g的倍數」的數量,再扣掉「GCD 恰好是2g, 3g, …」的數量。/ A counting trick: count pairs whose GCD is a multiple ofg, then subtract those whose GCD is exactly2g, 3g, …. - frequency array / 計數陣列:
freq[v]記錄數值v出現幾次,讓我們用值域而非索引來思考。/freq[v]stores how many times valuevappears. - prefix sum / 前綴和:把
exact[1..g]累加,得到「GCD ≤ g 的數對數量」,可直接對應排序後的位置。/ Running total of exact counts, giving how many pairs have GCD ≤ g. - binary search / 二分搜尋:在單調遞增的前綴和上,用 O(log) 找出查詢索引落在哪個 GCD 值。/ On the monotonic prefix array, locate which GCD value a query index falls into in O(log).
- harmonic sum / 調和級數:
maxV/1 + maxV/2 + … ≈ maxV·ln(maxV),是「對每個 g 遍歷其倍數」的總成本。/ The cost of looping over multiples of everyg.
思路
中文: 最直接的想法是枚舉所有數對,算出每一對的 GCD,全部排序後直接回答查詢。但 n 最大到 10^5,數對數量約 5×10^9,光是產生 gcdPairs 就不可能,這條路必須放棄。關鍵觀察是:數值本身很小(最多 5×10^4),所以我們不該以「數對」為單位思考,而應以「GCD 的值」為單位思考——我只需要知道每個可能的 GCD 值 g 各出現幾次,就等於知道了整個排序後的陣列(它是一段段相同的值)。
於是問題轉成:對每個 g,有多少數對的 GCD 恰好是 g?直接算「恰好」很難,但算「GCD 是 g 的倍數」很容易:一個數對的 GCD 是 g 的倍數,等價於兩個數都能被 g 整除。設 c[g] 為 nums 中能被 g 整除的元素個數,那麼「兩數都被 g 整除」的數對數就是 c[g]*(c[g]-1)/2。這個數量包含了 GCD 是 g, 2g, 3g, … 的所有情形。用容斥原理,從大到小遍歷 g,把已算好的 exact[2g], exact[3g], … 扣掉,剩下的就是 exact[g]。
得到每個 GCD 值的出現次數後,對 g 由小到大做前綴和 prefix[g] = GCD ≤ g 的數對數。排序後的 gcdPairs 就是:先 exact[1] 個 1,再 exact[2] 個 2……所以要找 gcdPairs[q],只需找到最小的 g 使得 prefix[g] > q,用二分搜尋即可。預處理是調和級數 O(maxV·log maxV),每個查詢 O(log maxV),完全可行。
English: The naive idea—enumerate all pairs, compute every GCD, sort, and index—dies immediately: with n up to 10^5 there are ~5×10^9 pairs, far too many to even list. The saving observation is that the values are small (≤ 5×10^4). So instead of thinking pair-by-pair, think value-by-value: if I know, for each possible GCD g, how many pairs have that GCD, I effectively know the whole sorted array, since it's just runs of equal values.
Counting pairs with GCD exactly g directly is awkward, but counting pairs whose GCD is a multiple of g is easy: that happens exactly when both numbers are divisible by g. Let c[g] be how many elements of nums are divisible by g; then the number of such pairs is c[g]*(c[g]-1)/2. That total lumps together GCD values g, 2g, 3g, …. By inclusion–exclusion, iterate g from large to small and subtract the already-computed exact[2g], exact[3g], …; what remains is exact[g].
Once we have the count for each GCD value, take a prefix sum over g ascending: prefix[g] = number of pairs with GCD ≤ g. The sorted gcdPairs is exact[1] copies of 1, then exact[2] copies of 2, and so on. To answer gcdPairs[q], binary-search for the smallest g with prefix[g] > q. Preprocessing is the harmonic cost O(maxV·log maxV), and each query is O(log maxV).
逐步走查 / Walkthrough
Input: nums = [2,3,4], queries = [0,2,2]. Here maxV = 4, and freq = {2:1, 3:1, 4:1}.
Step 1 — compute exact[g] from large g to small (inclusion–exclusion):
| g | c[g] = # divisible by g |
both-divisible pairs c*(c-1)/2 |
subtract exact[2g]+exact[3g]+… |
exact[g] |
|---|---|---|---|---|
| 4 | freq[4]=1 | 0 | — | 0 |
| 3 | freq[3]=1 | 0 | — | 0 |
| 2 | freq[2]+freq[4]=2 | 2*1/2 = 1 |
exact[4]=0 |
1-0 = 1 |
| 1 | freq[1..4]=3 | 3*2/2 = 3 |
exact[2]+exact[3]+exact[4]=1 |
3-1 = 2 |
So GCD value 1 occurs 2 times, GCD value 2 occurs 1 time. (Sorted gcdPairs = [1,1,2], matching the statement.)
Step 2 — prefix sum over g ascending:
| g | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
prefix[g] |
2 | 3 | 3 | 3 |
Step 3 — answer each query by binary search for the smallest g with prefix[g] > q:
| query q | condition | smallest g with prefix[g] > q | answer |
|---|---|---|---|
| 0 | prefix[1]=2 > 0 ✓ | g = 1 | 1 |
| 2 | prefix[1]=2 not > 2; prefix[2]=3 > 2 ✓ | g = 2 | 2 |
| 2 | same as above | g = 2 | 2 |
Final answer: [1, 2, 2]. ✓
Solution — C
#include <stdlib.h>
/*
* 演算法 / Algorithm:
* 1) freq[v] 統計每個值出現次數 / count how often each value appears.
* 2) 對每個 g(由大到小),c=能被 g 整除的元素數,兩數皆整除的數對=c*(c-1)/2,
* 再用容斥扣掉 exact[2g],exact[3g],… 得到 GCD 恰為 g 的數對數 exact[g].
* 3) 對 g 做前綴和,二分搜尋回答每個查詢 / prefix-sum then binary search per query.
*/
int* gcdValuesForQueries(int* nums, int numsSize, int* queries,
int queriesSize, int* returnSize) {
int maxV = 0; // 值域上界 / largest value in nums
for (int i = 0; i < numsSize; i++) // 掃一遍找最大值 / scan for max
if (nums[i] > maxV) maxV = nums[i];
// calloc 配置並清零 / allocate zero-filled array; index = 值, 內容 = 出現次數
int* freq = calloc(maxV + 1, sizeof(int)); // freq[v] = count of value v
for (int i = 0; i < numsSize; i++)
freq[nums[i]]++; // 該值出現次數 +1 / bump count
// 用 long long:數對數可達 ~5e9,超過 int 上限 / pair counts overflow int
long long* exact = calloc(maxV + 1, sizeof(long long)); // exact[g] = pairs with gcd==g
for (int g = maxV; g >= 1; g--) { // 由大到小 / high g first for inclusion-exclusion
long long c = 0; // c = 能被 g 整除的元素個數 / #elements divisible by g
for (int m = g; m <= maxV; m += g) // 遍歷 g 的倍數 / walk multiples of g
c += freq[m]; // 累加這些值的出現次數 / sum their frequencies
long long both = c * (c - 1) / 2; // 兩數皆被 g 整除的數對數 / pairs both divisible by g
for (int m = 2 * g; m <= maxV; m += g) // 扣掉 gcd 是 2g,3g,… 的 / remove larger-multiple gcds
both -= exact[m]; // 容斥 / inclusion-exclusion subtraction
exact[g] = both; // 剩下的就是 gcd 恰為 g / what remains is gcd exactly g
}
// 前綴和:prefix[g] = gcd <= g 的數對總數 / running total of exact counts
long long* prefix = calloc(maxV + 1, sizeof(long long));
long long run = 0; // 累加器 / running sum
for (int g = 1; g <= maxV; g++) { // 由小到大累加 / accumulate ascending
run += exact[g];
prefix[g] = run; // gcd <= g 的數對數 / pairs with gcd <= g
}
int* ans = malloc(queriesSize * sizeof(int)); // 回傳陣列 / output array
for (int i = 0; i < queriesSize; i++) {
long long q = queries[i]; // 查詢的排序索引 / target index in sorted list
int lo = 1, hi = maxV; // 二分搜尋範圍 = 可能的 gcd 值 / search over gcd values
while (lo < hi) { // 找最小 g 使 prefix[g] > q / smallest g with prefix[g] > q
int mid = lo + (hi - lo) / 2; // 中點,避免 lo+hi 溢位 / midpoint w/o overflow
if (prefix[mid] > q) hi = mid; // 夠大,往左收 / big enough, keep left half
else lo = mid + 1; // 太小,往右找 / too small, go right
}
ans[i] = lo; // gcdPairs[q] 的值就是 lo / the answer value
}
free(freq); free(exact); free(prefix); // 釋放記憶體避免洩漏 / free to avoid leaks
*returnSize = queriesSize; // 回傳長度 / tell caller the size
return ans;
}
Solution — C++
#include <vector>
using namespace std;
/*
* 演算法 / Algorithm:
* 對每個 g 用「兩數皆被 g 整除」的數對數 (c*(c-1)/2),配合容斥算出 gcd 恰為 g 的數量,
* 再前綴和 + 二分搜尋回答查詢。/ Count pairs both divisible by g, inclusion-exclusion to get
* exact-gcd counts, prefix-sum, then binary search per query.
*/
class Solution {
public:
vector<int> gcdValuesForQueries(vector<int>& nums, vector<int>& queries) {
// *max_element 回傳最大值的迭代器,解參考取值 / max value in nums
int maxV = *max_element(nums.begin(), nums.end());
// vector<int> 自動初始化為 0 / frequency array, index = value
vector<int> freq(maxV + 1, 0);
for (int v : nums) // range-for:逐一取出元素 / iterate values
freq[v]++; // 該值計數 +1 / count this value
// long long 防溢位:數對數可達 ~5e9 / avoid int overflow on pair counts
vector<long long> exact(maxV + 1, 0); // exact[g] = # pairs with gcd == g
for (int g = maxV; g >= 1; --g) { // 由大到小 / descend for inclusion-exclusion
long long c = 0; // # elements divisible by g
for (int m = g; m <= maxV; m += g) // g 的倍數 / multiples of g
c += freq[m]; // 累加出現次數 / sum frequencies
long long both = c * (c - 1) / 2; // 兩數皆整除的數對 / pairs both divisible by g
for (int m = 2 * g; m <= maxV; m += g) // 扣掉更大倍數的 gcd / subtract larger-multiple gcds
both -= exact[m];
exact[g] = both; // gcd 恰為 g / gcd exactly g
}
// 前綴和:prefix[g] = gcd <= g 的數對數 / cumulative pair counts
vector<long long> prefix(maxV + 1, 0);
long long run = 0;
for (int g = 1; g <= maxV; ++g) // 由小到大累加 / accumulate ascending
prefix[g] = (run += exact[g]);
vector<int> ans; // 結果 / result
ans.reserve(queries.size()); // 預留空間避免重複配置 / preallocate for speed
for (long long q : queries) { // 逐個查詢 / for each query index
// lower_bound 找第一個 prefix[g] > q 的位置 / first g with prefix[g] > q
int lo = 1, hi = maxV; // 在 gcd 值域上二分 / binary search over gcd values
while (lo < hi) {
int mid = lo + (hi - lo) / 2; // 中點 / midpoint
if (prefix[mid] > q) hi = mid; // 夠大往左 / enough, shrink right bound
else lo = mid + 1; // 太小往右 / too small, move up
}
ans.push_back(lo); // 加入答案 / append answer
}
return ans;
}
};
複雜度 / Complexity
- Time: O(maxV·log maxV + q·log maxV) — 其中
maxV = 5×10^4。預處理時對每個g遍歷它的倍數,成本是調和級數maxV/1 + maxV/2 + … ≈ maxV·ln(maxV);每個查詢做一次二分搜尋O(log maxV),共q個查詢。與n幾乎無關(n只影響一次線性計數)。/ The multiples-loops sum to the harmonic costmaxV·ln(maxV); each of theqqueries is one binary search. - Space: O(maxV) —
freq、exact、prefix三個陣列大小都是值域maxV,與n無關;輸出佔O(q)。/ Three value-domain arrays plus theO(q)output.
Pitfalls & Edge Cases
- 整數溢位 / Overflow:數對數量最多約
n*(n-1)/2 ≈ 5×10^9,遠超過 32 位元int(約2.1×10^9)。所有計數與前綴和必須用long long,否則both和prefix會爆掉給出錯誤答案。/ Pair counts exceedint; uselong longeverywhere for counts. - 從大到小 vs 從小到大 / Iteration order:容斥時
exact[g]依賴exact[2g], exact[3g], …(更大的 g),所以必須由大到小計算;前綴和則相反,要由小到大累加。順序寫反會全錯。/ Inclusion-exclusion needsgdescending; the prefix sum needsgascending. - 二分搜尋的邊界 / Binary-search boundary:目標是「最小的
g使prefix[g] > q」(嚴格大於)。若寫成>=會把落在區間左端的查詢算錯一格(off-by-one)。/ Use strict>; using>=shifts answers by one. - 不要真的建出 gcdPairs / Don't materialize the array:
gcdPairs可能有5×10^9個元素,實體化會記憶體爆炸兼超時。整個解法的核心就是「只存每個 GCD 值的計數」而非展開。/ Never build the full sorted list; store per-value counts instead. - 重複值與相同數對 / Duplicates:如
nums=[2,2],c[2]=2給出2*1/2=1個數對,gcd=2,正確處理重複;公式c*(c-1)/2天然涵蓋i<j的無序數對,不會重複計數。/ Thec*(c-1)/2formula correctly handles duplicate values and counts each unordered pair once. - 值域用 nums 的最大值 / Sizing arrays:陣列開到
maxV(nums中的最大值)即可,開到常數5×10^4也行但略浪費;不可只開到n,因為索引是「數值」不是「位置」。/ Arrays are indexed by value, so size them bymaxV, not byn.