UVA11997 K Smallest Sums 题解

题意简述

一个大小为$n$的方阵,每行都选一个数,得到一个和。一共能得到$n^n$个和。求出最小的前$n$个。重复的算多次。$n<=700$。

思路框架

每两行都来一个多路归并即珂。

具体思路

今天去看了刘汝佳的小蓝书。感谢刘汝佳,让我知道优先队列原来还能这么用。

首先,每行里面顺序不重要,先排序再说。

前置知识:n路归并问题

我们应该都知道“二路归并”问题:两个有序的序列,要合并成一个有序的序列。解法很简单,维护两个指针,哪个小就把哪个加入答案,然后让指针++。利用它,我们珂以写出大名鼎鼎的「归并排序」算法。

$n$路归并问题就是:$n$个序列,每个序列都是有序的。要合并成一个长度为$n^2$的有序的序列。

但是我们发现,我们要维护$n$个指针,一次维护指针是$O(n)$的,我们一共有$n^2$的元素,就变成$O(n^3)$了。怎么样更快呢?我们珂以用优先队列来找到值最小的那个指针。这样维护一次指针是$O(logn)$的,总共就是$O(n^2logn)$的了。而且原问题中我们只需要求前面的$n$个,所以这会更快,变成每次$O(nlogn)$的时间。

如何转化

我们的原问题是有$n$行$n^n$个和的,如何转化?

首先我们珂以每两行之间逐个合并。然后对于两行$A$和$B$的问题,我们只需要生成这样$n$个序列即珂:
第一个:$A[1]+B[1],A[1]+B[2],…A[1]+B[n]$。
第二个:$A[2]+B[1],A[2]+B[2],…A[2]+B[n]$。

第$n$个:$A[n]+B[1],A[n]+B[2]…A[n]+B[n]$。
然后跑一个$n$路归并即珂。这样做$n-1$次,每次都只求前面$n$个小的,我们就珂以求出$n^n$个和中最小的$n$个了。

实现细节:关于如何维护指针

如果您足够强,您完全不用看这个。

我们维护两个值,$s,b$。$s$表示目前的$A[a]+B[b]$,$b$表示在$B$中的下标。我们完全不用知道$A$中的下标,因为$A[a]$是不会改变的。我们有$(s,b)$,下一个指针$(s’,b’)$其实就是$(s-B[b]+B[b+1],b+1)$。

在优先队列中,我们显然是要以$s$为关键字判断优先级的。

—— 刘汝佳的思路

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <bits/stdc++.h>
using namespace std;
namespace Flandre_Scarlet
{
#define N 822
#define F(i,l,r) for(int i=l;i<=r;++i)
#define D(i,r,l) for(int i=r;i>=l;--i)
#define Fs(i,l,r,c) for(int i=l;i<=r;c)
#define Ds(i,r,l,c) for(int i=r;i>=l;c)
#define MEM(x,a) memset(x,a,sizeof(x))
#define FK(x) MEM(x,0)
#define Tra(i,u) for(int i=G.Start(u),__v=G.To(i);~i;i=G.Next(i),__v=G.To(i))
#define p_b push_back
#define sz(a) ((int)a.size())
#define iter(a,p) (a.begin()+p)

void R1(int &x)
{
x=0;char c=getchar();int f=1;
while(c<'0' or c>'9') f=(c=='-')?-1:1,c=getchar();
while(c>='0' and c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=(f==1)?x:-x;
}
int n;
int a[N][N];
void Input()
{
F(i,1,n) F(j,1,n) R1(a[i][j]);
F(i,1,n) sort(a[i]+1,a[i]+n+1);
}

struct node{int s,b;};bool operator<(node a,node b){return a.s>b.s;}
//用一个struct维护指针,排序条件是以s为关键字的
priority_queue<node> Q;
void Merge(int a[N],int b[N],int c[N]) //A和B序列中生成的n^2个和中,排序之后前面n个存在C数组中
//Merge: O(nlogn)/次
{
while(!Q.empty()) Q.pop(); //勿忘清空啊
F(i,1,n) Q.push((node){a[i]+b[1],1}); //队列的初始状态
F(i,1,n) //只求前面n个,i循环到n即珂
{
node Min=Q.top();Q.pop();
c[i]=Min.s;
Q.push((node){Min.s-b[Min.b]+b[Min.b+1],Min.b+1}); //求出下一个指针
}
}
void Soviet()
{
F(i,2,n)
{
Merge(a[1],a[i],a[1]); //不断合并,合并的结果都放在a[1]里面
} //总共O(n^2logn)
F(i,1,n) printf("%d%c",a[1][i]," \n"[i==n]);
}

#define Flan void
Flan IsMyWife()
{
while(~scanf("%d",&n) and n)
{
Input();
Soviet();
}
}
}
int main()
{
Flandre_Scarlet::IsMyWife();
getchar();getchar();
return 0;
}
w