重修字符串

之前学得太屑了,现在重修一下。

一.普通字典树(trie):

1.效用:

维护一个字符串的集合,能够高效地插入新的字符串到集合中,也能高效地查找某个字符串或某个前缀是否在集合中。

2.实现:

(1):如何插入一个新的字符串,insert

从根节点一直往下匹配,若无此字母,则新开一个关于此字母的节点,若匹配完成,则给当前节点标记末尾。

inline void ins(string s)
{
    int x=0,len=s.size();
    for(register int i=0;i<len;i++)
    {
        int c=s[i]-'a';
        if(!ch[x][c])
        {
            cnt++;
            ch[x][c]=cnt;
        }
        x=ch[x][c];
    }
    end[x]=true;
    return;
}

(2):如何查询一个字符串是否在字典树中,query(ask)

直接扫一遍,查询是否有,若无直接返回不。

inline bool ask(string s)
{
    int x=0,len=s.size();
    for(register int i=0;i<len;i++)
    {
        int c=s[i]-'a';
        if(ch[x][c]==0)
            return false;
        x=ch[x][c];
    }
    return end[x];
}

典型例题:

例一 P2580 于是他错误的点名开始了

经典的 \(\text{trie}\) 树板子,就是要注意一下在哪里记录。

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+5;

inline int read()
{
    register int x=0,f=1;
    register char ch=getchar();
    while(!isdigit(ch))
    {
        if(ch=='-')
            f=-1;
        ch=getchar();
    }
    while(isdigit(ch))
    {
        x=(x<<1)+(x<<3)+(ch^48);
        ch=getchar();
    }
    return x*f;
}

int ch[MAXN][27],cnt;
int en[MAXN];
int vis[MAXN];

inline void ins(string s)
{
    int x=0;
    int len=s.size();
    for(register int i=0;i<len;i++)
    {
        int c=s[i]-'a';
        if(!ch[x][c])
        {
            cnt++;
            ch[x][c]=cnt;
        }
        x=ch[x][c];
    }
    en[x]=1;
    return;
}

inline int ask(string s)
{
    int x=0,len=s.size();
    for(register int i=0;i<len;i++)
    {
        int c=s[i]-'a';
        if(!ch[x][c])
            return 3;
        x=ch[x][c];
    }
    if(!en[x])
        return 3;
    if(!vis[x])
    {
        vis[x]++;
        return 1;
    }
    return 2;
}

int n,m;
string s;

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    for(register int i=1;i<=n;i++)
        cin>>s,ins(s);
    cin>>m;
    for(register int i=1;i<=m;i++)
    {
        cin>>s;
        int ans=ask(s);
        if(ans==1)
        {
            printf("OK\n");
            continue;
        }
        else if(ans==2)
        {
            printf("REPEAT\n");
            continue;
        }
        else if(ans==3)
        {
            printf("WRONG\n");
            continue;
        }
    }
    return 0;
}

例二 SP4033 PHONELST-Phone List

\(2\) 倍经验:UVA11362 Phone List(一毛一样)
\(2.5\) 倍经验:UVA644 Immediate Decodability(基本一样)

若当前插入的是 \(s\)
情况一:别的串是 \(s\) 的前缀,那么 \(s\) 到遍历结束之前,一定至少有一个染色的点。

情况二:\(s\) 是别的串的前缀,那么 \(s\) 的插入过程不会有任何新的节点产生,只需要判断 \(tot\) 是否没变。

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+5;

inline int read()
{
    register int x=0,f=1;
    register char ch=getchar();
    while(!isdigit(ch))
    {
        if(ch=='-')
            f=-1;
        ch=getchar();
    }
    while(isdigit(ch))
    {
        x=(x<<1)+(x<<3)+(ch^48);
        ch=getchar();
    }
    return x*f;
}

int ch[MAXN][27],cnt;
int en[MAXN];

inline bool solve(string s)
{
    int x=0,len=s.size();
    bool flag=true;
    for(register int i=0;i<len;i++)
    {
        int c=s[i]-'0';
        if(!ch[x][c])
        {
            cnt++;
            ch[x][c]=cnt;
            flag=false;
        }
        x=ch[x][c];
        if(en[x])
            return true;
    }
    en[x]=true;
    if(flag)
        return true;
    return false;
}

