E题: More Fantastic Chess Problem
原题链接:https://ac.nowcoder.com/acm/contest/11261/E
题目大意
在 k k k 维空间中下国际象棋,棋盘两角为 ( 1 , 1 , . . . , 1 ) (1,1,...,1) (1,1,...,1) 和 ( a 1 , a 2 , . . . a k ) (a_1,a_2,...a_k) (a1,a2,...ak) ,棋子移动规则如下:
- 国王 - 选择一个非空的维度子集,坐标在这些维度上加 1 1 1 或减 1 1 1 ;
- 皇后 - 选择一个非空的维度子集和正整数 x x x ,坐标在这些维度上加 x x x 或减 x x x ;
- 城堡 - 选择一个维度和正整数 x x x ,坐标在这个维度上加 x x x 或减 x x x ;
- 主教 - 选择两个维度和正整数 x x x ,坐标在这些维度上加 x x x 或减 x x x ;
- 骑士 - 选择两个维度,坐标在其中一个维度上加 2 2 2 或减 2 2 2 ,在另一个维度上加 1 1 1 或减 1 1 1 ;
- 士兵 - 规则太复杂了,不考虑这个棋子;
现在有一个
k
(
1
≤
k
≤
3
⋅
1
0
5
)
k(1\le k\le 3·10^5)
k(1≤k≤3⋅105) 维,大小为
a
1
⋅
a
2
⋅
.
.
.
⋅
a
k
(
1
≤
a
i
≤
1
0
6
)
a_1·a_2·...·a_k(1\le a_i\le 10^6)
a1⋅a2⋅...⋅ak(1≤ai≤106) 的棋盘和一个棋子,将该棋子放在特定格子上,求其在一次移动中可达的不同单元格数量。
有
q
(
0
≤
q
≤
3
⋅
1
0
5
)
q(0\le q\le 3·10^5)
q(0≤q≤3⋅105) 次操作,每次操作会将棋子按规则移动到另一个位置,并需要再次求解可达的单元格数量。
答案很大,对
998244353
998244353
998244353 取模。
题解
(下文讲解顺序根据难度而定)
城堡
显然,城堡在第
i
i
i 维度上可移动达到位置数为
a
i
−
1
(
去
掉
自
己
所
在
的
位
置
)
a_i-1(去掉自己所在的位置)
ai−1(去掉自己所在的位置) ,累加求解即可。
答案:
a
n
s
=
∑
i
=
1
k
(
a
i
−
1
)
ans=\sum\limits^k_{i=1}(a_i-1)
ans=i=1∑k(ai−1) 。
修改:操作对答案无影响。
国王
国王在移动时对于每个维度有三种选择:加
1
1
1 ,不变,或减
1
1
1 。
我们可以对于每个维度进行计算选择数(若三种选择都可则为
3
3
3 ,加
1
1
1 或减
1
1
1 不可(贴边界
1
1
1 或
a
i
a_i
ai )则为2,加
1
1
1 和减
1
1
1 都不可(即该维度无法移动,上下边界相等
a
i
=
1
a_i=1
ai=1 )则为1),然后根据乘法原理相乘即可。
注意不能所有维度均不变,所以最终答案还要减
1
1
1 。
答案:
a
n
s
=
−
1
+
∏
i
=
1
k
(
3
−
(
b
i
=
1
)
−
(
b
i
=
a
i
)
)
ans=-1+\prod\limits^k_{i=1}(3-(b_i=1)-(b_i=a_i))
ans=−1+i=1∏k(3−(bi=1)−(bi=ai)) 。
修改:将累乘项除去(采用逆元改为乘法)原来的选择数,再乘上新的选择数即可。
骑士
我们求出骑士第
i
i
i 维度移动
1
1
1 的选择数(同国王计算),记为
s
u
m
1
i
sum1_i
sum1i ,移动
2
2
2 的选择数(同理)记为
s
u
m
2
i
sum2_i
sum2i ,根据乘法原理不难初步得到答案为
(
∑
i
=
1
k
s
u
m
1
i
)
⋅
(
∑
i
=
1
k
s
u
m
2
i
)
(\sum\limits^k_{i=1}sum1_i)·(\sum\limits^k_{i=1}sum2_i)
(i=1∑ksum1i)⋅(i=1∑ksum2i) ,但显然,在同一维度进行移动
1
1
1 和移动
2
2
2 是非法的,我们减去这些情况。
答案:
a
n
s
=
(
∑
i
=
1
k
s
u
m
1
i
)
⋅
(
∑
i
=
1
k
s
u
m
2
i
)
−
∑
i
=
1
k
(
s
u
m
1
i
⋅
s
u
m
2
i
)
ans=(\sum\limits^k_{i=1}sum1_i)·(\sum\limits^k_{i=1}sum2_i)-\sum\limits^k_{i=1}(sum1_i·sum2_i)
ans=(i=1∑ksum1i)⋅(i=1∑ksum2i)−i=1∑k(sum1i⋅sum2i) 。
修改:对三个累加和分别开变量保存,然后修改第
i
i
i 维度时分别减去原先的
s
u
m
1
i
/
s
u
m
2
i
/
s
u
m
1
i
+
s
u
m
2
i
sum1_i/sum2_i/sum1_i+sum2_i
sum1i/sum2i/sum1i+sum2i ,再加上新的
s
u
m
1
i
/
s
u
m
2
i
/
s
u
m
1
i
+
s
u
m
2
i
sum1_i/sum2_i/sum1_i+sum2_i
sum1i/sum2i/sum1i+sum2i 即可。
皇后
皇后与国王类似,区别在于步长不一定。
我们写出皇后在第
i
i
i 维度时,对于不同步长
l
e
n
len
len 的选择数:
f
(
i
)
=
{
3
(
0
<
l
e
n
≤
min
(
b
i
−
1
,
a
i
−
b
i
)
)
2
(
m
i
n
(
b
i
−
1
,
a
i
−
b
i
)
<
l
e
n
≤
max
(
b
i
−
1
,
a
i
−
b
i
)
)
1
(
max
(
b
i
−
1
,
a
i
−
b
i
)
<
l
e
n
)
f(i)= \left\{ \begin{array}{rcl} 3&(0<len\le \min(b_i-1,a_i-b_i))\\ 2&(min(b_i-1,a_i-b_i)< len\le \max(b_i-1,a_i-b_i))\\ 1&(\max(b_i-1,a_i-b_i)<len) \end{array} \right.
f(i)=⎩⎨⎧321(0<len≤min(bi−1,ai−bi))(min(bi−1,ai−bi)<len≤max(bi−1,ai−bi))(max(bi−1,ai−bi)<len)
不难发现,该分段函数的每一个值呈现出区间的形式。
我们考虑用线段树维护
l
e
n
=
1
,
2
,
.
.
.
len=1,2,...
len=1,2,... 时的选择数累乘积,对每个维度计算
min
\min
min 与
max
\max
max ,然后进行分段区间乘法,最后区间求和即可。
最后记得减去区间总长度,因为每个
l
e
n
len
len 的方案中都有一种所有维度均不变的方案。
答案:好吧我直接写不出来建议看代码。
修改:区间除法(乘逆元)即可。
主教
主教与皇后类似,区别在于从选取区间子集变为只选取
2
2
2 个区间。
我们写出主教在第
i
i
i 维度时,对于不同步长
l
e
n
len
len 的选择数(只考虑移动):
f
(
i
)
=
{
2
(
0
<
l
e
n
≤
min
(
b
i
−
1
,
a
i
−
b
i
)
)
1
(
m
i
n
(
b
i
−
1
,
a
i
−
b
i
)
<
l
e
n
≤
max
(
b
i
−
1
,
a
i
−
b
i
)
)
0
(
max
(
b
i
−
1
,
a
i
−
b
i
)
<
l
e
n
)
f(i)= \left\{ \begin{array}{rcl} 2&(0<len\le \min(b_i-1,a_i-b_i))\\ 1&(min(b_i-1,a_i-b_i)< len\le \max(b_i-1,a_i-b_i))\\ 0&(\max(b_i-1,a_i-b_i)<len) \end{array} \right.
f(i)=⎩⎨⎧210(0<len≤min(bi−1,ai−bi))(min(bi−1,ai−bi)<len≤max(bi−1,ai−bi))(max(bi−1,ai−bi)<len)
我们设
s
u
m
1
i
sum1_i
sum1i 表示
l
e
n
=
i
len=i
len=i 时选择数为
1
1
1 的维度数,
s
u
m
2
i
sum2_i
sum2i 表示
l
e
n
=
i
len=i
len=i 时选择数为
2
2
2 的维度数。
每次选取两种选择组成一种移动方案,注意减去同时选取同一维度的非法情况。
答案:
∑
i
=
1
k
(
C
s
u
m
1
i
+
2
⋅
s
u
m
2
i
2
−
s
u
m
2
i
)
\sum\limits^k_{i=1}(C^2_{sum1_i+2·sum2_i}-sum2_i)
i=1∑k(Csum1i+2⋅sum2i2−sum2i) 。
修改:设
s
i
=
s
u
m
1
i
+
s
u
m
2
i
s_i=sum1_i+sum2_i
si=sum1i+sum2i ,将组合数项
C
s
u
m
1
i
+
2
⋅
s
u
m
2
i
2
C^2_{sum1_i+2·sum2_i}
Csum1i+2⋅sum2i2 写作
s
i
(
s
i
−
1
)
2
=
s
i
2
−
s
i
2
\frac{s_i(s_i-1)}{2}=\frac{s_i^2-s_i}{2}
2si(si−1)=2si2−si ,分离常数
1
2
\frac{1}{2}
21 (可计算后乘逆元),线段树区间维护
s
i
s_i
si 的平方和与累加和即可(操作时区间加法即可),
∑
i
=
1
k
s
u
m
2
i
\sum\limits^k_{i=1}sum2_i
i=1∑ksum2i 可预处理。
线段树维护区间平方和
首先我们考虑单项的修改,显然:
a
2
→
(
a
+
b
)
2
=
a
2
+
2
a
b
+
b
2
a^2\rightarrow (a+b)^2=a^2+2ab+b^2
a2→(a+b)2=a2+2ab+b2 。
然后转化到区间:
a
l
2
+
a
l
+
1
2
+
.
.
.
+
a
r
2
→
(
a
l
+
b
)
2
+
(
a
l
+
1
+
b
)
2
+
.
.
.
+
(
a
r
+
b
)
2
=
a
l
2
+
2
a
l
b
+
b
2
+
a
l
+
1
2
+
2
a
l
+
1
b
+
b
2
+
.
.
.
+
a
r
2
+
2
a
r
b
+
b
2
=
(
a
l
2
+
a
l
+
1
2
+
.
.
.
+
a
r
2
)
+
2
b
(
a
l
+
a
l
+
r
+
.
.
.
+
a
r
)
+
(
r
−
l
+
1
)
b
2
\begin{aligned} a^2_l+a^2_{l+1}+...+a^2_r\rightarrow& (a_l+b)^2+(a_{l+1}+b)^2+...+(a_r+b)^2\\ =&a^2_l+2a_lb+b^2+a^2_{l+1}+2a_{l+1}b+b^2+...+a^2_r+2a_rb+b^2\\ =&(a^2_l+a^2_{l+1}+...+a^2_r)+2b(a_l+a_{l+r}+...+a_r)+(r-l+1)b^2 \end{aligned}
al2+al+12+...+ar2→==(al+b)2+(al+1+b)2+...+(ar+b)2al2+2alb+b2+al+12+2al+1b+b2+...+ar2+2arb+b2(al2+al+12+...+ar2)+2b(al+al+r+...+ar)+(r−l+1)b2
同时维护区间平方和与累加和即可(修改时先改平方和沿用旧累加和)
参考代码
#include<bits/stdc++.h>
#define For(i,n,m) for(int i=n;i<=m;i++)
#define p 998244353
using namespace std;
void read(int &x){int ret=0;char c=getchar(),last=' ';while(!isdigit(c))last=c,c=getchar();while(isdigit(c))ret=ret*10+c-'0',c=getchar();x=last=='-'?-ret:ret;}
const int MAXK=3e5+5,MAXN=1e6+5;
int k,q,invx[MAXK];//invx记录逆元
string ask;
int a[MAXK],b[MAXK];
struct node{
int l,r,n,lazy;
}st[MAXN<<2];
struct Node{
int l,r,_2,_1,lazy;
}segtree[MAXN<<2];
int inv(int x){
if(invx[x])return invx[x];//访问时已计算则直接返回
invx[x]=1;
int xx=x;//记录原下标
for(int b=p-2;b;x=1ll*x*x%p,b>>=1)if(b&1)invx[xx]=1ll*invx[xx]*x%p;
return invx[xx];//计算完后返回
}
void solve_Rook()//城堡
{
int ans=-k;//相当于k个-1
For(i,1,k)ans=(1ll*ans+a[i])%p;//累加
printf("%d\n",ans);
int l,x,d;
while(q--){
read(l);
while(l--)read(x),read(d);//修改时假装读入即可
printf("%d\n",ans);
}
}
int get_King(int i)//计算国王第i维度的操作选择数
{
int ret=3;
if(b[i]==1)ret--;//下边界
if(b[i]==a[i])ret--;//上边界
return ret;
}
void solve_King()//国王
{
int ans=1;
For(i,1,k)ans=1ll*ans*get_King(i)%p;//累乘
printf("%d\n",ans-1);//注意-1
int l,x,d;
while(q--){
read(l);
while(l--){
read(x),read(d);
ans=1ll*ans*inv(get_King(x))%p;//乘逆元(即除法)
b[x]+=d;//修改b[x]
ans=1ll*ans*get_King(x)%p;//乘上新的选择数
}
printf("%d\n",ans-1);//注意-1
}
}
int get_Knight(int i,int num){//计算骑士第i维度移动num的选择数
int ret=2;
if(b[i]<=num)ret--;
if(b[i]>=a[i]+1-num)ret--;
return ret;
}
void solve_Knight()//骑士
{
int sum1=0,sum2=0,mul=0,_1,_2;
For(i,1,k){
_1=get_Knight(i,1);//第i维度移动1
_2=get_Knight(i,2);//第i维度移动2
sum1+=_1;//累加移动1
sum2+=_2;//累加移动2
mul+=_1*_2;//非法项
}
printf("%lld\n",(1ll*sum1*sum2%p-mul+p)%p);//注意减法+p再取模
int l,x,d;
while(q--){
read(l);
while(l--){
read(x),read(d);
_1=get_Knight(x,1);//原第x维度移动1
_2=get_Knight(x,2);//原第x维度移动2
sum1-=_1,sum2-=_2,mul-=_1*_2;//减去原先的
b[x]+=d;//修改b[x]
_1=get_Knight(x,1);//现第x维度移动1
_2=get_Knight(x,2);//现第x维度移动2
sum1+=_1,sum2+=_2,mul+=_1*_2;//更新
}
printf("%lld\n",(1ll*sum1*sum2%p-mul+p)%p);//注意减法+p再取模
}
}
void pushup_Queen(int x){st[x].n=(1ll*st[x<<1].n+st[x<<1|1].n)%p;}//皇后_x区间更新
void pushdown_Queen(int x){//皇后_x区间传递标记
st[x<<1].n=1ll*st[x<<1].n*st[x].lazy%p;
st[x<<1].lazy=1ll*st[x<<1].lazy*st[x].lazy%p;
st[x<<1|1].n=1ll*st[x<<1|1].n*st[x].lazy%p;
st[x<<1|1].lazy=1ll*st[x<<1|1].lazy*st[x].lazy%p;
st[x].lazy=1;//还原
}
void build_Queen(int x,int l,int r){//皇后_线段树建树
st[x].l=l,st[x].r=r,st[x].lazy=1;//注意lazy标记累乘初始化为1
if(l==r){//叶节点
st[x].n=1;//累乘,初始为1
return;
}
int mid=l+r>>1;
build_Queen(x<<1,l,mid);
build_Queen(x<<1|1,mid+1,r);
pushup_Queen(x);//更新
}
void update_Queen(int x,int l,int r,int mul){//皇后_线段树更新
if(st[x].l==l&&st[x].r==r){//找到区间
st[x].n=1ll*st[x].n*mul%p;//更新数值
st[x].lazy=1ll*st[x].lazy*mul%p;//更新lazy标记
return;
}
pushdown_Queen(x);//传递标记
int mid=st[x].l+st[x].r>>1;
if(r<=mid)update_Queen(x<<1,l,r,mul);
else if(l>mid)update_Queen(x<<1|1,l,r,mul);
else update_Queen(x<<1,l,mid,mul),update_Queen(x<<1|1,mid+1,r,mul);
pushup_Queen(x);//更新
}
void get_Queen(int i){//计算皇后第i维度的不同操作数对应区间,并更新线段树
int mi=min(b[i]-1,a[i]-b[i]);
int ma=a[i]-1-mi;
if(mi)update_Queen(1,1,mi,3);
if(mi<ma)update_Queen(1,mi+1,ma,2);
//update_Queen(1,ma+1,a[i],1);//乘1相当于没有乘
}
void remove_Queen(int i){//移除皇后第i维度的不同操作数在线段树上对应区间的影响
int mi=min(b[i]-1,a[i]-b[i]);
int ma=a[i]-1-mi;
if(mi)update_Queen(1,1,mi,inv(3));//乘逆元(即除法)
if(mi<ma)update_Queen(1,mi+1,ma,inv(2));//乘逆元(即除法)
//update_Queen(1,ma+1,a[i],inv(1));//乘1的逆元(除1)相当于没有乘
}
void solve_Queen()//皇后
{
int ma=0;
For(i,1,k)ma=max(ma,a[i]);//max(a[i])-1是皇后可能移动的最大步长(其实不-1也可以,不影响)
build_Queen(1,1,ma-1);//建树,注意ma-1
For(i,1,k)get_Queen(i);//初始化
printf("%d\n",(1ll*st[1].n-(ma-1)+p)%p);//注意ma-1
int l,x,d;
while(q--){
read(l);
while(l--){
read(x),read(d);
remove_Queen(x);//移除原先该维度
b[x]+=d;//修改b[x]
get_Queen(x);//更新当前该维度
}
printf("%d\n",(1ll*st[1].n-ma+1+p)%p);//注意ma-1
}
}
void pushup_Bishop(int x){//主教_x区间更新
segtree[x]._1=(1ll*segtree[x<<1]._1+segtree[x<<1|1]._1)%p;//累加和
segtree[x]._2=(1ll*segtree[x<<1]._2+segtree[x<<1|1]._2)%p;//平方和
}
void pushdown_Bishop(int x){//主教_x区间传递标记
segtree[x<<1]._2=(segtree[x<<1]._2+2ll*segtree[x].lazy%p*segtree[x<<1]._1%p+1ll*segtree[x].lazy*segtree[x].lazy%p*(segtree[x<<1].r-segtree[x<<1].l+1)%p+p)%p;//左子区间更新平方和
segtree[x<<1]._1=(segtree[x<<1]._1+1ll*segtree[x].lazy*(segtree[x<<1].r-segtree[x<<1].l+1)%p+p)%p;//左子区间更新累加和
segtree[x<<1].lazy=(1ll*segtree[x<<1].lazy+segtree[x].lazy+p)%p;//左子区间更新lazy标记
segtree[x<<1|1]._2=(segtree[x<<1|1]._2+2ll*segtree[x].lazy%p*segtree[x<<1|1]._1%p+1ll*segtree[x].lazy*segtree[x].lazy%p*(segtree[x<<1|1].r-segtree[x<<1|1].l+1)%p+p)%p;//右子区间更新平方和
segtree[x<<1|1]._1=(segtree[x<<1|1]._1+1ll*segtree[x].lazy*(segtree[x<<1|1].r-segtree[x<<1|1].l+1)%p+p)%p;//右子区间更新累加和
segtree[x<<1|1].lazy=(1ll*segtree[x<<1|1].lazy+segtree[x].lazy+p)%p;//右子区间更新lazy标记
segtree[x].lazy=0;//还原
}
void build_Bishop(int x,int l,int r){//主教_线段树建树
segtree[x].l=l,segtree[x].r=r;
if(l==r)return;
int mid=l+r>>1;
build_Bishop(x<<1,l,mid);
build_Bishop(x<<1|1,mid+1,r);
}
void update_Bishop(int x,int l,int r,int add){//主教_线段树更新
if(segtree[x].l==l&&segtree[x].r==r){//找到区间
segtree[x]._2=(segtree[x]._2+2ll*add*segtree[x]._1%p+1ll*add*add%p*(r-l+1)%p+p)%p;//更新平方和
segtree[x]._1=(segtree[x]._1+1ll*add*(r-l+1)%p+p)%p;//更新累加和
segtree[x].lazy=(1ll*segtree[x].lazy+add+p)%p;//更新lazy标记
return;
}
pushdown_Bishop(x);//传递标记
int mid=segtree[x].l+segtree[x].r>>1;
if(r<=mid)update_Bishop(x<<1,l,r,add);
else if(l>mid)update_Bishop(x<<1|1,l,r,add);
else update_Bishop(x<<1,l,mid,add),update_Bishop(x<<1|1,mid+1,r,add);
pushup_Bishop(x);//更新
}
int get_Bishop(int i){//计算主教第i维度的不同操作数对应区间,并更新线段树
int mi=min(b[i]-1,a[i]-b[i]);
int ma=a[i]-1-mi;
if(mi)update_Bishop(1,1,mi,2);
if(ma>mi)update_Bishop(1,mi+1,ma,1);
//update_Bishop(1,ma+1,a[i],0);//加0相当于没有加
return mi;
}
int remove_Bishop(int i){//移除主教第i维度的不同操作数在线段树上对应区间的影响
int mi=min(b[i]-1,a[i]-b[i]);
int ma=a[i]-1-mi;
if(mi)update_Bishop(1,1,mi,-2);
if(ma>mi)update_Bishop(1,mi+1,ma,-1);
//update_Bishop(1,ma+1,a[i],0);//减0相当于没有减
return mi;
}
void solve_Bishop()//主教
{
int ma=0,ans=0;
For(i,1,k)ma=max(ma,a[i]);//max(a[i])-1是主教可能移动的最大步长(其实不-1也可以,不影响)
build_Bishop(1,1,ma-1);//建树,注意ma-1
For(i,1,k)ans=(1ll*ans+get_Bishop(i))%p;//初始化线段树,并更新sum2累加和
printf("%lld\n",((1ll*segtree[1]._2-segtree[1]._1+p)*inv(2)%p-ans+p)%p);//注意减法+p再取模
int l,x,d;
while(q--){
read(l);
while(l--){
read(x),read(d);
ans=(1ll*ans-remove_Bishop(x)+p)%p;//移除原先该维度,并更新sum2累加和
b[x]+=d;//修改b[x]
ans=(1ll*ans+get_Bishop(x))%p;//更新当前该维度,并更新sum2累加和
}
printf("%lld\n",((1ll*segtree[1]._2-segtree[1]._1+p)*inv(2)%p-ans+p)%p);//注意减法+p再取模
}
}
int main()
{
read(k),read(q);
cin>>ask;
For(i,1,k)read(a[i]);
For(i,1,k)read(b[i]);
if(ask=="Rook")solve_Rook();//城堡
if(ask=="King")solve_King();//国王
if(ask=="Knight")solve_Knight();//骑士
if(ask=="Queen")solve_Queen();//皇后
if(ask=="Bishop")solve_Bishop();//主教
return 0;
}