首页 > 其他分享 >P7963 [NOIP2021] 棋局

P7963 [NOIP2021] 棋局

时间:2022-10-07 17:55:06浏览次数:79  
标签:P7963 棋局 return 并查 val int init 棋子 NOIP2021

给定 \(n\times m\) 的棋盘,连有横纵 \(2\) 种无向边,有 \(3\) 种类型的边:

  1. 只允许按照这条边走 \(1\) 步
  2. 允许继续走边权为 \(2\) 的边,但不允许改变方向
  3. 允许继续走边权为 \(3\) 的边,可以改变方向
    走到不同颜色等级 \(\leq\) 自己等级的棋子时可以吃掉棋子并停下,求先后放下 \(q\) 个棋子时每个棋子最多能走到的位置。
    \(n\times m \leq 2\times 10^5,q\leq 10^5\)

感谢这篇博客让我 \(2\) 天做懂了这道神仙题。(写下这篇题解也算是对于 TA 的题解进行补充)

首先肯定会想到维护棋盘上格子的连通性,可以将所有 \(2\) 号道路连成的连续一条(行/列)维护在同一并查集内(所以需要 \(2\) 个并查集),将所有 \(3\) 号道路连成的连通块维护在同一并查集内。然而问题是放上棋子的过程就是断开连通性的过程,实在难以维护,想到的对策是离线所有询问,然后从后往前扫一遍,一边将连通性还原,一边统计答案。

吃子呢?首先 \(1,2\) 号道路好想,只用判断道路末端是否有棋子,若有则判断能否吃掉,如果能吃掉就计入答案。 \(3\) 号道路需要用一个数据结构来维护道路可抵达的所有棋子等级(其实是 \(2\) 个,分别维护 \(2\) 种颜色),然后查询某一连通块内指定颜色的比查询棋子等级小的元素个数(代码中的 \(query1\) )。因为是维护每个并查集,所以还要涉及到合并操作,所以这里选择动态开点权值线段树的合并。

但是仅仅这样统计答案是不行的,比如

那么 \(1\) 号点就会被算 \(2\) 次。(既在 \(3\) 号并查集内,又在 \(2\) 号横向并查集内)

既然有重复的,那么就判重。利用 \(2\) 号边构成并查集内元素的连续性,可以在并查集中额外加入 \(maxn,minn\) 来得到这一段区间的最大值最小值,然后再额外创建 \(2\) 个线段树来维护横/竖的元素个数,然后可以通过查询区间和来得到被算重的元素个数(实际实现时通过前面提到的查询前缀函数 \(query1(r)-query1(l-1)\) 来实现区间和)。

然后是吃子的问题,所有的 \(1,2\) 道路的吃子都需要查询是否在 \(3\) 道路的吃子中,然而仅凭借等级是无法确定唯一棋子的,所以需要采取一种高级的离散化方法,让所有棋子的等级全部不同而不改变比大小的原则。具体的,让不同等级的棋子依然按照原顺序,而相同等级的棋子按照输入的先后顺序排序,这样之前的棋子就会比之后的棋子小,依然满足比大小的原则,而离散化之后可以通过唯一的等级确定某一棋子是否在线段树内。

最后是一些细节,比如通过用 \(e[i][j]\) 表示标号为 \(j\) 的棋子在 \(i\) 方向上的边权,用 \(val[i]\) 来记录 \(i\) 标号上的点是否有棋子,若有则为输入的顺序,线段树插入时多带一个 \(flag\) ,为 \(1\) 是插入,为 \(-1\) 是删除,还有一些都写在注释里了(震惊,我还会写注释)。

#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define mp std::make_pair
#define pii std::pair<int,int>
#define chkmin(_A,_B) (_A=std::min(_A,_B))
#define chkmax(_A,_B) (_A=std::max(_A,_B))