int t,n;
string s;
bool vis;

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>t;
    while(t--)
    {
        memset(ch,0,sizeof(ch));
        memset(en,0,sizeof(en));
        vis=false;
        cnt=0;
        cin>>n;
        for(register int i=1;i<=n;i++)
        {
            cin>>s;
            if(solve(s))
                vis=true;
        }
        if(vis)
        {
            printf("NO\n");
            continue;
        }
        else
        {
            printf("YES\n");
            continue;
        }
    }
    return 0;
}

例三 P3879 阅读理解

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+10;
const int N=1005;

int ch[MAXN][27],cnt;
string s;
bitset<N>ed[MAXN];
int t,n,m;

inline void insert(int x,string s)
{
    int x=0;
    int len=s.size();
    for(register int i=0;i<len;i++)
    {
        int c=s[i]-'a';
        if(!trie[x][c])
            trie[x][c]=++cnt;
        x=trie[x][c];
    }
    ed[x][c]=true;
}

inline void ask(string s)
{
    int x=0;
    bool flag=1;
    for(register int i=0;i<len;i++)
    {
        int c=s[i]-'a';
        if(ch[x][c]==0)
        {
            flag=0;
            break;
        }
        x=ch[x][c];
    }
    if(flag)
        for(register int i=1;i<=n;i++)
            if(ed[x][i])
                printf("%d ",i);
    puts("");
}

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    for(register int i=1;i<=n;i++)
    {
        cin>>t;
        for(register int j=1;j<=t;j++)
        {
            cin>>s;
            insert(i,s);
        }
    }
    cin>>m;
    for(register int i=1;i<=m;i++)
    {
        cin>>s;
        ask(s);
    }
    return 0;
}

例四 P2922 Secret Message G

讲实话,题面有点糊。

我们来分析一下:

1.\(M\) 条信息,\(N\) 条暗号。
2.\(1\) 条信息是 \(1\) 条暗号的前缀,或\(1\) 条暗号是 \(1\) 条信息的前缀。

那么做法就很显然了。

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+5;
const int N=1e4+5;

int m,n,ch[MAXN][2],cnt;
int k,sum[MAXN],ed[MAXN];

inline void ins(int t,int p[])
{
    int x=0;
    for(register int i=1;i<=t;i++)
    {
        if(!ch[x][p[i]])
            ch[x][p[i]]=++cnt;
        x=ch[x][p[i]];
        sum[x]++;
    }
    ed[x]++;
    return;
}

inline int ask(int t,int p[])
{
    int x=0,ans=0;
    for(register int i=1;i<=t;i++)
    {
        if(ch[x][p[i]]==0)
            return ans;
        x=ch[x][p[i]];
        ans+=ed[x];
    }
    return ans-ed[x]+sum[x];
}

int p[MAXN];

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0); 
    cin>>m>>n;
    for(register int i=1;i<=m;i++)
    {
        cin>>k;
        for(register int i=1;i<=k;i++)
            cin>>p[i];
        ins(k,p);
    }
    for(register int i=1;i<=n;i++)
    {
        cin>>k;
        for(register int i=1;i<=k;i++)
            cin>>p[i];
        printf("%d\n",ask(k,p));
    }
    return 0;
}

例五 P2292 L语言

这题卡空间真恶心

这道题虽然正解是 AC 自动机,题解区都是 AC 自动机的解法,但作为一个专业整活人,我决定来尝试一下 \(\text{trie}\) 树的写法。

首先我们可以想到在 \(\text{trie}\) 树上做 \(\text{dfs}\),但很显然 这样的复杂度是 \(O(n·len)\),前 \(80\%\) 能轻松过,但更新的数据会被卡掉。

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;

const int N=1e6+5;
const int M=26;
bool en[N],vis[N<<1];
int ch[N][M],cnt,ans;
string ss;

inline void insert(string s)
{
    int x=0,len=s.size();
    for(register int i=0;i<len;i++)
    {
        int c=s[i]-'a'; 
        if(ch[x][c]==0)
            ch[x][c]=++cnt;
        x=ch[x][c];
    }
    en[x]=true;
    return;
}

