题解:P17198 [KOI 2026 #2] 杂技
lailai0916 · · 题解
题意简述
Alice 必须始终站在 Bob 左侧。Alice 只能向右走,Bob 只能向左走,两人还可以使用当前位置的任意跳板。对每组起点与终点,判断能否经过若干合法动作到达。
解题思路
把相邻格子之间的空隙编号为
Alice 向右走会删除区间左端的若干空隙,Bob 向左走会删除区间右端的若干空隙。因此,普通行走只能把当前区间缩小为一个非空子区间。真正需要处理的是能够扩张区间的跳跃。
考虑一块从
- 若
y<x ,只有 Alice 使用它才能扩张范围。当当前区间包含空隙x 时,Alice 可以先走到格子x ,再向左跳到y ,从而把左端扩张到y ; - 若
y>x ,只有 Bob 使用它才能扩张范围。当当前区间包含空隙x-1 时,Bob 可以先走到格子x ,再向右跳到y ,从而把右端扩张到y-1 。
Alice 向右跳或 Bob 向左跳只会缩小区间,普通行走已经能够完成同样的效果,所以不必单独保留。
对每个空隙
把每个空隙看作一个有向图结点,并从
直接连边可能产生平方级边数。建立一棵以空隙为叶子的线段树,并进行如下连边:
- 每个内部结点向两个儿子连边;
- 将
[l_i,r_i] 分成O(\log N) 个线段树结点,从叶子i 向这些结点连边。
从被选中的线段树结点沿儿子边向下,恰好能够到达区间中的全部真实叶子。因此,优化后的图与直接区间连边具有相同的空隙可达关系。图中有
图中可能存在环。使用非递归 Tarjan 算法求强连通分量,并将图缩成 DAG。对每个分量,先记录其中真实空隙叶子的最小和最大编号。Tarjan 弹出分量的顺序是缩点图的逆拓扑序,所有出边指向的分量都会更早弹出。因此,按照分量编号递增处理,可以用所有后继的答案更新当前分量。
由此可得每个空隙
询问的起始状态
右端为:
每个区间
目标状态
分别对
正确性证明
普通行走只能缩小空隙区间。所有能够扩大区间的动作恰好是 Alice 向左跳和 Bob 向右跳,并分别对应从触发空隙到所覆盖区间的有向边。若触发空隙已经覆盖,两人总能保持另一人在区间另一侧,先走到跳板再完成该次扩张;反之,任何实际扩张也必然产生图中的对应转移。因此实际可扩张空隙与图中的可达空隙完全相同。
线段树结点向儿子的边只负责展开区间,叶子向区间分解结点的边恰好覆盖
起始状态覆盖
参考代码
#include <bits/stdc++.h>
using namespace std;
const int N=200005;
const int V=524293;
const int E=8000005;
const int K=20;
int head[V],to[E],nxt[E],cnt;
int lo[N],hi[N];
int dfn[V],low[V],tim,id[V],cc;
bool ins[V];
int stk[V],top,dfs[V],it[V];
int chead[V],cnxt[V],cl[V],ch[V];
int lg[N],mn[K][N],mx[K][N];
void add(int x,int y)
{
to[++cnt]=y;
nxt[cnt]=head[x];
head[x]=cnt;
}
void tarjan(int s)
{
int tp=0;
dfs[0]=s;
it[0]=head[s];
dfn[s]=low[s]=++tim;
stk[++top]=s;
ins[s]=1;
while(tp>=0)
{
int x=dfs[tp];
int &i=it[tp];
if(i)
{
int y=to[i];
i=nxt[i];
if(!dfn[y])
{
dfn[y]=low[y]=++tim;
stk[++top]=y;
ins[y]=1;
dfs[++tp]=y;
it[tp]=head[y];
}
else if(ins[y])low[x]=min(low[x],dfn[y]);
}
else
{
if(low[x]==dfn[x])
{
cc++;
while(1)
{
int y=stk[top--];
ins[y]=0;
id[y]=cc;
if(y==x)break;
}
}
tp--;
if(tp>=0)low[dfs[tp]]=min(low[dfs[tp]],low[x]);
}
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n,m;
cin>>n>>m;
int g=n-1;
for(int i=1;i<=g;i++)lo[i]=hi[i]=i;
for(int i=1;i<=m;i++)
{
int x,y;
cin>>x>>y;
if(y<x&&x<=g)lo[x]=min(lo[x],y);
if(y>x&&x>=2)hi[x-1]=max(hi[x-1],y-1);
}
int b=1;
while(b<g)b<<=1;
int vc=b*2-1;
for(int i=1;i<b;i++)
{
add(i,i<<1);
add(i,i<<1|1);
}
for(int i=1;i<=g;i++)
{
int x=b+i-1;
int l=b+lo[i]-1,r=b+hi[i]-1;
while(l<=r)
{
if(l&1)add(x,l++);
if(!(r&1))add(x,r--);
l>>=1;
r>>=1;
}
}
for(int i=1;i<=vc;i++)
{
if(!dfn[i])tarjan(i);
}
for(int i=1;i<=cc;i++)cl[i]=g+1;
for(int i=1;i<=g;i++)
{
int j=id[b+i-1];
cl[j]=min(cl[j],i);
ch[j]=max(ch[j],i);
}
for(int i=1;i<=vc;i++)
{
cnxt[i]=chead[id[i]];
chead[id[i]]=i;
}
for(int i=1;i<=cc;i++)
{
for(int j=chead[i];j;j=cnxt[j])
{
for(int k=head[j];k;k=nxt[k])
{
int y=id[to[k]];
if(y==i)continue;
cl[i]=min(cl[i],cl[y]);
ch[i]=max(ch[i],ch[y]);
}
}
}
for(int i=1;i<=g;i++)
{
int j=id[b+i-1];
mn[0][i]=cl[j];
mx[0][i]=ch[j];
}
for(int i=2;i<=g;i++)lg[i]=lg[i>>1]+1;
for(int i=1;i<K;i++)
{
int len=1<<i;
for(int j=1;j+len-1<=g;j++)
{
mn[i][j]=min(mn[i-1][j],mn[i-1][j+(len>>1)]);
mx[i][j]=max(mx[i-1][j],mx[i-1][j+(len>>1)]);
}
}
int q;
cin>>q;
while(q--)
{
int a,b,c,d;
cin>>a>>b>>c>>d;
int l=a,r=b-1;
int k=lg[r-l+1];
int p=r-(1<<k)+1;
int x=min(mn[k][l],mn[k][p]);
int y=max(mx[k][l],mx[k][p]);
cout<<(x<=c&&d-1<=y?"YES":"NO")<<'\n';
}
return 0;
}