int t,n,m;
int e[4][200005],ans[100005],val[200005];
//val用来存储每个点是否有棋子,若有则val[i]表示位置上的点在棋盘上被放上去的顺序(输入的顺序)
//e[i][j]表示编号j的棋子在i方向上的边的权值
int dx[4]={-1,0,1,0};
int dy[4]={0,-1,0,1};
char s[200005];
struct chess{
    int col,lv,x,y,id;
}a[100005];
inline bool cmp_lv(const chess &A,const chess &B){return (A.lv==B.lv)?(A.id<B.id):(A.lv<B.lv);}
//等级相同按照来到棋盘上的顺序排序,可以实现相同等级来到棋盘早的可以被来到棋盘晚的吃到
inline bool cmp_id(const chess &A,const chess &B){return A.id<B.id;}
inline int pos1(int x,int y){return (x-1)*m+y;}
inline int pos2(int x,int y){return (y-1)*n+x;}
inline pii getxy(int pos)   {return mp((pos-1)/m+1,(pos-1)%m+1);}
void merge_All_ST(int,int);
//--------------------DSU--------------------//
struct DSU{
    int f[200005],sz[200005],maxn[200005],minn[200005];
    //maxn和minn用于处理同一并查集内元素的最大编号和最小编号
    int getfa(const int &x){
        return (f[x]==x)?x:f[x]=getfa(f[x]);
    }
    inline void merge(int x,int y,int flag=0){
        x=getfa(x),y=getfa(y);
        if(x==y)
            return;
        if(sz[x]<=sz[y])
            std::swap(x,y);
        sz[x]+=sz[y];
        f[y]=x;
        chkmax(maxn[x],maxn[y]);
        chkmin(minn[x],minn[y]);
        if(flag)
            merge_All_ST(x,y);
    }
    inline void init(const int &p){
        for(int i=0;i<=p;++i){
            f[i]=maxn[i]=minn[i]=i;
            sz[i]=1;
        }
    }
    inline int getmaxn(const int &x){return maxn[getfa(x)];}
    inline int getminn(const int &x){return minn[getfa(x)];}
    inline int getsize(const int &x){return sz[getfa(x)];}
}dsu[3];
//dsu[0]用来处理类型为3的道路,dsu[1/2]用来处理横竖两个方向的合并情况

//--------------------SegmentTree--------------------//
struct SegmentTree{
    struct node{
        int ls,rs,sz;
    }nd[8000005];
    int root[200005],ncnt;
    #define ls(_p) (nd[_p].ls)
    #define rs(_p) (nd[_p].rs)
    void init(){
        memset(root,0,sizeof root);
        for(int i=0;i<=8000000;++i){
            nd[i].ls=nd[i].rs=nd[i].sz=0;
        }
        ncnt=0;
    }
    int ins(int p,const int &l,const int &r,const int &x,const int &flag=1){
        //flag=1,添加;flag=-1,删除
        if(p==0)
            p=++ncnt;
        if(l==r){
            if(flag==-1)
                return 0;
            if(nd[p].sz==0)
                nd[p].sz++;
            return p;
        }
        int mid=(l+r)>>1;
        if(x<=mid)
            ls(p)=ins(ls(p),l,mid,x,flag);
        else
            rs(p)=ins(rs(p),mid+1,r,x,flag);
        nd[p].sz=nd[ls(p)].sz+nd[rs(p)].sz;
        return (nd[p].sz)?p:0;
    }
    int merge(const int &p,const int &q,const int &l,const int &r){
        if(p==0 || q==0)
            return p+q;
        if(l==r){
            nd[p].sz=std::min(nd[p].sz+nd[q].sz,1);
            return p;
        }
        int mid=(l+r)>>1;
        ls(p)=merge(ls(p),ls(q),l,mid);
        rs(p)=merge(rs(p),rs(q),mid+1,r);
        nd[p].sz=nd[ls(p)].sz+nd[rs(p)].sz;
        return p;
    }
    int query1(const int &p,const int &l,const int &r,const int &x){
        //查询小于等于某一元素的元素个数
        if(p==0 || x==0)
            return 0;
        if(l==r)
            return nd[p].sz;
        int mid=(l+r)>>1;
        if(x<=mid)
            return query1(ls(p),l,mid,x);
        else
            return nd[ls(p)].sz+query1(rs(p),mid+1,r,x);
    }
    int query2(const int &p,const int &l,const int &r,const int &x){
        //查询某一元素是否在并查集内
        if(p==0 || x==0)
            return 0;
        if(l==r)
            return nd[p].sz;
        int mid=(l+r)>>1;
        if(x<=mid)
            return query2(ls(p),l,mid,x);
        else
            return query2(rs(p),mid+1,r,x);
    }
    int querynum(const int &p,const int &l,const int &r){
        //查询[l,r]内在这一并查集内的元素个数
        return query1(root[p],1,n*m,r)-query1(root[p],1,n*m,l-1);
    }
}st[4];
//st[0/1]存储等级,st[2/3]存储行/列中的元素个数
int q;
void merge_All_ST(int x,int y){
    //将x,y并查集对应的线段树进行合并(并查集是y合并到x,所以线段树也是y合并到x)
    st[0].root[x]=st[0].merge(st[0].root[x],st[0].root[y],1,q);
    st[1].root[x]=st[1].merge(st[1].root[x],st[1].root[y],1,q);
    st[2].root[x]=st[2].merge(st[2].root[x],st[2].root[y],1,n*m);
    st[3].root[x]=st[3].merge(st[3].root[x],st[3].root[y],1,n*m);
}