inline void dfs(int pos, int cur)
{
    int c=ss[pos]-'a';
    cur=ch[cur][c];
    if(cur)
        dfs(pos+1,cur);
    if(en[cur]==true)
    {
        ans=max(pos+1,ans);
        if(vis[pos]==false)
            dfs(pos+1,0);
        vis[pos]=true;
    }
    return;
}

int n,m;

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(register int i=1;i<=n;i++)
    {
        string word;
        cin>>word;
        insert(word);
    }
    for(register int i=1;i<=m;i++)
    {
        memset(vis,false,sizeof(vis));
        ans=0;
        cin>>ss;
        dfs(0,0);
        cout<<ans<<endl;
    }
    return 0;
}

二.01字典树(01trie):

1.定义:

一颗只含 \(01\) 串的字典树,用于插入一些转化成二进制的整数。

2.实现:

insert函数:

inline void ins(int pos)
{
    int x=0;
    for(register int i=31;i>=0;i--)
    {
        int c=(pos>>i)&1;
        if(!ch[x][c])
            ch[x][c]=++cnt;
        x=ch[x][c];
    }
    return;
}

ask函数:

inline int ask(int pos)
{
    int ans=0;
    int x=0;
    for(register int i=31;i>=0;i--)
    {
        int c=(pos>>i)&1;
        if(ch[x][c^1])
        {
            ans+=(c^1)<<i;
            x=ch[x][c^1];
        }
        else
        {
            ans+=c<<i;
            x=ch[x][c];
        }
    }
    return ans;
}

典型例题:

例一 HDU4825 Xor Sum

思路:

1.\(n\) 个正整数转化成二进制并插入 \(\text{trie}\) 树;
2.询问时将一个整数 \(x\) 转化成二进制从根节点(高位)开始搜索,尽量取与 \(x\) 位上数值相反的数。

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=4e6+5;

int ch[MAXN][2];
int cnt;

inline void ins(int pos)
{
    int x=0;
    for(register int i=31;i>=0;i--)
    {
        int c=(pos>>i)&1;
        if(!ch[x][c])
            ch[x][c]=++cnt;
        x=ch[x][c];
    }
    return;
}

inline int ask(int pos)
{
    int ans=0;
    int x=0;
    for(register int i=31;i>=0;i--)
    {
        int c=(pos>>i)&1;
        if(ch[x][c^1])
        {
            ans+=(c^1)<<i;
            x=ch[x][c^1];
        }
        else
        {
            ans+=c<<i;
            x=ch[x][c];
        }
    }
    return ans;
}

inline void init()
{
    memset(ch,0,sizeof(ch));
    cnt=0;
    return;
}

int t;
int n,m;

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>t;
    for(register int i=1;i<=t;i++)
    {
        cin>>n>>m;
        init();
        for(register int j=1;j<=n;j++)
        {
            int x;
            cin>>x;
            ins(x);
        }
        printf("Case #%d:\n",i);
        for(register int j=1;j<=m;j++)
        {
            int x;
            cin>>x;
            printf("%d\n",ask(x));
        }
    }
    return 0;
}

例二 HDU5536 Chip Factory

Code

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e3+5;

int a[MAXN];
int ch[MAXN*35][2],cnt;
int num[MAXN*35];

inline void ins(int pos,int val)
{
    int x=0;
    for(register int i=31;i>=0;i--)
    {
        int c=(pos>>i)&1;
        if(!ch[x][c])
            ch[x][c]=++cnt;
        x=ch[x][c];
        num[x]+=val;
    }
    return;
}

inline int ask(int pos)
{
    int x=0,ans=0;
    for(register int i=31;i>=0;i--)
    {
        int c=(pos>>i)&1;
        if(ch[x][c^1] && num[ch[x][c^1]])
        {
            ans|=1<<i;
            x=ch[x][c^1];
        }
        else
            x=ch[x][c];
    }
    return ans;
}

inline void init()
{
    memset(ch,0,sizeof(ch));
    memset(num,0,sizeof(num));
    cnt=0;
    return;
}

int t;
int n;
int ans;

