[NOI2021] 密码箱 (平衡树,连分数,Stern-Brocot 树,矩阵)
阅读原文时间:2023年07月09日阅读:1

题面

记忆犹新

题解

f

f

f 函数值给得非常明显,一看就给人一种熟悉感——这不是连分数吗?

众所周知,连分数有个递推公式,即

p

i

=

a

i

p

i

1

+

p

i

2

q

i

=

a

i

q

i

1

+

q

i

2

p_i=a_ip_{i-1}+p_{i-2}\\ q_i=a_iq_{i-1}+q_{i-2}

pi​=ai​pi−1​+pi−2​qi​=ai​qi−1​+qi−2​

而且不用管约分,因为根据这个式子算出来的一定是最简分数。

可以发现两者的转移式子几乎一模一样,只是初值不同。

于是,一种方式是用矩阵维护转移,分子分母的转移式子相同,只用存一份。

这样就可以解决只有 APPEND 操作的数据了。

但是我们发现,如果把转移跟

a

a

a 序列绑定在一起,很不好搞。我们其实可以把转移和操作序列绑定:

我们把向量矩阵定义为

(

p

i

,

p

i

1

,

p

i

2

)

(p_i,p_{i-1},p_{i-2})

(pi​,pi−1​,pi−2​) 和

(

q

i

,

q

i

1

,

q

i

2

)

(q_i,q_{i-1},q_{i-2})

(qi​,qi−1​,qi−2​) ,我们不妨从

p

p

p 入手,翻译操作类型代表的矩阵,

  • W 类型:最后一个+1,也就是

    p

    i


    •  ⁣ ⁣

    =

    p

    i

    1

    p_i+\!\!=p_{i-1}

    pi​+=pi−1​ ,这个可以转化为矩阵

    (

    1

    0

    0

    1

    1

    0

    0

    0

    1

    )

    \left(\begin{matrix}1&0&0\\1&1&0\\0&0&1\end{matrix}\right)

    ⎝⎛​110​010​001​⎠⎞​ 。

E 类型,分两种小情况:

  • WE:前面是个 W ,保证了

    a

    a

    a 序列最后一个元素大于 1,那么把它减 1,再添两个 1,此时的 E转移矩阵为

    (

    2

    1

    1

    1

    0

    1

    0

    0

    0

    )

    \left(\begin{matrix}2&1&1\\-1&0&-1\\0&0&0\end{matrix}\right)

    ⎝⎛​2−10​100​1−10​⎠⎞​ 。

  • _E:除了前面是 W 之外的情况,容易发现

    a

    a

    a 序列最后一个元素必定为 1,那么把倒数第二个数+1,此时比较麻烦,要牵扯到

    p

    i

    2

    p_{i-2}

    pi−2​ ,这也是我定义

    3

    ×

    3

    3\times3

    3×3 矩阵的原因。从递推公式上算,转移矩阵应该是

    (

    1

    0

    0

    0

    1

    0

    1

    1

    1

    )

    \left(\begin{matrix}1&0&0\\0&1&0\\1&1&1\end{matrix}\right)

    ⎝⎛​101​011​001​⎠⎞​ 。

为了方便,我们算出

W

E

:

(

2

1

1

1

0

1

0

0

0

)

=

X

:

(

1

0

1

0

1

1

0

0

0

)

×

_

E

:

(

1

0

0

0

1

0

1

1

1

)

{\tt WE}:\left(\begin{matrix}2&1&1\\-1&0&-1\\0&0&0\end{matrix}\right)={\tt X}:\left(\begin{matrix}1&0&1\\0&1&-1\\0&0&0\end{matrix}\right)\times{\tt\_E}:\left(\begin{matrix}1&0&0\\0&1&0\\1&1&1\end{matrix}\right)

WE:⎝⎛​2−10​100​1−10​⎠⎞​=X:⎝⎛​100​010​1−10​⎠⎞​×_E:⎝⎛​101​011​001​⎠⎞​ ,用这个

X

