【题外话】
感谢UBUNTU为保存程序做出贡献:https://paste.ubuntu.com ;
感谢洛谷OJ的私人题库保存题面:https://www.luogu.org ;
现在我的题解的所有程序均保存在UBUNTU上,需要时单击超链接查看 ;
由于题目的不确定性,现在所有测试数据的建立全部来自于参加本次巨石杯的选手
OYCY & LSH 下面的程序为本人程序,暂且未知评测状态,会有误差,会及时更正!!!
5月14日 绿城育华NOIP巨石杯试卷解析
T1 最大的最小
【地址】https://www.luogu.org/problem/show?pid=U8888
【题面】
给出k组数据(x,y)表示有x个y在一个数列中,现在需要你在这个数列当中,把这些数两两配对,每两个配对的数的和设为sum,我们取最大的sum记为Maxsum,请输出Maxsum的最小值,正所谓“最大的最小”。
输入格式:第1行一个数k,表示有k组数,接下来k行每行两个数x,y表示在数列中有x个y
输出格式:1行1个数表示Maxsum的最小值
样例#1
输入
4
1 1
3 4
2 2
2 3
输出 6
范围:x,y,k<=100,000
【样例解释】
对于样例我们可以制造出这么一个序列:14442233
可以发现,对于该序列进行所谓的配对有6!种可能,在这6!种排法中可以发现这样配对可以求出Maxsum 的最小值:
14 24 24 33 得出的最小值为6,没有另外一种方法比它更优了
【解析】
对于样例我们可以发现纯暴力时间为O(0.5(x的总和)!)
远远超过时间限制,而且不能实现!
于是想到最优解的贪心求法!
简单分析样例我们发现,上述的样例解释是基于这样一个数列的:12233444,
发现该数列是数列14442233的排序结果,所以我们第一个想到的是对数列快排!
对于本题,暴力法求解是最容易想到的方法!
但是如果用到qsort的话,暴力法的时间复杂度是O(k*x*y*log(k*x*y)),
100%超过时间限制TLE,于是暴力法无法AC。
于是,对于读入数据不能暴力存储,怎么办,结构体存储!
定义: rec=record x,n:longint; end;
这里的x表示数x,n表示数x有n个;
对于数x进行快排,同时交换整个结构体;这样求出了一个有序的数列。
由于可能有重复的数x,所以需要去重,而qsort后x相同的几个数一定是连续的。
所以再用2层while(时间复杂度趋于线性)去重即可。
这样就得出了rec类型的数组a,从头尾两端逐步逼近:由于对于一样的头尾的值,得出的总和一样,所以不必要进行冗余计算
取头尾指针指向的两个数的个数求最小值,即为接下来有几个一样的和数!
如果某一个数为0,那么就下一个。同时找出所谓的Maxsum即可。
until 头尾指针相交;
主要程序段:
while i<=k do begin
lasti:=i; sum:=ta[i].n;
while (ta[i].x=ta[i+1].x)and(i<=k) do begin
inc(i); sum:=sum+ta[i].n; end;
inc(p); a[p].x:=ta[lasti].x; a[p].n:=sum;
inc(i);
end;//排序后去重
i:=1; j:=p;
maxsum:=-maxlongint;
while i<j do begin
step:=min(a[i].n,a[j].n);
sum:=a[i].x+a[j].x;
dec(a[i].n,step); dec(a[j].n,step);
if a[i].n=0 then begin inc(i); end;
if a[j].n=0 then begin dec(j); end;
if maxsum<sum then maxsum:=sum;
end;//贪心求解
对于这个贪心,我们需要有理论上的严格证明,为什么排序过后就是所求的数列?
证明:“不排序的数列不是所求最优的数列”如下:
以第一和最后一个之和为例,如果,第一个不与最后一个取和,那么第一个数势必和除最后一个数的数取和,那么得出的数势必比原来的数字要小,由于整个数列的和一定,那么对于这个Maxsum来说势必受到这个和的小而大,所以得出的Maxsum太大不是所求最优的解,所以命题“不排序的数列不是所求最优的数列”成立,贪心正确!
【程序】https://paste.ubuntu.com/24585852/
type rec=record x,n:longint; end; var k,i,j,p,lasti,maxsum,sum,step:longint; ta,a:array[-1000..100000]of rec; function min(a,b:longint):longint; begin if a>b then exit(b) else exit(a); end; function max(a,b:longint):longint; begin if a<b then exit(b) else exit(a); end; procedure qsort(l,r:longint); var i,j,mid:longint; t:rec; begin i:=l; j:=r; mid:=ta[(l+r)div 2].x; while i<j do begin while ta[i].x<mid do inc(i); while ta[j].x>mid do dec(j); if i<=j then begin t:=ta[i]; ta[i]:=ta[j]; ta[j]:=t; inc(i);dec(j); end; end; if l<j then qsort(l,j); if r>i then qsort(i,r); end; begin readln(k); for i:=1 to k do readln(ta[i].n,ta[i].x); qsort(1,k); i:=1; p:=0; while i<=k do begin lasti:=i; sum:=ta[i].n; while (ta[i].x=ta[i+1].x)and(i<=k) do begin inc(i); sum:=sum+ta[i].n; end; inc(p); a[p].x:=ta[lasti].x; a[p].n:=sum; inc(i); end; i:=1; j:=p; maxsum:=-maxlongint; while i<j do begin step:=min(a[i].n,a[j].n); sum:=a[i].x+a[j].x; dec(a[i].n,step); dec(a[j].n,step); if a[i].n=0 then begin inc(i); end; if a[j].n=0 then begin dec(j); end; if maxsum<sum then maxsum:=sum; end; writeln(maxsum); end.
T2 愤怒的小鸟
【地址】https://www.luogu.org/problem/show?pid=U9243
【题面】
有一数轴上有n只猪,坐标分别为X1 、X2、X3……Xn,现在玩家有k只炸弹鸟,撞到地面就会发生爆炸。每一只小鸟的能量都是w,落到点P(点表示的数)后会使P±w的范围内的猪收到伤害,求最小的能量的值w
输入格式
第1行有2个数n,k 表示有n只猪,用户有k只鸟(可以不全用完)
第2行有n个数表示每只猪的坐标(不保证有序)
输出格式
1行,表示最小的能量值w
输入
5 3
-1 1 2 6 9
输出
2
范围
对于100%的数据
x[i]<=100,000,000
n,k<=100,000
【解析】
首先看到数据范围x[i]<=100,000,000,范围如此之大想到不能用暴力法求解。那么非常奇怪,这么大的数据范围用什么办法可以通过呢?一开始想用贪心但是好像毫无线索。
画个图仔细分析,对于样例,你可以在下方建立一个数轴。
-1 1 2 6 9 画上一个点,表示这是一个猪。
看看在能量值为1时且所用的mink<=3是否可行?答案是不能!
能量值++,为2时,能否保证所用mink<=3?答案是可以你可以把2个猪放在坐标为2和7处就可以炸死所有的猪!
这样看来这么大的程序,又有坐标,不由的让人想到二分答案算法(准确的说是思想)
好了,事实上本题的标解就是二分答案,笔者也是看了数据范围后枉然想到二分答案的!
总结下二分答案的思想:以求最小值的最大值(最小值最大化)为例,尝试一个可能的答案,如果这个答案符合题目条件,那么它肯定是“最小”(可行解),但不一定是“最大”(最优解),然后我们换个更大的可能答案,如果也符合条件,那这个新可行解就更优,不断重复即可。
二分答案的前提:
1.答案区间上下限确定,即最终答案在哪个范围是容易知道的。
2.检验某值是否可行。
3.可行解满足区间单调性,即若x是可行解,则在答案区间内x+1(或是x-1)可能也可行。
解释一下什么是区间单调性:函数的单调性(monotonicity)也可以叫做函数的增减性。当函数 f(x) 的自变量在其定义区间内增大(或减小)时,函数值f(x)也随着增大(或减小),则称该函数为在该区间上具有单调性。
非常形象的解释!这样我们可以得出二分答案的基本格式:
while l<=r do begin
mid:=(l+r)div 2;
if pd(mid)=true then r:=mid-1 else l:=mid+1; end;
注意这里l和r在while循环开始时要赋值为区间的开始和结束区间结点。
而pd是一个function(pascal中叫函数,有返回值,返回值为boolean类型,表示能不能取)
是自我根据题目的意义来写,一般来说是用到贪心的知识来解答,所以二分答案和贪心一般是在一起的!
本题的重点不在这里,在于下面这个贪心,笔者寻求证明许久终于找到了拙劣的证明方法。首先我们来探讨一下pd函数的作用。
其实略略思考,二分答案的主体是什么?是能量值,所以pd传入的值为能量值w所以pd函数作用是判断在能量值为w的情况下最少投掷的鸟的个数并和k做比,若小于等于k则成立,大于k则不成立!
贪心的重点是判断在能量值为w的情况下最少投掷的鸟的个数!
考虑到两点之间在鸟威力为w的情况下让鸟的爆炸范围覆盖整个区间(不浪费)的情况一定是落点为两只猪的正中间,这样,对猪的坐标排序后,这个落点p为x+[(x+y)/2],爆炸范围是p±w,如果相邻两只猪是可以被一个鸟所摧毁的,那么判断第三只猪依次列推。而如果相邻两只猪不可以被一个鸟所摧毁,即两只猪会被2只鸟摧毁,那么依次拓展,由于前方所用的鸟的只数是最优的,所以一定会多1只鸟即两只相邻的猪中的前面一个猪,不管怎么样都需要一只额外的鸟来摧毁。这种算法是两重while循环嵌套可以完成的!
回过头看,这道题是不是符合用二分答案求解的三个前提?我们依次论证。
1、 答案区间上下限确定,即最终答案在哪个范围是容易知道的。
其实非常简单,炸弹的爆炸威力一定在0到相差最大的两只鸟的距离的一半的范围内,用数学语言来叙述就是(假设x1-xn排序完成)令l=1;r=(xn-x1)/2
能量的范围在区间[l,r]或者是(l-1,r+1)
2、 .检验某值是否可行。
上述贪心很好的证明这点,我们只要通过一个简单的贪心就可以判断是不是符合题目要求。
3、 可行解满足区间单调性。
首先区间我们人为的将其具有单调性(排序),而数据本身w范围也具有单调性,大就是大,小就是小(我们要求越小越好)
从中可以发现,本题完全可以用二分答案来解决,有关二分答案的具体内容在此不再赘述,希望通过这一题的解答来抛砖引玉。
【程序】
var a:array[1..100000]of int64; n,k,i,mid,l,r:longint; function pd(w:int64):boolean; var i,mink,lasti:longint; p:int64; begin i:=1; mink:=0; while i<=n do begin inc(mink); lasti:=i; p:=(a[i+1]+a[i])div 2; while (p+w>=a[i+1])and(p-w<=a[i])and(i<=n) do begin inc(i); if i=n then break; p:=(a[lasti]+a[i+1])div 2; end; inc(i); end; if mink>k then exit(false) else exit(true); end; procedure qsort(l,r:longint); var t,i,j,mid:longint; begin i:=l; j:=r; mid:=a[(l+r)div 2]; while i<j do begin while a[i]<mid do inc(i); while a[j]>mid do dec(j); if i<=j then begin t:=a[i]; a[i]:=a[j]; a[j]:=t; inc(i);dec(j); end; end; if l<j then qsort(l,j); if r>i then qsort(i,r); end; begin readln(n,k); for i:=1 to n do read(a[i]); qsort(1,n); l:=0; r:=a[n]-a[1]; while l<r do begin mid:=(l+r)div 2; if pd(mid)=true then r:=mid else l:=mid+1; end; writeln(l); end.
T3 武林高手
【地址】https://www.luogu.org/problem/show?pid=U9308
【题面】
有B个帮派的高手功力可能不同,现在高手们约架,占据了n个据点,每个据点可能有多个高手,发功时,发功点的据点承受高手的原始功力,其他据点从左到右承受的功力依次递减。每个据点所承受的总功力等于所有高手对此据点造成的功力值之和
现在知道据点个数为n,帮派个数B与每个帮派高手的功力值,以及这些高手同时发功时各个据点所承受的功力,求至少有几个高手在发功
输入格式
第1行两个数,据点数n和帮派数B
第2行描述每个帮派,共计B个数表示该帮派的高手的功力值为V[i]
第3行描述n个数表示每个据点最后承受的功力值W[i]
输出格式 1行表示至少有几个高手在发功
输入输出样例
输入
5 2
5 7
0 17 16 20 19
输出 4
范围:对于100%的数据满足:
0<v[i]<=100,000
0<B<=100
n<=1,000,000
【解析】
这道题目其实是很难考虑的!
暴力bfs dfs可以骗分但是不是最优解,最优解为dp+模拟。
时间复杂度为O(max(w[i])*B),显然100,000*100=10,000,000不超时
建立递推式f[i] 表示在累积功力值增加i时的最少人数。
[问题]思考:f[i-v[j]]+1可以表示为什么?
好了,想得出来的看答案,想不出来了我们看下面的一段举例。
对于样例,
f[i-5]+1表示在增加能力值累积为i时从i-5 时最少人数增加一个功力值为5的高手
f[i-7]+1表示在增加能力值累积为i时从i-7 时最少人数增加一个功力值为7的高手
看到这里,你可能会恍然大悟!
[答案] f[i-v[j]]+1可以表示为在增加能力值累积为i时从i-v[i]时的最少人数中多了一个功力值为v[i]的高手.
而f[i-v[i]]+1只可能从有限步中取最小值得来,这个有限的步数即为B
所以动态转移方程为f[i]=min(f[i],f[i-v[j]]+1);
然后再对w数组进行处理(模拟的部分)
备份w为tw; w[i]=0àw[i+1]=tw[i+1]; w[i]<>0àw[i+1]=tw[i+1]-tw[i]+1
扫一遍w累加f[w[i]]即可得ans,输出ans(累加值)
【程序】https://paste.ubuntu.com/24604486/
var i,j,ans,n,B,maxw:longint; f:array[-1000000..1000000]of longint; v,w,tw:array[0..1000]of longint; function min(a,b:longint):longint; begin if a<b then exit(a) else exit(b); end; begin readln(n,B); for i:=1 to B do read(v[i]); for i:=1 to n do begin read(w[i]); if w[i]>maxw then maxw:=w[i]; end; fillchar(f,sizeof(f),$7f-1); f[0]:=0; for i:=1 to maxw do for j:=1 to B do f[i]:=min(f[i-v[j]]+1,f[i]); tw:=w; for i:=0 to n-1 do begin if tw[i]=0 then w[i+1]:=tw[i+1] else w[i+1]:=tw[i+1]-tw[i]+1; end; for i:=1 to n do begin inc(ans,f[w[i]]); end; writeln(ans); end.
杭州文澜中学 罗嘉诚
2017年5月28日