signed main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>t;
    while(t--)
    {
        cin>>n;
        ans=-1;
        init();
        for(register int i=1;i<=n;i++)
            cin>>a[i];
        for(register int i=1;i<=n;i++)
            ins(a[i],1);
        for(register int i=1;i<=n;i++)
        {
            ins(a[i],-1);
            for(register int j=i+1;j<=n;j++)
            {
                ins(a[j],-1);
                ans=max(ans,ask(a[i]+a[j]));
                ins(a[j],1);
            }
            ins(a[i],1);
        }
        printf("%lld\n",ans);
    }
    return 0;
}

例三 P4551 最长异或路径

思路:
1.定义 \(dp_a\) 表示从根节点到点 \(a\) 的路径上的边权异或和。
2. \(\text{dfs}\) 维护每个点 \(x\)\(dp_x\)
3.\(dp_x\) 依次询问最大的异或值,再插入 \(01\) \(\text{trie}\) 树。

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;

struct edge
{
    int to,nxt,len;
}e[MAXN<<1];

int head[MAXN],cnt;

inline void add(int x,int y,int z)
{
    e[++cnt].to=y;
    e[cnt].len=z;
    e[cnt].nxt=head[x];
    head[x]=cnt;
    return;
}

int n,tot,ch[MAXN*32][2];
int sum[MAXN];

inline void dfs(int x,int fa)
{
    for(register int i=head[x];i;i=e[i].nxt)
    {
        int y=e[i].to,z=e[i].len;
        if(y==fa)
            continue;
        sum[y]^=sum[x];
        sum[y]^=z;
        dfs(y,x);
    }
    return;
}

inline void ins(int pos)
{
    int x=0;
    for(register int i=31;i>=0;i--)
    {
        int c=(pos>>i)&1;
        if(!ch[x][c])
            ch[x][c]=++tot;
        x=ch[x][c];
    }
    return;
}

inline int ask(int pos)
{
    int ans=0;
    int x=0;
    for(register int i=31;i>=0;i--)
    {
        int c=(pos>>i)&1;
        if(ch[x][c^1])
        {
            ans+=1<<i;
            x=ch[x][c^1];
        }
        else
            x=ch[x][c];
    }
    return ans;
}

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    for(register int i=1;i<=n-1;i++)
    {
        int x,y,z;
        cin>>x>>y>>z;
        add(x,y,z);
        add(y,x,z);
    }
    dfs(1,0);
    for(register int i=1;i<=n;i++)
        ins(sum[i]);
    int ans=0;
    for(register int i=1;i<=n;i++)
        ans=max(ans,ask(sum[i]));
    printf("%d\n",ans);
    return 0;
}

例四 CF842D Vitya and Strange Lesson

考虑常用的操作异或集合的手段,我的第一反应是线性基,然而线性基只能插入,不能修改,再者她也不能查询 \(mex\) 这么奇怪的东西。

先不考虑异或,显然 \(mex\) 是有可二分性的,我们需要一个数据结构支持查询小于某个数的数是否都出现过,然后我们就可以二分了,我的第一反应是值域线段树,显然是可以做的,直接在线段树上二分就可以了,具体来说走到线段树的某个节点时,看一下左儿子存在的数是不是等于左儿子对应的值域大小,即看一下左儿子是不是满的。然而,线段树也不能维护异或这么奇怪的东西。

实际上我们可以选取和线段树结构相同的另一个数据结构(或者说她们就是同一个东西),\(01\) \(\text{trie}\)

我们也可以在 \(01\) \(\text{trie}\) 上二分查 \(mex\),并且走到 \(\text{trie}\) 上某个点时,如果异或的数这一位为 \(1\),意义就是交换 \(\text{trie}\) 的左右儿子,然后就可以用和线段树一样的方法做了。

有一个细节就是,在计算 \(\text{trie}\) 一个点的 \(\text{size}\) 时,注意一个数出现多次只计算一次。

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=3e5+5;

int ch[MAXN*32][2],cnt;
int siz[MAXN*32];
int tag;
int n,m;

inline void ins(int pos)
{
    int x=0;
    for(register int i=31;i>=0;i--)
    {
        int c=(pos>>i)&1;
        if(!ch[x][c])
            ch[x][c]=++cnt;
        x=ch[x][c];
    }
    return;
}

