对一种维护值域取自数列元素的可重集且支持全局下标加、插入元素、全局求和的数据结构的探索
缘起
yukicoder 1172 题面如下。
给定长度为 \(k\) 的序列 \(c_1,c_2,\dots,c_k\),以及一个长度为 \(k\) 的序列 \(b_0,b_1,\dots,b_{k-1}\)。对于任意正整数 \(x\),有序列 \(a_0,a_1,\dots,a_{x-1}\),满足
梓有一个长度为 \(n\) 的序列 \(d\),初始时满足所有 \(d_i = 0\)。进行 \(m\) 次操作,每次操作给出 \(l,r\),并按照序列 \(a\) 的方式,生成一个长度为 \(r-l\) 的序列 \(a'\),即对于所有 \(l \le j < r\),执行 \(d_j \leftarrow d_j + a'_{j-l}\)。
最后求 \(d\) 的每一位,对 \(10^9+7\) 取模。
\(n \le 2\times 10^5,k \le 200\)。
这题我的做法是这样的:
对于要求的答案 \(d_i\),可以把式子写出来:
由于 \([i<r_j]=1-[r_j \le i]\),则有
这就可以把答案强拆成两个问题,对于每个询问,给定一个限制 \(f_j\),对于满足这样限制的 \(j\),求 \(a_{i-l_{j}}\) 的和。
操作离线,对 \(f_j\) 做扫描线,同时动态维护一个集合,代表当前给 \(d_i\) 做了贡献的所有 \(a_x\)。
因为集合里的元素必然是形如 \(a_{i-l_j}\),且给 \(i-1\) 做过贡献的,依然能留下来给 \(i\) 做贡献,故我们需要每次将原来就在集合里面的那些 \(a_x\) 变成 \(a_{x+1}\)。然后将恰好在该时刻满足条件的那些操作加入集合。这些操作结束之后,我们需要计算这个集合的和,也就是 \(d_i\) 的答案。
由于 \(a_i\) 的生成有一定的性质,我们可以从中挖掘。
它满足一个类似于卷积的递推结构,而且它只与前 \(k\) 个有关。而且这题是可以接受 \(O(nk)\) 的。于是,我们考虑,当我们插入一个元素的时候,他给哪些答案有哪些贡献。
考虑 \(t\) 时刻(即计算 \(d_t\) 的时候),\(a_j\) 加入了集合。那么考虑 \(t+r\) 时刻,这个 \(a_j\) 就变成了 \(a_{j+r}\)。
对于 \(a_j\) 到 \(a_{j+k-1}\) 这 \(k\) 个元素,由于没有足够 \(k\) 个的历史元素,所以他们不能通过递推的方式求出。那我们直接暴力给所有 \(i \in [t,t+k-1],d_i \leftarrow d_i+a_{j-t+i}\) 就行了。
从第 \(k\) 项开始,它的值完全由递推决定。我们称它成熟。由于所有成熟元素满足同一个递推,它们的和必然也满足同一个递推,所以可以把它们压缩成一个 \(k\) 维状态 \(s\)。
所以多维护 \(n\) 个容器 \(ok_i\),表示恰好在时刻 \(i\) 成熟的元素。插入元素的时候,就给它的成熟时刻的容器插一个数进去就行了。
考虑面前在时刻 \(t\),\(s_i\) 表示在时刻 \(t-k+i+1\) 成熟的元素之和。显然 \(ok_i\) 里面的元素会给其贡献,而每个时刻的 \(s_{k-1}\) 需要加入答案里。
查完一个时刻答案,递推到下一个时刻也是容易的,处理出下一个时刻的 \(s_{k-1}\)(实际上就是这一时刻的 \(s_{k}\)),值为 \(\sum _{j=1}^k c_js_{k-j}\)。然后把当前时刻的 \(s\) 全体左移一位,就是下一时刻的 \(s\) 了。
复杂度是 \(O(nk)\) 的。
::::success[code]
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
#define int long long
using namespace std;
const int N=2e5+220,mod=1e9+7;
int a[N],b[N],c[N];
int n,m,k;
int ans[N],d[N];
int s[N],add[N];
vector <int> ok[N];
struct node{
int l,f,r;
bool friend operator <(node x,node y){
if(x.f!=y.f) return x.f<y.f;
else return x.l<y.l;
}
}q[N];
void solve(){
sort(q+1,q+m+1);
int t=1;
rep(i,0,n-1){
for(auto j:ok[i]) rep(r,1,k) s[r-1]=(s[r-1]+a[j+r])%mod;
while(t<=m&&q[t].f<=i){
int j=i-q[t].l;
rep(r,0,min(k,n-i)-1) add[i+r]=(add[i+r]+a[j+r])%mod;
if(i+k<n) ok[i+k].push_back(j);
t++;
}
d[i]=(s[k-1]+add[i])%mod;
if(i<n-1){
int nxt=0;
rep(l,1,k) nxt=(nxt+c[l]*s[k-l]%mod)%mod;
rep(j,0,k-2) s[j]=s[j+1];
s[k-1]=nxt%mod;
}
}
rep(i,0,n) s[i]=add[i]=0,ok[i].clear();
}
signed main(){
ios::sync_with_stdio();cin.tie(0);cout.tie(0);
cin>>k>>n>>m;
rep(i,0,k-1) cin>>b[i];
rep(i,1,k) cin>>c[i];
rep(i,0,n-1){
if(i<k) a[i]=b[i];
else rep(j,1,k) a[i]=(a[i]+a[i-j]*c[j]%mod)%mod;
}
rep(i,1,m) cin>>q[i].l>>q[i].r,q[i].f=q[i].l;
solve();
rep(i,0,n-1) ans[i]=d[i],d[i]=0;
rep(i,1,m) swap(q[i].f,q[i].r);
solve();
rep(i,0,n-1) cout<<(ans[i]-d[i]+mod)%mod<<"\n";
return 0;
}
::::
当然这题肯定有更易的写法,但这个写法不禁勾起了我研究这样数据结构的兴趣:
- 维护的是一个可重集
- 集合中的元素来自定数列
- 支持全局下标加
- 支持插入元素
- 支持全局和

浙公网安备 33010602011771号