圆桌会议

圆桌会议

题目描述

N个人顺时针围在一圆桌上开会,他们对身高很敏感.因此决定想使得任意相邻的两人的身高差距最大值最小.如果答案不唯一,输出字典序最小的排列,指的是身高的排列.

输入格式 1864.in

多组测试数据。第一行:一个整数ng, 1 <= ng <= 5.表示有ng组测试数据。
每组测试数据格式如下:
第一行: 一个整数N, 3 <= N <= 50
第二行, 有个N整数,i个整数表示第i个人的身高hi, 1<=hi<=1000.按顺指针给出N个人的身高.空格分开。

输出格式 1864.out

字典序最小的身高序列,同时满足相邻的两人的身高差距最大值最小。共ng行,每行对应一组输入数据。

输入样例 1864.in

2
5
1 3 4 5 7
4
1 2 3 4

输出样例 1864.out

1 3 5 7 4
1 2 4 3

    话说网上关于这题的做法多种多样,怎么做的都有……

    结合老师兴奋的讲解,我决定对这几种做法都进行一次总结。因为,都、很、好,很有借鉴意义。

    这一题的做法大致可以分成四种,贪心、DP、二分+贪心、二分+网络流。

    先说一个不难得出又十分重要的大前提吧。我们要使得身高差和字典序更优,那么身高序列一定是单峰的(即左边是先单调上升,右边再单调下降)。至于为什么,留给读者自行思考。[滑稽]

    另外,做法一、二所有的假设、操作都是基于从小到大排序的。

 

一、  二分+(玄学)贪心

    题目中要求身高差最大的最小。根据多年经验想到二分答案。二分出一个最大的身高差,然后进行验证。

    可是如何验证呢?由于时间复杂度较大,枚举自然是不可能的。其实显而易见,如果以最优的排列方式都无法满足二分出的身高差mid,那么才能证明这个mid是不合法的。

    那么,如何做最优的排列方式呢?

    下面是zyc大佬的玄学贪心做法:

    我们先考虑左边这一段的放法。很显然,左边放什么,对右边能放什么是有影响的。为了降低这个影响,对于左边的序列,就让他们的身高差在满足mid的情况下,尽可能大。

    然后,剩余的都放到右边去。

    如果这样放左边,都放不到a[n](峰顶), 那么这个mid一定不合法,lo=mid。如果这样放右边,右边某身高差都超过mid,那么mid也一定不合法,lo=mid。

    如果上述两种情况都不满足,则mid合法,且答案可能更小,hi=mid。

    最后,hi即为最小的最大身高差。

    但是,上述的放法,很显然并不是最小字典序的放法。对于最小字典序来说,左边的递增序列的差当然要尽可能小。(为什么不能直接做左边呢?因为尽可能取小的,很可能导致整个序列不合法)。因此,应用贪心,让右边的递减序列的身高差 合法时尽可能大。剩下的按照升序放左边即可。

    是不是特别玄学……贪心博大精深……

    zyc的神奇代码如下:

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <vector>
#include <queue>
using namespace std;
const int INF=0x3FFFFFFF;
bool vis[60];
int down[60];
int cur;
int h[60];

int main()
{
    int t;
    cin>>t;
    while (t--)
    {
        int n;
        cin>>n;
        for (int i=1; i<=n; i++) cin>>h[i];
        sort(h+1,h+n+1);
        h[n+1]=INF;
        
        int L=0,R=h[n]-h[1];
        while (L+1<R)
        {
            int mid=(L+R)/2;
            for (int i=1; i<=n; i++) vis[i]=0;
            int maxh=h[1];
            vis[1]=1;

            int tail=1;
            while (tail<n)
            {
                while (h[tail+1]-maxh<=mid) tail++;
                if (vis[tail]) break;
                vis[tail]=1;
                maxh=h[tail];
            }

            if (tail<n) L=mid;            
            else
            {
                vis[1]=vis[n]=0;
                bool p=1;
                for (int i=1; i<n; i++)
                if (!vis[i])
                    for (int j=i+1; j<=n; j++)
                    if (!vis[j])
                    {
                        if (h[j]-h[i]>mid) p=0;
                        break;
                    }
                if (p) R=mid;
                else L=mid;
            }
        }

        for (int i=1; i<=n; i++) vis[i]=0;
        cur=1,down[1]=1;
        int tail=2;
        while (tail<n)
        {
            while (h[tail+1]-h[down[cur]]<=R) tail++;
            vis[tail]=1;
            down[++cur]=tail;
        }

        vis[1]=vis[n]=0;
        for (int i=1; i<=n; i++)
        if (!vis[i]) cout<<h[i]<<" ";
        for (int i=n; i>0; i--)
        if (vis[i]) cout<<h[i]<<" ";
        cout<<endl;
    }

    return 0;
}