inline int ask(int pos)
{
    int x=0;
    int ans=0;
    for(register int i=31;i>=0;i--)
    {
        int c=(pos>>i)&1;
        if(siz[ch[x][c]]==(1<<(i+1))-1)
        {
            ans+=1<<i;
            x=ch[x][c^1];
        }
        else
            x=ch[x][c];
    }
    return ans;
}

inline void dfs(int x)
{
    siz[x]=1;
    for(register int i=0;i<=1;i++)
    {
        int y=ch[x][i];
        if(!y)
            continue;
        dfs(y);
        siz[x]+=siz[y];
    }
    return;
}

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(register int i=1;i<=n;i++)
    {
        int x;
        cin>>x;
        ins(x);
    }
    dfs(0);
    for(register int i=1;i<=m;i++)
    {
        int x;
        cin>>x;
        tag^=x;
        printf("%d\n",ask(tag));
    }
    return 0;
}

例五 CF817E Choosing The Commander

思路:
明显是一个 \(01 \text{trie}\) 处理插入删除操作的题目,只是多了一个查询操作。
对于插入删除操作,直接弄一个 \(\text{cnt}\) 数组来记录插入和删除操作,然后套板子即可,不再赘述。
然后考虑询问。
我们可以做出这样的操作,对于当前讨论的这一位

  • \(l=1\),则统计 \(k=0\) 子树上的答案,并向 \(k=1\) 的子树继续遍历,因为此时可以保证整个左子树都小于 \(l_i\)
  • \(l=0\),则向 \(k=1\) 的子树继续遍历。

然后这题还要注意的一点是,它的异或 \(\text{tag}\) 并不是累积的,所以每次要清空。

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=3e6+5;

int ch[MAXN][2],cnt;
int num[MAXN];
int n;

inline void ins(int pos,int val)
{
    int x=0;
    for(register int i=31;i>=0;i--)
    {
        int c=(pos>>i)&1;
        if(!ch[x][c])
            ch[x][c]=++cnt;
        x=ch[x][c];
        num[x]+=val;
    }
    return;
}

inline int ask(int pos,int tag)
{
    int x=0;
    int ans=0;
    for(register int i=31;i>=0;i--)
    {
        int t=(pos>>i)&1,c=(tag>>i)&1;
        if(t==1)
        {
            ans+=num[ch[x][c]];
            x=ch[x][c^1];
        }
        else
            x=ch[x][c];
        if(x==0)
            return ans;
    }
    return ans;
}

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    for(register int i=1;i<=n;i++)
    {
        int op,x;
        cin>>op>>x;
        if(op==1)
            ins(x,1);
        if(op==2)
            ins(x,-1);
        if(op==3)
        {
            int y;
            int tag=0;
            cin>>y;
            tag^=x;
            printf("%d\n",ask(y,tag));
        }
    }
    return 0;
}

三.可持久化01 trie:

可持久化01 trie模板:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e7+5;
int cnt[MAXN];
int ch[MAXN][2],s[MAXN];

inline void ins(int now,int ls,int x,int p)//now为当前版本节点,ls为上个版本对应节点(rt[i]与rt[i-1])
{
    if(p<0) return;
    int t=(x>>p)&1;//取出第p位
    ch[now][!t]=ch[ls][!t];//与上一个版本相连
    ch[now][t]=++cnt;//给新节点
    s[ch[now][t]]=s[ch[ls][t]]+1;//记录在前缀中出现次数
    ins(ch[now][t],ch[ls][t],x,p-1);
    return;
}

inline int ask(int l,int r,int x,int p)//l为左边界,r为右边界
{
    if(p<0) return 0;
    int t=(x>>p)&1;
    if(s[ch[r][!t]]>s[ch[l][!t]]) return (1<<p)+ask(ch[l][!t],ch[r][!t],x,p-1);
    else return ask(ch[l][t],ch[r][t],x,p-1);
}

典型例题

例一 P4735 最大异或和

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e7+5;

int n,m;
int cnt;
int ch[MAXN][2],s[MAXN];
int rt[MAXN];
int sum[MAXN];

inline void insert(int now,int ls,int x,int p)
{
    if(p<0) return;
    int t=(x>>p)&1;
    ch[now][!t]=ch[ls][!t];
    ch[now][t]=++cnt;
    s[ch[now][t]]=s[ch[ls][t]]+1;
    insert(ch[now][t],ch[ls][t],x,p-1);
    return;
}

