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

分类 笔记 下的文章

Description

Link.

给出一个数列,要求将其分成几段,使每段的和非严格递增,求最小的每段的和的平方和。

Solution

注意到 $a_{i}$ 为正,对于正数有这样的结论:

$$ a^{2}+b^{2}<(a+b)^{2} $$

正确性显然。这告诉我们,分的段越多越好。

现在我们来考虑如何使每段的和非严格递增。

考虑暴力 DP,设 $f_{i,j}$ 为前 $i$ 个数然后上一个 breakpoint 是 $j$​ 的最小平方和,转移:

$$ f_{i,j}=\min_{1\le k<j,pre_{i}-pre_{j}\ge pre_{j}-pre_{k}}\{f_{j,k}+(pre_{i}-pre_{j})^{2}\} $$

$pre$ 为前缀和。当然这个 DP 什么结论都没用,没有任何技术含量。

所以我们考虑优化。

你会发现这个 $k$ 不是每次都要重头枚举,他是单调的,于是你维护 $f_{j,k}$ 最小值即可,复杂度变成了 $\mathcal{O}(n^{2})$。

考虑上面的结论,你会发现我们只需要找打第一个可行的转移点 $k$ 即可。

意味着我们可以维护一个 $g_{i}$ 来表示:前 $i$ 个数,我们最大的一个合法 $k$。这个东西肯定是单调不降的。

转移即:

$$ g_{i}=\max_{pre_{i}-pre{j}\ge pre_{j}-pre_{g_{j}}}\{j\} $$

变形得:

$$ g_{i}=\max_{pre_{i}\ge pre_{j}\times2-pre_{g_{j}}}\{j\} $$

显然判定条件的右边有单调性($pre_{j}+(pre_{j}-pre_{g_{j}})$),左边也有单调性。

然后单调队列优化即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef __int128 _LL;
int n,a[40000010],taskID,f[40000010],que[40000010],head,tail;
LL pre[40000010];
_LL ans;
template<typename 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<typename T>
void write(T x,char las='\n')
{
    static int stk[100],top=0,f=0;
    if(x<0)    x=-x,f=1;
    do stk[++top]=x%10,x/=10; while(x);
    if(f)    putchar('-');
    while(top)    putchar(stk[top--]^'0');
    putchar(las);
}
void generateData(int taskID)
{
    if(taskID)    for(int i=1;i<=n;++i)    read(a[i]);
    else
    {
        static int p[100010],l[100010],r[100010],b[40000010];
        LL x,y,z;
        int m;
        read(x),read(y),read(z),read(b[1]),read(b[2]),read(m);
        const int MOD=(1<<30);
        for(int i=1;i<=m;++i)    read(p[i]),read(l[i]),read(r[i]);
        for(int i=3;i<=n;++i)    b[i]=(LL(x)*b[i-1]%MOD+LL(y)*b[i-2]%MOD+z)%MOD;
        for(int j=1;j<=m;++j)
        {
            for(int i=p[j-1]+1;i<=p[j];++i)    a[i]=(b[i]%(r[j]-l[j]+1))+l[j];
        }
    }
    for(int i=1;i<=n;++i)    pre[i]=pre[i-1]+a[i];
}
int main()
{
    read(n),read(taskID);
    generateData(taskID^1);
    for(int i=1;i<=n;++i)
    {
        while(head<tail&&pre[i]>=(pre[que[head+1]]<<1)-pre[f[que[head+1]]])    ++head;
        f[i]=que[head];
        while(head<tail&&(pre[i]<<1)-pre[f[i]]<=(pre[que[tail]]<<1)-pre[f[que[tail]]])    --tail;
        que[++tail]=i;
    }
    for(int i=n;i;i=f[i])    ans+=_LL(pre[i]-pre[f[i]])*(pre[i]-pre[f[i]]);
    write(ans);
    return 0;
}

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.

Given is a sequencen $A$ of $n$ intergers.

Construct a stricly increasing sequence $B$ of $n$ intergers that makes the sum of $|B_{i}-A_{i}|$ the smallest.

Solution

First, we make $a_{i}:=a_{i}-i$. Then we just make "strictly increasing" become "unstrictly increasing".

  1. for $a_{1}\le a_{2}\le\cdots\le a_{n}$:

    When $B$ is the same as $A$, it gets the minimum answer.

  2. for $a_{1}\ge a_{2}\ge\cdots\ge a_{n}$:

    When for each $i$, $B_{i}=A_{\lfloor\frac{n}{2}\rfloor}$, it gets the minimum answer.

Maybe we can divide $A$ into m parts.

Such like: $[l_{1},r_{1}],\cdots,[l_{m},r_{m}]$

