[BZOJ3032]七夕祭(中位数)

传送门
竟然是权限题!!!


  目前正在学习的lydrainbowcat著《算法竞赛进阶指南》里面的题目,里面的讲解非常好。强推这本书,很棒!
  题意和题目都灰常棒!好评!
  非常有意思!
  以下题解出自《算法竞赛进阶指南》(有改动因为我不想抄写。。和版权也有关):

我们发现:
  交换左右两个相邻的摊点只会改变某两列中cl感兴趣的摊点数而对这一行没有影响。
  相同的,交换上下两个相邻的摊点只会改变某两行中cl感兴趣的摊点数对这一列没有影响。
  所以我们可以将题目拆分成两个问题分别计算:
  1、通过左右交换最少次数使得每列中cl感兴趣的点相同
  2、通过上下交换最少次数使得每行中cl感兴趣的点相同

  以第一个问题为例,首先判断可不可行,因为每列要一样,所以如果T不能被m整除,那么则条件不可行,即列不可能达到要求。
  
可行的话:
  我们在输入的时候可以统计一开始每列中cl感兴趣的摊点总数,记为c1~cm,那么他们应该是不均匀的,这时候我们要花最小的代价把他们调配均匀,这就契合了一个经典问题:均分纸牌
  
在这里再讲一讲均分纸牌:

  均分纸牌就是有m个人,每个人有c[i]个纸牌,可以给左右两边的人牌,我们最少需要多少次数使得每个人手里的纸牌数一样。
  
  首先最后人们手里的纸牌数都会为平均数avg也就是T/m(保证T可以整除m)
  然后让a[i]=c[i]-avg也就是说这个人最少需要运动a[i]步使得他手中的牌等于平均数
那么我们对a[i]做一个前缀和suma[i],表示在1~i这个区间的所有牌转移出去所需要的最小步数,那么我们对这个suma[i]的绝对值(有负数啊)做一次求和sum,就表示我们把所有区间的牌都转移到了其他区间,那么此时就是均分状态。

  那么这时候就有个问题了,负数呢??其实我们给别人一个负数的牌,就相当于别人给我们一个这个负数的相反数这么多的牌,所以它可以抵消正数,可以直接累加处理。
那么结果就是:

  设 a[i]=c[i]avg a [ i ] = c [ i ] − a v g
   suma[i]=ij=1a[j] s u m a [ i ] = ∑ j = 1 i a [ j ]
  那么需要的最少步数就是 mi=1|suma[i]| ∑ i = 1 m | s u m a [ i ] |

  然而这道题并不是单纯的均分纸牌,而是“环形均分纸牌”,因为头和尾是连在一起的。
那么我们就需要找个地方把环断开,设这个位置为k(对于环常用枚举断点),那么相对于断点在0而言(也就是普通的均分纸牌,我们可以把他视作断点在0)前缀和就需要减掉 suma[k] s u m a [ k ]

  根据上方的公式答案即为 mi=1|suma[i]suma[k]| ∑ i = 1 m | s u m a [ i ] − s u m a [ k ] |
  
  当k取什么值最小又成为了问题,这与书中提到的“货仓选址”问题有关,这里不赘述,大家经过思考也应该能很快想出来k应该是在中位数的位置最优。
那么排序后找到中位数k问题即可迎刃而解。

  书中提到:本题可以转换成多个经典的简单问题求解。我们应该熟练基础问题,触类旁通,将各种模型之间的简化,拓展和联系作为算法学习与设计的脉络。

  感触良多。


#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int N=100010;
int x[N],y[N],s1[N],s2[N];
int main()
{
    int n,m,T;scanf("%d%d%d",&n,&m,&T);
    for(int i=1;i<=T;i++)
    {
        int a,b; scanf("%d%d",&a,&b);
        x[a]++,y[b]++;
    }
    if(T%m!=0 && T%n!=0)
    {
        printf("impossible\n");
        return 0;
    }
    for(int i=1;i<=n;i++) x[i]-=T/n;
    for(int i=1;i<=m;i++) y[i]-=T/m;
    long long ans=0;
    if(T%n==0)
    {
        for(int i=1;i<=n;i++) s1[i]=s1[i-1]+x[i];
        sort(s1+1,s1+n+1);
        long long k=s1[(n+1)/2];
        for(int i=1;i<=n;i++)
            ans+=abs(k-s1[i]);
    }
    if(T%m==0)
    {
        for(int i=1;i<=m;i++) s2[i]=s2[i-1]+y[i];
        sort(s2+1,s2+m+1);
        long long k=s2[(m+1)/2];
        for(int i=1;i<=m;i++)
            ans+=abs(k-s2[i]);
    }
    if(T%n==0 && T%m==0) printf("both ");
    else if(T%n==0 && T%m!=0) printf("row ");
    else printf("column ");
    printf("%lld\n",ans);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值