inline bool caneat(const int &x,const int &y){
    if(y==0 || y>=x)
        return 0;
    return (a[x].col!=a[y].col && a[x].lv>=a[y].lv);
}

int main(){
    scanf("%d",&t);
    while(t--){
        scanf("%d %d %d",&n,&m,&q);
        memset(e,0,sizeof e);
        memset(val,0,sizeof val);
        memset(ans,0,sizeof ans);
        dsu[0].init(n*m);
        dsu[1].init(n*m);
        dsu[2].init(n*m);
        st[0].init();
        st[1].init();
        st[2].init();
        st[3].init();
        for(int i=1;i<=n;++i){
            scanf("%s",s+1);
            for(int j=1;j<m;++j)
                e[1][pos1(i,j+1)]=e[3][pos1(i,j)]=s[j]-'0';
        }
        for(int i=1;i<n;++i){
            scanf("%s",s+1);
            for(int j=1;j<=m;++j)
                e[0][pos1(i+1,j)]=e[2][pos1(i,j)]=s[j]-'0';
        }
        for(int i=1;i<=q;++i){
			scanf("%d %d %d %d",&a[i].col,&a[i].lv,&a[i].x,&a[i].y);
			a[i].id=i;
		}
        std::sort(a+1,a+q+1,cmp_lv);
        for(int i=1;i<=q;++i)
            a[i].lv=i;
        std::sort(a+1,a+q+1,cmp_id);
        for(int i=1;i<=q;++i)
            val[pos1(a[i].x,a[i].y)]=i;   
        for(int i=1;i<=n;++i){
            for(int j=1;j<=m;++j){
                //先将同一连通块上的并查集维护,再维护线段树信息,可以省去合并线段树的复杂度
                int id=pos1(i,j);
                for(int k=0;k<4;++k){
                    if(e[k][id]>1){
                        int nxt=pos1(i+dx[k],j+dy[k]);
                        if(val[id]==0 && val[nxt]==0){
                            dsu[(e[k][id]==3)?0:(k%2+1)].merge(id,nxt);
                        }
                    }
                }
            }
        }
        for(int i=1;i<=n;++i){
            for(int j=1;j<=m;++j){
                //维护线段树信息,包括自己所在的行列和可以到达的棋子的等级
                int id=pos1(i,j);
                int idfa=dsu[0].getfa(id);
                st[2].root[idfa]=st[2].ins(st[2].root[idfa],1,n*m,pos2(i,j));
                st[3].root[idfa]=st[3].ins(st[3].root[idfa],1,n*m,id);
                for(int k=0;k<4;++k){
                    if(e[k][id]==3){
                        int nxt=pos1(i+dx[k],j+dy[k]);
                        if(val[nxt]){
                            int col=a[val[nxt]].col;
                            st[col].root[idfa]=st[col].ins(st[col].root[idfa],1,q,a[val[nxt]].lv);
                        }
                    }
                }
            }
        }
        for(int i=q;i>=1;--i){
            int id=pos1(a[i].x,a[i].y);
            int col=a[i].col;
            for(int j=0;j<4;++j){
                //处理边权为3的线段树,把棋子自身删除
                if(e[j][id]==3){
                    int nxt=pos1(a[i].x+dx[j],a[i].y+dy[j]);
                    int nxtfa=dsu[0].getfa(nxt);
                    st[col].root[nxtfa]=st[col].ins(st[col].root[nxtfa],1,q,a[i].lv,-1);
                }
            }
            for(int j=0;j<4;++j){
                //合并边权为3的所有并查集,同时合并线段树
                if(e[j][id]==3){
                    int nxt=pos1(a[i].x+dx[j],a[i].y+dy[j]);
                    if(val[nxt] && val[nxt]<i)
                        continue;
                        //若出现还没有被删除的棋子,直接跳过
                    dsu[0].merge(id,nxt,1);
                }
            }
            int idfa=dsu[0].getfa(id);
            //首先将并查集内的所有确定点加入ans,再将所有边界上颜色不同级别更小的棋子加入
            ans[i]=dsu[0].getsize(id)+st[1^col].query1(st[1^col].root[idfa],1,q,a[i].lv);
            for(int j=0;j<4;++j){
                //合并边权为2的并查集
                if(e[j][id]==2){
                    int nxt=pos1(a[i].x+dx[j],a[i].y+dy[j]);
                    if(val[nxt] && val[nxt]<i)
                        continue;
                        //若出现还没有被删除的棋子,直接跳过
                    dsu[j%2+1].merge(id,nxt);
                }
            } 
            //计算边权为2的贡献,由于编号的影响同一列上的元素并不连续,所以要除以m
            int mx1=dsu[1].getmaxn(id),mx2=dsu[2].getmaxn(id),mn1=dsu[1].getminn(id),mn2=dsu[2].getminn(id);
            ans[i]+=mx2-mn2+1+(mx1-mn1)/m+1;
            int dmx=pos2(getxy(mx1).first,getxy(mx1).second);
            int dmn=pos2(getxy(mn1).first,getxy(mn1).second);
            //将2,3重复的部分删除(将2中的行/列放到3中的并查集内找)
            ans[i]-=st[2].querynum(idfa,dmn,dmx)+st[3].querynum(idfa,mn2,mx2);
            //特殊处理2的边界,若可以吃子且没有被3的边界包含进去就计入答案
            if(e[0][mn1]==2 && caneat(i,val[mn1-m]))
                if(!st[1^col].query2(st[1^col].root[idfa],1,q,a[val[mn1-m]].lv))
                    ++ans[i];
            if(e[1][mn2]==2 && caneat(i,val[mn2-1]))
                if(!st[1^col].query2(st[1^col].root[idfa],1,q,a[val[mn2-1]].lv))
                    ++ans[i];
            if(e[2][mx1]==2 && caneat(i,val[mx1+m]))
                if(!st[1^col].query2(st[1^col].root[idfa],1,q,a[val[mx1+m]].lv))
                    ++ans[i];
            if(e[3][mx2]==2 && caneat(i,val[mx2+1]))
                if(!st[1^col].query2(st[1^col].root[idfa],1,q,a[val[mx2+1]].lv))
                    ++ans[i];
            //处理边权为1的边
            for(int j=0;j<4;++j){
                if(e[j][id]==1){
                    int nxt=pos1(a[i].x+dx[j],a[i].y+dy[j]);
                    //若有棋子判断是否能吃,然后都要判断是否被包含
                    if(val[nxt] && val[nxt]<i){
                        if(caneat(i,val[nxt]) && (st[1^col].query2(st[1^col].root[idfa],1,q,a[val[nxt]].lv)==0))
                            ++ans[i];
                    }
                    else if(dsu[0].getfa(id)!=dsu[0].getfa(nxt)){
                        ans[i]=ans[i]+1;
                    }
                    
                }
            }
            //在计算边权为2时自己多算了1次,减掉
            --ans[i];
        }
        for(int k=1;k<=q;++k){
            printf("%d\n",ans[k]);
        }
    }
    return 0;
}