that satisfies: for each $i$, sequence $A[l_{i},r_{i}]$ is unstrictly increasing/decreasing.

So we can get the answer to each interval.

Let's consider how to merge the answers.

When we're merging two intervals, we can just get the new median of the two intervals.


So things above are just bullshit.

Parallel Searching!

FUCK YOU.

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL INF=1e18;
int n;
LL a[1000010],b[1000010],ans;
void solve(LL l,LL r,int fr,int ba)
{
    if(l>=r||fr>ba)    return;
    LL mid=(l+r)>>1,tmp=0,mn=INF,pos=0;
    for(int i=fr;i<=ba;++i)    tmp+=abs(a[i]-mid-1);
    mn=tmp,pos=fr-1;
    for(int i=fr;i<=ba;++i)
    {
        tmp-=abs(a[i]-mid-1);
        tmp+=abs(a[i]-mid);
        if(tmp<mn)    mn=tmp,pos=i;
    }
    for(int i=fr;i<=pos;++i)    b[i]=mid;
    for(int i=pos+1;i<=ba;++i)    b[i]=mid+1;
    solve(l,mid,fr,pos),solve(mid+1,r,pos+1,ba);
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;++i)    scanf("%lld",&a[i]),a[i]-=i;
    solve(-INF,INF,1,n);
    for(int i=1;i<=n;++i)    ans+=abs(a[i]-b[i]);
    printf("%lld\n",ans);
    for(int i=1;i<=n;++i)    printf("%lld ",b[i]+i);
    return 0;
}

Description

Link.

Given is a tree. Every node initially has a color which is different from others'. (called $col_{x}$)

Def: $\text{dis}(x,y)$: the number of different colors on the simple path between x and y.

Supporting the following operations:

  1. RELEASE x: For each $y$ on $\text{path}(x,rt)$, make $col_{y}$=a new color which doesn't exist before.
  2. RECENTER x: Make $x$ become the new root after running RELEASE x.
  3. REQUEST x: Print: for each $y$ in $\text{subtree}(x)$, the sum of $\text{dis}(y,rt)$ divided the number of nodes in $\text{subtree}(x)$.

Solution

Link Cut Tree.

We can know that $\text{dis}(x,rt)$ is the number of Fake Edges on $\text{path}(x,rt)$ pluses one.

If we change a Real Edge $(u,v)$, where $dep_{u}<dep_{v}$ into a Fake Edge, for each node $x$ in $\text{subtree}(v)$, $\text{dis}(x,rt)$ will be decreased by $1$.

Vice versa.

In order to support such operation: decrease the subtree by $1$, we can fix the DFS order of the given tree.

However, we also need to change the root. How can we fix the DFS order of the given tree?