inline int ask(int l,int r,int x,int p)
{
    if(p<0) return 0;
    int t=(x>>p)&1;
    if(s[ch[r][!t]]>s[ch[l][!t]]) return (1<<p)+ask(ch[l][!t],ch[r][!t],x,p-1);
    else return ask(ch[l][t],ch[r][t],x,p-1);
}

int main()
{
    scanf("%d%d",&n,&m);
    rt[0]=++cnt;
    insert(rt[0],0,0,23);
    for(register int i=1;i<=n;i++)
    {
        int op;
        scanf("%d",&op);
        sum[i]=sum[i-1]^op;
        rt[i]=++cnt;
        insert(rt[i],rt[i-1],sum[i],23);
    }
    for(register int i=1;i<=m;i++)
    {
        char op;
        int l,r,k;
        cin>>op;
        if(op=='A')
        {
            scanf("%d",&k);
            n++;
            sum[n]=sum[n-1]^k;
            rt[n]=++cnt;
            insert(rt[n],rt[n-1],sum[n],23);
        }
        else
        {
            scanf("%d%d%d",&l,&r,&k);
            l--,r--;
            printf("%d\n",ask(rt[l-1],rt[r],k^sum[n],23));
        }
    }
}

例二 P5283 异或粽子

给你\(n\)个数字,然后让你给出\(k\)各不同的区间\([l,r]\),然求区间最大异或和。
我们想,\(S_i=a_1\ xor\ a_2...a_i\),
所以可以将区间的异或转为两个值异或。
可以拿\(01\ trie\)来乱搞一下。
因为其实从高位置低位,若让其尽可能大,则要让两个异或值尽可能大。

点击查看代码
#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int MAXN=1e6+5;

int n,k,rt[MAXN],cnt;
int a[MAXN],ans;

struct node
{
    int ch[2],siz,id;
}t[MAXN*40];

inline void insert(int &now,int pre,int bit,int id,int val)
{
    now=++cnt; t[now]=t[pre];t[now].siz++;
    if(bit==-1){t[now].id=id;return;}
    if((val>>bit)&1) insert(t[now].ch[1],t[pre].ch[1],bit-1,id,val);
    else insert(t[now].ch[0],t[pre].ch[0],bit-1,id,val);
    return;
}

inline int ask(int u,int v,int bit,int val)
{
    if(bit==-1) return t[v].id;
    int d=(val>>bit)&1;
    if(t[t[v].ch[d^1]].siz-t[t[u].ch[d^1]].siz>0) return ask(t[u].ch[d^1],t[v].ch[d^1],bit-1,val);
    return ask(t[u].ch[d],t[v].ch[d],bit-1,val);
}

struct Node
{
    int l,r,x,id,val;
    Node(int ll=0,int rr=0,int xx=0)
    {
        l=ll;
        r=rr;
        x=xx;
        id=ask(rt[l-1],rt[r],31,a[x]);
        val=a[x]^a[id-1];
    }
};

inline bool operator<(const Node &a,const Node &b)
{
    return a.val<b.val;
}

priority_queue<Node>pq;

signed main()
{
    scanf("%lld%lld",&n,&k);
    for(register int i=1;i<=n;i++)
    {
        int op;
        scanf("%lld",&op);
        a[i]=a[i-1]^op;
    }
    for(register int i=1;i<=n;i++)
    {
        rt[i]=rt[i-1];
        insert(rt[i],rt[i],31,i,a[i-1]);
    }
    for(register int i=1;i<=n;i++)
        pq.push(Node(1,i,i));
    while(k--)
    {
        Node u=pq.top();
        pq.pop();
        ans+=u.val;
        if(u.l<u.id)
            pq.push(Node(u.l,u.id-1,u.x));
        if(u.id<u.r)
            pq.push(Node(u.id+1,u.r,u.x));
    }
    printf("%lld\n",ans);
    return 0;
}

三.KMP算法:

1.解决的问题:

在一个字典串中查询一个查询串的位置。

2.实现:

我们可以直接暴力匹配,但显然,时间复杂度会炸,于是就考虑 \(\text{KMP}\)