二、   玄学贪心

 

    这一种做法更加强大,直接贪心……

    首先,求最小身高差。比如说有:2 5 7 9 15 19 28这些数。如果只是2 5,那身高差只能是3。现在运行到7。怎么做能让7插进去之后最优呢?对于5来说,7放到它的右边是最优的,但实际上这样,首末的差距又会变大。因此最优策略应是将其放在2和5的中间。再运行到9,此时显然应放在7和5中间,序列变成2 5 9 7……

    这样类推下去不难发现,每插进一个数,都将其放入前面序列中的最大值和第二大值之间,就能获得最优解。为什么呢?你不管放在哪里,都会与它左右两边的数形成新的差距。这个新差距自然是越小越好。怎么办? 跟它差距最小的,只能是前面序列中的前两个最大值。因此,此求最小身高差的贪心策略是正确的。证明完毕。

    通过上述的贪心方法,我们可以求出最小身高差ans。但是上述插数的方法却未必是最优字典序,还应再求。

    还是上面那个例子:2 5 7 9 15 19 28,可以求出最小身高差为13。根据字典序的规则,我们让小的数尽可能留在前面(当然最好是按照升序排列所有的数)。但是,由于有ans的限制,有时候不得不进行调整。

    定义cur, i,cur初始化为1。 i从1开始进行枚举,如果存在a[i]-a[cur]>ans, 则表明,i后面的值跟a[cur]匹配也一定会超过ans ,因而需要一个数去后面(下降部分),跟a[cur]匹配。为了保证字典序最优,这个去后面的值为a[i-1]。然后,将cur移到i的位置,继续枚举i……

    在执行上述过程的途中,将要去后面的标记一下。最后,按升序输出前面的值,按降序输出后面的值,即为答案。

    正确性:上述的方法,既可以保证答案的合法,又可以使得最少的值去后坡段,因而可以求出最优的字典序。

    这个玄学贪心,太妙了~~~~~~~~~

    代码如下:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int MAXN=55;
int g,n,a[MAXN],ans,v[MAXN],result[MAXN];
int main()
{
    cin>>g;
    while(g--)
    {
        memset(v,0,sizeof(v));
        cin>>n;
        for(int i=1;i<=n;i++)    cin>>a[i];
        sort(a+1,a+1+n);
        ans=a[2]-a[1];
        for(int i=3;i<=n;i++)    ans=max(ans,a[i]-a[i-2]);
    
        int cur=1;  
        for(int i=1; i<=n; i++)  
            if(a[i]-a[cur]> ans)  
            {  
                  i--;  
                   v[i] = true;  
                   cur= i;  
               }  
           int head=0,tail=n;  
        for(int i=1;i<=n;i++)  
            if(!v[i])  
                result[++head] = a[i];  
            else  
                result[tail--] = a[i];  
                
        for(int i=1;i<=n;i++)
            cout<<result[i]<<" ";
        cout<<endl;
    }
    return 0;
}

三、   双路DP

由于这是一个单峰,在山峰的两边,可以看做是从大到小,一个一个放的,具有明显的阶段性,求最小身高差的时候,就可以用双路DP解决。

(此处从大到小排序)

    (这种双路DP,跟之前的变形合唱队列是类似的)

    设f[i][j]表示左坡段的最底值为a[i],右坡段的最底值为a[j]时的最小的最大身高差距。

    首先f[1][0]=0(即只有峰顶的时候)

    然后进行状态转移。第i个数可以放左边,也可以放右边,道理是一样的。那就讨论放左边的吧。

    考虑一下,前一个状态(即子问题)是什么?第i-1个,可能放在左边,也可能放在右边,如果放在左边,则子问题为f[i-1][k];如果放右边,则子问题为f[k][i-1](0<=k<=i-2)。将a[i]放进序列中,势必会产生一个新的差距,最大新差距为max(a[i-1]-a[i],a[k]-a[i])。然后拿它跟子问题的答案取MAX, 即为这种插法的身高差。将所有插法的身高差取一个min,就是f[i][j]了。

    最后f[n][k](0<=k<=n-1)(f[k][n]答案跟它一样)的最小值即为最小身高差。

    好了,现在再来弄字典序的问题。很黄很暴力……

    我们现在从第一个开始放起。从头到尾,都希望尽可能小。第一个自然是最小值,后面从2到n, 对于每个位置,在身高差限制内,都从小到大一一尝试(前提是它没有被放),对于每个尝试,都在剩下的未做序列中,做双路DP,看是否能满足已知的最优身高差。如果能够满足,则停止枚举,当前的值,即为答案中这个位置应放的值。

    时间复杂度:O(N^4)

    此处不附代码了,这题磨死我了,没心情写了。

四、  重重重头戏——二分+网络流

    写做法?算了吧。昨天yy了一个二分图匹配的方法,跟ywq雷同,写完自己都觉得不对。

    然后我个吃瓜群众静静看着教练争论,表示听得不太懂。

    网络流蒟蒻默默吃瓜。此处留坑待填。

    不过话说网络流是万能的,A+B problem都能做。[滑稽]

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值