In mathematics you don't understand things, you just get used to them.

标签 suffix structures 下的文章


date: '2023-10-09'
title: 'Solution -「BZOJ 4231」回忆树'


Desc.

Link.

树 $T=(V,E)$, 询问 $Q=\{q_i\}$,每次询问:

  • $u~v~s$, 问从 $u$ 到 $v$ 的简单路径上的字符拼接起来字符串中, $s$ 出现了多少次.

Sol.

真毒瘤... 😅

$s$ 对询问 $q_i$ 的贡献可以分为三种:

  • 在 $\lang u,lca(u,v)\rang$ 上出现;
  • 在 $\lang lca(u,v),v\rang$ 上出现;
  • 跨过 $lca(u, v)$.

其中第三种贡献可能的情况不超过 $\mathcal O(2|s|)$ 种, 拉出来跑哈希或者 KMP 即可.

对于剩下两种的情况, 我们以模式串的正反串建立 ACAM, 然后对原树 DFS, 那么从根节点到当前节点的树链上组成的字符串即为文本串.

代码太难打了, 打了一半就跑路了.

using pii = pair<int, int>;
using vvp = vector<vector<pii>>;
const int SZ = 3e5;
typedef struct AhoCorasickAutomaton {
    int tot = 1, nxt[SZ + 5][26], fail[SZ + 5], dfn[SZ + 5], out[SZ + 5], num;
    vvi grp;
    void insert(const bsi& s, int& pos) {
        int u = 0;
        for (int i=0;i<(int)s.length();++i) {
            if (!nxt[u][s[i]-'a']) nxt[u][s[i]-'a'] = tot++;
            u = nxt[u][s[i]-'a'];
        }
        pos = u;
    }
    void build() {
        queue<int> que;
        for (int v : nxt[0]) if (v) que.push(v);
        while (!que.empty()) {
            int u = que.front();
            for (int i=0;i<26;++i) if (nxt[u][i]) fail[nxt[u][i]] = nxt[fail[u]][i], que.push(nxt[u][i]);
                else nxt[u][i] = nxt[fail[u]][i];
        }
        grp = vvi(tot);
        for (int i=1;i<tot;++i) grp[fail[i]].pb(i);
        dfs(0);
    }
    void dfs(int u) {
        dfn[u] = num++;
        for (int v:grp[u]) dfs(v);
        out[u] = num;
    }
    int bit[SZ + 5];
    void upd(int p, int d) {
        for (p=dfn[p]+1;p<=tot;p+=p&-p) bit[p] += d;
    }
    int Ask(int p) {
        int res = 0;
        for (p=dfn[p];p;p-=p&-p) res += bit[p];
        return res;
    }
    int Ask(int l, int r) { return Ask(r)-Ask(l); }
} ACAM;
ACAM acam[2];
const int N = 1e5;
int n, q, fa[23][N + 5], ht, dep[N + 5], to[N + 5];
vvp grp;
void dfs(int u, int Fu) {
    fa[0][u] = Fu; dep[u] = dep[Fu] + 1;
    for (int i=1;i<=ht;++i) fa[i][u] = fa[i-1][fa[i-1][u]];
    for (const auto& [v, ch] : grp[u]) if (v != Fu) dfs(v, u), to[v] = ch-'a';
}
int getLca(int u, int v) {
    if (dep[u] < dep[v]) swap(u, v);
    for (int i=ht;i>=0;--i) if (dep[fa[i][u]] >= dep[v]) u = fa[i][u];
    if (u == v) return u;
    for (int i=ht;i>=0;--i) if (fa[i][u] != fa[i][v]) u = fa[i][u], v = fa[i][v];
    return fa[0][u];
}
struct Query {
    int u, v, lca, pos[2];
};
int jump(int u, int d) {
    for (int i=0;i<=ht;++i) if (d&(1<<i)) u = fa[i][u];
    return u;
}
bsi extract(int u, int v, int lca, int len) {
    bsi res, tmp;
    u = jump(u, dep[u]-dep[lca]-len);
    v = jump(v, dep[v]-dep[lca]-len);
    while (u != lca) res.pb(to[u]), u = fa[0][u];
    while (v != lca) tmp.pb(to[v]), v = fa[0][v];
    reverse(allu(tmp));
    return res.append(tmp);
}
int solve_passing(const bsi& text, const bsi& pat) {
    static const ull BASE = 1331;
    static ull pw[SZ + 5], h[SZ + 5];
    static void* __tmp = ([]() {
        pw[0] = 1;
        for (int i=1;i<SZ+5;++i) pw[i] = pw[i-1]*BASE;
        return pw;
    })();
    auto get_hash = [&](int l, int r) { return h[r]-h[l-1]*pw[r-l+1]; };
    int m = text.length(), k = pat.length();
    memset(h, 0, m*8);
    ull H = 0;
    for (int x:pat) H = H*BASE+x;
    for (int i=0;i<m;++i) h[i+1] = h[i]*BASE+text[i];
    int res = 0;
    for (int i=1;i<=m-k+1;++i) res += get_hash(i, i+k-1) == H;
    return res;
}
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    cin >> n >> q; ht = ceil(log2(n));
    grp = vvp(n);
    for (int i=1,u,v;i<n;++i) {
        char c; cin >> u >> v >> c; u--; v--;
        grp[u].eb(v, int(c-'a')); grp[v].eb(u, int(c-'a'));
    }
    dfs(0, n);
    vector<Query> queries(q);
    vector<vvp> mnt(2, vvp(n));
    for (auto& [u, v, lca, pos] : queries) {
        static int qid = 0;
        string _let;
        cin >> u >> v >> _let; u--; v--;
        int len = _let.length();
        bsi let;
        for (int i=0;i<len;++i) let.pb(_let[i]-'a');
        lca = getLca(u, v);
        acam[0].insert(let, pos[0]); reverse(allu(let)); acam[1].insert(let, pos[1]);
        if (lca != u && lca != v) {
            bsi s = extract(u, v, lca, len);
            cout << solve_passing(s, let) << "\n";
        } else {
            if (dep[u]-dep[lca] >= len) mnt[0][u].eb(qid, 1), mnt[0][jump(u, dep[u]-dep[lca]-len+1)].eb(qid, -1);
            if (dep[v]-dep[lca] >= len) mnt[1][v].eb(qid, 1), mnt[1][jump(v, dep[v]-dep[lca]-len+1)].eb(qid, -1);
        }
    }
    acam[0].build(), acam[1].build();
    static int ans[N + 5];
    auto dfs2 = [&](auto self, int u, int Fu, vi cur) {
        for (int i:{0,1}) acam[i].upd(cur[i], 1);
        for (const auto& [v, ignore] : grp[u]) {
        }
    };
    dfs2(dfs2, 0, n, {0, 0});
    for (int i=0;i<q;++i) cout << ans[i] << "\n";
}


