题解 P2178 【[NOI2015]品酒大会】
题解
更好的阅读体验点击这里 ,请各位大佬到我的blog看看。
知识点:
后缀树,后缀数组,LCP,树形DP
分析:
这道题是我初学后缀结构的近期训练最后一道练习题,一开始只想到
这道题的题意是这样的:我们要求出有多少个子串的LCP大于等于每个
下面先简介两种方法:
方法一:后缀数组
第一问实际上是求有多少个区间
具体地,把
记录的时候要注意,因为我们合并的时候都是合并排名相邻的一些后缀,所以出现在一个集合中的所有后缀必定是连续的,所以我们只需要记录每个集合的左右端点就可以了。合并的时候也只用更改端点的值就好了。
对于第二问,我们在合并的时候可以把
时间复杂度:
方法二:后缀树
把后缀树建出来之后,我们直观地发现LCP就变成了LCA(两个点的LCA节点在后缀树上对应的节点的长度值就是LCP的长度)。而每个子树下的后缀节点的个数,就是那棵树的贡献。
如图:
对于每个蓝点的子树统计好后缀节点的个数,然后对其LCA(红点)进行统计,然后途中的“len”就是他们LCP的长度(假设从红点往上一直走了很多个节点后到达了整棵后缀树的根节点(黄点),之间的距离(边权之和)就是len)。
具体地,我们可以先建出后缀树。然后进行树形DP(理解为DFS也行)。怎么统计后缀节点的个数呢?这个相当简单,只需要对其子节点的子树信息进行累加就可以了,而我们知道怎么样的节点才可能是后缀节点。在建树的时候,直接加进去的一定是后缀点,通过一条原来很长完整的边(里面包含不止一个字符的)的分裂得到的点,一定不是后缀点,这个在建树的时候判断一下就好了。但是答案一定是这个吗?
当然不是!
回到那幅图,考虑现在在红点统计答案。这时候的单位贡献就是
但是这个式子求出来的最大复杂度是
考虑对式子进行展开,先对原式
第一步,我们得到
第二步,我们发现这个式子好像差了点什么,所以继续添加项,添加为
第三步,对第二步的式子进行化简,写成原式
然后这个式子就可以
对于第二问,我们要设法更新
不过千万注意,一定要弄懂其中的逻辑关系。这里计算出来的值(最小次小的乘积,最大次大的乘积)都只是用来更新
具体地,初始化一定要做好。把每个后缀节点的初始最大最小值设为
最后的求一次后缀和与后缀最大值,就从后面往前更新(因为题目说
时间复杂度:
小结:
对比两种不同的方法,其实本质上是相同的。方法一中合并
代码:
这道题我一共交了四次才AC。我用的是方法二(感觉更好写)。
可以看见其实细节很多。我觉得对于更新第二问的答案时是最为麻烦的。大家千万小心。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=300010;
typedef long long ll;
int n,m,tot,cnt,tp,rt,head[maxn<<1];//注意节点数会变成2n,所以存树的时候也一定要开这么大的空间
ll b[maxn];//这个是题目中给出的a[i]
const ll inf=1000000007;//事实证明INF要开到比1e9大才可以
char s[maxn];
struct node
{
int fa,p,len,siz;//父亲,当前位置,后缀长度,子树大小
ll _min1,_max1,_min2,_max2;//最小,最大,次小,次大
int trans[27];
}a[maxn<<1];
struct data
{
int nxt,to;
}edge[maxn<<1];//存树,边只用开2n就够了,因为只需要用单向边来存父亲到儿子就好了
struct result
{
ll sum,val;//后缀节点的对数,最大值
}ans[maxn];//记录答案
#define min(a,b) (a<b?a:b)//最小
#define max(a,b) (a>b?a:b)//最大
#define isdigit(c) (c>=48&&c<=57)//判断是否是数字,用于读入优化
#define swap(a,b) (a^=b^=a^=b)//交换
ll read()
{
ll x=0,f=1;
char c=getchar();
while (!isdigit(c))
f=c=='-'?-1:1,c=getchar();
while (isdigit(c))
x=(x<<1)+(x<<3)+(c^48),c=getchar();
return x*f;
}
int ins(int u,int p,int c)//建后缀树
{
int x=++tot,v;
a[x].len=a[u].len+1;
a[x].p=p;
a[x].siz=1;//这一定是后缀点
a[x]._min1=a[x]._max1=b[p];//注意最小最大赋值成a[i]
a[x]._min2=inf;//其余赋值成正负无穷
a[x]._max2=-inf;
for (;u&&!a[u].trans[c];a[u].trans[c]=x,u=a[u].fa);
if (!u)
a[x].fa=rt;
else
{
v=a[u].trans[c];
if (a[v].len==a[u].len+1)
a[x].fa=v;
else
{
int w=++tot;
a[w]=a[v];
a[w].len=a[u].len+1;
a[w]._max1=a[w]._max2=-inf;//分裂点没有权值,只能被别人更新,所以初始为正负无穷
a[w]._min1=a[w]._min2=inf;
a[w].siz=0;//分裂点一定不是后缀点
a[v].fa=a[x].fa=w;
for (;u&&a[u].trans[c]==v;a[u].trans[c]=w,u=a[u].fa);
}
}
return x;
}
void cmp(int u,int v)//用来更新最大最小次大次小值,u是父亲,v是儿子
{
int i,j;
ll p[10];
memset(p,0,sizeof(p));
p[1]=a[u]._max1;
p[2]=a[u]._max2;
p[3]=a[v]._max1;
p[4]=a[v]._max2;
for (i=1;i<=3;i++)//冒泡
for (j=1;j<=4-i;j++)
if (p[j]<p[j+1])
swap(p[j],p[j+1]);
a[u]._max1=p[1];
a[u]._max2=p[2];
memset(p,0,sizeof(p));
p[1]=a[u]._min1;
p[2]=a[u]._min2;
p[3]=a[v]._min1;
p[4]=a[v]._min2;
for (i=1;i<=3;i++)
for (j=1;j<=4-i;j++)
if (p[j]>p[j+1])
swap(p[j],p[j+1]);
a[u]._min1=p[1];
a[u]._min2=p[2];
}
void dfs(int u)//树形DP统计答案
{
int i,v;
ll tmp=0;//tmp是每个子树的节点个数的平方之和
for (i=head[u];i;i=edge[i].nxt)
{
v=edge[i].to;
dfs(v);
a[u].siz+=a[v].siz;//加上子树大小
tmp+=(a[v].siz+0ll)*(a[v].siz+0ll);//记录平方和
cmp(u,v);//统计最大最小值
}
if (a[u].siz<2)//构不成LCP就返回
return;
ans[a[u].len].sum+=((a[u].siz+0ll)*(a[u].siz+0ll)-tmp)/2ll;//记录有多少点对
ans[a[u].len].val=max(ans[a[u].len].val,max((a[u]._max1*a[u]._max2),(a[u]._min1*a[u]._min2)));//记录此处的乘积最大值
}
void add(int u,int v)//把树形结构建立起来
{
edge[++cnt]=(data){head[u],v};
head[u]=cnt;
}
int main()
{
int i,j;
scanf("%d%s",&n,s+1);
for (i=1;i<=n;i++)
b[i]=read();
rt=tp=++tot;
for (i=n;i>=1;i--)
{
tp=ins(tp,i,s[i]-'a'+1);
ans[i].val=-(inf*inf);//初始化每个节点,注意是要INF的平方才行(因为这个是乘积)
}
for (i=2;i<=tot;i++)
add(a[i].fa,i);//建边
a[rt]._min1=a[rt]._min2=inf;
a[rt]._max1=a[rt]._max2=-inf; //根节点要更新
dfs(rt);//树形DP
for (i=n-1;i>=0;i--)//统计后缀和、后缀最大值
{
if (ans[i].sum==0)//如果当前为0,那么全都为0
ans[i].val=0;
if (i<=n-2&&ans[i+1].sum!=0)//上一个不为0,可以用来更新
ans[i].val=max(ans[i].val,ans[i+1].val);
ans[i].sum+=ans[i+1].sum;//更新和
}
for (i=0;i<=n-1;i++)
printf("%lld %lld\n",ans[i].sum,ans[i].val);
return 0;
}
然后我又交到的UOJ上,发现被人Hack掉了。
in:
2
aa
1000000000 -1000000000
out:
1 -1000000000000000000
1 -1000000000000000000
这样的数据可以慢慢调,总能搞好的。
总结:
这是我人生第一道洛谷黑题啊!
虽然用了一天去写,但也感觉很值得。
思路很有趣,很多的实现细节也很有价值。