这题做法比较多,最起码有四种:双路 DP、二分+贪心、二分+网络流和直接贪心。
在介绍各种做法之前首先明确一条基本事实,最优方案一定是呈单峰状排列的。即对于从小到大排好序的 h (以下所有讨论均建立在此前提上),形如
解法一:双路 DP。
这里的思想其实跟省赛前集训做过的“变形合唱队形”一题很类似,都是先确定好一个中心,然后向两边插入。如图:
将最高点摆好,两边不断从上到下摆编号递减(显然身高也递减)的点。设左边摆到了第
i
个,右边摆到了第
不妨用
f[i][j]
表示目前为止的最小的最大身高差。此时待放点
k
可以放在左边,则
最终的答案就是
min{min{f[2][i]},min{f[i][2]}}
。
但这样仅仅算出了最小的最大身高差。如何输出字典序最小的方案呢?
显然,要令字典序最小,我们当然希望尽可能按从小到大输出。但是,有时候为了满足身高差的限制,不得不进行必要的调整,将点放到后半段去。
这里就有一种很巧妙的方法,可以判断点能否放在前半段,使身高差限制仍然成立。那就是:枚举+DP。听起来很暴力,但数据范围比较小,并不会有问题。
具体地,一开始尝试将
h2
放在前半段。那么我们就考虑能否这样放,只要把剩下的点(除了 1 和 2)再做一遍 DP,若求得的结果不大于一开始的答案,说明这样放是可行的。于是就可以从小到大不断尝试,当发现将某个点放在前半段会使得到的身高差大于答案时,则要将其放在后半段。最终就可以得到一个序列。
总结一下,其实从中可以归纳出字典序一类问题的一种通用方法:逐一尝试。当然不是指枚举出所有可能的序列,而是尽量尝试放更优的,并判断能否这样放。
解法二:二分+贪心
不难理解,这题的答案显然是具有单调性的,因此完全可以二分答案。现在的问题是,对于一个被二分到的最大身高差
k
,如何判断:是否存在一种排列方式,使所有相邻两个人之间的身高差均不大于
可以这样操作:交替插入前半段和后半段的值。例如:首先将
h1
摆在第一位,将
h2
摆在第二位。接下来就要考虑将哪一个放到最后一位。答案是,将满足
hj−hi≤k
中最大的
j
放在最后一位。为什么要这样操作呢?首先,这样满足了在环中首尾之间的身高差限制,显然是合法的。而且将合法且最大的放在后面,肯定可以使字典序尽量小。否则如果将其它更小的放在后面,虽然也合法,但字典序会变大。因此这样做是正确的。之后再考虑放第三位,倒数第二位……
需要注意的是,最终放置完之后还需要检查一下整个序列的合法性。即对于任意的
另外,这样得到的序列,不仅适用于检查,而且也一定是最终输出时字典序最小的。
解法三:二分+网络流
在二分时,除了可以贪心判断之外,还可以跑一遍最大流。构图方法如下:
将每个点复制一份,就得到了一个二分图。若
hj−hi≤k
,则
i
向
为什么可以这样构图呢?我们不妨将一条
还可以有另外一种构图方法:将每个点
i
拆成
至于方案的输出,则可以参考解法一和解法二。
解法四:直接贪心。
不妨考虑从左到右一个一个插入序列中。可以发现,对于前三个人,无论如何安排,其最小差值总是一定的,都是
h3−h1
,又因为要求字典序最小,所以直接不用动。考虑到第四个人时,可以发现,将其放在第二个人和第三个人之间可以使差值最小。类似地,后面每一个人都应放在序列的倒数第二个和最后一个之间。
这样就可以求出最小的差值,之后的方案输出参考解法一和解法二即可。
下面的代码中,求身高差部分是解法四,但字典序输出与上面两种有所不同,在这篇博客中有详细的讲解,可以参考。
参考代码:
//1864.cpp
#include <algorithm>
#include <cstdio>
#include <cstdlib>
&35;#35;include <cstring>
#include <iostream>
#include <deque>
using namespace std;
const int MAXN = 50 + 10;
int N;
int h[MAXN], ans[MAXN];
bool flag[MAXN];
int main(void) {
freopen("1864.in", "r", stdin);
freopen("1864.out", "w", stdout);
int ng; scanf("%d", &ng);
while (ng--) {
int N; scanf("%d", &N);
for (int i = 0; i < N; i++) scanf("%d", &h[i]);
sort(h, h + N);
int diff = h[2] - h[0];
for (int i = 1; i + 2 < N; i++) diff = max(diff, h[i + 2] - h[i]);
// printf("%d\n", diff);
memset(flag, false, sizeof flag);
for (int i = 0, j = 0; i < N; i++)
if (h[i] - h[j] > diff) flag[j = --i] = true; //当超过身高差时,将 (i-1) 放到后面
int front = 0, rear = N - 1;
for (int i = 0; i < N; i++) if (flag[i]) ans[rear--] = h[i]; else ans[front++] = h[i];
for (int i = 0; i < N; i++) printf("%d ", ans[i]); putchar('\n');
}
return 0;
}
总结一下,这题的一题多解都非常妙。特别是贪心,越做题才越发现它的神奇之处。有的情况下,贪心的策略不一定是唯一的,但一定要本着有理有据的原则,最好能够有相对严谨的证明。总的来说,还是要靠多练习,归纳一些方法,找找感觉吧。