date: '2023-10-09'
title: 'Solution -「CF 710F」String Set Queries'


Desc.

Link.

维护一个字符串集合,支持三种操作:

  1. 加字符串;
  2. 删字符串;
  3. 查询集合中的所有字符串在给出的模板串中出现的次数.

强制在线.

Sol.

アメリカ民謡研究会非常🐂🍺. 开始理解 AC 自动机了...

题面容易让人想到根号分治, 然而实际上本题可以进行二进制拆分. 这样我们一共维护了 $\mathcal O(log_2 n)$ 个 AC 自动机, 查询的总复杂均摊应该比较明显是 $\mathcal O(m\log_2n)$, 而修改操作, 每个点只会被合并不超过 $\mathcal O(log_2n)$ 次, 因此也是对的.

const int N = 3e5;
int q, tot, totNode = 1, rt[N + 5], nxt[N + 5][26], cntEnd[N + 5], fail[N + 5], cntIns[N + 5], sum[N + 5];
bool isChild[N + 5][26];
int merge(int u, int v) {
    if (!u || !v) return u+v;
    cntEnd[u] += cntEnd[v];
    for (int i=0;i<26;++i) {
        nxt[u][i] = merge(isChild[u][i]*nxt[u][i], isChild[v][i]*nxt[v][i]);
        if (nxt[u][i]) isChild[u][i] = 1;
    }
    return u;
}
void build(int st) {
    queue<int> que;
    fail[st] = st;
    for (int i=0;i<26;++i) if (isChild[st][i]) fail[nxt[st][i]] = st, que.push(nxt[st][i]);
        else nxt[st][i] = st;
    while (!que.empty()) {
        int u = que.front(); que.pop();
        sum[u] = sum[fail[u]]+cntEnd[u];
        for (int i=0;i<26;++i) if (isChild[u][i]) fail[nxt[u][i]] = nxt[fail[u]][i], que.push(nxt[u][i]);
            else nxt[u][i] = nxt[fail[u]][i];
    }
}
void ins(const string& s, int dlt) {
    cntIns[tot] = 1;
    int u = rt[tot] = totNode++;
    for (int i=0,iend=s.length();i<iend;++i) {
        if (!nxt[u][s[i]-'a']) nxt[u][s[i]-'a'] = totNode++;
        isChild[u][s[i]-'a'] = 1;
        u = nxt[u][s[i]-'a'];
    }
    cntEnd[u] += dlt;
    while (tot >= 1 && cntIns[tot] == cntIns[tot-1]) {
        rt[tot-1] = merge(rt[tot], rt[tot-1]);
        cntIns[tot-1] *= 2;
        tot--;
    }
    build(rt[tot++]);
}
int Ask(int st, const string& s) {
    int res = 0, u = st;
    for (int i=0,iend=s.size();i<iend;++i) u = nxt[u][s[i]-'a'], res += sum[u];
    return res;
}
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    cin >> q;
    while (q--) {
        int opt; string s; cin >> opt >> s;
        if (opt == 1) ins(s, 1);
        else if (opt == 2) ins(s, -1);
        else {
            int ans = 0;
            for (int i=0;i<tot;++i) ans += Ask(rt[i], s);
            cout << ans << "\n";
            cout.flush();
        }
    }
}

