题解:B3646 数列前缀和 3
UnionRE
·
·
题解
Part 0 前言
如何发明 Sqrt Tree??
Part 1 怎么做?
设运算符为 \odot(当然这里是矩阵乘法)。
考虑使用由乃救爷爷的做法(当然也是区间静态半群信息查询那道题的第一篇题解),很容易写出分块:
分 \sqrt n 个块,记录:
然后查询就很好做了。l,r 不在一个块那就直接做,O(1)。在一个块就是 O(\sqrt n) 暴力查。
期望复杂度 O(1)。
:::success[code]
#include<bits/stdc++.h>
using namespace std;
constexpr int mod=1145141;
const int maxn=1e6+10,masq=sqrt(maxn)+10;
struct sqr{
int a[3][3],ns;
bool ie;
sqr(){ie=1;memset(a,0,sizeof(a));}
sqr(int ns_){ns=ns_;ie=0;memset(a,0,sizeof(a));}
sqr(int ns_,bool _){ns=ns_;ie=_;}
friend sqr multiply(sqr A,sqr B){
int tn=A.ns;
sqr C(tn);
for(int i=0;i<tn;i++){
for(int j=0;j<tn;j++){
for(int k=0;k<tn;k++){
C.a[i][j]+=1ll*A.a[i][k]*B.a[k][j]%mod;
C.a[i][j]%=mod;
}
}
}
return C;
}
friend sqr operator*(sqr A,sqr B){
if(A.ie&&B.ie)return sqr();
else if(A.ie&&!B.ie)return B;
else if(!A.ie&&B.ie)return A;
else return multiply(A,B);
}
friend sqr merge(sqr A,sqr B){return A*B;}
int xorv(){
int ans=0;
for(int i=0;i<ns;i++){
for(int j=0;j<ns;j++)ans^=a[i][j];
}
return ans;
}
};
int n,k,B;
sqr a[maxn],f[masq][masq],b[masq],suf[masq][masq],pre[masq][masq];
int bel(int x){return (x-1)/B+1;}//block id
int L(int bid){return (bid-1)*B+1;}
int R(int bid){return min(bid*B,n);}
void build(){
B=sqrt(n);
for(int i=1;i<=n;i++)b[bel(i)]=merge(b[bel(i)],a[i]);
for(int i=1;i<=bel(n);i++){
f[i][i]=b[i];
for(int j=i+1;j<=bel(n);j++)f[i][j]=merge(f[i][j-1],b[j]);
}
for(int i=1;i<=bel(n);i++){
for(int j=L(i);j<=R(i);j++)pre[i][j-L(i)+1]=merge(pre[i][j-L(i)],a[j]);
for(int j=R(i);j>=L(i);j--)suf[i][j-L(i)+1]=merge(a[j],suf[i][j-L(i)+2]);
}
}
sqr query(int l,int r){
if(bel(l)==bel(r)){
sqr ans=sqr();
for(int i=l;i<=r;i++)ans=ans*a[i];
return ans;
}
else return merge(merge(suf[bel(l)][l-L(bel(l))+1],f[bel(l)+1][bel(r)-1]),pre[bel(r)][r-L(bel(r))+1]);
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0);
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
int q;
cin>>n>>k>>q;
for(int i=1;i<=n;i++){
sqr M(k,0);
for(int x=0;x<k;x++){
for(int y=0;y<k;y++){
cin>>M.a[x][y];
}
}
a[i]=M;
}
build();
int ans=0;
while(q--){
int l,r;
cin>>l>>r;
ans^=query(l,r).xorv();
}
cout<<ans;
return 0;
}
:::
Part 2 我随便卡!
是的,如你所见,让 l,r 在同一个块即可。
Part 3 我乱搞,你卡不了!
我们把第一个块的块长随便搞一下,让数组有个偏移,这样 l=k\sqrt n+1,r=(k+1)\sqrt n-1 就不在一个块了。
还可以乱搞块长,反正是想怎么搞就怎么搞。
Part 4 我在打 CF,这样还是会被 hack!
我们使出绝招:
O(n\log \log n)- 期望 O(1) 最坏 O(\log \log n) 区间半群信息查询
考虑进行类似递归分块操作,可以做到 \log \log n 层的诡异东西。然后就是期望 O(1) 最坏 O(\log \log n) 的 RMQ。空间 O(n\log\log n)。
注意这个与递归分块不同,递归分块整块不是 O(1) 的。而这个是,所以递归 B 层复杂度为 O(B+\sqrt[2^B]{n})。
:::info[复杂度分析]{open}
设每层每块长度为 f(Q)。Q 为当前层的长度。
考虑两层的情形:
我们第一层预处理复杂度就是 $(n/f(n))^2$,第二层 $O((n/f(n))(f(n)/f(f(n)))^2)=O(nf(n)/(f(f(n)))^2)$。
期望复杂度为 $O((f(f(n)))^2/n)$。最坏复杂度 $f(f(n))$。要让预处理复杂度为线性,$f(f(n))\ge \sqrt[4]n$。所以取 $f(n)=\sqrt n$ 最优。
$B^{2^B}=n$。这个是 $B=O(\log \log n)$ 的。**复杂度全部来源于层数。**
实际可以取 $B=4\sim 5$,基本上可以看做 $O(1)$。
:::
空间复杂度也是 $O(n\log \log n)$。
:::success[deepseek 的 $2$ 层代码]
```cpp
#include <bits/stdc++.h>
using namespace std;
constexpr int mod = 1145141;
const int maxn = 1e6 + 10;
const int maxBlock = 1000 + 10; // 外层块数,sqrt(1e6)≈1000
struct sqr {
int a[3][3];
int ns;
bool ie; // true 表示单位矩阵(乘法中直接返回另一方)
sqr() : ns(0), ie(true) { memset(a, 0, sizeof(a)); }
sqr(int ns_) : ns(ns_), ie(false) { memset(a, 0, sizeof(a)); }
sqr(int ns_, bool isI) : ns(ns_), ie(isI) { memset(a, 0, sizeof(a)); }
friend sqr multiply(const sqr& A, const sqr& B) {
int tn = A.ns;
sqr C(tn);
for (int i = 0; i < tn; ++i) {
for (int k = 0; k < tn; ++k) {
if (A.a[i][k] == 0) continue;
long long aik = A.a[i][k];
for (int j = 0; j < tn; ++j) {
C.a[i][j] = (C.a[i][j] + aik * B.a[k][j]) % mod;
}
}
}
return C;
}
friend sqr operator*(const sqr& A, const sqr& B) {
if (A.ie && B.ie) return sqr(); // 单位元 × 单位元
if (A.ie) return B;
if (B.ie) return A;
return multiply(A, B);
}
int xorv() const {
int ans = 0;
for (int i = 0; i < ns; ++i)
for (int j = 0; j < ns; ++j)
ans ^= a[i][j];
return ans;
}
};
int n, k, q;
sqr a[maxn]; // 原始矩阵
// 第一层(外层)分块
int B1, B2; // 外层块大小、内层块大小
int nb1; // 外层块数
int L1[maxBlock], R1[maxBlock]; // 每个外层块的左右端点(1‑based)
// 外层块内的前缀 / 后缀(用于跨外层块查询的左右散块)
vector<sqr> pre1[maxBlock]; // pre1[bid][pos] 表示块bid内前pos个的乘积 (pos从0开始)
vector<sqr> suf1[maxBlock]; // suf1[bid][pos] 表示块bid内从pos到块尾的乘积 (pos从1开始)
// 外层块间乘积 f1[i][j] (i<=j)
sqr f1[maxBlock][maxBlock];
// 每个外层块内部的内层块间乘积
vector<vector<sqr>> f2[maxBlock]; // f2[bid][i][j] 表示bid块内内层块i..j的乘积 (i<=j)
// 定位外层块
int bel1(int x) {
for (int i = 1; i <= nb1; ++i)
if (L1[i] <= x && x <= R1[i])
return i;
return -1;
}
void build() {
// ---- 确定分块大小 ----
B1 = (int)sqrt(n);
if (B1 < 1) B1 = 1;
B2 = (int)pow(n, 0.25);
if (B2 < 1) B2 = 1;
// ---- 外层块划分 ----
nb1 = (n + B1 - 1) / B1;
for (int i = 1; i <= nb1; ++i) {
L1[i] = (i - 1) * B1 + 1;
R1[i] = min(i * B1, n);
}
// ---- 预处理外层块间乘积 f1 ----
// 先计算每个外层块的乘积 bOut[i]
sqr bOut[maxBlock];
for (int i = 1; i <= nb1; ++i) {
bOut[i] = sqr(); // 单位矩阵
for (int x = L1[i]; x <= R1[i]; ++x)
bOut[i] = bOut[i] * a[x];
}
// 块间前缀
for (int i = 1; i <= nb1; ++i) {
f1[i][i] = bOut[i];
for (int j = i + 1; j <= nb1; ++j)
f1[i][j] = f1[i][j - 1] * bOut[j];
}
// ---- 预处理每个外层块内部 ----
for (int bid = 1; bid <= nb1; ++bid) {
int len = R1[bid] - L1[bid] + 1;
// 1) 块内前后缀
pre1[bid].resize(len + 1); // 下标 0..len
suf1[bid].resize(len + 2); // 下标 1..len+1
pre1[bid][0] = sqr(); // 单位矩阵
for (int pos = 1; pos <= len; ++pos) {
int idx = L1[bid] + pos - 1;
pre1[bid][pos] = pre1[bid][pos - 1] * a[idx];
}
suf1[bid][len + 1] = sqr(); // 单位矩阵
for (int pos = len; pos >= 1; --pos) {
int idx = L1[bid] + pos - 1;
suf1[bid][pos] = a[idx] * suf1[bid][pos + 1];
}
// 2) 内层分块
int nb2 = (len + B2 - 1) / B2; // 该外层块内的内层块数
// 计算每个内层块的乘积
vector<sqr> innerProd(nb2 + 1);
for (int t = 1; t <= nb2; ++t) {
int l = L1[bid] + (t - 1) * B2;
int r = min(L1[bid] + t * B2 - 1, R1[bid]);
innerProd[t] = sqr(); // 单位矩阵
for (int x = l; x <= r; ++x)
innerProd[t] = innerProd[t] * a[x];
}
// 内层块间乘积 f2[bid][i][j] (i,j 1..nb2)
f2[bid].assign(nb2 + 2, vector<sqr>(nb2 + 2));
for (int i = 1; i <= nb2; ++i) {
f2[bid][i][i] = innerProd[i];
for (int j = i + 1; j <= nb2; ++j)
f2[bid][i][j] = f2[bid][i][j - 1] * innerProd[j];
}
}
}
// 查询区间 [l, r]
sqr query(int l, int r) {
int bl = bel1(l), br = bel1(r);
if (bl == br) {
// 同外层块
int pos_l = l - L1[bl] + 1;
int pos_r = r - L1[bl] + 1;
int len = R1[bl] - L1[bl] + 1;
int il = (pos_l - 1) / B2 + 1;
int ir = (pos_r - 1) / B2 + 1;
if (il == ir) {
// 同内层块 → 直接暴力
sqr res = sqr(); // 单位矩阵
for (int x = l; x <= r; ++x)
res = res * a[x];
return res;
} else {
// 左散块(从 l 到该内层块末尾)
int end_l = L1[bl] + il * B2 - 1;
if (end_l > R1[bl]) end_l = R1[bl];
sqr left = sqr();
for (int x = l; x <= end_l; ++x)
left = left * a[x];
// 右散块(下一个内层块开头到 r)
int start_r = L1[bl] + (ir - 1) * B2;
sqr right = sqr();
for (int x = start_r; x <= r; ++x)
right = right * a[x];
// 中间完整内层块
sqr mid = sqr(); // 单位矩阵
if (il + 1 <= ir - 1)
mid = f2[bl][il + 1][ir - 1];
return left * mid * right;
}
} else {
// 跨外层块
int pos_l = l - L1[bl] + 1;
int pos_r = r - L1[br] + 1;
sqr left = suf1[bl][pos_l]; // 左块后缀 [l, R1[bl]]
sqr mid = sqr(); // 中间完整外层块
if (bl + 1 <= br - 1)
mid = f1[bl + 1][br - 1];
sqr right = pre1[br][pos_r]; // 右块前缀 [L1[br], r]
return left * mid * right;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k >> q;
for (int i = 1; i <= n; ++i) {
sqr M(k, false);
for (int x = 0; x < k; ++x)
for (int y = 0; y < k; ++y)
cin >> M.a[x][y];
a[i] = M;
}
build();
int ans = 0;
while (q--) {
int l, r;
cin >> l >> r;
ans ^= query(l, r).xorv();
}
cout << ans << '\n';
return 0;
}
```
:::
### Part 5 $B=3$ 呢?!我怎么只看到了 $2$ 层
别看了:https://www.luogu.com.cn/record/291387538 获得高贵 80 TLE。
### Part 6 ……$1$ 层的实现的实际表现?
- 本题可以通过(GCC 9 情况)。
- ST 表板子可以通过(GCC 9 情况)。
### Part 7 Sqrt Tree?!
没错这玩意就是再改一改就能支持 $O(1)$ 查询了!
首先有二分高度就不用暴力往下找了。$O(\log \log\log n)$。
其实还能[优化](https://oi-wiki.org/ds/sqrt-tree/#%E4%BC%98%E5%8C%96%E8%AF%A2%E9%97%AE%E5%A4%8D%E6%9D%82%E5%BA%A6)。不过可能没必要(?)