【HNOI 2018】游戏

Problem

Description

一次小 \(G\) 和小 \(H\) 在玩寻宝游戏,有 \(n\) 个房间排成一列,编号为 \(1,2,…,n\),相邻房间之间都有 \(1\) 道门。其中一部分门上有锁(因此需要对应的钥匙才能开门),其余的门都能直接打开。

现在小 \(G\) 告诉了小 \(H\) 每把锁的钥匙在哪个房间里(每把锁有且只有一把钥匙),并作出 \(p\) 次指示:第 \(i\) 次让小 H 从第 \(S_i\) 个房间出发,去第 \(T_i\) 个房间寻宝。但是小 \(G\) 有时会故意在指令里放入死路,而小 \(H\) 也不想浪费多余的体力去尝试,于是想事先调查清楚每次的指令是否存在一条通路。

你是否能为小 \(H\) 作出解答呢?

Input Format

第一行三个整数\(n\),\(m\),\(p\),代表共有 \(n\) 个房间,\(m\) 道门上了锁,以及 \(p\) 个询问。

接下来 \(m\) 行每行有两个整数\(x\),\(y\),代表第 \(x\) 到第 \(x + 1\) 个房间的门上有把锁,并且这把锁的钥匙被放在了第 \(y\) 个房间里。输入保证 \(x\) 不重复。

接下来 \(p\) 行,其中第 \(i\) 行是两个整数 \(S_i\),\(T_i\),代表一次询问。

Output Format

输出 \(m\) 行,每行一个大写的 YESNO 分别代表能或不能到达。

Sample

Input 1

5 4 5
1 3
2 2
3 1
4 4
2 5
3 5
4 5
2 1
3 1

Output 1

YES
NO
YES
YES
NO

Input 2

此组样例满足特性:\(y \le x\) 恒成立

7 5 4
2 2
3 3
4 2
5 3
6 6
2 1
3 4
3 7
4 5

Output 2

YES
YES
NO
NO

Explanation

Explanation for Input 1

第一个询问 \(S = 2\)、\(T = 5\) 的一条可行路线是:\(2 \rightarrow 3 \rightarrow 2 \rightarrow 1 \rightarrow 2 \rightarrow 3 \rightarrow 4 \rightarrow 5\)。

Explanation for Input 2

第一个询问 \(2\) 和 \(1\) 房间之间没有锁所以为一条通路。

Range

测试点编号 n m 其他特性
1 \(\le 1000\) \(\le 1000\)
2 \(\le 1000\) \(\le 1000\)
3 \(\le 10^5\) \(\le 10^5\) \(y \le x\) 恒成立
4 \(\le 10^5\) \(\le 10^5\) \(y \le x\) 恒成立
5 \(\le 10^5\) \(\le 10^5\)
6 \(\le 10^5\) \(\le 10^5\)
7 \(\le 10^6\) \(\le 10^6\) \(y \le x\) 恒成立
8 \(\le 10^6\) \(\le 10^6\) \(y \le x\) 恒成立
9 \(\le 10^6\) \(\le 10^6\)
10 \(\le 10^6\) \(\le 10^6\)

Algorithm

线段树,单调栈。

Mentality

基本思路:设 \(r[i]\) 与 \(l[i]\) 为从 \(i\) 出发的能到达的区间,求出后对于每个询问 \(O(1)\) 判断就好。

这一题我们先从 \(60\) 分的做法看起:

  • 暴力拓展左右端点,复杂度 \(O(n^2)\),\(20pts\) 。
  • 对于所有 \(y\le x\) 的点用单调栈,\(40pts\) 。

暴力并不难,我们看看 \(40pts\) 的单调栈怎么做。其实不能算得上是什么神仙思路,类似于并查集,各扇门将序列分成了一些小区间,如果你通过此区间能到达其他区间,那么其他区间能到达的所有区间你也能到达。这就是此题单调栈的思路。

首先,既然所有 \(y\le x\) ,可以知道,我们向左走,一旦碰到一扇门就肯定走不过去了,那么我们只需要考虑最多能向右走多远就行了。

这个向右走的过程我们使用单调栈来维护:如果我们此时能到达的区间里有通向右边相邻区间的钥匙,那一旦到达当前区间,我必定能到达下一个区间,那么将两个区间合并为一个区间即可。

此处应有小代码:

bool pd(int x)
{
    return key[r[x]]>=l[x]&&key[r[x]]<=r[x];
}
for(int i=n;i>=1;i--)
{
    stack[++top]=r[i];//r[i]为初始区间右端点
    while(pd(i))//如果当前区间右端点的钥匙在区间内
        r[i]=stack[--top];//更新右端点并栈内合并区间
}

