题解:B3646 数列前缀和 3

· · 题解

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)。不过可能没必要(?)