「Gym103409H」Popcount Words

题目

点这里看题目。

分析

首先自然是研究一下 \(w()\) 有没有什么比较好的性质。

这个其实猜都猜得到,\(w()\) 显然应当存在一定的倍增结构。具体地来说,我们考察一种特殊情况:

定义 \(W_{n}=w(0,2^n-1),\overline{W_n}=w(2^n,2^{n+1}-1)\),则我们不难得到:

  • \(W_0=\mathtt{0},W_1=\mathtt{1}\)
  • \(W_n=W_{n-1}+\overline{W_{n-1}},\overline{W_n}=\overline{W_{n-1}}+W_{n-1},n\in \mathbb N_+\)

当然,由此我们还不难得出一个简单的性质:

\[w(k2^n,(k+1)2^n-1)= \begin{cases} W_n,\operatorname{popcount}(k)\bmod 2=0\\ \overline{W_n},\operatorname{popcount}(k)\bmod 2=1 \end{cases},k,n\in \mathbb N \]

理解起来很容易,从 \(k2^n\) 一路 +1 到 \((k+1)2^n-1\),真正在变化的只有低 \(n\) 位,因而 \(k\)\(\operatorname{popcount}\) 可以和低位的 \(\operatorname{popcount}\) 分开。

这个性质有可能是在后面要用到的时候才想起来去找的,不过无伤大雅,反正很容易发现就是了。

下面考察一下 \(S\) 的结构。既然有了 \(W\) 这样的基础元素,我们就可以直接通过二进制,将 \(w(l,r)\) 拆分成 \(O(\log r)\)\(W\)\(\overline W\) 拼接的结果。

具体过程有点像是树状数组

  • 设正整数 \(x\) 在二进制下最低位为 \(\operatorname{lowbit}(x)\)

  • 先从 \(l\) 上升,每次加入 \(w(l,l+2^{\operatorname{lowbit}(l)}-1)\),再令 \(l=l+2^{\operatorname{lowbit}(l)}\)

    \(l+2^{\operatorname{lowbit}(l)}>r\) 时结束。

  • 再逐步逼近 \(r\),每次加入 \(w(l,l+2^{\operatorname{lowbit}(r-l)}-1)\),再令 \(l=l+2^{\operatorname{lowbit}(r-l)}\)

    \(l=r\) 时结束。

需要注意的是,最终我们形成的是 \(w(l,r-1)\),因此需要对于右端点稍稍调整一下,无伤大雅。


回到原问题。多模式串匹配的问题,首先反手建立一个 AC 自动机不多说。哈哈哈,学麻了,一开始居然没有想到 AC 自动机,还试图多次询问单独跑 KMP

注意到,最终我们的问题,可以被转化为:求 AC 自动机上每个结点各自被经过了多少次。我们已经将 \(S\) 拆分为了 \(O(n\log r)\)\(W\)\(\overline W\),所以可以看作是将 \(W,\overline W\) 按顺序放到 AC 自动机上分段转移。

注意到两点:

  • 这是一个典型的离线查询的问题。
  • \(W,\overline W\) 本身就含有倍增的结构。

我们不由得想到了打标记,结合 \(W,\overline W\) 则可以确定是逆序下放倍增标记的方法。

更准确地描述是:我们只需要确定起始点 \(u\) 和转移的序列 \(W_n\)\(\overline {W_n}\),即可确定标记具体打在了哪里。因此,可以考虑状态 \(g_{u,n,0/1}\),表示从 \(u\) 出发,转移了 \(W_n\) 或者 \(\overline {W_n}\),这样的情况\(S\) 中总共出现了多少次。

记录辅助状态 \(f_{u,n,0/1}\) 表示从 \(u\) 出发,转移了 \(W_n\) 或者 \(\overline{W_n}\) 后的终点;\(h_{u,n,0/1}\) 表示仅考虑由 \(S\) 拆分出来的 \(W\)\(\overline W\) 序列,从 \(u\) 出发转移了 \(W_n\) 或者 \(\overline{W_n}\) 的情况出现了多少次。

可以写出如下的转移:

\[g_{u,n,k}=h_{u,n,k}+g_{u,n+1,k}+\sum_{f_{v,n,1-k}=u}g_{v,n+1,1-k} \]

举个例子,当 \(k=0\) 时,简单来说就是:考虑直接出发;考虑从 \(W_{n+1}\) 拆出来的部分(前半段是 \(W_n\));考虑从 \(\overline{W_{n+1}}\) 拆出来的部分(后半段是 \(W_{n}\))。

这个可以做到 \(O((\sum|p|)\log r)\) 的预处理和下放,总时间复杂度为 \(O((n+\sum|p|)\log r)\)

