UVA 11997 K Smallest Sums 優先隊列 多路合並
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
......
表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 優先隊列 多路合並