学了五年 KMP

拜谢 oi-wiki。字符串下标从 $0$ 开始,$s_{[l, r]}$ 闭区间子串,定义 $\displaystyle \pi_i = \max_{j \in [0, i]} j[s_{[0, j-1] = s_{[i-j+1, i]}}]$。规定 $\pi_0 = 0$。

朴素求 border 有一个小优化,可以把复杂度从 $O(n^3)$ 降成 $O(n^2)$,这里直接蒯份 oi-wiki 的代码:

// C++ Version
vector<int> prefix_function(string s) {
  int n = (int)s.length();
  vector<int> pi(n);
  for (int i = 1; i < n; i++)
    for (int j = pi[i - 1] + 1; j >= 0; j--)  // improved: j=i => j=pi[i-1]+1
      if (s.substr(0, j) == s.substr(i - j + 1, j)) {
        pi[i] = j;
        break;
      }
  return pi;
}

复杂度 $O\left(\left(\sum \pi_i-\pi_{i-1}\right)\times T\right) = O(\pi_{n-1}\times T) = O(n \times T)$,其中 $T$ 是字符串比较的复杂度。

注意到 border 每次至多增长 $1$(我们考虑从 $i-1$ 转移到 $i$),且增长的充要条件是 $s_i = s_{\pi_{i-1}}$。如果不能增长,我们就考虑一种「递归子问题」式的处理方法,即令 $S$ 为「所有出现在 $\pi_{i-1}$ 决策链中的下标集合」,也就是 $\pi$ 定义的那个 $\max$ 里面并且后面的 iversion brackets 为真的 $j$,并且记 $S$ 中的最大值为 $j^{(0)}$,次大值为 $j^{(1)}$,以此类推。又注意到 $s_{0, j-1} = s_{[i-j+1, i]} = s_{\pi_i-j, \pi_i-1}$($\overbrace{\underbrace{s_0 ~ s_1}_j ~ s_2 ~ s_3}^{\pi[i]} ~ \dots ~ \overbrace{s_{i-3} ~ s_{i-2} ~ \underbrace{s_{i-1} ~ s_{i}}_j}^{\pi[i]} ~ s_{i+1}$),所以得到 $j^{(n)} = \pi_{j^{(n-1)}-1}$。

