[USACO Jan07]考试Schul解题报告
题目
分析
这道题比较有意思。
首先,我们不用t和p来表示分数,我们用(x,y),代表满分为x的卷子得了y分。这样更加直观:把一份卷子视作向量,那么它的“分数率”也就是其斜率。最终的分数率自然就是所有向量之和的斜率。
先考虑一个问题:假如对某个给定的d,现在已经按老师的方法确定了最终的分数率G,那么我们能否改变略去的d份试卷,使得分数率高于G呢?
这时我们可以采用在一类二分答案+网络流/最短路题里常用的思路:对每一份试卷i,算出pi=yi-G*xi(我们把pi称作(xi,yi)的G-截距)。那么,如果我们能够挑出N-d份试卷,使得其pi之和大于零,那就意味着把这些试卷加一块的分数率大于G。
于是更进一步地,若老师选中的试卷中的最小pi(记为worst_in[d])小于老师略去的试卷中的最大pi(记为best_out[d]),就意味着对于当前的d,Bessie可以得到一个更高的分数率。
这样就有了一个O(N^2)的算法:枚举每一个d,计算出G。然后据此计算出worst_in[d]和best_out[d],把二者比一比,若worst_in[d]<best_out[d],则这个d就是答案之一。
但这还远远不够。怎么办呢?
不妨以best_out为例。在这里我们试图算出分数率最小的d份卷子中,最大的pi。可以发现,过这d份卷子在直角坐标系中的对应点做斜率为G的直线束,其中最靠上那条直线的截距就是这个“最大的pi”。若形象地描述,可以想象我们拿斜率为G的直线从y轴正方向无穷远处向下移动,碰到的第一个点就是那个pi最大的点。
这很像一个凸包模型,而事实也的确如此。我们不妨将求best_out的模型用几何语言描述:
①我们按照极角逆时针序,不断往平面上添加点(极角逆时针序也就是斜率升序,也就是分数率从小到大排序)。
②在每一个点处,都有一个G,我们试图找出当前点集中的G-截距最大点。
③在处理的过程中,G不断变大(可以想象,我们不断在老师选中的试卷中扔掉分数率最小的那个,则剩下的分数率自然越来越大)。
可以发现,那个G-截距最大点一定位于当前点集的上凸线上。
那么,怎么维护上凸线呢?按极角序加点并不能线性地维护上凸线(想到Graham没……它维护的是整个点集的凸包)。当然你可以写一个神奇的数据结构或者CDQ分治啥的,当然我们有更简单的方法——
大力出奇迹,新加点时,直接删掉x坐标大于等于它的所有点!这样就变成了一个从左向右加点,维护上凸线的模型。
为什么这么做是对的呢?
假设当前的情形是这样的:
其中O是原点,P是新添加的点,蓝色折线是上凸线,黑色直线垂直于x轴。
(ps:这个上凸线其实画的不科学,不要在意这些细节,意会即可)
取横坐标大于等于P点的C点。我们需要证明:无论在现在还是未来,P点的G-截距都比C点的G-截距大。
设P(x1,y1),C(x2,y2),k=y1/x1.
显然y2/x2<k(因为C先于P加入点集,而且分数率两两不等)
∴y1-kx1=0, y2-kx2<0
∴y1-kx1>y2-kx2, y2-y1<k(x2-x1)
我们来看总分数率G。
G有一个性质:对于当前的d,G一定大于d号点(也就是P)的斜率。原因很简单:G是所有比d斜率大的点加起来的斜率,自然大于d的斜率了。
结合③,我们可以得到:当前的G大于P的斜率k,而且以后的G也一定大于k。
而对于G>k,y2-y1<G(x2-x1)仍然成立(因为x2-x1非负),故无论现在还是将来,P点的G-截距都大于C点的G-截距。
直观地看,当前的G比红线大,以后还会更大,那么斜率为G的直线截到的最上点一定不可能是C、D。
但是,直接把凸线上P右边那些点删去之后,剩余的凸线可能并非P左边那些点形成的凸线——中间的点可能之前被删去,这里没有加上。然而这个其实并没有问题,因为我们可以发现,如果一个点之前已从凸线上删去,那么它无论何时都不会成为答案。
综上,每次新加P时,维护这个上凸线的方法就是:
1)从上凸线的右侧删去x坐标大于等于P的点。
2)从上凸线的右侧删去加上P后不再在凸线上的点。
3)不断从上凸线右侧删点,直到G-截距不再下降,这时上凸线的最右点也就是G-截距最大点。其G-截距也就是当前的best_out[d]。
再来看worst_in。
其模型是:
①按极角序从高到低(分数率从大到小)加点。
②每次找G-截距最小点。
③G不断减小。
而维护凸线的方法就是:从右到左加点,维护下凸线(顺便说一句,USACO官方题解中是非常别扭地从上到下加点,维护右凸线……其实是一样的)。即每次删去所有x坐标>=P的点。
此法的正确性证明其实和best_out大为不同。画个图:
P是新加点,竖直黑色虚线垂直于x轴,A是被删掉的点。
设P(x1,y1),A(x2,y2),k=y1/x1,那么显然k=y1/x1<y2/x2(A的极角序比P大)。
∴y1-kx1=0<y2-kx2
∴y1-y2<k(x1-x2).
而x1-x2>0(注意,证明依赖于这一点,而那个右凸线的方法实际是由y1-y2>0推出x1-x2>0),所以若将来某个G'使得A的G'-截距小于P的G'-截距,那么一定有G'<k(否则上式右端只会更大)。
这里又回到G的性质了:当我们枚举到d时,G一定不小于d+1号点的斜率。
那么,如果将来G'<k,就一定是新加入了一个斜率比G'更小的点B(x3,y3),满足y3/x3<G'<k<y2/x2.
但这时,观察上式可以发现,必有y3-G'x3<0<y2-G'x2,换言之,B比A更优。
直观上,若将来A比P优,那么G'至多是红色虚线的斜率,这个斜率必然小于P的斜率,因此必定有个比P斜率还要小的点B,其G'-截距优于A(拿着红色虚线卡一卡,就会发现必然先碰到B)。
为了避免读者可能的迷惑,我又画了一个斜率比P大的点T。显然,当G'>k时,T就可能优于P了(比如绿色虚线就是先卡到T再卡到P的),因而上述论证不再成立。你可以直观地意识到,“证明依赖于x1-x2>0”的含义。
因此每次新加P时维护下凸线的方法就是:
1)从下凸线左侧删去x坐标小于等于P的点。
2)从下凸线左侧删去加上P后不再在凸线上的点。
3)不断从下凸线左侧删点,直到G-截距不再上升,这时下凸线的最左点也就是G-截距最小点。其G-截距就是当前的worst_in[d+1](d+1是因为按照老师的算法略去了d个点,留下的第一个点是d+1)。
最后,每一个best_out[d]>worst_in[d+1]的d都是答案。
代码
#include<iostream> #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #include<vector> using namespace std; const int SIZEN=50010; class Point{ public: double x,y; Point(double _x=0,double _y=0){ x=_x; y=_y; } }; void print(const Point &p){ cout<<"("<<p.x<<" "<<p.y<<")"; } Point operator + (const Point &a,const Point &b){ return Point(a.x+b.x,a.y+b.y); } Point operator - (const Point &a,const Point &b){ return Point(a.x-b.x,a.y-b.y); } double cross(const Point &a,const Point &b){ return a.x*b.y-b.x*a.y; } double dir_area(const Point &o,const Point &a,const Point &b){//以O为视点看a,b return cross(a-o,b-o); } bool cmp_angle(const Point &a,const Point &b){ return dir_area(Point(0,0),a,b)>0; } double calc(const Point &a,double m){ return a.y-m*a.x; } int N; Point P[SIZEN]; double ratio[SIZEN]; double best_out[SIZEN],worst_in[SIZEN]; Point H[SIZEN]; void work(void){ sort(P+1,P+1+N,cmp_angle); Point now(0,0); for(int i=N;i>=1;i--){ now=now+P[i]; ratio[i]=now.y/now.x; } int tot=0; for(int i=1;i<N;i++){ while(tot>=1&&H[tot-1].x>=P[i].x) tot--;//滤掉右边的 while(tot>=2&&dir_area(H[tot-2],H[tot-1],P[i])>=0) tot--;//滤掉不在上凸线上的 H[tot++]=P[i];//上凸线新加点 while(tot>=2&&calc(H[tot-1],ratio[i+1])<=calc(H[tot-2],ratio[i+1])) tot--;//滤掉不被当前斜率卡住的 best_out[i]=calc(H[tot-1],ratio[i+1]); } tot=0; for(int i=N;i>=1;i--){ while(tot>=1&&H[tot-1].x<=P[i].x) tot--;//滤掉左边的 while(tot>=2&&dir_area(H[tot-2],P[i],H[tot-1])<=0) tot--;//滤掉不在下凸线上的 H[tot++]=P[i];//下凸线新加点 while(tot>=2&&calc(H[tot-1],ratio[i])>=calc(H[tot-2],ratio[i])) tot--;//滤掉不被当前斜率卡住的 worst_in[i]=calc(H[tot-1],ratio[i]); } vector<int> ans; for(int i=1;i<N;i++) if(worst_in[i+1]<best_out[i]) ans.push_back(i); printf("%d\n",ans.size()); for(int i=0;i<ans.size();i++) printf("%d\n",ans[i]); } void read(void){ scanf("%d",&N); for(int i=1;i<=N;i++) scanf("%lf%lf",&P[i].y,&P[i].x); } int main(){ freopen("schul.in","r",stdin); freopen("schul.out","w",stdout); read(); work(); return 0; }