【洛谷2403】[SDOI2010] 所驼门王的宝藏(Tarjan+dfs遍历)
大致题意: 一个由\(R*C\)间矩形宫室组成的宫殿中的\(N\)间宫室里埋藏着宝藏。由一间宫室到达另一间宫室只能通过传送门,且只有埋有宝藏的宫室才有传送门。传送门分为3种,分别可以到达同行的任一宫室(横天门)、同列的任一宫室(纵寰门)和以该宫室为中心周围8个的任一宫室(自 由 门)。现在你可以从任一宫室开始寻宝,并可以在任一宫室结束寻宝,请求出最多可获得的宝藏数目(每个宝藏只能获得一次)。
一个简单的想法
显然,我们可以将每个宫室与它能到达的宫室之间连一条边。由于可能会出现环,我们就需要用\(Tarjan\)来把环缩点,缩点的过程中要注意记录每个环上的宫室数目。
缩完点后,我们就可以从每个入度为1的点开始对图进行遍历,求出最多能走过的宫室数。而这个步骤可以用dfs轻松实现。
这样就好了吗?
不,还有一些细节。
小技巧优化
首先,我们要注意的是,直接把每个宫室与它能到达的宫室之间两两建边,建出的边的规模是\(O(N^2)\)的,而\(N≤100000\),这么多边我们存不下。虽然洛谷数据水,这样也能过。
这时就要用到一个小技巧:对于同一行的横天门,我们不需要将其两两之间连边,只要保证最后能够连成一个环即可;而该行其他类型的门,也不需要将每一个横天门向其连边,只要让第一个出现的横天门向所有其他类型的门连边即可。同理,对于同一行的纵寰门,也可以进行同样的处理,让边数从\(O(n^2)\)降到了\(O(n)\)大大减少了边数。
还有,就算你进行了这样的操作,最后还是有可能会TLE,这时,我们可以发现,好像时间复杂度最大的就是最后的dfs遍历了。我们可以考虑用记忆化,用\(vist[i]\)来记录从缩点后编号为\(i\)的点出发,最多能得到的宝物数目即可。
这样,就可以AC了。
代码
#include<bits/stdc++.h>
#define max(a,b) ((a)>(b)?(a):(b))
#define min(a,b) ((a)<(b)?(a):(b))
#define LL long long
#define N 100000
using namespace std;
typedef pair<int,int> point;
int n,r,c,d=0,ee=0,nee=0,cnt=0,ans=0,top=0,lnk[N+5],nlnk[N+5],dfn[N+5],low[N+5],vis[N+5],Stack[N+5],sum[N+5],In[N+5],vist[N+5];
struct door
{
int x,y,Type,pos,col;
}a[N+5];
struct edge
{
int to,nxt;
}e[2*N+5],ne[2*N+5];
map<point,int> mp;
inline char tc()
{
static char ff[100000],*A=ff,*B=ff;
return A==B&&(B=(A=ff)+fread(ff,1,100000,stdin),A==B)?EOF:*A++;
}
inline void read(int &x)
{
x=0;int f=1;char ch;
while(!isdigit(ch=tc())) if(ch=='-') f=-1;
while(x=(x<<3)+(x<<1)+ch-'0',isdigit(ch=tc()));
x*=f;
}
inline void write(int x)
{
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar(x%10+'0');
}
inline void add(int x,int y)//添加一条新边
{
if(x^y) e[++ee].to=y,e[ee].nxt=lnk[x],lnk[x]=ee;
}
inline void nadd(int x,int y)//添加一条缩点后的新边
{
if(x^y) ne[++nee].to=y,ne[nee].nxt=nlnk[x],nlnk[x]=nee;
}
inline bool cmp_x(door x,door y)//以行为第一关键字进行sort,使每一行都是横天门在最前面
{
if(x.x^y.x) return x.x<y.x;//判断是否在同一行,不是同一行的尽量让行小的在前面
if(x.Type==1&&y.Type^1) return true;//对于同行的,让横天门在最前面
if(x.Type^1&&y.Type==1) return false;
return x.y<y.y;//如果都是或都不是横天门,则列小的在前面
}
inline bool cmp_y(door x,door y)//以列为第一关键字进行sort,使每一列都是纵寰门在最前面
{
if(x.y^y.y) return x.y<y.y;//判断是否在同一列,不是同一列的尽量让列小的在前面
if(x.Type==2&&y.Type^2) return true;//对于同列的,让纵寰门在最前面
if(x.Type^2&&y.Type==2) return false;
return x.x<y.x;//如果都是或都不是纵寰门,则行小的在前面
}
inline bool cmp_z(door x,door y)//对自 由 门的特殊处理,让自 由 门在最前面
{
if(x.Type==3&&y.Type^3) return true;//让自 由 门在最前面
if(x.Type^3&&y.Type==3) return false;
return x.pos<y.pos;//如果都是或都不是自 由 门,则编号小的在前面
}
inline bool cmp_pos(door x,door y)//以编号为第一关键字进行sort,让编号小的在前面,变回读入时的顺序
{
return x.pos<y.pos;
}
inline void Tarjan(int x)//利用Tarjan缩点
{
dfn[x]=low[x]=++d,Stack[++top]=x,vis[x]=1;//记录当前节点的dfs序与当前节点所能到达的dfs序最小的点,将当前节点加入栈中,并标记当前节点在栈中
for(register int i=lnk[x];i;i=e[i].nxt)//枚举从当前节点出发的每一条边
{
if(!dfn[e[i].to]) Tarjan(e[i].to),low[x]=min(low[x],low[e[i].to]);//如果这个节点没访问过,就先对这个节点进行操作,然后更新当前节点能到达的dfs序最小的点
else if(vis[e[i].to]) low[x]=min(low[x],low[e[i].to]);//否则,如果这个点在栈中,就进行更新
}
if(low[x]==dfn[x])//如果当前节点就是当前节点能到达的dfs序最小的点,则对当前强连通分量进行缩点
{
sum[a[x].col=++cnt]=1,vis[x]=0;//给当前节点加入一个新的强连通分量,并将这个新的强连通分量的大小赋值为1,标记当前节点已出栈
while(Stack[top]^x) ++sum[a[Stack[top]].col=cnt],vis[Stack[top--]]=0;//将栈中当前节点之上的节点一一弹出,并更新这个新的强连通分量的大小
--top;//将当前节点弹出
}
}
inline void dfs(int x)//dfs遍历缩点后的图
{
for(register int i=nlnk[x];i;i=ne[i].nxt)//枚举每一条边
{
if(!vist[ne[i].to]) dfs(ne[i].to);//如果这个节点未被访问过,就访问该节点
vist[x]=max(vist[x],vist[ne[i].to]+sum[ne[i].to]);//更新从当前节点出发能得到的最多的宝物数目
}
}
int main()
{
register int i,j;
for(read(n),read(r),read(c),i=1;i<=n;++i)
read(a[i].x),read(a[i].y),read(a[i].Type),a[i].pos=i,mp[make_pair(a[i].x,a[i].y)]=i;
int fst;
sort(a+1,a+n+1,cmp_x);//对接下来对横天门的操作的预处理
for(i=1,fst=0;i<=n;++i)
{
while(i<=n&&a[i].Type^1) ++i;//只要当前门不是横天门,就跳过这个门
if(i>n) continue;
fst=i;//将i标记为该行第一个横天门
while(i<=n&&a[i].x==a[i+1].x&&a[i+1].Type==1) add(a[i].pos,a[i+1].pos),++i;//将前一个横天门与当前横天门连边
add(a[i].pos,a[fst].pos);//将最后一个横天门与第一个横天门连边,形成一个环
while(i<=n&&a[i].x==a[i+1].x) add(a[fst].pos,a[++i].pos);//将该行第一个横天门与该行其他类型的门连边
}
sort(a+1,a+n+1,cmp_y);//对接下来对纵寰门的操作的预处理
for(i=1,fst=0;i<=n;++i)
{
while(i<=n&&a[i].Type^2) ++i;//只要当前门不是纵寰门,就跳过这个门
if(i>n) continue;
fst=i;//将i标记为该列第一个纵寰门
while(i<=n&&a[i].y==a[i+1].y&&a[i+1].Type==2) add(a[i].pos,a[i+1].pos),++i;//将前一个纵寰门与当前纵寰门连边
add(a[i].pos,a[fst].pos);//将最后一个纵寰门与第一个纵寰门连边,形成一个环
while(i<=n&&a[i].y==a[i+1].y) add(a[fst].pos,a[++i].pos);//将该列第一个纵寰门与该列其他类型的门连边
}
sort(a+1,a+n+1,cmp_z);//对接下来对自 由 门的操作的预处理
for(i=1;i<=n&&a[i].Type==3;++i)//枚举每一个自 由 门
{
//枚举每个自 由 门周围的8个宫室,将这个门与周围有宝藏的宫室相连
if(mp[make_pair(a[i].x-1,a[i].y)]) add(a[i].pos,mp[make_pair(a[i].x-1,a[i].y)]);
if(mp[make_pair(a[i].x+1,a[i].y)]) add(a[i].pos,mp[make_pair(a[i].x+1,a[i].y)]);
if(mp[make_pair(a[i].x,a[i].y-1)]) add(a[i].pos,mp[make_pair(a[i].x,a[i].y-1)]);
if(mp[make_pair(a[i].x,a[i].y+1)]) add(a[i].pos,mp[make_pair(a[i].x,a[i].y+1)]);
if(mp[make_pair(a[i].x-1,a[i].y-1)]) add(a[i].pos,mp[make_pair(a[i].x-1,a[i].y-1)]);
if(mp[make_pair(a[i].x+1,a[i].y+1)]) add(a[i].pos,mp[make_pair(a[i].x+1,a[i].y+1)]);
if(mp[make_pair(a[i].x+1,a[i].y-1)]) add(a[i].pos,mp[make_pair(a[i].x+1,a[i].y-1)]);
if(mp[make_pair(a[i].x-1,a[i].y+1)]) add(a[i].pos,mp[make_pair(a[i].x-1,a[i].y+1)]);
}
sort(a+1,a+n+1,cmp_pos);//按照读入时的顺序重新排序
for(i=1;i<=n;++i)
if(!dfn[a[i].pos]) Tarjan(i);//用Tarjan缩点
for(i=1;i<=n;++i)
for(j=lnk[i];j;j=e[j].nxt)
if(a[i].col^a[e[j].to].col) nadd(a[i].col,a[e[j].to].col),++In[a[e[j].to].col];//更新缩点之后点与点之间的边
for(i=1;i<=cnt;++i)
if(!In[i]) dfs(i),ans=max(ans,vist[i]+sum[i]);//贪心的思想,从入度为0的点出发肯定能得到最优答案
return write(ans),0;
}
待到再迷茫时回头望,所有脚印会发出光芒