【牛客多校 2021 第10场 E】More Fantastic Chess Problem (分析与数据结构)

题目链接
【题意】

​ 定义 k k k 维度国际象棋是一个在 a 1 × a 2 × ⋯ × a k a_1\times a_2\times \dots \times a_k a1×a2××ak 的棋盘上进行的游戏,其中规定棋盘的对角位置分别为 ( 1 , 1 , … , 1 ) (1,1,\dots ,1) (1,1,,1) ( a 1 , a 2 , … , a k ) (a_1,a_2,\dots ,a_k) (a1,a2,,ak)

​ 定义国际象棋中的棋子 King (国王) , Queen (王后) , Rook (车) , Bishop (象) , Knight (马)  \text{King (国王) , Queen (王后) , Rook (车) , Bishop (象) , Knight (马) } King (国王) , Queen (王后) , Rook () , Bishop () , Knight ( 的走法如下:

  • King: \text{King:} King:

    每次行动可以从 k k k 个维度中任意选一些维度,让棋子在这些维度上所处的位置都 + 1 +1 +1 ,同时任选另一些维度,让棋子在这些维度上所处的位置都 − 1 -1 1 。其余维度上位置保持不变。行动后不能留在原位,也不能走出棋盘外。

  • Queen: \text{Queen:} Queen:

    每次行动可以选一个正整数 x x x ,接下来从 k k k 个维度中任意选一些维度,让棋子在这些维度上所处的位置都 + x +x +x ,同时任选另一些维度,让棋子在这些维度上所处的位置都 − x -x x 。其余维度上位置保持不变。行动后不能留在原位,也不能走出棋盘外。

  • Rook: \text{Rook:} Rook:

    每次行动可以选一个正整数 x x x ,接下来从 k k k 个维度中选择某一个维度,让棋子在这个维度上的位置 + x +x +x 或者 − x -x x 。行动后不能走出棋盘外。

  • Bishop: \text{Bishop:} Bishop:

    每次行动可以选一个正整数 x x x ,接下来从 k k k 个维度中选择两个不同的维度,让棋子在这两个维度上的位置各自 + x +x +x 或者 − x -x x (注意:不必同加同减)。行动后不能走出棋盘外。

  • Knight: \text{Knight:} Knight:

    每次行动,从 k k k 个维度中选择两个不同的维度,让棋子在其中一个维度上的位置 + 1 +1 +1 或者 − 1 -1 1 ,在另一个维度上的位置 + 2 +2 +2 或者 − 2 -2 2 。行动后不能走出棋盘外。

​ 现在棋盘上只有一颗棋子,给出这个棋子的类型和初始位置 ( b 1 , b 2 , … b k ) (b_1,b_2,\dots b_k) (b1,b2,bk) ,询问其一次行动后能到达的位置一共有多少个。接下来棋子还有 q q q 次移动,每次移动会变化 d d d 个维度,每个维度的变化会用 ( x , δ ) (x,\delta) (x,δ) 的形式给出,表示这次移动将 x x x 维度上的位置变化了 δ \delta δ 。保证每次移动是棋子的合法走法。每次移动后你仍然需要输出棋子在当前位置上,一次行动后能到达的位置一共有多少个。答案对大质数取模。

【数据范围】

k , q ≤ 3 × 1 0 5 ,    ∑ d ≤ 3 × 1 0 5 ,    a i ≤ 1 0 6 k,q\le 3\times 10^5,\ \ \sum d \le 3\times 10^5 ,\ \ a_i \le 10^6 k,q3×105,  d3×105,  ai106


【思路】

​ 本题是一道分析好题,其实现算法不难,在比赛中,其本身具有一定的分析深度。

​ 我们按照不同的棋子,给出其走法方案数的分析,以及维护方法。

​ 注意一点,在维护棋子的移动中,移动的维度的总数量是同 O ( k ) O(k) O(k) 的,所以我们只需要思考某个维度移动之后如何维护答案即可。

King \text{King} King

​ 我们发现各个维度之间的走法互不干扰,都有 + 1 , 0 , − 1 +1,0,-1 +1,0,1 三种选择,于是通常情况下有 3 k 3^k 3k 种走法,减去不动的一种走法是 3 k − 1 3^k-1 3k1 种。但是有的时候,当我们处于边界了,有些走法就不能被采用了。如果第 i i i 个维度的位置 b i = 1 b_i=1 bi=1 ,则不能再 − 1 -1 1 ,如果 b i = a i b_i=a_i bi=ai 则不能再 + 1 +1 +1

​ 于是我们可以维护一下,当前位置下,能有两种选择的维度的数量 c 2 c_2 c2 ,和能有三种选择的维度的数量 c 3 c_3 c3 ,那么当前的合法走法就有 2 c 2 × 3 c 3 − 1 2^{c_2}\times 3^{c_3} - 1 2c2×3c31 种。

​ 当某个维度发生移动时,我们先删掉它,根据他当前的位置更新 c 2 , c 3 c_2,c_3 c2,c3 的值,然后改变他的位置,再根据他新的位置更新 c 2 , c 3 c_2,c_3 c2,c3 的值即可。每次移动可以 O ( 1 ) O(1) O(1) 维护,每次询问时可以写个快速幂构造答案,总复杂度 O ( k log ⁡ k ) O(k\log k) O(klogk)

struct King{
    int cnt[4];
    inline int cnti(int x)
    {
        return 1+(b[x]<a[x])+(b[x]>1);
    }
    inline int ans()
    {
        return (1ll*qpow(2,cnt[2])*qpow(3,cnt[3])-1+MOD)%MOD;
    }
    void opt(int x,int dlt)
    {
        cnt[cnti(x)]--;b[x]+=dlt;cnt[cnti(x)]++;
    }
    void work()
    {
        memset(cnt,0,sizeof(cnt));
        for(int i=1;i<=n;i++)cnt[cnti(i)]++;
        printf("%d\n",ans());
        for(int i=1;i<=m;i++)
        {
            int num,x,dlt;
            scanf("%d",&num);
            for(int j=1;j<=num;j++)
            {
                scanf("%d%d",&x,&dlt);
                opt(x,dlt);
            }
            printf("%d\n",ans());
        }
    }
}king;
Queen \text{Queen} Queen

​ 我们发现 Queen \text{Queen} Queen King \text{King} King 的走法几乎一致,只是需要对移动距离 x ∈ [ 1 , n ] x\in[1,n] x[1,n] 全部做考虑。于是我们对每个移动距离 x x x 都维护一下 2 c x 2 × 3 c x 3 2^{c_{x2}}\times 3^{c_{x3}} 2cx2×3cx3 的值,那么答案就是 ( ∑ x = 1 v 2 c x 2 × 3 c x 3 ) − v (\sum_{x=1}^v 2^{c_{x2}}\times 3^{c_{x3}})- v x=1v2cx2×3cx3v ,其中 v = 1 0 6 v=10^6 v=106

​ 当某个维度发生移动时,我们先删掉它:

​ 根据他当前的位置 b i b_i bi ,我们令 u = a i − b i , v = b i − 1 u=a_i - b_i,v=b_i-1 u=aibi,v=bi1 ,对于 x > max ⁡ ( u , v ) x>\max(u,v) x>max(u,v) 的移动距离,其贡献不受影响;对于 x ≤ min ⁡ ( u , v ) x\le \min(u,v) xmin(u,v) 的移动距离,都少掉了一个 3 3 3 选择的维度,因此把他们都除以 3 3 3 ;对于 min ⁡ ( u , v ) < x ≤ max ⁡ ( u , v ) \min(u,v) <x\le \max(u,v) min(u,v)<xmax(u,v) 的移动距离,都少掉了一个 2 2 2 选择的维度,因此把他们都除以 2 2 2 。也就是说,对移动距离分三个区间,对其中两个做区间除法(乘法,乘以逆元)。

​ 然后我们改变这个维度,再考虑加入它:

​ 根据他新的位置 b i b_i bi ,我们令 u = a i − b i , v = b i − 1 u=a_i - b_i,v=b_i-1 u=aibi,v=bi1 ,和上面同理,正好相反,对于 x ≤ min ⁡ ( u , v ) x\le \min(u,v) xmin(u,v) 的移动距离,把他们的贡献都乘以 3 3 3 ;对于 min ⁡ ( u , v ) < x ≤ max ⁡ ( u , v ) \min(u,v) <x\le \max(u,v) min(u,v)<xmax(u,v) 的移动距离,把他们都乘以 2 2 2 。也是用两个区间乘法维护。

​ 于是可以用一个初始化每个位置都为 1 1 1 的线段树,维护区间乘法和 区间求和 线段树总和即可,总复杂度 O ( k log ⁡ v ) O(k\log v) O(klogv)

struct Queen{
    const static int VL = 1;
    const static int VR = 1000000;
    Segment_Tree seg;
    inline int ans()
    {
        return (seg.tr[1].sum-VR+MOD)%MOD;;
    }
    inline void add(int x)
    {
        int _b1 = min(a[x]-b[x],b[x]-1);
        int _b2 = max(a[x]-b[x],b[x]-1);
        if(_b1>0)seg.Multiply(1,VL,VR,1,_b1,3);
        if(_b2>_b1)seg.Multiply(1,VL,VR,_b1+1,_b2,2);
    }
    inline void del(int x)
    {
        int _b1 = min(a[x]-b[x],b[x]-1);
        int _b2 = max(a[x]-b[x],b[x]-1);
        if(_b1>0)seg.Multiply(1,VL,VR,1,_b1,inv[3]);
        if(_b2>_b1)seg.Multiply(1,VL,VR,_b1+1,_b2,inv[2]);
    }
    void opt(int x,int dlt)
    {
        del(x);b[x]+=dlt;add(x);
    }
    void work()
    {
        seg.Build(1,VL,VR);
        for(int i=1;i<=n;i++)add(i);
        printf("%d\n",ans());
        for(int i=1;i<=m;i++)
        {
            int num,x,dlt;
            scanf("%d",&num);
            for(int j=1;j<=num;j++)
            {
                scanf("%d%d",&x,&dlt);
                opt(x,dlt);
            }
            printf("%d\n",ans());
        }
    }
}queen;
Rook \text{Rook} Rook

​ 唯一没用的棋子,无论走到哪里,下一步始终都有 ( ∑ i = 1 k a i ) − k (\sum_{i=1}^{k} a_i)-k (i=1kai)k 种走法。

​ 于是直接输出 q q q 次答案就可以了,总复杂度 O ( q ) O(q) O(q)

struct Rook{
    void work()
    {
        int ans=0;for(int i=1;i<=n;i++)ans=(ans+a[i]-1)%MOD;
        for(int i=1;i<=m+1;i++)printf("%d\n",ans);
    }
}rook;
Bishop \text{Bishop} Bishop

​ 之前的维护给了我们一个类似于差分的启示:只要我们能够动态维护一个维度出现/消失带来的贡献变化,我们就可以动态维护答案。

​ 所以同理,我们可以这样来思考 Bishop \text{Bishop} Bishop

​ 该棋子有这个特点,如果棋子移动的两个维度以及其方向都选定了,那么走法是两个方向上能走的最大距离中较小的那一个。

​ 如果加入一个维度,根据棋子目前的位置,这个维度在正方向上最多可以走 u u u 的距离,在负方向上最多可以走 v v v 的距离,那么这个维度会带来多少贡献呢?选中这个维度后,在这个维度上的走法无非就是 u + v u+v u+v 种,如果选走正方向:

​ 那么考虑其他所有维度的所有方向:如果其能走的长度 x ≤ v x\le v xv ,那么能带来 x x x 的贡献,如果 x ≥ v x\ge v xv 那么就能带来 v v v 的贡献。 所以只需要统计其他方向距边界距离小于等于 v v v 的这些距离的总和,以及距边界距离大于 v v v 的方向个数 × v \times v ×v 的值即可。

​ 如果选走负方向,把 v v v 换成 u u u ,计算方式完全一致。

​ 同样,如果是删除一个维度,计算其失去的贡献也是一样的。

​ 可以发现,要这样计算贡献,需要对现有的方向的距边界距离做一个权值上的维护,支持单点修改和区间查询。

​ 于是可以用两个权值树状数组,一个维护权值的数量,一个维护权值的和。总复杂度 O ( k log ⁡ v ) O(k\log v) O(klogv)

struct Bishop{
    const static int VL = 1;
    const static int VR = 1000000;
    BIT bnum,bsum;
    int ANS;
    inline int add(int x)
    {
        int ret=0;
        ret=(1ll*ret+bsum.getsum(a[x]-b[x])+1ll*(a[x]-b[x])*(bnum.getsum(VR)-bnum.getsum(a[x]-b[x])))%MOD;
        ret=(1ll*ret+bsum.getsum(b[x] - 1 )+1ll*(b[x] - 1 )*(bnum.getsum(VR)-bnum.getsum(b[x] - 1 )))%MOD;
        bnum.add(a[x]-b[x],1);bnum.add(b[x]-1,1);
        bsum.add(a[x]-b[x],a[x]-b[x]);bsum.add(b[x]-1,b[x]-1);
        return ret;
    }
    inline int del(int x)
    {
        int ret=0;
        bnum.add(a[x]-b[x],-1);bnum.add(b[x]-1,-1);
        bsum.add(a[x]-b[x],b[x]-a[x]+MOD);bsum.add(b[x]-1,1-b[x]+MOD);
        ret=(1ll*ret+bsum.getsum(a[x]-b[x])+1ll*(a[x]-b[x])*(bnum.getsum(VR)-bnum.getsum(a[x]-b[x])))%MOD;
        ret=(1ll*ret+bsum.getsum(b[x] - 1 )+1ll*(b[x] - 1 )*(bnum.getsum(VR)-bnum.getsum(b[x] - 1 )))%MOD;
        return ret;
    }
    inline int ans()
    {
        return ANS;
    }
    void opt(int x,int dlt)
    {
        ANS=(ANS-del(x)+MOD)%MOD;b[x]+=dlt;ANS=(ANS+add(x))%MOD;
    }
    void work()
    {
        ANS=0;
        for(int i=1;i<=n;i++)ANS=(ANS+add(i))%MOD;
        printf("%d\n",ans());
        for(int i=1;i<=m;i++)
        {
            int num,x,dlt;
            scanf("%d",&num);
            for(int j=1;j<=num;j++)
            {
                scanf("%d%d",&x,&dlt);
                opt(x,dlt);
            }
            printf("%d\n",ans());
        }
    }
}bishop;
Knight \text{Knight} Knight

​ 同上,我们发现 Knight \text{Knight} Knight 的移动也是选中两个维度,但是其在一个维度上走的方式很有限,可以类似 Bishop \text{Bishop} Bishop 的方式来维护加入一个维度和删除一个维度带来的贡献变化,同时维护现有方向的各种走法的数量。

​ 该棋子的特点是,如果棋子移动的两个维度以及其方向都选定了,一个维度走 1 1 1 的距离,另一个维度走 2 2 2 步。

​ 我们直接用两个变量,分别维护当前考虑的维度中,有 c 1 c_1 c1 个方向可以走 1 1 1 的距离,有 c 2 c_2 c2 个方向可以走 2 2 2 的距离。

​ 如果加入一个维度,根据棋子目前的位置,这个维度在正方向上最多可以走 u u u 的距离,在负方向上最多可以走 v v v 的距离。根据 u , v u,v u,v 1 , 2 1,2 1,2 的大小关系,就可以维护其带来的贡献。

​ 删除一个维度同理。维护是 O ( 1 ) O(1) O(1) 的。

​ 总复杂度 O ( k ) O(k) O(k)

struct Knight{
    int cnt[4];
    int ANS;
    inline int add(int x)
    {
        int ret=0;
        int u=a[x]-b[x],v=b[x]-1;
        if(u>=1)ret=(ret+cnt[2])%MOD;
        if(u>=2)ret=(ret+cnt[1])%MOD;
        if(v>=1)ret=(ret+cnt[2])%MOD;
        if(v>=2)ret=(ret+cnt[1])%MOD;
        if(u>=1)cnt[1]++;
        if(u>=2)cnt[2]++;
        if(v>=1)cnt[1]++;
        if(v>=2)cnt[2]++;
        return ret;
    }
    inline int del(int x)
    {
        int ret=0;
        int u=a[x]-b[x],v=b[x]-1;
        if(u>=1)cnt[1]--;
        if(u>=2)cnt[2]--;
        if(v>=1)cnt[1]--;
        if(v>=2)cnt[2]--;
        if(u>=1)ret=(ret+cnt[2])%MOD;
        if(u>=2)ret=(ret+cnt[1])%MOD;
        if(v>=1)ret=(ret+cnt[2])%MOD;
        if(v>=2)ret=(ret+cnt[1])%MOD;
        return ret;
    }
    inline int ans()
    {
        return ANS;
    }
    void opt(int x,int dlt)
    {
        ANS=(ANS-del(x)+MOD)%MOD;b[x]+=dlt;ANS=(ANS+add(x))%MOD;
    }
    void work()
    {
        ANS=0;
        memset(cnt,0,sizeof(cnt));
        for(int i=1;i<=n;i++)ANS=(ANS+add(i))%MOD;
        printf("%d\n",ans());
        for(int i=1;i<=m;i++)
        {
            int num,x,dlt;
            scanf("%d",&num);
            for(int j=1;j<=num;j++)
            {
                scanf("%d%d",&x,&dlt);
                opt(x,dlt);
            }
            printf("%d\n",ans());
        }
    }
}knight;

【其余部分的代码】

(树状数组、线段树、主函数,拼上上面那几个结构体就可以跑了)

#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
#define MAXN 310000
#define MAXM 1100000
#define MOD 998244353
using namespace std;
int n,m;
int a[MAXN],b[MAXN];
int inv[MAXM];
string str;
inline int qpow(int x,int y){int ret=1;while(y){if(y&1)ret=1ll*ret*x%MOD;x=1ll*x*x%MOD;y>>=1;}return ret;}
void init_inv()
{
    inv[0]=1;
    for(int i=1;i<MAXN;i++)inv[i]=1ll*inv[i-1]*i%MOD;
    inv[MAXN-1]=qpow(inv[MAXN-1],MOD-2);
    for(int i=MAXN-1;i>=1;i--)
    {
        int tmp = 1ll * inv[i-1] * inv[i] % MOD; 
        inv[i-1] = 1ll * inv[i] * i % MOD; 
        inv[i] = tmp;
    }
}

struct BIT{
    int q[MAXM];
    BIT(){memset(q,0,sizeof(q));}
    void add(int x,int v){x++;for(int i=x;i<MAXM;i+=i&-i)q[i]=(q[i]+v)%MOD;}
    int getsum(int x){x++;int ret=0;for(int i=x;i;i-=i&-i)ret=(ret+q[i])%MOD;return ret;}
};

struct Segment_Tree{
    struct node{
        int l,r,mul,sum;
        node(){l=r=sum=mul=0;}
    }tr[MAXM*4];
    int num;
    Segment_Tree(){num=1;}
    void pushup(int x)
    {
        tr[x].sum=(tr[tr[x].l].sum+tr[tr[x].r].sum)%MOD;
    }
    void Build(int x,int L,int R)
    {
        tr[x].mul=1;
        if(L==R)
        {
            tr[x].sum=1;return;
        }
        int mid=(L+R)/2;
        tr[x].l=++num;Build(num,L,mid);
        tr[x].r=++num;Build(num,mid+1,R);
        pushup(x);
    }
    inline void mul_node(int x,int v)
    {
        tr[x].mul=1ll*tr[x].mul*v%MOD;
        tr[x].sum=1ll*tr[x].sum*v%MOD;
    }
    void pushdown(int x)
    {
        if(tr[x].mul!=1)
        {
            int L=tr[x].l,R=tr[x].r;
            mul_node(L,tr[x].mul);mul_node(R,tr[x].mul);
            tr[x].mul=1;
        }
    }
    void Multiply(int x,int L,int R,int al,int ar,int v)
    {
        if(R<al||ar<L)return;
        if(al<=L&&R<=ar){mul_node(x,v);return;}
        int mid=(L+R)/2;pushdown(x);
        Multiply(tr[x].l,L,mid,al,ar,v);Multiply(tr[x].r,mid+1,R,al,ar,v);
        pushup(x);
    }
};

int main()
{
    init_inv();
    scanf("%d%d",&n,&m);
    cin>>str;
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    for(int i=1;i<=n;i++)scanf("%d",&b[i]);
    if(str=="King")king.work();
    if(str=="Queen")queen.work();
    if(str=="Rook")rook.work();
    if(str=="Bishop")bishop.work();
    if(str=="Knight")knight.work();
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值