小结:

  1. 对于 \([l,r]\) 的拆分的思想值得学习,而不应该被局限于此题。

    关键在于,\(w\) 和树状数组有一定的相似性:树状数组中,每个位置记录了长为 \(2^{\operatorname{lowbit}}\) 的后缀和;而在 \(w\) 中,移动 \(2^{\operatorname{lowbit}}\) 步的字符串可以化归为 \(W\) 或者 \(\overline W\)

  2. 这里的倍增标记法的联想和运用确实很巧妙。典型的离线查询问题中,标记确实是很好用的。

  3. 哈哈哈,下次看到多模式串匹配别再犯傻了。

代码

复杂度写得这么对,到我这儿怎么就开始卡常了?

#include <cstdio>
     
#define rep( i, a, b ) for( int i = (a) ; i <= (b) ; i ++ )
#define per( i, a, b ) for( int i = (a) ; i >= (b) ; i -- )

typedef long long LL;

const int MAXN = 1e5 + 5, MAXV = 5e5 + 5, MAXLOG = 35;

template<typename _T>
void read( _T &x ) {
    x = 0; char s = getchar(); bool f = false;
    while( ! ( '0' <= s && s <= '9' ) ) { f = s == '-', s = getchar(); }
    while( '0' <= s && s <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ( s - '0' ), s = getchar(); }
    if( f ) x = -x;
}

template<typename _T>
void write( _T x ) {
    if( x < 0 ) putchar( '-' ), x = -x;
    if( 9 < x ) write( x / 10 );
    putchar( x % 10 + '0' );
}

LL cnt[MAXV];

LL g[MAXV][MAXLOG][2];
int f[MAXV][MAXLOG][2];

int q[MAXV];
int ch[MAXV][2], fail[MAXV];
int ntot = 0;

int ed[MAXN];
int segL[MAXN], segR[MAXN];

char buf[MAXV];

int N, Q;

inline int Lowbit( const int &x ) {
    return x & ( - x );
}

inline int Insert( const char *str ) {
    int p = 0, x;
    for( int i = 0 ; str[i] ; i ++ ) {
        x = str[i] - '0';
        if( ! ch[p][x] ) ch[p][x] = ++ ntot;
        p = ch[p][x];
    }
    return p;
}

inline void Build() {
    int h = 1, t = 0;
    if( ch[0][0] ) fail[q[++ t] = ch[0][0]] = 0;
    if( ch[0][1] ) fail[q[++ t] = ch[0][1]] = 0;
    while( h <= t ) {
        int u = q[h ++];
        ( ch[u][0] ? fail[q[++ t] = ch[u][0]] : ch[u][0] ) = ch[fail[u]][0];
        ( ch[u][1] ? fail[q[++ t] = ch[u][1]] : ch[u][1] ) = ch[fail[u]][1];
    }
}

int main() {
    read( N ), read( Q );
    rep( i, 1, N ) read( segL[i] ), read( segR[i] );
    rep( i, 1, Q ) {
        scanf( "%s", buf );
        ed[i] = Insert( buf );
    }
    Build();
    rep( i, 0, ntot ) 
        f[i][0][0] = ch[i][0], 
    f[i][0][1] = ch[i][1];
    rep( j, 1, 29 ) rep( i, 0, ntot )
        f[i][j][0] = f[f[i][j - 1][0]][j - 1][1],
    f[i][j][1] = f[f[i][j - 1][1]][j - 1][0];
    int p = 0;
    rep( i, 1, N ) {
        int l = segL[i], r = segR[i] + 1;
        while( l + Lowbit( l ) <= r ) {
            g[p][__builtin_ctz( l )][__builtin_parity( l )] ++;
            p = f[p][__builtin_ctz( l )][__builtin_parity( l )];
            l += Lowbit( l );
        }
        while( l < r ) {
            g[p][31 - __builtin_clz( r - l )][__builtin_parity( l )] ++;
            p = f[p][31 - __builtin_clz( r - l )][__builtin_parity( l )];
            l += 1u << ( 31 - __builtin_clz( r - l ) );
        }
    }
    per( j, 28, 0 )
        rep( i, 0, ntot ) {
        g[i][j][0] += g[i][j + 1][0];
        g[f[i][j][0]][j][1] += g[i][j + 1][0];
        g[i][j][1] += g[i][j + 1][1];
        g[f[i][j][1]][j][0] += g[i][j + 1][1];
    }
    rep( i, 0, ntot )
        cnt[ch[i][0]] += g[i][0][0],
    cnt[ch[i][1]] += g[i][0][1];
    per( i, ntot, 1 ) {
        int u = q[i];
        cnt[fail[u]] += cnt[u];
    }
    rep( i, 1, Q )
        write( cnt[ed[i]] ), putchar( '\n' );
    return 0;
}
posted @ 2022-05-15 17:07  crashed  阅读(152)  评论(0编辑  收藏  举报