「Luogu P4247 [清华集训2012]序列操作」
题目大意
给出一个序列,支持区间加,区间变为相反数,查询区间中取 \(c\) 个数的乘积的所有方案的总和.
分析
先从没有修改开始,查询区间中选 \(c\) 个数乘积的所有方案的总和,\(c\) 的范围很小,所以可以想到用线段树维护,对于每个节点维护一个数组 \(f\),\(f_i\) 表示这个节点的区间中选 \(i\) 个数的乘积的所有方案的总和,至于合并,可以枚举两颗子树中选取的数的个数,然后相乘计算贡献(相当于一个两个多项式相乘).
对于变为相反数的操作还是很简单的,可以发现如果是偶数个数相乘那么其中的数全部变为相反数并不会对结构产生影响,如果是奇数个数相乘那最后的结果也会变成相反数,所以只要奇偶性判断一下就好了.
然后是区间加的操作,也是本题的重点.
先从找规律开始:
对于 \(a_1a_2\) 中每个数都加上 \(x\) 以后,\((a_1+x)(a_2+x)=a_1a_2+(a_1+a_2)x+x^2\)
对于 \(a_1a_2a_3\) 中每个数都加上 \(x\) 以后,\((a_1+x)(a_2+x)(a_3+x)=a_1a_2a_3+(a_1a_2+a_1a_3+a_2a_3)x+(a_1+a_2+a_3)x^2+x^3\)
对于 \(a_1a_2a_3a_4\) 中每个数都加上 \(x\) 以后,\((a_1+x)(a_2+x)(a_3+x)(a_4+x)=a_1a_2a_3a_4+(a_1a_2a_3+a_1a_2a_4+a_1a_3a_4+a_2a_3a_4)x+(a_1a_2+a_1a_3+a_1a_4+a_2a_3+a_2a_4+a_3a_4)x^2+(a_1+a_2+a_3+a_4)x^3+x^4\)
\(\ldots\)
对于 \(a_1a_2a_3\cdots a_{n-1}a_{n}\) 中每个数都加上 \(x\) 以后,\((a_1+x)(a_2+x)(a_3+x)\cdots(a_{n-1}+x)(a_{n}+x)=\sum_{i=0}^{n}f_{n-i}x^i\)(其中 \(f_i\) 同线段树的标记,为选出 \(i\) 个数的乘积的所有方案的和,注意 \(f_0=1\))
但是这样时间复杂度还是要 \(\mathcal{O}(n)\) 显然是不优秀的.
那么考虑在 \(a\) 选出 \(j\) 个数的贡献,对于 \(a_{p_1}a_{p_2}a_{p_3}\cdots a_{p_{j-1}}a_{p_j}\) 这样的一段乘积会出现在所有包含这这些数的方案中,那么这段数的贡献就是这段数的乘积 \(\times\) 这段数出现在的方案的个数 \(\times\) 在不同长度的方案中这段数需要乘上的 \(x\) 的幂次.而对于所有的长度为 \(j\) 的 \(p\) 数组对于的 \(a\) 数组中的数的乘积的和就是 \(f_j\),于是只要计算出出现的方案数之后就变得很好计算了,至于方案数的计算可以发现这 \(j\) 个数是固定不能变的,如果要计算对于 \(f_i\) 的贡献时就是需要在 \(n-j\) 个数中再选取 \(i-j\) 个数,所以方案数就是 \(C_{n-j}^{i-j}\),而这个东西可以预处理以后做到 \(O(1)\) 计算,于是这道题就做好了.
最后的时间复杂度是 \(\mathcal{O}(N\log_2N\times c^2)\)(\(c\) 为 \(\min(N,20)\)).
代码
#include<bits/stdc++.h>
#define REP(i,first,last) for(int i=first;i<=last;++i)
#define DOW(i,first,last) for(int i=first;i>=last;--i)
using namespace std;
const int MAXN=5e4+7;
const long long mod=19940417;
int N,M;
long long arr[MAXN];
long long c[MAXN][22];//通过杨辉三角计算组合数
struct LazyTag//懒标记
{
bool flips;
long long add;
void Clean()//清空懒标记
{
flips=0;
add=0;
}
}ForMake;
long long Mod(long long a)//因为有负数,所以手写一个取模
{
a%=mod;
a+=mod;
a%=mod;
return a;
}
LazyTag MakeLazyTag(long long add,bool flips=0)//建一个懒标记(没啥用)
{
ForMake.add=Mod(add);
ForMake.flips=flips;
return ForMake;
}
struct SegmentTree//线段树部分
{
long long choose[22];//每个节点需要计算当前区间用选c个数的乘积的的所有方案的和
int len;//min(right-left+1,20)
LazyTag tag;
}sgt[MAXN*4];
#define LSON (now<<1)
#define RSON (now<<1|1)
#define MIDDLE ((left+right)>>1)
#define LEFT LSON,left,MIDDLE
#define RIGHT RSON,MIDDLE+1,right
#define NOW now_left,now_right
void PushUp(int now)//合并
{
REP(i,0,sgt[now].len)
{
sgt[now].choose[i]=0;
}
REP(i,0,sgt[LSON].len)//枚举左右子树取的数的个数
{
REP(j,0,sgt[RSON].len)
{
if(i+j<=sgt[now].len)
{
sgt[now].choose[i+j]+=sgt[LSON].choose[i]*sgt[RSON].choose[j]%mod;//计算对于当前区间的贡献
sgt[now].choose[i+j]=Mod(sgt[now].choose[i+j]);
}
}
}
}
void Build(int now=1,int left=1,int right=N)//建树
{
sgt[now].len=min(20,right-left+1);
if(left==right)
{
sgt[now].choose[0]=1;//注意选0个数为1
sgt[now].choose[1]=Mod(arr[left]);
return;
}
Build(LEFT);
Build(RIGHT);
PushUp(now);
}
long long power[22]={1};
#define LEN (right-left+1)
void DownAdd(LazyTag tag,int now,int left,int right)//加的操作
{
if(!tag.add)
{
return;
}
REP(i,1,sgt[now].len)//先计算加的数的幂次
{
power[i]=Mod(power[i-1]*tag.add);
}
DOW(i,sgt[now].len,1)
{
REP(j,0,i-1)
{
//计算当前必须选的长度对于当前的长度的贡献
sgt[now].choose[i]+=(sgt[now].choose[j]*power[i-j]%mod*c[LEN-j][i-j])%mod;
sgt[now].choose[i]%=mod;
}
}
sgt[now].tag.add+=tag.add;
}
void DownFlips(LazyTag tag,int now,int left,int right)//区间变为相反数
{
if(!tag.flips)
{
return;
}
sgt[now].tag.flips^=1;
sgt[now].tag.add=mod-sgt[now].tag.add;//注意需要将加标记也取反
REP(i,1,sgt[now].len)
{
if(i&1)//奇数位置变为相反数
{
sgt[now].choose[i]=mod-sgt[now].choose[i];
}
}
}
void PushDown(int now,int left,int right)//下传懒标记
{
DownFlips(sgt[now].tag,LEFT);
DownFlips(sgt[now].tag,RIGHT);
DownAdd(sgt[now].tag,LEFT);
DownAdd(sgt[now].tag,RIGHT);
sgt[now].tag.Clean();
}
//修改部分不多说
void UpdataAdd(int now_left,int now_right,int add,int now=1,int left=1,int right=N)
{
if(now_right<left||right<now_left)
{
return;
}
if(now_left<=left&&right<=now_right)
{
DownAdd(MakeLazyTag(add),now,left,right);
return;
}
PushDown(now,left,right);
UpdataAdd(NOW,add,LEFT);
UpdataAdd(NOW,add,RIGHT);
PushUp(now);
}
void UpdataFlips(int now_left,int now_right,int now=1,int left=1,int right=N)
{
if(now_right<left||right<now_left)
{
return;
}
if(now_left<=left&&right<=now_right)
{
DownFlips(MakeLazyTag(0,1),now,left,right);
return;
}
PushDown(now,left,right);
UpdataFlips(NOW,LEFT);
UpdataFlips(NOW,RIGHT);
PushUp(now);
}
long long answer[22];//记录答案
long long help[22];//帮助计算
bool check=0;//记录当前是不是第一个找到的区间
void BeforeQuery()//查询前预处理
{
REP(i,0,21)
{
answer[i]=0;
}
answer[0]=1;
check=0;
}
void Query(int now_left,int now_right,int now=1,int left=1,int right=N)//查询
{
if(now_right<left||right<now_left)
{
return;
}
if(now_left<=left&&right<=now_right)
{
if(!check)//如果没有查询到过就赋值
{
REP(i,0,sgt[now].len)
{
answer[i]=Mod(sgt[now].choose[i]);
}
check=1;
}
else//如果查询到过需要像合并两颗子树的方式一样合并
{
REP(i,0,20)
{
help[i]=answer[i];
answer[i]=0;
}
REP(i,0,20)
{
REP(j,0,sgt[now].len)
{
if(i+j<=20)
{
answer[i+j]+=help[i]*sgt[now].choose[j]%mod;
answer[i+j]=Mod(answer[i+j]);
}
}
}
}
return;
}
PushDown(now,left,right);
Query(NOW,LEFT);
Query(NOW,RIGHT);
}
int main()
{
scanf("%d%d",&N,&M);
REP(i,1,N)
{
scanf("%lld",&arr[i]);
}
c[0][0]=1;//杨辉三角计算组合数
REP(i,1,N)
{
c[i][0]=1;
REP(j,1,20)
{
c[i][j]=Mod(c[i-1][j]+c[i-1][j-1]);
}
}
Build();//建树
char opt;
int left,right,c;
REP(i,1,M)
{
cin>>opt;
scanf("%d%d",&left,&right);
if(opt=='I')
{
scanf("%d",&c);
UpdataAdd(left,right,c);//区间加
}
if(opt=='R')
{
UpdataFlips(left,right);//区间变为相反数
}
if(opt=='Q')
{
scanf("%d",&c);
BeforeQuery();
Query(left,right);//查询
printf("%lld\n",answer[c]);
}
}
return 0;
}