for (int i=1,j; i<n; ++i) {
    for (j = brd[i-1]; j && s[i] != s[j];) {
        j = brd[j-1];
    }
    brd[i] = j+(s[i] == s[j]);
}

考虑复杂度,即证明我们只会跳 $O(n)$ 次的 border。我记得我两年前证过,好像用到了字符集大小远小于字符串规模的前提。不管了。我他吗不证了。

Trie,爱你

Trie 的空间复杂度实际上是需要均摊证明是 $O(n\times\Sigma)$ 的,其中 $n$ 是所有字符串的总长。

Aho-Corasick DFA

非常喜欢 ouuan 对一些事情的看法与做法,学习应该更贴切于本质而不是功利主义渲染下的作用与价值。

我们需要了解 DFA 是什么。DFA 是一个被描述为 $(\Sigma, Q, start, F, \delta)$ 的五元组:

  • 字符集($\Sigma$),该自动机只能输入这些字符。

  • 状态集合($Q$)。如果把一个DFA看成一张有向图,那么 DFA 中的状态就相当于图上的顶点。

  • 起始状态($start$),$start\in Q$,是一个特殊的状态。起始状态一般用 $s$ 表示,为了避免混淆,本文中使用 $start$。

  • 接受状态集合($F$),$F\subseteq Q$,是一堆特殊的状态。

  • 转移函数($\delta$),$\delta$ 是一个接受两个参数返回一个值的函数,其中第一个参数和返回值都是一个状态,第二个参数是字符集中的一个字符。如果把一个DFA看成一张有向图,那么 DFA 中的转移函数就相当于顶点间的边,而每条边上都有一个字符。
  • —— ouuan

    我们先来看 KMP 中的狭义 AC DFA 结构(事实上 KMP 本身即是一个 DFA,不过主体是一条链而已),其失配指针指向当前前缀的最长真后缀构成的字符串在哪个前缀。

    所以说 oi-wiki 上将对于失配指针的描述改成长度实际上是不太传统的,但也不影响 oi-wiki 写得非常好。

    AC DFA 已经更好理解了,无非是把 border 的定义从链上搬到了 trie 树上。

    字符串匹配

    我们需要考虑 border 的存在会为字符串匹配产生怎样的影响,传统的朴素做法是以模式串为主元,通过挪动模式串的头指针在文本串中的位置做匹配。当 $[j, m) \neq [i, i+m-j-1]$,也就是说 $[i-j, i)=[0,j)$,然后我舍弃 $[\delta(j-1)+1, j)$ 的匹配长度,再尝试 $j$ 和 $i$。换言之,我想要求到的是以 $i$ 结尾的最长匹配长度。这就是匹配。

    总结地说,我们使用失配指针优化匹配的过程,实际上是在找原串中每个前缀的最长后缀,满足这个后缀是模式串的一个前缀,这样看,失配指针的构造就非常自然了。

    转而看到 AC DFA 上,当你在 DFA 上的转移不能继续进行时,就会跳到失配指针上,使得你的决策变得更劣(因为不存在不劣的决策,可以理解为一个小贪心吧)。

    其他

    Q:为什么 AC DFA 上搞事情每个结点可能要继承其失配指针的信息?

    A:因为这说明当前结点存在一个极长真后缀是所以插入模式串中的一个。

    Part. 1 基本信息

    Part. 1-1 SAM 的构成。

    SAM 由两个东西构成,一个是一个 DAWG,还有一棵外向树,叫 parent tree。

    比如,给你一个字符串 $S=\sf abbabb$,它的 SAM 长成这样:

    Graph

    SAM 的 DAWG 大概可以理解为把字符串的所有后缀插入一个 Trie。当然如果你暴力插,点数为 $\mathcal{O}(n^2)$。

    不过显然我们可以把一些重复的结点 rua 在一起,点数差不多就成了 $\mathcal{O}(n)$,还要带个 $2$ 的常数。

    然后,$S$ 的子串都可以被 SAM 的 DAWG 上的某条路径表示,很显,对吧。

    DAWG 的边就是上图中的黑边,蓝边就是 parent tree 的树边。

    Part. 1-2 符号约定

    我们称 $S[l,r]$ 为字符串 $S$ 的 $[l,r]$ 的子串,相信大家都懂,下标从 $1$ 开始。

    我们称一个集合 $\text{endpos}(S[l,r])$ 为:对于字符串 $S$,$S[l,r]$ 在 $S$ 中出现的区间为 $[l_{1},r_{1}],\cdots,[l_{k},r_{k}]$,$\text{endpos}(S[l,r])=\{r_{1},\cdots,r_{k}\}$。

    对于两个子串 $x,y$,如果 $\text{endpos}(x)=\text{endpos}(y)$,则称 $x,y$ 在同一个 $\text{endpos}$ 等价类中。

    显然在 DAWG 上,从根节点到一个结点 $u$ 能组成的字符串的长度是不同的(不同的路径组成的字符串长度不一定等),我们称从根节点到一个结点 $u$ 能组成的最长的一个字符串的长度为 $\text{maxlen}(u)$,最短的称为 $\text{minlen}(u)$。

    Part. 2 需要知道的

    Part. 2-1 $\text{enspos}$ 的性质

    引理 1:对于两个 $S$ 的非空子串 $x,y$(不妨设 $|x|<|y|$),若 $\text{endpos}(x)=\text{endpos}(y)$,则 $x$ 为 $y$ 的一个真后缀。

    Obviously。

    引理 2:对于两个 $S$ 的非空子串 $x,y$(不妨设 $|x|\le|y|$),则

    $$ \begin{cases} \text{endpos}(x)\subseteq\text{endpos}(y),x\text{ is a suffix of } y, \\ \displaystyle\\ \text{endpos}(x)\cap\text{endpos}(y),\text{otherwise} \end{cases}

    $$ Obviously。 > **引理 3**:在一个 $\text{endpos}$ 等价类中,将类中的所有子串按长度非递增的顺序排序。每个子串都不会比它前一个子串长,与此同时每个子串也是它前一个子串的后缀。换句话说,对于同一等价类的任一两子串,较短者为较长者的后缀,且该等价类中的子串长度恰好覆盖整个区间 $[x,y]$。 由引理 1,可知这些子串不会等长(对于两个串,较短串为较长串的真后缀),后面 obviously。 说得简单一点,把一个等价类里面最长的那个字符串拿出来,其他所有串都是该串的 suffix。 ### Part. 2-2 后缀链接 Link 后缀链接是在原串的 DAWG 上的点连出的边。后缀链接的链接遵循某种规则,且最后构成的是一棵树,我们把后缀链接连出来的树称为 *Parent Tree*,在后文我们将讲解这种规则。 我们先来看看一个串 $S=\sf aababa$ 的 Parent Tree 长成副什么样子: ![61905.png](http://61.186.173.89:2019/2021/03/17/d015dd9f7e4ab.png) 图是从 command_block 那里拿来的,可以沟通删除。(已经修正了原图的勘误) 为了说明方便,我们以一个任意的等价类来说明,我们称这个等价类中长度最大的串为 $S_{\max}$,同理有 $S_{\min}$。 考虑在 $S_{\max}$ 前面加上一个字符,称为新串为 $S_{\text{new}}$,显然 $S_{\text{new}}$ 一定不和 $S_{\max}$ 在同一等价类里。 我们把上述 *加字符* 的操作看为分裂出儿子。有了这些,我们可以得出一些性质: 1. 设 Parent Tree 上的父亲为 $f$,儿子为 $u$,有 $\text{minlen}(u)=\text{maxlen}(f)+1$,显然。 2. 点数边数皆为 $\mathcal{O}(n)$,不考虑证明,背着。 3. 在 Parent Tree 上,一个结点的父亲一定是该结点的后缀,显然。 最后板子自己理解性背住吧,构造方法不想写了。 ```cpp struct SuffixAutomaton { int ID(char c) { return c-'a'; } struct node { int len,link,ch[26]; }nodes[3000010]; int n,cntot,las,siz[3000010]; char s[1000010]; vector<int> e[3000010]; void init(int len,char c[]) { n=len; for(int i=1;i<=n;++i) s[i]=c[i]; nodes[0].len=las=cntot=0; nodes[0].link=-1; } void extend(char c) { int cur=++cntot,one=las,ano=0; nodes[cur].len=nodes[las].len+1; while(~one&&!nodes[one].ch[ID(c)]) { nodes[one].ch[ID(c)]=cur; one=nodes[one].link; } if(one==-1) nodes[cur].link=0; else { ano=nodes[one].ch[ID(c)]; if(nodes[one].len+1==nodes[ano].len) nodes[cur].link=ano; else { int clone=++cntot; nodes[clone].len=nodes[one].len+1; nodes[clone].link=nodes[ano].link; memcpy(nodes[clone].ch,nodes[ano].ch,sizeof(int)*26); while(~one&&nodes[one].ch[ID(c)]==ano) { nodes[one].ch[ID(c)]=clone; one=nodes[one].link; } nodes[ano].link=nodes[cur].link=clone; } } siz[las=cur]=1; } void pre() { for(int i=1;i<=n;++i) extend(s[i]); for(int i=1;i<=cntot;++i) e[nodes[i].link].push_back(i); } void dfs(int x) { for(int i=0;i<e[x].size();++i) { int y=e[x][i]; dfs(y); siz[x]+=siz[y]; } if(siz[x]^1) ans=max(ans,siz[x]*nodes[x].len); } }SAM; ``` 代码中的 `siz` 是 $\text{endpos}$ 集合大小。

    Description

    Link.

    给定字符串,正整数集合 $A,B$,满足 $\forall u\in A,v\in B,1\le u,v\le n$。

    求 $\sum_{i\in A}\sum_{j\in B}\text{LCP}(A,B)$。

    Solution

    双倍经验是 SvT,只不过 SvT 这屑玩意儿卡常。

    先反转串,然后插入 SAM。众所周知

    把字符串反转后插入 SAM 后,两个原串的后缀在 parent tree 上的 $\text{LCA}$ 是这两个后缀的 $\text{LCP}$。

    然后你就可以搞两个 DP,分别跑 $A$ 子树大小,$B$ 子树大小。

    注意根节点需要特殊处理,因为我们是跨子树跑的 DP。不过 SvT 不需要,不知道是不是我的问题(应该就是)。

    #include<bits/stdc++.h>
    using namespace std;
    typedef long long LL;
    int n,m,dfn[500010],fa[500010][21],dep[500010],sjc,pos[200010],onepower[500010],anopower[500010],onef[500010],anof[500010];
    char s[200010];
    LL ans;
    struct SuffixAutomaton
    {
        #define ID(c) ((c)-'a')
        vector<int> e[500010];
        int n,cntot,las,len[500010],pre[500010],ch[500010][26];
        char s[200010];
        void init(int _n,char c[])
        {
            n=_n;
            for(int i=1;i<=n;++i)    s[i]=c[i];
            cntot=las=1;
        }
        void extend(char c)
        {
            int cur=++cntot,one=las,ano=0;
            len[cur]=len[las]+1,las=cur;
            while(one&&!ch[one][ID(c)])    ch[one][ID(c)]=cur,one=pre[one];
            if(one==0)    pre[cur]=1;
            else
            {
                ano=ch[one][ID(c)];
                if(len[one]+1==len[ano])    pre[cur]=ano;
                else
                {
                    int clone=++cntot;
                    len[clone]=len[one]+1;
                    pre[clone]=pre[ano];
                    memcpy(ch[clone],ch[ano],sizeof(ch[ano]));
                    while(one&&ch[one][ID(c)]==ano)    ch[one][ID(c)]=clone,one=pre[one];
                    pre[ano]=pre[cur]=clone;
                }
            }
        }
        void build()
        {
            for(int i=1;i<=n;++i)    extend(s[i]),pos[i]=las;
            for(int i=2;i<=cntot;++i)    e[pre[i]].emplace_back(i);
        }
    }SAM;
    void dfs(int x,int las)
    {
        dfn[x]=++sjc,fa[x][0]=las,dep[x]=dep[las]+1;
        for(int i=1;i^21;++i)    fa[x][i]=fa[fa[x][i-1]][i-1];
        for(int y : SAM.e[x])    dfs(y,x);
    }
    int LCA(int one,int ano)
    {
        if(dep[one]<dep[ano])    swap(one,ano);
        for(int i=20;~i;--i)    if(dep[fa[one][i]]>=dep[ano])    one=fa[one][i];
        if(one^ano)
        {
            for(int i=20;~i;--i)    if(fa[one][i]^fa[ano][i])    one=fa[one][i],ano=fa[ano][i];
            return fa[one][0];
        }
        else    return one;
    }
    bool cmp(int one,int ano){return dfn[one]<dfn[ano];}
    struct VirtualTree
    {
        vector<int> e[500010];
        vector<int> build(vector<int> poi)
        {
            sort(poi.begin(),poi.end(),cmp);
            poi.erase(unique(poi.begin(),poi.end()),poi.end());
            int len=poi.size();
            for(int i=1;i<len;++i)    poi.push_back(LCA(poi[i-1],poi[i]));
            sort(poi.begin(),poi.end(),cmp);
            poi.erase(unique(poi.begin(),poi.end()),poi.end());
            len=poi.size();
            for(int i=1;i<len;++i)    e[LCA(poi[i-1],poi[i])].push_back(poi[i]);
            return poi;
        }
    }VRT;
    template<class T>
    void read(T &hhh)
    {
        T x=0,f=1;
        char c=getchar();
        while(c<'0'||c>'9')
        {
            if(c=='-')    f=-1;
            c=getchar();
        }
        while(c>='0'&&c<='9')    x=(x<<3)+(x<<1)+(c^'0'),c=getchar();
        if(~f)    hhh=x;
        else    hhh=-x;
    }
    template<class T>
    void write(T x,char las='\n')
    {
        static int st[100],top=0;
        if(x<0)    putchar('-'),x=-x;
        do st[++top]=x%10,x/=10; while(x);
        while(top)    putchar(st[top--]^'0');
        putchar(las);
    }
    void exdfs(int x)
    {
        for(int y : VRT.e[x])    exdfs(y),onef[x]+=onef[y],anof[x]+=anof[y];
        for(int y : VRT.e[x])    ans+=(LL)SAM.len[x]*(onef[x]-onef[y])*anof[y];
        ans+=(LL)((onepower[x]&anopower[x])+onepower[x]*anof[x]+anopower[x]*onef[x])*SAM.len[x];
        onef[x]+=onepower[x],anof[x]+=anopower[x];
    }
    int main()
    {
        read(n),read(m);
        scanf("%s",s+1);
        reverse(s+1,s+n+1);
        SAM.init(n,s),SAM.build();
        dfs(1,0);
        while(m--)
        {
            int ones,anos,x;
            read(ones),read(anos);
            vector<int> key,tmp;
            while(ones--)    read(x),key.push_back(pos[n-x+1]),onepower[pos[n-x+1]]=1;
            while(anos--)    read(x),key.push_back(pos[n-x+1]),anopower[pos[n-x+1]]=1;
            tmp=VRT.build(key);
            ans=0,exdfs(tmp[0]);
            write(ans);
            for(int now : tmp)    onef[now]=anof[now]=0,VRT.e[now].clear(),onepower[now]=anopower[now]=0;
        }
        return 0;
    }