[SMOJ1864]圆桌会议

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013686535/article/details/77340397

这题做法比较多,最起码有四种:双路 DP、二分+贪心、二分+网络流和直接贪心。

在介绍各种做法之前首先明确一条基本事实,最优方案一定是呈单峰状排列的。即对于从小到大排好序的 h(以下所有讨论均建立在此前提上),形如

h1<<hN<<h1
的排列一定是最优的。如果在 h1hn 的序列中出现了 hi>hi+1,那么交换 hihi+1 的位置之后显然得到的是更优而字典序更小的方案,所以从 hihi+1 必然是递增的。

解法一:双路 DP。
这里的思想其实跟省赛前集训做过的“变形合唱队形”一题很类似,都是先确定好一个中心,然后向两边插入。如图:

将最高点摆好,两边不断从上到下摆编号递减(显然身高也递减)的点。设左边摆到了第 i 个,右边摆到了第 j 个,则下一个要摆的是第 min(i,j)1 个。

不妨用 f[i][j] 表示目前为止的最小的最大身高差。此时待放点 k 可以放在左边,则 f[k][j]=min(f[k][j],max(f[i][j],h[i]h[k]));也可以放在右边,则 f[i][k]=min(f[i][k],max(f[i][j],h[j]h[k]))
最终的答案就是 min{min{f[2][i]},min{f[i][2]}}
但这样仅仅算出了最小的最大身高差。如何输出字典序最小的方案呢?
显然,要令字典序最小,我们当然希望尽可能按从小到大输出。但是,有时候为了满足身高差的限制,不得不进行必要的调整,将点放到后半段去。
这里就有一种很巧妙的方法,可以判断点能否放在前半段,使身高差限制仍然成立。那就是:枚举+DP。听起来很暴力,但数据范围比较小,并不会有问题。
具体地,一开始尝试将 h2 放在前半段。那么我们就考虑能否这样放,只要把剩下的点(除了 1 和 2)再做一遍 DP,若求得的结果不大于一开始的答案,说明这样放是可行的。于是就可以从小到大不断尝试,当发现将某个点放在前半段会使得到的身高差大于答案时,则要将其放在后半段。最终就可以得到一个序列。
总结一下,其实从中可以归纳出字典序一类问题的一种通用方法:逐一尝试。当然不是指枚举出所有可能的序列,而是尽量尝试放更优的,并判断能否这样放。

解法二:二分+贪心
不难理解,这题的答案显然是具有单调性的,因此完全可以二分答案。现在的问题是,对于一个被二分到的最大身高差 k,如何判断:是否存在一种排列方式,使所有相邻两个人之间的身高差均不大于 k
可以这样操作:交替插入前半段和后半段的值。例如:首先将 h1 摆在第一位,将 h2 摆在第二位。接下来就要考虑将哪一个放到最后一位。答案是,将满足 hjhik 中最大的 j 放在最后一位。为什么要这样操作呢?首先,这样满足了在环中首尾之间的身高差限制,显然是合法的。而且将合法且最大的放在后面,肯定可以使字典序尽量小。否则如果将其它更小的放在后面,虽然也合法,但字典序会变大。因此这样做是正确的。之后再考虑放第三位,倒数第二位……
需要注意的是,最终放置完之后还需要检查一下整个序列的合法性。即对于任意的 i<n,是否都有 |hihi+1|k
另外,这样得到的序列,不仅适用于检查,而且也一定是最终输出时字典序最小的。

解法三:二分+网络流
在二分时,除了可以贪心判断之外,还可以跑一遍最大流。构图方法如下:

将每个点复制一份,就得到了一个二分图。若 hjhik,则 ij 连一条容量为无穷大的边。从源点向所有的 i>1 连一条容量为 1 的边,向 1 连一条容量为 2 的边;从汇点向所有的 i<n 连一条容量为 1 的边,向 n 连一条容量为 2 的边。若最大流为 2,则说明 k 是可行的。
为什么可以这样构图呢?我们不妨将一条 sijn 的流认为在排列中 j 恰好在 i 的右侧。但需要特殊考虑的是头尾之间,因此源点到 1 的边容量为 2,其实可以理解为从 1 出发,看能不能走两条互不重叠的递增路径,同一路径中相邻的点差值均不大于 k。如果存在,就相当于一条是前半段,另一条是后半段。
还可以有另外一种构图方法:将每个点 i 拆成 i1i2,且对于任意 1<i<ni1i2 的边容量为 1。s 到 11 的边、1112 的边、n1n2 的边和 n2t 的边容量均为 2。若 hjhik,则 i2j1 连一条容量为无穷大的边。这样虽然可能导致跳过中间的一些点,但一定能给出正确的判断。原因:既然跳到后面去都可以成立,再加上中间的点只会变得更优,至少不可能变得更差。
至于方案的输出,则可以参考解法一和解法二。

解法四:直接贪心。
不妨考虑从左到右一个一个插入序列中。可以发现,对于前三个人,无论如何安排,其最小差值总是一定的,都是 h3h1,又因为要求字典序最小,所以直接不用动。考虑到第四个人时,可以发现,将其放在第二个人和第三个人之间可以使差值最小。类似地,后面每一个人都应放在序列的倒数第二个和最后一个之间。
这样就可以求出最小的差值,之后的方案输出参考解法一和解法二即可。

下面的代码中,求身高差部分是解法四,但字典序输出与上面两种有所不同,在这篇博客中有详细的讲解,可以参考。
参考代码:

//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;
}


总结一下,这题的一题多解都非常妙。特别是贪心,越做题才越发现它的神奇之处。有的情况下,贪心的策略不一定是唯一的,但一定要本着有理有据的原则,最好能够有相对严谨的证明。总的来说,还是要靠多练习,归纳一些方法,找找感觉吧。

阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页