\(40pts\) 解决了,那 \(100\) 分呢?

其实也很简单,我们唯一的难点仅在于左端点也会更新。所以我们只需要能在当前右端点条件下快速求出左端点能扩展到多远即可。

根据 \(y\le x\) 的情况,我们可以类推得到:向左走时,一旦钥匙在门左边,那就肯定过不去;向右走时,一旦钥匙在门右边,那肯定也过不去。

于是我们可以求出 \(ll[i]\) 与 \(rr[i]\) 来代表 \(i\) 所能到达区间的理论上界 (最大的左右端点只可能是 \(ll\) 与 \(rr\))。

那不难发现,我当前能走到的区间是 \([l,r]\) ,而对于 \(l\) 的扩展,一定能扩展到 \([ll,l-1]\) 里最右边的,钥匙位置 \(>r\) 的位置后边。

为什么呢?因为根据我们的筛法,区间 \([ll,l-1]\) 内的门的钥匙肯定都在门右边 (不然 \(ll\) 只会更右) ,则此区间内门的钥匙要么 \(\le r\) 要么 \(>r\) ,同时又在自己右边,那钥匙位置 \(\le r\) 的门肯定都可以打开的呀!

那么和之前是一模一样的,只是判断单调栈弹出之前先更新左端点即可:

bool pd(int x)
{
    update_l(x);
    return key[r[x]]>=l[x]&&key[r[x]]<=r[x];
}
for(int i=n;i>=1;i--)
{
    stack[++top]=r[i];
    while(pd(i))
        r[i]=stack[--top];
}

至于具体怎么更新左端点呢?我们只需要找到区间 \([ll,l-1]\) 中,第一个 \(\ge r[i]\) 的位置就好。我们可以用线段树维护区间钥匙位置最大值,并在线段树上查找就好,设查找到的位置为 \(pos\) ,左端点更新为 \(pos+1\) 即可。

Code

#include<iostream>
#include<cstdio>
using namespace std;
#define ls (o<<1)
#define rs ((o<<1)+1)
#define mid ((l+r)>>1)
int n,m,Q,key[1000001],ll[1000001],rr[1000001],l[1000001],r[1000001];
int now,top,stack[1000001];
int L,R,X,ans,maxx[4000001];
void build(int o,int l,int r)
{
    if(l==r)
    {
        maxx[o]=key[l];
        return;
    }
    build(ls,l,mid),build(rs,mid+1,r);
    maxx[o]=max(maxx[ls],maxx[rs]);
}
void query(int o,int l,int r)
{
    if(ans)return;
    if(l==r)
    {
        ans=l;
        return;
    }
    if(mid<R&&maxx[rs]>X)query(rs,mid+1,r);//显然先找右区间哇
    if(mid>=L&&maxx[ls]>X)query(ls,l,mid);
}
bool pd(int x)
{
    L=ll[x],R=l[x]-1,X=r[x],ans=0;
    query(1,1,n);
    l[x]=!ans?ll[x]:ans+1;//更新左端点
    return key[r[x]]>=l[x]&&key[r[x]]<=r[x];
}
int main()
{
    freopen("4436.in","r",stdin);
    freopen("4436.out","w",stdout);
    cin>>n>>m>>Q;
    int x,t,now,tp;
    for(int i=1;i<=m;i++)
        scanf("%d",&x),scanf("%d",&key[x]);
    now=1,tp=1;
    for(int i=1;i<=n;i++)
    {
        if(key[i-1])
        {
            now=i;
            if(key[i-1]<i)tp=i;
        }
        ll[i]=tp,l[i]=now;
    }
    now=n,tp=n;
    for(int i=n;i>=1;i--)
    {
        if(key[i])
        {
            now=i;
            if(key[i]>i)tp=i;
        }
        rr[i]=tp,r[i]=now;
    }//处理出 l,r,ll,rr
    build(1,1,n);//建树
    stack[++top]=n;
    for(int i=n;i>=1;i--)
    {
        stack[++top]=r[i];
        while(pd(i))
            r[i]=stack[--top];
    }//求出 l,r
    while(Q--)
    {
        scanf("%d%d",&x,&t);
        printf(t>=l[x]&&t<=r[x]?"YES\n":"NO\n");
    }
}
上一篇:HNOI 世界树 虚树


下一篇:POJ 2586 Y2K Accounting Bug