{\tt X}

X 矩阵进行 WE 的计算,那么相当于所有的 E 都先看成是 _E 类型,然后若出现了 WE ,就在 WE 中间乘上矩阵

X

\tt X

X 。

现在,该有的推理都已经结束了,开始我们的平衡树之旅了。

平衡树中要记录两个 char 表示左右端点的字符,四个矩阵,分别记录无颜色翻转无旋转无颜色翻转有旋转有颜色翻转无旋转有颜色翻转有旋转这四种情况下的区间转移矩阵,还有懒标记,以及平衡树必有的 sonsiz,和随机值。

CODE

为了使

3

×

3

3\times3

3×3 的矩阵过掉,卡常数颇费心思。

#include<map>
#include<cmath>
#include<queue>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
#define MAXN 100005
#define ENDL putchar('\n')
#define LL long long
#define DB double
#define lowbit(x) ((-x) & (x))
LL read() {
    LL f = 1,x = 0;char s = getchar();
    while(s < '0' || s > '9') {if(s=='-')f = -f;s = getchar();}
    while(s >= '0' && s <= '9') {x=x*10+(s-'0');s = getchar();}
    return f * x;
}
void putint(int x) {
    if(!x) return ;
    putint(x/10);putchar(x%10+'0');
    return ;
}
const int MOD = 998244353;
int n,m,i,j,s,o,k;
int Q,X = 123127;
int my_rand() {
    return X = (X *2333ll + 2344243ll) % MOD;
}
char ss[MAXN<<1];
struct mat{
    int n,m;
    int s[3][3];
    mat(){n=m=0;}
    mat(int x) {n=m=3;memset(s,0,sizeof(s));for(int i = 0;i < 3;i ++)s[i][i]=x;}
    void set(int N,int M) {n=N;m=M;memset(s,0,sizeof(s));}
}Ap,Aq,W,WE,_E,nm1;
void MD(int &x) {if(x>=MOD)x-=MOD;}
// p_-2 = 0, p_-1 = 1; q_-2 = 1, q_-1 = 0;
mat operator * (mat a,mat b) {
    mat c;c.n = a.n;c.m = b.m;
    c.s[0][0] = (a.s[0][0]*1ll*b.s[0][0]+a.s[0][1]*1ll*b.s[1][0]+a.s[0][2]*1ll*b.s[2][0]) % MOD;
    c.s[0][1] = (a.s[0][0]*1ll*b.s[0][1]+a.s[0][1]*1ll*b.s[1][1]+a.s[0][2]*1ll*b.s[2][1]) % MOD;
    c.s[0][2] = (a.s[0][0]*1ll*b.s[0][2]+a.s[0][1]*1ll*b.s[1][2]+a.s[0][2]*1ll*b.s[2][2]) % MOD;

    c.s[1][0] = (a.s[1][0]*1ll*b.s[0][0]+a.s[1][1]*1ll*b.s[1][0]+a.s[1][2]*1ll*b.s[2][0]) % MOD;
    c.s[1][1] = (a.s[1][0]*1ll*b.s[0][1]+a.s[1][1]*1ll*b.s[1][1]+a.s[1][2]*1ll*b.s[2][1]) % MOD;
    c.s[1][2] = (a.s[1][0]*1ll*b.s[0][2]+a.s[1][1]*1ll*b.s[1][2]+a.s[1][2]*1ll*b.s[2][2]) % MOD;

    c.s[2][0] = (a.s[2][0]*1ll*b.s[0][0]+a.s[2][1]*1ll*b.s[1][0]+a.s[2][2]*1ll*b.s[2][0]) % MOD;
    c.s[2][1] = (a.s[2][0]*1ll*b.s[0][1]+a.s[2][1]*1ll*b.s[1][1]+a.s[2][2]*1ll*b.s[2][1]) % MOD;
    c.s[2][2] = (a.s[2][0]*1ll*b.s[0][2]+a.s[2][1]*1ll*b.s[1][2]+a.s[2][2]*1ll*b.s[2][2]) % MOD;
    return c;
}
struct it{
    char L,R;char f1,f2;
    mat m[2][2];
    it() {L=R=0;f1=f2=0;}
    it(int x){f1=f2=0;m[0][0]=m[0][1]=m[1][0]=m[1][1]=mat(1);f1=f2=0;}
    void flip() {
        if(L=='E') L='W'; else L='E';
        if(R=='E') R='W'; else R='E';
        f1 ^= 1;
    }
    void reverse() {
        swap(L,R);f2 ^= 1;
    }
}tre[(MAXN*2)<<2]={it(1)},nmb[(MAXN*2)<<2]={it(1)};
it merg(it a,it b) {
    if(!a.L) return b; if(!b.L) return a;
    it c;c.L = a.L;c.R = b.R;
    if(a.R=='W'&&b.L=='E') c.m[0][0] = a.m[a.f1][a.f2]*WE*b.m[b.f1][b.f2],c.m[1][1] = b.m[1^b.f1][1^b.f2]*WE*a.m[1^a.f1][1^a.f2];
    else c.m[0][0] = a.m[a.f1][a.f2]*b.m[b.f1][b.f2],c.m[1][1] = b.m[1^b.f1][1^b.f2]*a.m[1^a.f1][1^a.f2];

    if(a.R=='E'&&b.L=='W') c.m[0][1] = b.m[b.f1][1^b.f2]*WE*a.m[a.f1][1^a.f2],c.m[1][0] = a.m[1^a.f1][a.f2]*WE*b.m[1^b.f1][b.f2];
    else c.m[0][1] = b.m[b.f1][1^b.f2]*a.m[a.f1][1^a.f2],c.m[1][0] = a.m[1^a.f1][a.f2]*b.m[1^b.f1][b.f2];
    return c;
}
bool lz[(MAXN*2)<<2][2];
int sn[(MAXN*2)<<2][2],siz[(MAXN*2)<<2],hp[(MAXN*2)<<2],CNT;
// -----------------------------------------Treap!!!
struct np{
    int s[2];np(){s[0]=s[1]=0;}
};
int update(int x) {
    int ls = sn[x][0],rs = sn[x][1];
    siz[x] = siz[ls] + siz[rs] + 1;
    tre[x] = merg(tre[ls],merg(nmb[x],tre[rs]));
    return x;
}
void pushdown(int a) {
    int ls = sn[a][0],rs = sn[a][1];
    if(lz[a][0]) {
        tre[ls].flip(); tre[rs].flip();
        nmb[ls].flip(); nmb[rs].flip();
        lz[ls][0] ^= 1; lz[rs][0] ^= 1;
        lz[a][0] = 0;
    }
    if(lz[a][1]) {
        tre[ls].reverse(); tre[rs].reverse();
        nmb[ls].reverse(); nmb[rs].reverse();
        swap(sn[ls][0],sn[ls][1]); swap(sn[rs][0],sn[rs][1]);
        lz[ls][1] ^= 1; lz[rs][1] ^= 1;
        lz[a][1] = 0;
    }return ;
}
int maketree(char C) {
    int a = ++ CNT;
    lz[a][0] = lz[a][1] = 0;
    sn[a][0] = sn[a][1] = 0;
    hp[a] = my_rand();
    mat nw,nw2;
    if(C == 'W') nw = W,nw2 = _E;
    else nw = _E,nw2 = W;
    tre[a].L = tre[a].R = C;
    tre[a].m[0][0]=tre[a].m[0][1]=nw;
    tre[a].m[1][0]=tre[a].m[1][1]=nw2;
    nmb[a] = tre[a]; siz[a] = 1;
    return a;
}
np Split(int x,int rk) {
    np as = np();
    if(!x) return as;
    pushdown(x);
    int d = (siz[sn[x][0]]+1 <= rk);
    if(d) rk -= siz[sn[x][0]]+1;
    as = Split(sn[x][d],rk);
    sn[x][d] = as.s[d^1];
    as.s[d^1] = update(x);
    return as;
}
int Merge(int p1,int p2) {
    if(!p1 || !p2) return p1+p2;
    pushdown(p1); pushdown(p2);
    if(hp[p1] < hp[p2]) {
        sn[p1][1] = Merge(sn[p1][1],p2);
        return update(p1);
    }
    sn[p2][0] = Merge(p1,sn[p2][0]);
    return update(p2);
}
int Flip(int x,int l,int r) {
    np p2 = Split(x,r);
    np p1 = Split(p2.s[0],l-1);
    int a = p1.s[1];
    tre[a].flip();nmb[a].flip();
    lz[a][0] ^= 1;
    return Merge(Merge(p1.s[0],p1.s[1]),p2.s[1]);
}
int Reverse(int x,int l,int r) {
    np p2 = Split(x,r);
    np p1 = Split(p2.s[0],l-1);
    int a = p1.s[1];
    tre[a].reverse();nmb[a].reverse();
    lz[a][1] ^= 1; swap(sn[a][0],sn[a][1]);
    return Merge(Merge(p1.s[0],p1.s[1]),p2.s[1]);
}
//---------------------------------------Treap Forever!!!
int root = 0;
int main() {
    nm1 = mat(1);
    Ap.set(1,3); Aq.set(1,3);
    W.set(3,3); WE.set(3,3); _E.set(3,3);
    // p-1= 1, q-1= 0
    // p0 = 0, q0 = 1;
    // p1 = 1, q1 = 1;
    Ap.s[0][0] = 1; Ap.s[0][1] = 0; Ap.s[0][2] = 1;
    Aq.s[0][0] = 1; Aq.s[0][1] = 1; Aq.s[0][2] = 0;

    int tm1[3][3] = {{1,0,0},{1,1,0},{0,0,1}};
    memcpy(W.s,tm1,sizeof(W.s));

    int tm2[3][3] = {{1,0,1},{0,1,MOD-1},{0,0,0}};
    memcpy(WE.s,tm2,sizeof(WE.s));

    int tm3[3][3] = {{1,0,0},{0,1,0},{1,1,1}};
    memcpy(_E.s,tm3,sizeof(_E.s));

    n = read();Q = read();
    scanf("%s",ss + 1);
    for(int j = 1;j <= n;j ++) {
        root = Merge(root,maketree(ss[j]));
    }

    it as = tre[root];
    mat sp = Ap*as.m[as.f1][as.f2],sq = Aq*as.m[as.f1][as.f2];
    putint(sp.s[0][0]);putchar(' ');putint(sq.s[0][0]);ENDL;
    while(Q --) {
        char tps[15],cc;
        scanf("%s",tps);
        if(tps[0] == 'A') {
            cc = getchar();
            while(cc == ' ') cc = getchar();
            n ++;
            root = Merge(root,maketree(cc));
        }
        else if(tps[0] == 'F') {
            s = read();o = read();
            root = Flip(root,s,o);
        }
        else {
            s = read();o = read();
            root = Reverse(root,s,o);
        }
        it as = tre[root];
        mat sp = Ap*as.m[as.f1][as.f2],sq = Aq*as.m[as.f1][as.f2];
        putint(sp.s[0][0]);putchar(' ');putint(sq.s[0][0]);ENDL;
    }
    return 0;
}

过掉以后,发现由于序列特殊性,连分数其实可以用 2×2 的矩阵。

Stern-Brocot 树

有同学在考场上发现了规律,那就是:W 操作相当于在

Stern-Brocot

\texttt{Stern-Brocot}

Stern-Brocot 树上跳左子树,E 操作相当于跳右子树,于是,我们可以只在矩阵中记录两个值(当然分子分母要分开算):当前点左边的值和右边的值,往左跳相当于把左边的值加到右边,往右跳就把右边的数加到左边。

这二者的矩阵都非常好推,比连分数好推多了。

这个结论是怎么发现的呢?打表找规律

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器