【题解】APIO 2009 会议中心
嗯,为了让自己的flag不倒加深对错题的印象+好好理解经典题,我来写题解啦~
首先是题面 传送门
Siruseri 政府建造了一座新的会议中心。许多公司对租借会议中心的会堂很感兴趣,他们希望能够在里面举行会议。
对于一个客户而言,仅当在开会时能够独自占用整个会堂,他才会租借会堂。 会议中心的销售主管认为:最好的策略应该是将会堂租借给尽可能多的客户。
显然,有可能存在不止一种满足要求的策略。 例如下面的例子。总共有 4 个公司。他们对租借会堂发出了请求,并提出了他们所需占用会堂的起止日期(如下表所示)。
开始日期 结束日期
公司1 4 9
公司2 9 11
公司3 13 19
公司4 10 17
上例中,最多将会堂租借给两家公司。租借策略分别是租给公司 1 和公司 3, 或是公司 2 和公司 3,也可以是公司 1 和公司 4。注意会议中心一天最多租借给一个公司,所以公司 1 和公司 2 不能同时租借会议中心,因为他们在第九天重合了。
销售主管为了公平起见,决定按照如下的程序来确定选择何种租借策略:首先,将租借给客户数量最多的策略作为候选,将所有的公司按照他们发出请求的顺序编号。对于候选策略,将策略中的每家公司的编号按升序排列。最后,选出其中字典序最小的候选策略作为最终的策略。
例中,会堂最终将被租借给公司 1 和公司 3:3 个候选策略是 {(1,3),(2,3),(1,4)}。而在字典序中(1,3)<(1,4)<(2,3)。 你的任务是帮助销售主管确定应该将会堂租借给哪些公司。
其实我们可以将这个题抽象成一张图
(图中也就是样例 其中红色的数字是线段编号 黑色的数字的线段左、右端点对应的的位置)
我们要选出尽量多条不重合的线段,并保证字典序最小
可以轻易看出 我们最多可以选择2条线段 选择的方案有:{1,3} {1,4}{2,3}
但题目要求的字典序最小 那么答案就是{1,3}了
如果只需要求第一问的话,我们可以轻易地求出答案(按 r 大小排序然后贪心就可以了)
但是 这题没有那么简单!它要求求出字典序最小的最优选择方案 这也是这题的精(du)妙(liu)之处
我们发现 我们按排序贪心出的答案,虽然总的线段数是正确的,但是它不一定是字典序最小的答案,这就很令人头大
考虑一条线段 如果它是答案的话,把它加入答案集合,是不会影响能取到的线段总数的
(线段[L1,R1]和线段[L2,R2]是当前答案集合中,左边和右边离区间[L,R]最近的两个,且一定不与当前线段有重合的部分)
如图 如果线段[L,R]属于我们要求的答案集合 加入线段[L,R]的话,是不会对整体答案产生影响的,因为其不会L1之前的和R2之后的区间产生影响(想一下,如果能够影响这些区间的话,说明我们选择的这个线段与这些区间有重合的部分,而这种情况是不可能存在的),我们可以缩小一下范围,对于(R1,L2)而言,答案也不会产生影响。
于是,我们可以按字典序枚举一下所有的线段 如果所加入的线段不与任何一条已有的线段有重合 且不会对答案产生影响 那么我们就把它加入答案(实际上直接输出该线段的编号就好啦~
这里对答案不会产生影响 我们可以用一个式子来表示 ans[R1+1,L-1]+ans[R+1,L2-1]+1==ans[R1+1,L2-1] (ans[i,j]表示区间[i,j]内能取到的线段总数的最大值)
如果该式子成立 就说明它属于我们的答案集合
那么 我们现在的问题就变成了 如何求ans[i,j]
暴力出奇迹
显然 为了保证总体复杂度尽量优秀 我们要尽量快的求解
暴力求解显然是不行的
那么 该咋办呢???????????
当然 总有巨佬想出招 显然不是我
考虑对于每一条线段 选择它以后 下一条线段的最优解是确定的
那么我们可以预处理出每一条线段的下一步最优解
但是 如果一步一步地慢慢跳显然复杂度是不正确的
想到了啥??
倍增!
我们可以通过倍增来快速求解
我们用fa[i][j]表示 从第i条线段开始 往后跳2j 步后跳到了哪条线段
这样我们就可以愉快地log(n)地求解ans[i,j]了!!!
这题还有一个很关键的地方
我们可以先做一个O(n)预处理 处理每个线段的前一个最优线段是啥 然后再倒着推回去(于是有了我们的fa 数组)
对于一个线段[l,r],我们用pre[r]表示 在区间[1,r]中,能取到的最多线段数(不包括自己)是由哪个位置转移过来的,然后d[pre[r]]表示是从哪个线段转移过来的。
最开始初始化
for(int i=1;i<=n;i++) if(pre[in[i].r]<in[i].l) pre[in[i].r]=in[i].l,d[in[i].r]=in[i].id;
这样做也可以去掉和自己的r相同但是比自己长的区间(妙 啊)
然后是转移
for(int i=1;i<=tot;i++) if(pre[i]<pre[i-1]) pre[i]=pre[i-1],d[i]=d[i-1];
然后我们就可以愉快地求出自己是由哪条线段转移过来的啦
for(int i=1;i<=n;i++) fa[in[i].id][0]=d[in[i].l-1];
然后整个流程大体就是 读入->预处理出线段之间的转移(注意要离散化)->建立倍增数组->贪心求出整体最优解->按字典序逐个枚举并输出答案
整个代码如下:

#include <iostream> #include <cstdio> #include <set> #include <algorithm> #include <cstring> using namespace std; const int N=2e5+111,inf=1e9+7; struct interval{ int l,r,id; }in[N]; bool cmp1(interval aa,interval bb) { return aa.r<bb.r; } bool cmp2(interval aa,interval bb) { return aa.id<bb.id; } struct node{ int v,id; bool operator < (const node b) const {return v<b.v;} }w[N]; int n,tot,ans=1; int lsh[N<<1],fa[N][25],pre[N<<1],d[N<<1]; set<node>s; int get_ans(int l,int st) { int res=0; for(int i=20;i>=0;i--) if(fa[st][i]&&in[fa[st][i]].l>l) res+=1<<i,st=fa[st][i]; return res; } int main() { freopen("convention.in","r",stdin); freopen("convention.out","w",stdout); scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%d%d",&in[i].l,&in[i].r); in[i].id=i; lsh[++tot]=in[i].l,lsh[++tot]=in[i].r; } sort(lsh+1,lsh+tot+1); tot=unique(lsh+1,lsh+tot+1)-lsh-1; for(int i=1;i<=n;i++) { in[i].l=lower_bound(lsh+1,lsh+tot+1,in[i].l)-lsh; in[i].r=lower_bound(lsh+1,lsh+tot+1,in[i].r)-lsh; } sort(in+1,in+n+1,cmp1); for(int i=1;i<=n;i++) if(pre[in[i].r]<in[i].l) pre[in[i].r]=in[i].l,d[in[i].r]=in[i].id; for(int i=1;i<=tot;i++) if(pre[i]<pre[i-1]) pre[i]=pre[i-1],d[i]=d[i-1]; for(int i=1;i<=n;i++) fa[in[i].id][0]=d[in[i].l-1]; for(int i=1;i<=20;i++) for(int j=1;j<=n;j++) fa[j][i]=fa[fa[j][i-1]][i-1]; int res=-1,p=0; for(int i=1;i<=n;i++) if(in[i].l>res) res=in[i].l,p=in[i].id; fa[n+1][0]=p; for(int i=1;i<=20;i++) fa[n+1][i]=fa[fa[n+1][i-1]][i-1]; for(int i=20;i>=0;i--) if(fa[p][i]) ans+=1<<i,p=fa[p][i]; printf("%d\n",ans); sort(in+1,in+n+1,cmp2); s.insert((node){-1,0});s.insert((node){inf,n+1}); set<node>::iterator t; for(int i=1;i<=n;i++) { if(s.lower_bound((node){in[i].l,0})!=s.lower_bound((node){in[i].r,0})) continue; t=s.lower_bound((node){in[i].l,0}); node R=*t;t--; node L=*t; if(get_ans(L.v,i)+get_ans(in[i].r,R.id)+1!=get_ans(L.v,R.id)) continue; printf("%d ",i); s.insert((node){in[i].l,i}); s.insert((node){in[i].r,i}); } return 0; }
注意一下小细节 就可以愉快地AC啦~
这么妙的思想 当然不是我自己能想出来的 文中部分内容引自PhantasmDragon巨佬的题解
(P.S.这题真的很妙 顺便膜拜一下去年10月就已经切了这道题的ZZX巨佬TAT)
感谢阅读~