\(CF1535F\ \ String\ Distance\)

题意

给 \(n\) 个长度均为 \(len\) 的字符串 \(T_1,T_2,\dots T_n\),定义 \(f(a,b)\) 为将 \(a,b\) 排序后相等的最小排序次数,若无解则为 \(1337\)(这好像是个黑客用语)。求

\[\sum _{i=1}^{n}\sum _{j=i+1}^{n}f(T_i,T_j)
\]

其中

\[n\times len \leq 2\times 10^5
\]

思路分析

按照神xrlong的思路模拟即可,%%%%%%

神说,两个 trie 树,二维偏序肆意维护就过了,不过他终于在我的无数发问下补充了一些细节%%%%%

首先发现 \(f\) 是个诈骗函数,其值只可能为 \(1,2,1337\),因为规定没有相同的串,字符集相同时最多对两个串都排一次序就完成了,所以我们计算 \(f=1,2\) 和 \(1337\) 的数量即可。

总操作数显然为

\[kobe=\sum _{i=1}^{n}\sum _{j=i+1}^{n}1= \frac{n\times (n-1)}{2}
\]

  • 故事的开始前,先学习一下 \(nb\) 的二维数点:

类似于二维前缀和,只不过我们没有那么大的空间和时间开销,还是一样的思路:

\[ans(x_1,x_2,y_1,y_2)= sum(x_1-1,y_1-1)+sum(x_2,y_2)-sum(x_1-1,y_2)-sum(x_2,y_1-1)
\]

然后我们考虑记录每个 \(sum\) 的 \(qx,qy,c.\ c\) 是该 \(sum\) 是应加上还是减去的贡献。然后排序,树状数组扫描线维护即可。

q[++tot]={x1-1,y1-1,1};
q[++tot]={x2,y2,1};
q[++tot]={x1-1,y2,-1};
q[++tot]={x2,y1-1,-1};
sort(q+1,q+tot+1,[](Q a,Q b){return a.qx<b.qx;});
for(int i=1,j=1;i<=tot;i++){
for(;lty[j].x<=q[i].qx&&j<=n;j++)
add(lty[j].y);
ans+=query(q[i].qy)*q[i].c;
}

(建议先做一下P2163园丁的烦恼)


求\(sum(f=1337)\)

这个比较水,考虑对新读入的字符串桶排,然后塞到一个 \(trie\) 树里,那么从根节点到每个叶子的路径即为一个字符集,记录字符集的个数和一些乱七八糟的东西,然后肆意求出。

CODE
//求sum(f=1337)
int trk8[N][27],numk8;
int c[30],cnt[N];
vector<int>k8he;
int belong[N];//标记叶子节点属于的字符集
int NUM;//字符集个数
vector<int >order[N];//按照字符集顺序遍历
void jjdw(char str[],int id){
int p=0;for(int i=1;i<=26;++i) c[i]=0;
for(int i=0;i<len;++i) c[str[i]-'a'+1]++;//桶排序
for(int u=1;u<=26;u++){
if(c[u]){
for(int i=1;i<=c[u];++i){
if(!trk8[p][u]) trk8[p][u]=++numk8;
p=trk8[p][u];
}
}
}
if(!cnt[p]){
k8he.push_back(p);
belong[p]=++NUM;
}
order[belong[p]].push_back(id);
lty[id].bel=belong[p];
cnt[p]++;
}
int calc_k8(){//这个计算很好理解
int kk=0,k8sum=n;
for(int i=0;i<k8he.size()-1;++i){
int p=k8he[i];
k8sum-=cnt[p];
if(k8sum>0) kk+=cnt[p]*(k8sum);
}
return kk;
}

求\(sum(f=1)\)

这是本题精髓,考虑一个串,找出其中所有的极长不下降子串(这里不是最长,我被坑惨了),例如串 \(abcdcbdfa\) 中的极长串是 \(abcd,c,bdf,a\),对于每个子串,它会将把原串 \(s\) 分成一个前缀 \(s[1\dots l]\) 和后缀 \(s[r\dots len]\),如果存在一个串 \(t\),使得:

  • \(s\) 和 \(t\) 属于同一字符集,\(belong[s]==belong[t]\).

  • \(s,t\) 的前缀和后缀相等,\(s[1\dots l]=t[1\dots l],s[r\dots len]=t[r\dots len]\)