我们用如下方法匹配:

image

image

我们找到一个前缀与一个后缀,即图中的阴影部分。
且我们的工作原理就是 \(i\) 指针不动,\(j\) 指针向前跳,这样才能实现将查询串向后拖的效果。

关键点:

  • 一旦字符串失配,我们希望查询串能尽可能多的跳跃,而不是逐位移动;
  • 能够跳跃的最大距离由查询串的最长公共前后缀的长度决定。

对于上面的例子:

  • 失配前字串 \(\text{x y x y x y}\)
  • 前缀集合 \(\text{{x,xy,xyx,xyxy,xyxyx}}\)
  • 后缀集合 \(\text{{yxyxy,xyxy,yxy,xy,y}}\)
  • 最长公共前后缀 \(\text{xyxy}\)

然后我们可以用个 \(\text{PMT(Partial Match Table)}\) 表来理解一下:

image

那么如何求 \(\text{nxt}\) 数组呢?

  • \(\text{nxt}\) 数组也是字符串匹配的过程;
  • 将目标串查询串当作主串,并把查询串自己当作目标串,自己匹配自己;
  • \(i\) 应该从 \(1\) 开始。

下图展示了其工作过程:

image

image

image

\(\text{getnxt}\) 函数:

int nxt[MAXN];
int len1,len2;

inline void getnxt(string s)
{
    nxt[0]=-1;
    int i=0,j=-1;
    while(i<len2)
    {
        if(j==-1 || s2[i]==s2[j])
            i++,j++,nxt[i]=j;
        else
            j=nxt[j];
    }
    return;
}

\(\text{KMP}\) 函数:

inline void KMP(string s1,string s2)
{
    getnxt(s2);
    int i=0,j=0;
    while(i<len1)
    {
        if(j==len2-1 && s1[i]==s2[j])
        {
            printf("%d\n",i-j+1);
            j=nxt[j];
        }
        if(j==-1 || s1[i]==s2[j])
            i++,j++;
        else
            j=nxt[j];
    }
    return;
}

典型例题

例一 P3375 【模板】KMP字符串匹配

模板题。。。

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+5;

int nxt[MAXN];
int len1,len2;
string s1,s2;

inline void getnxt(string s2)
{
    nxt[0]=-1;
    int i=0,j=-1;
    while(i<len2)
    {
        if(j==-1 || s2[i]==s2[j])
            i++,j++,nxt[i]=j;
        else
            j=nxt[j];
    }
    return;
}

inline void KMP(string s1,string s2)
{
    getnxt(s2);
    int i=0,j=0;
    while(i<len1)
    {
        if(j==len2-1 && s1[i]==s2[j])
        {
            printf("%d\n",i-j+1);
            j=nxt[j];
        }
        if(j==-1 || s1[i]==s2[j])
            i++,j++;
        else
            j=nxt[j];
    }
    return;
}

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>s1>>s2;
    len1=s1.size(),len2=s2.size();
    KMP(s1,s2);
    for(register int i=1;i<=len2;i++)
        printf("%d ",nxt[i]);
    return 0;
}

例二 P4391

结论题。。。

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e7+5;

int nxt[MAXN];
int n;
int len;

inline void getnxt(string s)
{
    nxt[0]=-1;
    int i=0,j=-1;
    while(i<n)
    {
        if(j==-1 || s[i]==s[j])
            i++,j++,nxt[i]=j;
        else
            j=nxt[j];
    }
    return;
}

string s;

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    cin>>s;
    getnxt(s);
    printf("%d\n",n-nxt[n]);
    return 0;
}

例三 P1470 最长前缀 Longest Prefix

思路:
1.将给出的串当作字典串。
2.将集合中的串当作查询串做 \(\text{KMP}\) 匹配。
3.使用差分数组,标记第一个查询串出现的位置。
4.维护前缀和,从 \(1\) 开始枚举,第一次没有标记的位置 \(\text{break}\),输出答案。

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e7+5;
const int N=300;

int nxt[MAXN];
string B[N],s;
int cnt;
int dif[MAXN];
string s1;

