1. 程式人生 > >UVA 11997 K Smallest Sums 優先隊列 多路合並

UVA 11997 K Smallest Sums 優先隊列 多路合並

algorithm span 大白 while logs truct %d 算法 省賽

  vjudge 上題目鏈接:UVA 11997

  題意很簡單,就是從 k 個數組(每個數組均包含 k 個正整數)中各取出一個整數相加(所以可以得到 kk 個結果),輸出前 k 小的和。

  這時訓練指南上的一道題,這道題的簡化版其實在 15 年的廣東省省賽出現過,當時是以送分題的形式出現的,可我還是沒能做出來,歸根到底還是看書不夠,接觸的題型不夠多。

*************************************************************大白書上的講解開始***********************************************************************

  在解決這個問題之前,先看看它的簡化版:給出兩個長度為 n 的有序表 A 和 B,分別在 A 和 B 中任取一個數並相加,可以得到 n2 個和。求這些和中最小的 n 個和。

  這個問題可以轉化為多路歸並排序問題:即把 k 個有序表合並成一個有序表(假定每個表已經是升序排列)—— 用優先隊列維護每個表的“當前元素”。如果一共有 n 個元素,則時間復雜度為 O(nlogk)。此時我們需要把這 n2 個和組織成如下 n 個有序表:

  表1:A1 + B1 <= A1 + B2 <= A1 + B3 <= ....
  表2:A2 + B1 <= A2 + B2 <= A2

+ B3 <= ....

  ......

  表n:An + B1 <= An + B2 <= An + B3 <= ....

  其中第 a 張表裏的元素形如 Aa + Bb 。用二元組 (s, b) 來表示一個元素,其中 s = Aa + Bb 。為什麽不保存 A 的下標 a 呢?因為我們用不到 a 的值。如果我們需要得到一個元素 (s, b) 在表 a 中的下一個元素 (s‘, b+1),只需要計算 s‘ = Aa + Bb+1 = Aa + Bb - Bb + Bb+1 = s - Bb + Bb+1,並不需要知道 a 是多少。代碼裏可以用到如下結構體來表示。

struct Item {
    int s, b;   // s = A[a] + B[b]。這裏的 a 並不重要,因此不保存
    Item(int s, int b): s(s), b(b) { }
    bool operator < (const Item &rhs) const {
        return s > rhs.s;
    }
};

  因為在任意時刻,優先隊列中恰好有 n 個元素,一共取了 n 次最小值,因此時間復雜度為 O(nlogn)。代碼如下:

//假設 A 和 B 的元素已經從小到大排序好
void merge(int *A, int *B, int *C, int n)
{
    priority_queue<Item> q;
    for(int i = 0; i < n; ++i)
        q.push(Item(A[i] + B[0], 0));
    for(int i = 0; i < n; ++i) {
        Item item = q.top();  q.pop();  // 取出 A[a] + B[b]
        C[i] = item.s;
        int b = item.b;
        if(b + 1 < n)   q.push(Item(item.s - B[b] + B[b + 1], b + 1));
        // 加入 A[a] + B[b + 1] = s - B[b] + B[b + 1]
    }
}

  而這題不是兩個表,而是 k 個表,怎麽辦呢?兩兩合並就可以了(想一想,為什麽),代碼如下:

const int maxn = 768;
int A[maxn][maxn];

int main()
{
    int n;
    while(scanf("%d", &n) == 1) {
        for(int i = 0; i < n; ++i) {
            for(int j = 0; j < n; ++j)  scanf("%d", &A[i][j]);
            sort(A[i], A[i] + n);
        }
        for(int i = 0; i < n; ++i)          // 兩兩合並
            merge(A[0], A[i], A[0], n);     // (*)
        
        printf("%d",A[0][0]);               // 輸出結果
        for(int i = 0; i < n; ++i)
            printf(" %d", A[0][i]);
        printf("\n");
    }
    return 0;
}

  註意(*)處,merge 函數對 A[0] 又讀又寫,會有問題嗎?(其實仔細觀察函數就可以發現不會有任何影響,因為原來的值讀完一次後就再也沒用了,所以可以重復利用空間,便有了之後的寫)。程序的復雜度為 O(k2logk)。另外,沒有必要在一開始就把所有 k2 個元素保存在二維數組 A 中,而是可以每次只讀 k 個元素,然後合並,從而大大降低空間復雜度。

*************************************************************大白書上的講解結束***********************************************************************

  以上是大白書上的講解,根據它的思路我自己實現了本題的代碼:

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
#define  For(i,s,t)  for(int i = (s); i < (t); ++i)
const int K = 760;

int b[K],c[K];

struct Item
{
    int sum, idx;
    Item(int _sum, int _idx): sum(_sum), idx(_idx) {}
    bool operator < (const Item &rhs) const {
        return sum > rhs.sum;
    }
};

void _merge(int *b, int *c, int n)
{
    priority_queue<Item> pq;
    For(i, 0, n) {
        pq.push(Item(b[i] + c[0], 0));
    }
    For(i, 0, n) {
        Item Min = pq.top();
        pq.pop();
        b[i] = Min.sum;
        if(Min.idx + 1 < n) {
            pq.push(Item(Min.sum - c[Min.idx] + c[Min.idx + 1], Min.idx + 1));
        }
    }
}

int main()
{
    int k;
    while(~scanf("%d",&k)) {
        For(i, 0, k) {
            scanf("%d", b + i);
        }
        sort(b, b + k);
        For(p, 1, k) {
            For(i, 0, k) {
                scanf("%d", c + i);
            }
            sort(c, c + k);
            _merge(b, c, k);
        }
        printf("%d", b[0]);
        For(i, 1, k) {
            printf(" %d", b[i]);
        }
        puts("");
    }
    return 0;
}

  感覺這道題蘊含的算法思想挺經典的,以後可能還會碰到,需要好好體會才行。

UVA 11997 K Smallest Sums 優先隊列 多路合並