Let's have a discussion. Denote $x$ for the current operating node, $rt$ for the current root.

  1. if $rt=x$: modify the whole tree directly.
  2. if $rt$ isn't in $\text{subtree}(x)$: modify $\text{subtree}(x)$.
  3. if $rt$ is in $\text{subtree}(x)$: modify $\text{subtree}(x)$ and cancel the modfication of $\text{subtree}(rt)$
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
vector<int> e[100010];
int n,m,indfn[100010],outdfn[100010],sjc,fa[100010][20],dep[100010],rtnow=1;
#define check(x,f) ((indfn[x]<indfn[f])|(indfn[x]>outdfn[f])) // check whether x isn't in subtree(f)
void dfs(int x,int las)
{
    dep[x]=dep[las]+1,fa[x][0]=las,indfn[x]=++sjc;
    for(int i=1;i^20;++i)    fa[x][i]=fa[fa[x][i-1]][i-1];
    for(unsigned int i=0;i<e[x].size();++i)    if(e[x][i]^las)    dfs(e[x][i],x);
    outdfn[x]=sjc;
}
int getkth(int x,int k)
{
    if(k==0)    return x;
    else
    {
        for(int i=0;i^20;++i)    if((k>>i)&1)    x=fa[x][i];
        return x;
    }
}
struct LinearTree
{
    struct node
    {
        LL val,tag;
    }nodes[400010];
    void turn(int x,int l,int r)
    {
        if(nodes[x].tag)
        {
            int mid=(l+r)>>1;
            nodes[x<<1].val+=(mid-l+1)*nodes[x].tag;
            nodes[x<<1|1].val+=(r-mid)*nodes[x].tag;
            nodes[x<<1].tag+=nodes[x].tag;
            nodes[x<<1|1].tag+=nodes[x].tag;
            nodes[x].tag=0;
        }
    }
    void ins(int l,int r,int x,int fr,int ba,int val)
    {
        if(fr>ba||l>ba||r<fr)    return;
        if(l>=fr&&r<=ba)    nodes[x].val+=(r-l+1)*val,nodes[x].tag+=val;
        else
        {
            int mid=(l+r)>>1;
            turn(x,l,r);
            ins(l,mid,x<<1,fr,ba,val);
            ins(mid+1,r,x<<1|1,fr,ba,val);
            nodes[x].val=nodes[x<<1].val+nodes[x<<1|1].val;
        }
    }
    LL find(int l,int r,int x,int fr,int ba)
    {
        if(fr>ba||l>ba||r<fr)    return 0;
        if(l>=fr&&r<=ba)    return nodes[x].val;
        else
        {
            int mid=(l+r)>>1;
            turn(x,l,r);
            return find(l,mid,x<<1,fr,ba)+find(mid+1,r,x<<1|1,fr,ba);
        }
    }
    void modify(int x,LL val)
    {
        if(rtnow==x)    ins(1,n,1,1,n,val);
        else if(check(rtnow,x))    ins(1,n,1,indfn[x],outdfn[x],val);
        else
        {
            int tmp=getkth(rtnow,dep[rtnow]-dep[x]-1);
            ins(1,n,1,1,indfn[tmp]-1,val);
            ins(1,n,1,outdfn[tmp]+1,n,val);
        }
    }
}lrt;
struct LinkCutTree
{
    #define wis(x) (nodes[nodes[x].fa].ch[1]==(x))
    #define isrt(x) ((nodes[nodes[x].fa].ch[0]^(x))&&(nodes[nodes[x].fa].ch[1]^(x)))
    struct node
    {
        int ch[2],fa;
        bool rev;
    }nodes[100010];
    void turn_down(int x)
    {
        if(nodes[x].rev)
        {
            swap(nodes[x].ch[0],nodes[x].ch[1]);
            if(nodes[x].ch[0])    nodes[nodes[x].ch[0]].rev^=1;
            if(nodes[x].ch[1])    nodes[nodes[x].ch[1]].rev^=1;
            nodes[x].rev=0;
        }
    }
    void turn_whole(int x)
    {
        if(!isrt(x))    turn_whole(nodes[x].fa);
        turn_down(x);
    }
    void rotate(int x)
    {
        int f=nodes[x].fa,ff=nodes[f].fa,t=wis(x);
        nodes[x].fa=ff;
        if(!isrt(f))    nodes[ff].ch[wis(f)]=x;
        nodes[f].ch[t]=nodes[x].ch[t^1];
        nodes[nodes[x].ch[t^1]].fa=f;
        nodes[x].ch[t^1]=f;
        nodes[f].fa=x;
    }
    void splay(int x)
    {
        turn_whole(x);
        while(!isrt(x))
        {
            int f=nodes[x].fa;
            if(!isrt(f))    rotate((wis(x)^wis(f))?x:f);
            rotate(x);
        }
    }
    int findleft(int x)
    {
        turn_down(x);
        while(nodes[x].ch[0])    x=nodes[x].ch[0],turn_down(x);
        return x;
    }
    void access(int x)
    {
        for(int y=0;x;y=x,x=nodes[x].fa)
        {
            splay(x);
            if(nodes[x].ch[1])    lrt.modify(findleft(nodes[x].ch[1]),1);
            if(y)    lrt.modify(findleft(y),-1);
            nodes[x].ch[1]=y;
        }
    }
    void makert(int x){access(x),splay(x),nodes[x].rev^=1;}
}lct;
char opt[20];
int opx;
template<typename 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;
}
int main()
{
    read(n),read(m);
    for(int i=1,x,y;i<n;++i)
    {
        read(x),read(y);
        e[x].emplace_back(y);
        e[y].emplace_back(x);
    }
    dfs(1,0);
    for(int i=1;i<=n;++i)    lrt.ins(1,n,1,indfn[i],indfn[i],dep[i]),lct.nodes[i].fa=fa[i][0];
    while(m--)
    {
        scanf("%s",opt),read(opx);
        if(strcmp(opt,"RELEASE")==0)    lct.access(opx);
        else if(strcmp(opt,"RECENTER")==0)    lct.makert(opx),rtnow=opx;
        else
        {
            if(rtnow==opx)    printf("%.10f\n",double(lrt.find(1,n,1,1,n))/n);
            else if(check(rtnow,opx))    printf("%.10f\n",double(lrt.find(1,n,1,indfn[opx],outdfn[opx]))/(outdfn[opx]-indfn[opx]+1));
            else
            {
                int tmp=getkth(rtnow,dep[rtnow]-dep[opx]-1);
                printf("%.10f\n",double(lrt.find(1,n,1,1,indfn[tmp]-1)+lrt.find(1,n,1,outdfn[tmp]+1,n))/(indfn[tmp]+n-outdfn[tmp]-1));
            }
        }
    }
    return 0;
}

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;
}