爆0了。。。。
先看B,这道题是我比较擅长的。
尝试质因子分解失败,发现根据gcd连续段不超过log以后影魔即可。
但发现线段树直接下传标记后多个log,想怎么去掉log。
虽然线段树本质不同的长度只有log个,可以预处理。
但是尝试失败了。标记无法合并。
最后写了80分,挂了。。。。
暴力没时间打了。。。
题解:
B
最简单的一道题。
根据经典结论,以一个点x为右端点,[1,x-1]为左端点的gcd值最多只有log种(新年的复读机)
原因是如果gcd值在添加一个数后变化,则新的gcd值一定是原gcd值的约数,至少会/2
实际上,可以把每一个右端点个相同gcd区间提取出来,问题转化成求把矩形[l,r][x,x]处乘上某个值v,求一个矩形内的点数。
如果直接树套树会tle。考虑扫描线(影魔)。
如果扫到[l,r]单点+,则以后这个矩形的贡献可能变化,不可做。
如果扫到x区间+,且把询问[l,r]拆成前缀[1,r]-[1,l-1]后区间查[l,r],则可以做。
可以使用bits简单维护。
被卡常的代码:
#include<bits/stdc++.h>
using namespace std;
#define mo 998244353
#define N 200010
struct no{
int x,i,t;
};
struct nn{
int x,y,i;
}st[N],ss[N],qv[N*20];
int n,q,a[N],tp,tt,ans[N],id[N],ii,b1[N],b2[N];
vector<no>v[N],vv[N];
int qp(int x,int y){
int r=1;
for(;y;y>>=1,x=1ll*x*x%mo)
if(y&1)r=1ll*r*x%mo;
return r;
}
void ad(int x,int y){
int va=qp(y,x);
for(int i=x;i<=n;i+=i&-i){
b1[i]=1ll*b1[i]*y%mo;
b2[i]=1ll*b2[i]*va%mo;
}
}
int qu(int x){
int ans=1,v1=1,v2=1;
for(int i=x;i;i-=i&-i){
v1=1ll*v1*b1[i]%mo;
v2=1ll*v2*b2[i]%mo;
}
v1=qp(v1,x+1);
v2=qp(v2,mo-2);
return 1ll*v1*v2%mo;
}
inline char nc(){
return getchar();
}
inline int read(){
char ch=nc();int sum=0;
while(!(ch>='0'&&ch<='9'))ch=nc();
while(ch>='0'&&ch<='9')sum=sum*10+ch-48,ch=nc();
return sum;
}
signed main(){
freopen("easy.in","r",stdin);
freopen("easy.out","w",stdout);
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++){
a[i]=read();
b1[i]=b2[i]=1;
}
b1[0]=b2[0]=1;
for(int i=1;i<=q;i++){
int l=read(),r=read();
v[r].push_back((no){l,r,i});
v[l-1].push_back((no){l,r,-i});
ans[i]=1;
}
for(int i=1;i<=n;i++){
tt=0;
for(int j=1;j<=tp;j++)
st[j].i=__gcd(st[j].i,a[i]);
st[++tp]=(nn){i,i,a[i]};
int la=0;
for(int j=1;j<tp;j++)
if(st[j].i!=st[j+1].i){
ss[++tt]=(nn){la+1,st[j].y,st[j].i};
la=st[j].y;
}
ss[++tt]=(nn){la+1,i,a[i]};
tp=tt;
for(int j=1;j<=tp;j++){
st[j]=ss[j];
vv[i].push_back((no){st[j].x,st[j].y,st[j].i});
}
}
for(int i=1;i<=n;i++){
for(no x:vv[i]){
ad(x.x,x.t);
ad(x.i+1,qp(x.t,mo-2));
}
for(no x:v[i]){
if(x.t>=0)
ans[x.t]=1ll*(1ll*qu(x.i)*qp(qu(x.x-1),mo-2)%mo)*ans[x.t]%mo;
else
ans[-x.t]=1ll*(1ll*qp(1ll*qu(x.i)*qp(qu(x.x-1),mo-2)%mo,mo-2))*ans[-x.t]%mo;
}
}
for(int i=1;i<=q;i++)
printf("%d
",ans[i]);
}
A
dp of dp
有时,一些题目中,判定过程比较复杂,但是状态数较少,并且是个自动机。
可以dp of dp,把状态压成一个数,然后在转移的时候顺便判定。
这样子,只要知道是否达到终态,即可知道是否合法。
这种类型的题目比如zjoi 麻将,codechef maxdigittree等。
此题也可以dp of dp。
首先考虑怎么判断合法。
注意到每次我们选择的分界点必须是单调递增的,因为分解点前面的字符串长度会变为1。
所以题目中给的过程相当于维护一个栈。每次向栈中插入两个字符。
插入后判断是否清空整个栈。
如果清空,则把第一个字符和前面的全部合并再插入第二个字符。
可以使用dp解决。
设(f_{i,0/1,0/1})表示插入0/1后变为0/1是否可能。
可以简单转移。
对于计数,dp of dp。
设(tr_{s,0/1,0/1})表示s状态在插入0/1,0/1后能走到的状态。
每次统计走到的状态即可。
C
看到题目可以想到dp。
设(f_i)表示前i是否可以被合法划分。
显然可以得到(f_i|=f_j),其中(j+1...i)可以被合法划分。
看数据范围十分大,所以不能直接dp。要发掘dp的性质。
维护序列的前缀xor数组(t)
引理1:如果把划分出的每一段作前缀xor,则每一段都由集合s的数线性组合而成。
引理2:最后的序列段数(<=2^k)
证明可以考虑每段值的前缀xor和pre。
如果pre有两个位置(i,j)的值是相同的,则(i~j)的部分全部可以合并。
引理3:如果两个位置(j<i),且(dp_j=1,pre_i=pre_j)
显然j能更新到的,i也能更新到。
所以对于每一个pre只需要保留编号最小的。
设(g_i)表示值为i且标号最小的位置。则显然g只有最多(2^{card(s)})个元素。
可以使用bfs更新g。
引理4:对于每种值,只需要取出编号最小的更新。
更新操作相当于寻找后继,并且更新后继的g。
编号更小的值找到的后继显然更小,可以更好的更新后面的g。
所以可以使用优先队列进行更新。
每个值只会被访问一次。所以时间复杂度正确。
考虑分整个序列的xor和(s)讨论。
如果(s>0),则bfs一下,看(g_s)是否存在。
如果(s=0),则只需要查询(t)数组是否存在集合中的数。
这是因为划分出的第一段的和肯定要是s内的数。
由于(s xor s=0),所以后面的段的xor和也是(s),符合条件
接下来的问题是如何进行区间xor/查询位置(x)后第一个值为(v)的数。
分块。每个块维护bitset bt,(bt_x)表示(x)这个值是否出现。
再维护两个标记(t1,t2),(t1)表示整个块被打的标记,(t2)为散块被打的标记
在查询时顺序扫描一个数(x)后的每个块,如果在bitset查询出(x),则在内部暴力扫描,查看是否有(x)。
一个块的真实值可以通过它的标记(t1,t2)得知。
否则需要进行区间更新。
在区间更新的时候,(t1,t2)可以方便的计算。
总结:
题目都不是非常难,但是有挑战性。
C的idea十分不错。