← 題庫 / Archive
2026-07-17 Daily Hard ArrayHash TableMathBinary SearchCombinatoricsCountingNumber TheoryPrefix Sum

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 indices i < j; there are n*(n-1)/2 of them.
  • inclusion–exclusion / 容斥原理:一種計數技巧。要算「GCD 恰好等於 g」的數量,先算「GCD 是 g 的倍數」的數量,再扣掉「GCD 恰好是 2g, 3g, …」的數量。/ A counting trick: count pairs whose GCD is a multiple of g, then subtract those whose GCD is exactly 2g, 3g, ….
  • frequency array / 計數陣列freq[v] 記錄數值 v 出現幾次,讓我們用值域而非索引來思考。/ freq[v] stores how many times value v appears.
  • 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 every g.

思路

中文: 最直接的想法是枚舉所有數對,算出每一對的 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 cost maxV·ln(maxV); each of the q queries is one binary search.
  • Space: O(maxV)freqexactprefix 三個陣列大小都是值域 maxV,與 n 無關;輸出佔 O(q)。/ Three value-domain arrays plus the O(q) output.

Pitfalls & Edge Cases

  • 整數溢位 / Overflow:數對數量最多約 n*(n-1)/2 ≈ 5×10^9,遠超過 32 位元 int(約 2.1×10^9)。所有計數與前綴和必須用 long long,否則 bothprefix 會爆掉給出錯誤答案。/ Pair counts exceed int; use long long everywhere for counts.
  • 從大到小 vs 從小到大 / Iteration order:容斥時 exact[g] 依賴 exact[2g], exact[3g], …(更大的 g),所以必須由大到小計算;前綴和則相反,要由小到大累加。順序寫反會全錯。/ Inclusion-exclusion needs g descending; the prefix sum needs g ascending.
  • 二分搜尋的邊界 / Binary-search boundary:目標是「最小的 g 使 prefix[g] > q」(嚴格大於)。若寫成 >= 會把落在區間左端的查詢算錯一格(off-by-one)。/ Use strict >; using >= shifts answers by one.
  • 不要真的建出 gcdPairs / Don't materialize the arraygcdPairs 可能有 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 的無序數對,不會重複計數。/ The c*(c-1)/2 formula correctly handles duplicate values and counts each unordered pair once.
  • 值域用 nums 的最大值 / Sizing arrays:陣列開到 maxVnums 中的最大值)即可,開到常數 5×10^4 也行但略浪費;不可只開到 n,因為索引是「數值」不是「位置」。/ Arrays are indexed by value, so size them by maxV, not by n.