【题解】[IOI2008] Teleporters 传送器
(强烈建议观看洛谷良心题面↑不要去黑暗爆炸)
Statement
给定一条直线,上面有 \(N\) 对传送门,每当你到达某个传送器的两个端点之一时,传送器都会立即将你传送到该传送器的另一个端点。被传送到另一个端点之后,必须继续沿这条直线路线向东行进;无法避开前进路上的任何传送器端点。记传送次数为得分。
现在允许增加 \(M\) 对新的传送门(同样计分),可以在任意实数坐标上,但所有端点必须唯一,且位于起点和终点之间。
求能获得的最高分数。
Solution
不愧是IOI,很简单的一道题,但就是想不到。大概是我菜罢。
考虑对整个数轴按照给定的传送门分段,每一个传送门把当前段分成两段。
显然,每个段内部都不会有其他传送门(不然就会再次分段),按照这个来进行建图。
建图:
-
到达每个段有且仅有一种方式:走这个段左端点处的传送门。
-
离开这个段也只有一种方式:走右端点处的传送门(因为如果是左端点肯定是进入这段而不是出去,如果是中间那么只能向右走)。
-
这样,把每个段看成一个点,那么每个点只有唯一的出入和入度,起始点除外。
-
也就是说,我们一定会从起点入,从终点出。
现在我们有若干个连通块。不难发现,除了起点到终点是一条简单路径,其他点一定在一个环上(这个很显然吧,都是唯一入度/出度,怎么说也不会连没啊)。
来考虑加传送门。
有三种方式:
- 连接两个不在同一个连通块的点。
- 那么这两个点分成两部分,所在连通块合并。
- 连接两个同一连通块的点。
- 这样会把一个连通块拆成两半,对答案没有任何贡献。直接忽略。
- 放在某一段内部,成为一个独立的点。
- 这种情况下,可以先成点再连入答案连通块,但是要花费两个代价。
那么解法就很明显了。
将所有原图中的连通块(都是链/环)按大小降序排序,然后从大到小依次用第一种方法连到“起点-终点”的路径上去。如果连通之后还有剩余,采用第三种方法就能花费两倍代价累计入答案。如果剩余个数是奇数,就将一个连到终点前面的地方,能增加一个贡献。
实现的时候,直接特殊处理起点到终点的路径,然后找环即可。
Code
菜鸡的代码又臭又长……洛谷的机子也不太行,BZOJ 上面直接过去了,洛谷还要开 O2……
//Author: RingweEH
const int N=1e6+10;
struct Transport //存所有传送门(door)
{
int l,r;
}d[N];
struct Node
{
int pos,typ,group; //left:0,right:1
bool operator < ( const Node &tmp ) const { return pos<tmp.pos; }
}a[N<<1];
int n,m,ans=0,cnt=0,siz[N<<1];
bool vis[N<<1];
int Find( int x ) //找 next door
{
Node tmp; tmp.pos=x;
return lower_bound( a+1,a+1+2*n,tmp )-a;
}
void Beginning()
{
int now=1,nxt;
while ( now!=2*n+1 )
{
vis[now]=1; ans++;
if ( a[now].typ==0 ) nxt=Find( d[a[now].group].r+1 );
else nxt=Find( d[a[now].group].l+1 );
now=nxt;
}
vis[now]=1;
}
void Get_Subgraph( int x,int cnt )
{
int now=x,nxt;
while ( !vis[now] )
{
vis[now]=1; siz[cnt]++;
if ( a[now].typ==0 ) nxt=Find( d[a[now].group].r+1 );
else nxt=Find( d[a[now].group].l+1 );
now=nxt;
}
}
int main()
{
n=read(); m=read();
for ( int i=1; i<=n; i++ )
{
d[i].l=read(),d[i].r=read();
int now=(i-1)*2+1;
a[now].pos=d[i].l,a[now].typ=0,a[now].group=i; now++;
a[now].pos=d[i].r,a[now].typ=1,a[now].group=i;
}
sort( a+1,a+1+2*n );
Beginning();
for ( int i=1; i<=2*n; i++ )
if ( !vis[i] ) Get_Subgraph( i,++cnt );
sort( siz+1,siz+1+cnt );
for ( int i=cnt; i>=1; i-- )
{
ans+=2; m--; ans+=siz[i];
if ( !m ) break;
}
if ( m&1 ) m--,ans++;
ans+=2*m;
printf( "%d\n",ans );
return 0;
}
BZOJ 上另一份代码 真的短……
fread
也是真的快……
#include<cstdio>
const int N=2000010,BUF=20000000;
int n,m,mx,i,x,y,ans,g[N],f[N];bool v[N],flag;char Buf[BUF],*buf=Buf;
inline void read(int&a){for(a=0;*buf<48;buf++);while(*buf>47)a=a*10+*buf++-48;}
inline void go(int x){
int t=0;
while(!v[x]){
v[x]=1;
if(g[x])x^=g[x],t++;
x++;
}
if(!flag)ans+=t;else f[t]++;
flag=1;
}
int main(){
fread(Buf,1,BUF,stdin);read(n),read(m);
for(i=1;i<=n;i++){
read(x),read(y);
if(y>mx)mx=y;
g[x]=g[y]=x^y;
}
v[mx+=5]=1;
for(i=0;i<=mx;i++)if(!v[i])go(i);
for(i=n*2;i;i--)while(m&&f[i])m--,f[i]--,ans+=i+2;
while(m>=2)m-=2,ans+=4;
printf("%d",ans+m);
}