【问题描述】
新的学生宿舍开放了,它由m栋建筑物构成,标号为1到m。开始时,学生宿舍都是空的,很快 有N个学生搬进去了。刚好每天搬进去一个。 每次有新同学搬进宿舍,那栋建筑将会举行一个大型的 party。party 的噪声和该建筑物里的 学生的数量相等。宿舍管理员不喜欢噪声,所以他们会不定期的清空某栋建筑物。清空的方法就是把 该栋建筑物的学生全部赶到另外的学生宿舍(这 m 栋宿舍以外的地方)。但是管理员最多只能清空 k 次。求这n天噪音之和的最小值。
【输入】
第一行三个整数n,m,k。
接下来有n行,每行一个整数bi(1<=bi<=m),表示第i天有一个学生搬进了bi栋宿舍。
【输出】
一个整数,表示这n天噪音之和的最小值。
【输入样例】
5 1 2
1 1 1 1 1
【输出样例】
7
【样例解释】
在第一天和第三天之后执行清空操作。这样每天的噪声为 1,1,2,1,2,所以噪声之 和为7。
【数据范围】
对于20%的数据满足:1<=n<=20
对于50%的数据满足:1<=n<=1000,1<=m<=100,1<=k<=50
对于100%的数据满足:1<=n<=1000000,1<=m<=100,1<=k<=5
【原题传送矩阵】
【思路梳理】
观察此题数据规模可以发现:n最大可以达到1000000,着实不小,而对于m和k来说却只有100、500,说明我们可以考虑使用动态规划。设置如下的状态函数:d[i][j]=对前i栋宿舍进行j次清扫所能够产生的最小值。由此一来就可以绕开n特别大而导致的超时超空间的问题,使得时间复杂度只有。
仔细分析问题后不难发现:学生进入宿舍的先后次序并不关键,例如A同学在第一天进入1宿舍,B同学在第二天进入了2宿舍,C同学在第三天进入了1宿舍,对此进行0次清扫,那么就有:总噪音值=(1+2)+1=4,改变这个顺序使得A、C先进入1宿舍,B在第三天进入2宿舍,可以发现总的噪音值仍然为4。那么我们在输入时就没有必要将学生们按照进入的先后顺序存储,直接记录每一个宿舍最后有多少个人即可。方便我们推导状态转移方程。
当我们当前在考虑第d[i][j]时,显然我们可以对第i栋建筑进行0次清扫(不清扫)、1次清扫……j次清扫,那么显然对前i-1栋宿舍我们就进行了j次清扫、j-1次清扫……0次清扫,不难得出状态转移方程如下:
d[i][j]=min { d[i-1][x]+g[i][j-x] | x=对前i-1栋宿舍进行的清扫次数,0<=x<=j }
g[i][j]=对第i栋宿舍进行j次清扫所能够带来的最小噪音值
然后考虑如何清扫能够带来最小的噪音值,即g数组的值。显然每一次分割的时候我们均分是更好的选择,故考虑对第i栋建筑进行j次清扫时,应尽可能相等地分成j+1个部分,每一次分割都是一次等差数列求和(。如果分割后有余数,就应该将他们平均分到前面几次清扫的尾部去,所以不难写出如下的程序。
#include<cstdio>
#include<iostream>
#include<algorithm>
#define maxn 1000005
#define maxm 105
#define maxk 505
#define inf 200000000000005ll
using namespace std;
int a[maxn],n,m,t,num[maxn];
long long g[maxm][maxk],d[maxm][maxk];
void read(int &n)
{
n=0;
char ch=getchar();
while(ch<'0' || ch>'9') ch=getchar();
while(ch>='0' && ch<='9')
{
n=n*10+ch-'0';
ch=getchar();
}
}
long long calc(int i,int j)//计算对第i个宿舍清理j次的最小权值
{
j++;//j加1的原因很简单,此时相当于是在添加j个挡板,将整个宿舍的人按照先后顺序分成j+1份
//当1个宿舍只清理0次时,显然分且仅分成了1组,j如果等于0,t就无意义了
//显然对于每一栋宿舍应该尽可能地均分
if(j>num[i]) return inf;//在一天中进行多次清扫显然不是合法行为,侵犯了公民依法集会的权利
long long t=num[i]/j,cnt=0,p=num[i]%j;
return (t+1)*(t+2)/2*p+t*(t+1)/2*(j-p);
}
void dp()
{
//d[i][j]=min {d[i-1][x]+calc(i,j-x) | 0<=x<=j }
//任何时候,”对前i栋建筑进行j次清扫“的值不会比“对i栋建筑进行j-1次清扫”的值更差
for(int i=1;i<=m;i++)
for(int j=0;j<=t;j++)
{
long long b=inf;
for(int k=0;k<=j;k++)
b=min(b,d[i-1][k]+calc(i,j-k));
d[i][j]=b;
}
cout<<d[m][t]<<endl;
}
int main()
{
scanf("%d%d%d",&n,&m,&t);
for(int i=1;i<=n;i++) read(a[i]),num[a[i]]++;
//顺序不重要,a数组不是必须,我们只需要统计进入每栋楼的人数即可
dp();
return 0;
}