标签:P7963,棋局,return,并查,val,int,init,棋子,NOIP2021
From: https://www.cnblogs.com/zhouzizhe/p/16760176.html

相关文章

  • 做题记录整理dp12 P7961 [NOIP2021] 数列(2022/9/26)
    P7961[NOIP2021]数列当时在考场上对于拿部分分这个感念不是很清晰,所以当时连暴力分都没拿。。。事实上这题在看了题解之后还是有很多地方没搞明白,比如最后统计答案,为什......
  • [NOIP2021] 方差
    首先考虑\(V[X]=E[X^2]-E[X]^2\),答案可以化作:\[n\sum_{i=1}^na^i-(\sum_{i=1}^na_i)^2\]然后观察操作,进行一次操作本质上是交换了差分序列中相邻两个数,也就是说我......
  • 棋局评估(不常见的搜索)
    棋局评估(MINMAX搜索+α-β剪枝)这是一个博弈的问题,在这里,你的对手希望他得高分,你希望你得高分,可是你分数高了他的分就低了。下棋的时候,你希望走出最好的局面,即使输也要分......
  • P7961 [NOIP2021] 数列
    题目描述给定整数\(n,m,k\),和一个长度为\(m+1\)的正整数数组\(v_0,v_1,\ldots,v_m\)。对于一个长度为\(n\),下标从\(1\)开始且每个元素均不超过\(m\)的......
  • P7960 [NOIP2021] 报数
    简要题意小Z在玩报数游戏,这个游戏有一个规则,就是对于一个正整数\(x\),如果满足\(7\midx\)或\(x\)的十进制写法中含有\(7\)或是十进制写法含有\(7\)的倍数,那么这......