此时 \(f(t,s)=1\)(这很显然,只将 \(t[l+1,r-1]\) 排序即可)

我们发现 \(\sum _{i=1}^{n}\sum _{j=i+1}^{n}f(T_i,T_j)\) 实际就是两两字符串的 \(f\),所以枚举每个字符串的每个极长不下降子串子串,求前缀和后缀和它分别相等的串的个数即可。

我们考虑对所有字符串正反建两遍 \(trie\),对叶子节点进行编号 \(1\) 到 \(n\),那么每个串 \(s\) 就可以表示为一个二元组 \((x,y)\),其中 \(x\) 是 \(s\) 在正 \(trie\) 上结束的叶子节点编号,\(y\) 同理。

我们发现:前缀相同的串在 \(trie\) 树上叶子节点的编号是连续的,考虑将前缀在正 \(trie\) 上跑,得到一个区间 \([l_1,r_1]\),再将后缀在反 \(trie\) 上跑,得到另一个区间 \([l_2,r_2]\),然后在矩形 \([l_1,r_1,l_2,r_2]\) 二维数点即可。直观地,对于 \(9\) 个字符串建出正 \(trie\),如下图:

反 \(trie\) 同理。

CODE
void ins1(char str[],int id){//正trie
int p=0;
for(int i=0;i<len;++i){
int u=str[i]-'a'+1;
if(!trie1[p][u]) trie1[p][u]=++num1;
p=trie1[p][u];
}
flag1[p]=1;
ed1[id]=p;
}
void ins2(char str[],int id){//反trie
int p=0;
for(int i=len-1;i>=0;i--){
int u=str[i]-'a'+1;
if(!trie2[p][u]) trie2[p][u]=++num2;
p=trie2[p][u];
}
flag2[p]=1;
ed2[id]=p;
}
void dfs1(int x){
if(flag1[x]){
l1[x]=r1[x]=++Id1;return;//给叶子结点编号
}
for(int i=1;i<=26;++i){
int y=trie1[x][i];
if(y){
dfs1(y);
if(!l1[x]) l1[x]=l1[y];
l1[x]=min(l1[x],l1[y]);
r1[x]=max(r1[x],r1[y]);
//求该节点覆盖区间
}
}
}
void dfs2(int x){
if(flag2[x]){
l2[x]=r2[x]=++Id2;return;
}
for(int i=1;i<=26;++i){
int y=trie2[x][i];
if(y){
dfs2(y);
if(!l2[x]) l2[x]=l2[y];
l2[x]=min(l2[x],l2[y]);
r2[x]=max(r2[x],r2[y]);
}
}
}

其中还有不少坑人的sm细节:

  • 对于每个区间的计算,在线算的话单次 \(O(len)\),我们考虑离线所有要查询的 \(al,ar\),这里 \(s[al,ar]\) 是一个极长不下降子串,考虑每个串所有的 \(al\) 都在正 \(trie\) 的一条链上(\(ar\) 同理),然后就可以总复杂度 \(O(n\times len)\) 水过去了。

  • 我们要统计矩形 \([l_1,r_1,l_2,r_2]\) 内的点,这些点必须与当前串在同一字符集里,我们可以预处理出每个串所属的字符集,然后在数点的时候判断与当前字符集是否一致,也就是这样:

for(int i=1,j=1;i<=tot;i++){
for(;lty[j].x<=q[i].qx&&j<=n;j++){
if(lty[j].bel==id)
add(lty[j].y);
}
count1+=query(q[i].qy)*q[i].c;
}

但是这样显然是 \(O(n^2)\) 的复杂度,狂 \(T\) 不止 \(\dots\)

这是xrlong让我打他的trie树清空做法,我看着眼前二百多行代码很是不舍,然后就出来一个跑飞快的优化

一个 \(nb\) 的优化,考虑将点集以字符集编号为第一关键字,横坐标第二关键字排序,这样可以去掉无用的遍历,也就是:

sort(lty+1,lty+n+1,[](Node a,Node b){
if(a.bel==b.bel) return a.x<b.x;
return a.bel<b.bel;
});//玄学优化(不玄学)
for(int i=1;i<=n;i++){
if(lty[i].bel!=lty[i-1].bel){
ql[lty[i].bel]=i;
qr[lty[i-1].bel]=i-1;
//每段字符集对应的左右端点
}
}
qr[lty[n].bel]=n;
//......
for(int id=1;id<=NUM;++id){
//...
for(int i=1,j=ql[id];i<=tot;++i){
for(;lty[j].x<=q[i].qx&&j<=qr[id];++j) add(lty[j].y);
count1+=query(q[i].qy)*q[i].c;
}

这样就跑飞快了。

  • 考虑跑完一个字符集,进行清空,我们当然不能 \(O(n)\) 清空,那么:

你怎么加进去就怎么清空$\ \ \ \ \ \ \ \(——\)xrlong$

int C[N],topc;
void add(int x){
C[++topc]=x;
for(int i=x;i<=n;i+=lowbit(i)) tr[i]++;
}
void clear(){
for(int x=1;x<=topc;x++){
for(int i=C[x];i<=n;i+=lowbit(i)){
if(!tr[i]) break;//剪个小枝
tr[i]=0;
}
}
topc=0;tot=0;
}
  • 每个字符串前缀后缀都相同的串,显然也包括它自己,所以最后要减去算了多少遍自己,即极长不下降子串个数。

求 \(sum(f=2)\)

小学生问题。


差不多了,拜谢 \(xrlong\)

也就二百多行,我码量大一定是因为我每个函数都写了两遍,dfs1,dfs2,ins1,ins2...

\(AC\ \ Code\)

#include<bits/stdc++.h>
using namespace std;
#define k8 1337
#define int long long
#define read read()
#define pt puts("")
inline int read
{
int 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();
return f*x;
}
void write(int x)
{
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar(x%10+'0');
return;
}
const int N = 2e5+10;
int n;
int ans;
int len;
char s[N];
string T[N];
int trie1[N][27],trie2[N][27];
int l1[N],l2[N],r1[N],r2[N];//每个trie树上某节点覆盖的范围[l,r]
int num1,num2;
int Id1,Id2;//给叶子结点编号
int ed1[N],ed2[N];//记录每个串结束的节点
bool flag1[N],flag2[N];//标记为叶子节点
struct Node{
int x,y;
int bel;//所属字符集
}lty[N];
void ins1(char str[],int id){//正trie
int p=0;
for(int i=0;i<len;++i){
int u=str[i]-'a'+1;
if(!trie1[p][u]) trie1[p][u]=++num1;
p=trie1[p][u];
}
flag1[p]=1;
ed1[id]=p;
}
void ins2(char str[],int id){//反trie
int p=0;
for(int i=len-1;i>=0;i--){
int u=str[i]-'a'+1;
if(!trie2[p][u]) trie2[p][u]=++num2;
p=trie2[p][u];
}
flag2[p]=1;
ed2[id]=p;
}
void dfs1(int x){
if(flag1[x]){
l1[x]=r1[x]=++Id1;return;//给叶子结点编号
}
for(int i=1;i<=26;++i){
int y=trie1[x][i];
if(y){
dfs1(y);
if(!l1[x]) l1[x]=l1[y];
l1[x]=min(l1[x],l1[y]);
r1[x]=max(r1[x],r1[y]);
//求该节点覆盖区间
}
}
}
void dfs2(int x){
if(flag2[x]){
l2[x]=r2[x]=++Id2;return;
}
for(int i=1;i<=26;++i){
int y=trie2[x][i];
if(y){
dfs2(y);
if(!l2[x]) l2[x]=l2[y];
l2[x]=min(l2[x],l2[y]);
r2[x]=max(r2[x],r2[y]);
}
}
}
//求sum(f=1337)
int trk8[N][27],numk8;
int c[30],cnt[N];
vector<int>k8he;
int belong[N];//标记叶子节点属于的字符集
int NUM;//字符集个数
vector<int >order[N];//按照字符集顺序遍历
void jjdw(char str[],int id){
int p=0;for(int i=1;i<=26;++i) c[i]=0;
for(int i=0;i<len;++i) c[str[i]-'a'+1]++;//桶排序
for(int u=1;u<=26;u++){
if(c[u]){
for(int i=1;i<=c[u];++i){
if(!trk8[p][u]) trk8[p][u]=++numk8;
p=trk8[p][u];
}
}
}
if(!cnt[p]){
k8he.push_back(p);
belong[p]=++NUM;
}
order[belong[p]].push_back(id);
lty[id].bel=belong[p];
cnt[p]++;
}
int calc_k8(){//这个计算很好理解
int kk=0,k8sum=n;
for(int i=0;i<k8he.size()-1;++i){
int p=k8he[i];
k8sum-=cnt[p];
if(k8sum>0) kk+=cnt[p]*(k8sum);
}
return kk;
}
int kobe;
int L[N],R[N],top;
int x_1[N<<2],x_2[N<<2],y_1[N<<2],y_2[N<<2];
void search1(string a){//离线查询
int p=0;
for(int i=1,j=0;i<=top;++i){
for(;j<L[i];j++){
int u=a[j]-'a'+1;
p=trie1[p][u];
}
x_1[i]=l1[p],x_2[i]=r1[p];
}
}
void search2(string a){
int p=0;
for(int i=top,j=len-1;i;i--){
for(;j>R[i];j--){
int u=a[j]-'a'+1;
p=trie2[p][u];
}
y_1[i]=l2[p],y_2[i]=r2[p];
}
}
int tot;
struct Q{
int qx,qy,c;
}q[N<<3];
int tr[N];
#define lowbit(i) (i&(-i))
int C[N],topc;
void add(int x){
C[++topc]=x;
for(int i=x;i<=n;i+=lowbit(i)) tr[i]++;
}
int query(int x){
int res=0;
for(int i=x;i;i-=lowbit(i)){
res+=tr[i];
}
return res;
}
void clear(){
for(int x=1;x<=topc;x++){
for(int i=C[x];i<=n;i+=lowbit(i)){
if(!tr[i]) break;
tr[i]=0;
}
}
topc=0;tot=0;
}
int ql[N],qr[N];
signed main()
{
n=read;
kobe=(n-1)*n/2;//总操作数
for(int i=1;i<=n;++i){
scanf(" %s",s);
T[i]=string(s);
if(!len) len=strlen(s);
ins1(s,i);ins2(s,i);//正反建trie
jjdw(s,i);
}
dfs1(0);dfs2(0);//编号
int ck8=calc_k8();//1337的个数
ans=k8*ck8;
kobe-=ck8;
if(!kobe){
write(ans);return 0;
}
for(int i=1;i<=n;++i){
lty[i].x=l1[ed1[i]];
lty[i].y=l2[ed2[i]];
}//每个串对应的坐标
sort(lty+1,lty+n+1,[](Node a,Node b){
if(a.bel==b.bel) return a.x<b.x;
return a.bel<b.bel;
});//玄学优化(不玄学)
for(int i=1;i<=n;i++){
if(lty[i].bel!=lty[i-1].bel){
ql[lty[i].bel]=i;
qr[lty[i-1].bel]=i-1;
}
}
qr[lty[n].bel]=n;//处理每个字符集左右边界
int al,ar;
int count1=0;
string a;
int self=n;//减去重复
for(int id=1;id<=NUM;++id){
for(int i:order[id]){
al=0;a=T[i];
for(ar=1;ar<len;++ar){//统计每个极长子串
if(a[ar]<a[ar-1]){
L[++top]=al,R[top]=ar-1;//离线下来一遍求,否则会T
al=ar;++self;
}
}
ar--;
L[++top]=al,R[top]=ar;
search1(a);search2(a);
for(int i=1;i<=top;++i){
int x1=x_1[i],x2=x_2[i],y1=y_1[i],y2=y_2[i];
q[++tot]={x1-1,y1-1,1};
q[++tot]={x2,y2,1};
q[++tot]={x1-1,y2,-1};
q[++tot]={x2,y1-1,-1};
}//离线二维数点
top=0;
}
sort(q+1,q+tot+1,[](Q a,Q b){return a.qx<b.qx;});
for(int i=1,j=ql[id];i<=tot;++i){
for(;lty[j].x<=q[i].qx&&j<=qr[id];++j) add(lty[j].y);
count1+=query(q[i].qy)*q[i].c;
}
clear();
}
count1-=self;
ans+=count1;
kobe-=count1;
ans+=kobe*2;
write(ans);
return 0;
}

唉,我是 \(fw\),感觉这个题真是我现在码力的极限了...

CF1535F String Distance的更多相关文章

  1. Codeforces CF#628 Education 8 C. Bear and String Distance

    C. Bear and String Distance time limit per test 1 second memory limit per test 256 megabytes input s ...

  2. CF 628C --- Bear and String Distance --- 简单贪心

    CF 628C 题目大意:给定一个长度为n(n < 10^5)的只含小写字母的字符串,以及一个数d,定义字符的dis--dis(ch1, ch2)为两个字符之差, 两个串的dis为各个位置上字符 ...

  3. Educational Codeforces Round 8 C. Bear and String Distance 贪心

    C. Bear and String Distance 题目连接: http://www.codeforces.com/contest/628/problem/C Description Limak ...

  4. codeforces 628C C. Bear and String Distance

    C. Bear and String Distance time limit per test 1 second memory limit per test 256 megabytes input s ...

  5. String Distance and Transform Process

    http://acm.hdu.edu.cn/showproblem.php?pid=1516 Problem Description String Distance is a non-negative ...

  6. hdu 1516 String Distance and Transform Process

    Problem DescriptionString Distance is a non-negative integer that measures the distance between two ...

  7. GO语言的开源库

    Indexes and search engines These sites provide indexes and search engines for Go packages: godoc.org ...

  8. 西南大学校园GIS平台

    系统架构是B/S,开发语言是C#.silverlight,开发平台是.NET,数据库为sqlserver,这是我读研究生时候自己做的作品,以自己的母校为地图,进行GIS相关的功能分析,核心的模块有:空 ...

  9. 根据地图上的两个点各自的x,y坐标,计算出2点之间的直线距离。显示为公里、米

    /** * calc_map_distance() , 根据地图上的两个点各自的x,y坐标,计算出2点之间的直线距离 * @param array $point_1 第1个点的x,y坐标 array( ...

  10. [简单]docx4j常用方法小结

    http://53873039oycg.iteye.com/blog/2194479?utm_source=tuicool&utm_medium=referral —————————————— ...

随机推荐

  1. PV的回收策略、访问策略和状态

    PersistentVolume(PV)的回收策略.访问策略和状态是Kubernetes存储管理中的重要概念. 回收策略 Retain:当PV的回收策略设置为Retain时,即使对应的Persiste ...

  2. Spring Boot命令指定环境启动jar包

    原文地址:Spring Boot命令指定环境启动jar包 - Stars-One的杂货小窝 记下通过命令行的方式去改变spring boot项目中的环境配置信息 命令 项目中有以下配置 applica ...

  3. 什么叫运行时的Java程序?

    Java程序的运行包含编写.编译和运行三个主要步骤. 1.在编写阶段: 开发人员在Java开发环境中输入程序代码,形成后缀名为.java的Java源文件. 2.在编译阶段: 使用Java编译器对源文件 ...

  4. Access注入-偏移注入

    Access注入-偏移注入 1.偏移注入使用场景及方法 一.偏移注入使用场景及方法 偏移注入使用的场景 1)在sql注入时遇到无法查询数据库字段名时,比如系统自带的数据权限不够无法访问系统自带库 2) ...

  5. windows下删除文件时提示“操作无法完成 因为文件已在。。。”解决方案

    解决方案:利用"资源监视器",如上图: 具体步骤: win+r,输入perfmon打开资源监视器 或者 右键--Windows 7任务栏--启动任务管理器--性能--资源监视器-- ...

  6. socket编程流程

    字节序转换(hton) #include <netinet/in.h> unsigned long int htonl(unsigned long int hostlong); unsig ...

  7. Python实现端口扫描

    实验环境 攻击主机IP:172.18.53.145 目标主机IP:172.18.53.28 脚本编写 思路:使用TCP协议遍历连接目标的所有端口,如果连接成功说明该端口开放,为了提升效率,使用多线程执 ...

  8. 记录--前端路由 hash 与 history 差异

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 简单介绍 Vue Router Vue Router 是 Vue.js 官方的路由插件,它和 Vue.js 是深度集成的,适合用于构建单页 ...

  9. JSON格式化 动态生成表格 表格转置 行列转换 Excel导出

    先看效果 初始化: JSON格式化 : 生成表格-方式1 : 生成表格-方式2 : Excel导出 需要行求和.列求和功能的查看 JSON格式化 动态生成表格 表格转置 行列转换 Excel导出 行求 ...

  10. 【直播回顾】OpenHarmony知识赋能六期第一课—OpenHarmony智能家居项目介绍

    6月16日晚上19点,知识赋能第六期第一节课 <OpenHarmony智能家居项目介绍> ,在OpenHarmony开发者成长计划社群内成功举行. 本次直播是"OpenHarmo ...