P9839 [lg noip D] 四暗刻单骑

· · 题解

Part 1. O(nm)

先观察题目的一些基本性质。

那么,一个人想和牌,他的策略一定是从某个时刻开始一直拿着某张牌,直到摸到下张一样的。

考虑某个人一直拿着第 i 张牌会产生什么效果:

那么我们可以在序列上模拟这个过程,如果牌的效果是胜利就选择胜利更早的;失败就选择失败得更晚的,因为可能后面能摸到胜利的牌而翻盘。如果当前已经到达了某个人手里的牌的胜利或失败时刻,则判定答案。

然而直接这样搞并不正确,可怜的樱雪喵写假了一天也没想明白哪里不对。

考虑这样的一组数据:牌堆依次为 3,1,4,3,4,初始手牌为 1,2
Alice 在第一轮是否把牌换成 3 都是平局,她更希望等到后面胜利。而 Bob 如果寄希望于后面胜利,不选择把 2 换成 1,他最后只能失败。而如果他的目标是平局,他会摸走牌堆中的 1,并成功平局。
这启发我们思考一个问题:存在一些情况,如果一个人目标是取得胜利,他就输了;但如果他的目标仅仅是保住平局,却能成功阻止对面赢。这是不能直接贪心判断的。

考虑用如下做法改变他们的目标:先钦定平局算 Alice 赢,这等价于 Alice 的目标是保住平局。如果此时 Bob 仍然能赢,才算作 Bob 必胜。反之同理。
如果正反都不满足条件,则说明存在一种方式使本来要输的人保住平局,答案为平局。

细节想不清楚的话很容易写假,可以参考代码理解。

const int N=2e5+5;
int id,n,m,k,a[N],flag;
vector<int> pos[N];
il int find(int x,int l,int r)
{
    auto qwq=upper_bound(pos[x].begin(),pos[x].end(),l);
    if(qwq==pos[x].end()||(*qwq)>r) return -1;
    return *qwq;
}
il int getw(int x,int l,int r,int fg=0)
{
    int qwq=find(x,l+fg,r);
    if(qwq==-1) return (l&1)==flag?n+1:-n-1;
    if((qwq&1)==(l&1)) return qwq;
    int nqwq=find(x,qwq,r);
    if(nqwq==-1) return (l&1)==flag?qwq:-qwq;
    else if((nqwq&1)==(l&1)) return qwq;
    else return -qwq;
}
il int solve(int x,int y,int l,int r)
{
    if(x==y)
    {
        int nxt=find(x,l-1,r);
        if(nxt==-1) return flag?1:-1;
        else if((nxt&1)==(l&1)) return 1;
        else return -1;
    }
    int wx=getw(x,l-2,r,1),wy=getw(y,l-1,r);
    for(int i=l;i<=r;i++)
    {
        if(wx==i) return 1; if(wy==i) return -1;
        if(-wx==i) return -1; if(-wy==i) return 1;
        if(x==y)
        {
            int nxt=find(x,i-1,r);
            if(nxt==-1) return flag?1:-1;
            else if((nxt&1)==(l&1)) return 1;
            else return -1;
        }
        if((i&1)==(l&1))
        {
            int nxt=getw(a[i],i,r);
            if(nxt>0&&(nxt<wx||wx<0)) wx=nxt,x=a[i];
            else if(nxt<0&&wx<0&&nxt<wx) wx=nxt,x=a[i];
        }
        else 
        {
            int nxt=getw(a[i],i,r);
            if(nxt>0&&(nxt<wy||wy<0)) wy=nxt,y=a[i];
            else if(nxt<0&&wy<0&&nxt<wy) wy=nxt,y=a[i];
        }
    }
    return flag?1:-1;
}
int main()
{
    n=read(),m=read(),k=read();
    for(int i=1;i<=n;i++) a[i]=read(),pos[a[i]].push_back(i);
    while(m--)
    {
        id++;
        int x=read(),y=read(),l=read(),r=read();
        flag=1; int res1=solve(x,y,l,r);
        flag=0; int res0=solve(x,y,l,r);
        if(res1==-1) printf("B\n");
        else if(res0==1) printf("A\n"); else printf("D\n");
    }
    return 0;
}

Part 2. O((n+m)\log n)

假设每张牌的结局是已经确定的,依旧不容易快速地维护答案,因为输了要尽可能晚输,赢了又要尽可能早赢。

我们继续观察性质。

注意到,到达某张手牌判定答案的时刻时,被判定的一定是赢的那张牌。换句话说,一个人必然不会一直拿着一张输的牌,直到因为这张牌而输掉。

考虑证明这个结论。假设 Alice 现在手里拿着一张输的牌,并且下一轮就要输了;这时候她又摸到了另一张要输的牌。因为这两张牌不同,它们输的位置也一定不同。那么新摸的这张牌一定输得比原来那张要晚。每当出现这种情况 Alice 就换牌,即可保证始终不因为输的牌而输掉。
当然这里要特判拿着初始手牌在第一轮就输掉的情况,因为此时他们没有选择权。