inline void getnxt(string s2)
{
    nxt[0]=-1;
    int i=0,j=-1;
    int len2=s2.size();
    while(i<len2)
    {
        if(j==-1 || s2[i]==s2[j])
            i++,j++,nxt[i]=j;
        else
            j=nxt[j];
    }
    return;
}

inline void KMP(string s1,string s2)
{
    getnxt(s2);
    int i=0,j=0;
    int len1=s1.size(),len2=s2.size();
    while(i<len1)
    {
        if(j==len2-1 && s1[i]==s2[j])
        {
            int st=i-j+1;
            dif[st]+=1;
            dif[i+2]-=1;
            j=nxt[j];
        }
        if(j==-1 || s1[i]==s2[j])
            i++,j++;
        else
            j=nxt[j];
    }
    return;
}

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    while(cin>>s1)
    {
        if(s1==".")
            break;
        cnt++;
        B[cnt]=s1;
    }
    while(cin>>s1)
        s=s+s1;
    for(register int i=1;i<=cnt;i++)
        KMP(s,B[i]);
    int len=s.size();
    for(register int i=1;i<=len;i++)
        dif[i]+=dif[i-1];
    for(register int i=1;i<=len;i++)
        if(dif[i]==0)
        {
            printf("%d\n",i-1);
            return 0;
        }
    cout<<s.size();
    return 0;
}

例四 CF1200E Compress Words

这道题基本的思路很容易想到,我们可以将当前需要拼接的串先放到已拼接好的串前面,那么不难发现,要求的就是这玩意儿的最长公共前后缀,那么这就是一个 \(\text{KMP}\)\(\text{nxt}\) 数组的模板。

但很显然,这样做的时间复杂度为 \(O(n·len)\)\(10^5 \times 10^6\) 会炸,所以我们考虑到,求 \(\text{nxt}\) 数组时,若串特别长,且最长公共前后缀特别短时,那么就会做很多无用的操作,就会导致 \(\text{TLE}\),那么我们可以考虑只截取开头和结尾的一小部分,就可以解决时间复杂度超的问题。

这里还要注意一个点,就是在拼接时,带拼接的串可能比已拼接的串还要长,那么我们就需要从两串之间取更小的串截取。

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+5;

int tot,len,maxn,minn;
int ans,nxt[MAXN];
char s[MAXN],t[MAXN*5];

inline void getnxt(char x[])
{
    tot=len=strlen(x+1);
    minn=min(len,maxn);
    x[++tot]='#';
    for(register int i=1;i<=minn;i++)
        x[++tot]=t[maxn-(minn-i)];
    nxt[1]=nxt[0]=0;
    for(register int i=1,j=0;i<tot;i++)
    {
        j=nxt[i];
        while(j && x[i+1]!=x[j+1])
            j=nxt[j];
        if(x[i+1]==x[j+1])
            j++;
        nxt[i+1]=j;
    } 
    for(register int i=nxt[tot]+1;i<=len;i++)
        t[++maxn]=x[i];
    return;
}

int n;

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    for(register int i=1;i<=n;i++)
    {
        cin>>s+1;
        getnxt(s);
    }
    for(register int i=1;i<=maxn;i++)
        cout<<t[i];
    return 0;
}

例五 P3435 OKR-Periods of Words

不同于其他题的是,这道题要我们找的是最短公共前后缀,那么我们就只需要将最长的求出,然后不停的j=nxt[j]的跳就行了。

Code

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e6+5;

int n,cnt;
int nxt[MAXN];
char a[MAXN];

inline void getnxt()
{
    nxt[1]=0;
    int j=0;
    for(register int i=2;i<=n;i++)
    {
        while(j>0 && a[i]!=a[j+1])
            j=nxt[j];
        if(a[i]==a[j+1])
            j++;
        nxt[i]=j;
    }
    return;
}

inline void KMP()
{
    int j=0;
    for(register int i=1;i<=n;i++)
    {
        j=i;
        while(nxt[j])
            j=nxt[j];
        if(nxt[i]!=0)
            nxt[i]=j;
        cnt+=i-j;
    }
    return;
}

signed main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    cin>>a+1;
    getnxt();
    KMP();
    printf("%lld\n",cnt);
    return 0;
}

posted @ 2022-08-07 21:04  Code_AC  阅读(47)  评论(3编辑  收藏  举报