因此我们只需判断一段区间内最早赢的牌被谁摸到了。

考虑离线询问,从左向右扫描 r。对于在 i 位置的牌,它的贡献只与它下一张相同的牌、下下张相同的牌是否在区间内有关。因此一张牌的贡献只会改变 3 次,可以每次修改暴力求新值。
那么对于每个询问,最早赢的牌即为区间内最小值所在的位置,判断这张牌位置的奇偶性即可。

我们需要一个数据结构支持单点修改,区间查询最小值和最小值的位置。这里使用线段树维护。

const int N=2e5+5,inf=1e9;
int id,n,m,k,a[N],flag;
vector<int> pos[N];
il int find(int x,int l,int r)
{
    auto qwq=upper_bound(pos[x].begin(),pos[x].end(),l);
    if(qwq==pos[x].end()||(*qwq)>r) return -1;
    return *qwq;
}
il int getw(int x,int l,int r,int fg=0)
{
    int qwq=find(x,l+fg,r);
    if(qwq==-1) return inf;
    if((qwq&1)==(l&1)) return qwq;
    int nqwq=find(x,qwq,r);
    if(nqwq==-1) return (l&1)==flag?qwq:inf;
    else if((nqwq&1)==(l&1)) return qwq;
    else return inf;
}
struct segtree
{
    struct node{int mn,pos;} tr[N<<2];
    #define ls (x<<1)
    #define rs (x<<1|1)
    #define mid (l+r>>1)
    il node pushup(const node &l,const node &r)
    {
        if(l.mn<r.mn) return l;
        else return r;
    }
    void build(int x,int l,int r) 
    {
        if(l==r) {tr[x]={inf,l};return;}
        build(ls,l,mid),build(rs,mid+1,r);
        tr[x]=pushup(tr[ls],tr[rs]);
    }
    void upd(int x,int l,int r,int p,int k)
    {
        if(l==r) {tr[x]={k,l};return;}
        if(p<=mid) upd(ls,l,mid,p,k);
        else upd(rs,mid+1,r,p,k);
        tr[x]=pushup(tr[ls],tr[rs]);
    }
    node query(int x,int l,int r,int ml,int mr) 
    {
        if(l==ml&&r==mr) return tr[x];
        if(mr<=mid) return query(ls,l,mid,ml,mr);
        else if(ml>mid) return query(rs,mid+1,r,ml,mr);
        else return pushup(query(ls,l,mid,ml,mid),query(rs,mid+1,r,mid+1,mr));
    }
}seg;
int ans[2][N];
vector<int> nd[N];
struct node{int x,y,l,r,id;};
vector<node> q[N];
il int calc(int x,int y,int l,int r)
{
    if(x==y)
    {
        int nxt=find(x,l-1,r);
        if(nxt==-1) return flag?1:-1;
        else if((nxt&1)==(l&1)) return 1;
        else return -1;
    }
    if(y==a[l])
    {
        int nxt=find(y,l,r);
        if(nxt!=-1&&((nxt&1)==(l&1))) return 1;
        else if(nxt==-1) return flag?1:-1;
    }
    int wx=getw(x,l-2,r,1),wy=getw(y,l-1,r);
    int ans=min(wx,wy),pos=(wx<wy)?l-2:l-1;
    segtree::node qwq=seg.query(1,1,n,l,r);
    if(qwq.mn<ans) ans=qwq.mn,pos=qwq.pos;
    if(ans==inf) return flag?1:-1;
    return ((l&1)==(pos&1))?1:-1;
}
void solve()
{
    seg.build(1,1,n);
    for(int r=1;r<=n;r++)
    {
        for(auto x:nd[r]) seg.upd(1,1,n,x,getw(a[x],x,r));
        for(auto i:q[r])
        {
            int x=i.x,y=i.y,l=i.l,r=i.r,id=i.id;
            ans[flag][id]=calc(x,y,l,r);
        }
    }
}
int main()
{
    n=read(),m=read(),k=read();
    for(int i=1;i<=n;i++) a[i]=read(),pos[a[i]].push_back(i);
    for(int i=1;i<=m;i++) 
    {
        int x=read(),y=read(),l=read(),r=read();
        q[r].push_back({x,y,l,r,i});
    }
    for(int i=1;i<=n;i++) 
    {
        int nxt=find(a[i],i,n);
        if(nxt==-1) continue;
        nd[nxt].push_back(i);
        int nnxt=find(a[i],nxt,n);
        if(nnxt!=-1) nd[nnxt].push_back(i);
    }
    for(flag=0;flag<2;flag++) solve();
    for(int i=1;i<=m;i++)
    {
        if(ans[1][i]==-1) printf("B");
        else if(ans[0][i]==1) printf("A");
        else printf("D");
        printf("\n");
    }
    return 0;
}