视频讲解:BV1254y137Rn
A. Fair Playoff
题目大意
有
4
4
4 位选手参加比赛,第
i
i
i 位选手的水平为
s
i
(
1
≤
s
i
≤
100
)
s_i(1 \leq s_i \leq 100)
si(1≤si≤100) 且每位选手的水平不同。如果两位选手比赛,那么水平更高的选手会获胜。
比赛采用以下赛制:
- 第一位选手和第二位选手比赛
- 第三位选手和第四位选手比赛
- 上述两场比赛的胜者进行决赛
如果是水平最高的两位选手在决赛相遇,则称比赛是公平的。
请判断比赛是否公平。
题解
简单的模拟题,有多种方法可以实现:
- 直接判断决赛的两位选手的水平是否为最高与次高
- 判断 m a x ( s 1 , s 2 ) > m i n ( s 3 , s 4 ) max(s_1,s_2)>min(s_3,s_4) max(s1,s2)>min(s3,s4) 和 m a x ( s 3 , s 4 ) > m i n ( s 1 , s 2 ) max(s_3,s_4)>min(s_1,s_2) max(s3,s4)>min(s1,s2) 是否都成立
- 判断 m i n ( m a x ( a , b ) , m a x ( c , d ) ) ≥ m a x ( m i n ( a , b ) , m i n ( c , d ) ) min(max(a,b),max(c,d)) \geq max(min(a,b),min(c,d)) min(max(a,b),max(c,d))≥max(min(a,b),min(c,d)) 是否成立
参考代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
int main()
{
int T,i,s[10],b[10];
scanf("%d",&T);
while(T--)
{
scanf("%d%d%d%d",&s[1],&s[2],&s[3],&s[4]);
for(i=1;i<=4;i++)
b[i]=s[i];
sort(b+1,b+5);
if(max(s[1],s[2])+max(s[3],s[4])==b[3]+b[4])
printf("YES\n");
else
printf("NO\n");
}
}
B. Array Reodering
题目大意
给定包含
n
(
1
≤
n
≤
2000
)
n(1 \leq n \leq 2000)
n(1≤n≤2000) 个元素的数组
a
(
1
≤
a
i
≤
1
0
5
)
a(1 \leq a_i \leq 10^5)
a(1≤ai≤105) 。
对于一对索引
i
,
j
i,j
i,j ,若
1
≤
i
<
j
≤
n
1 \leq i < j \leq n
1≤i<j≤n 且
g
c
d
(
a
i
,
2
a
j
)
>
1
gcd(a_i,2a_j)>1
gcd(ai,2aj)>1 ,则称其为好的索引对。
现在你可以对数组进行重新排列,求最大的好索引对数量。
题解
如果
a
i
a_i
ai 是偶数,那么必定满足
g
c
d
(
a
i
,
2
a
j
)
>
1
gcd(a_i,2a_j)>1
gcd(ai,2aj)>1 ,因此在重新排列时,应该将偶数都排在前面,奇数排在后面。
对于奇数的
a
i
,
a
j
a_i,a_j
ai,aj ,
g
c
d
(
a
i
,
2
a
j
)
=
g
c
d
(
a
i
,
a
j
)
gcd(a_i,2a_j)=gcd(a_i,a_j)
gcd(ai,2aj)=gcd(ai,aj) ,可以直接用两重循环在
O
(
N
2
)
O(N^2)
O(N2) 复杂度内求解。
也可以对每个数分解质因数后,用容斥处理。由于分解质因数后
a
i
a_i
ai 的质因数个数最多为
5
5
5 个,因此复杂度可以优化到
O
(
N
∗
2
5
)
O(N*2^5)
O(N∗25) 。(其实不优化也完全没事,还多打了好多代码)
参考代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=100100;
int npri[MAXN],sum[MAXN],a[MAXN];
vector<int> fac[MAXN];
int main()
{
int i,j,k,T,n,limit,tmp,num1,cnt,x;
ll ans;
for(i=2;i<MAXN;i++)
{
if(npri[i])
continue;
fac[i].push_back(i);
for(j=i+i;j<MAXN;j+=i)
{
npri[j]=1;
fac[j].push_back(i);
}
}
scanf("%d",&T);
while(T--)
{
scanf("%d",&n);
cnt=0;
for(i=1;i<=n;i++)
{
scanf("%d",&x);
if(x%2)
a[++cnt]=x;
}
ans=1ll*(n-1+cnt)*(n-cnt)/2;
sort(a+1,a+cnt+1);
memset(sum,0,sizeof(sum));
for(i=1;i<=cnt;i++)
{
limit=1<<(fac[a[i]].size());
for(j=1;j<limit;j++)
{
tmp=1;
num1=0;
for(k=0;k<fac[a[i]].size();k++)
{
if((j>>k)&1)
{
num1++;
tmp*=fac[a[i]][k];
}
}
if(num1&1)
ans+=sum[tmp];
else
ans-=sum[tmp];
sum[tmp]++;
}
}
printf("%lld\n",ans);
}
}
C. Unstable String
题目大意
给定一个仅由 0,1 和 ? 组成的字符串
s
(
1
≤
∣
s
∣
≤
2
⋅
1
0
5
)
s(1 \leq |s| \leq 2 \cdot 10^5)
s(1≤∣s∣≤2⋅105) 。
如果一个字符串由 0 和 1 组成,且相邻字符不同,则称其为不稳定字符串。
如果一个字符串由 0,1 和 ? 组成,且可以通过替换 ? 为 0 或 1 (每个 ? 可以替换为不同的字符),则称其为漂亮字符串。
求字符串
s
s
s 有多少个漂亮的连续子串。
题解
如果子串
s
[
l
,
r
]
s[l,r]
s[l,r] 是漂亮的,那么
s
[
i
,
r
]
(
l
≤
i
≤
r
)
s[i,r](l \leq i \leq r)
s[i,r](l≤i≤r) 也是漂亮的。
因此可以记录上一次的非法位置,那么非法位置之后的子串,都是漂亮的。
由于不确定 ? 变成 0 还是 1 ,因此不妨设:
- b e f 0 bef0 bef0 表示无法满足奇数位为 1 偶数位为 0 的前一个位置
- b e f 1 bef1 bef1 表示无法满足奇数位为 0 偶数位为 1 的前一个位置
如果连续子串右端点为 r r r ,则
- 左端点 l ∈ [ b e f 0 + 1 , r ] l \in [bef0+1,r] l∈[bef0+1,r] 时,子串 s [ l , r ] s[l,r] s[l,r] 可以构成奇数位为 1 1 1 偶数为为 0 0 0 的漂亮串;
- 左端点 l ∈ [ b e f 1 + 1 , r ] l \in [bef1+1,r] l∈[bef1+1,r] 时,子串 s [ l , r ] s[l,r] s[l,r] 可以构成奇数位为 0 0 0 偶数为为 1 1 1 的漂亮串;
因此当
r
=
i
r=i
r=i 时,
l
∈
[
m
i
n
(
b
e
f
0
,
b
e
f
1
)
+
1
,
r
]
l \in [min(bef0,bef1)+1,r]
l∈[min(bef0,bef1)+1,r] 时,子串
s
[
l
,
r
]
s[l,r]
s[l,r] 是漂亮串。
实现时,可以枚举右端点
r
r
r ,不断更新
b
e
f
0
bef0
bef0 和
b
e
f
1
bef1
bef1 ,并统计贡献。
参考代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=200200;
char s[MAXN];
int main()
{
int T,i,len,bef0,bef1;
ll ans;
scanf("%d",&T);
while(T--)
{
scanf("%s",&s);
len=strlen(s);
bef0=bef1=-1;
ans=0;
for(i=0;i<len;i++)
{
if(s[i]=='?')
ans+=max(i-bef0,i-bef1);
else if((s[i]-'0')==i%2)
{
ans+=i-bef0;
bef1=i;
}
else
{
ans+=i-bef1;
bef0=i;
}
}
printf("%lld\n",ans);
}
}
D. Playoff Tournament
题目大意
有
2
k
(
1
≤
k
≤
18
)
2^k(1 \leq k \leq 18)
2k(1≤k≤18) 支队伍参加淘汰赛,他们共进行
2
k
−
1
2^k-1
2k−1 场比赛。
其中前
2
k
−
1
2^{k-1}
2k−1 场比赛,分别为
1
1
1 队与
2
2
2 队比赛,
3
3
3 队与
4
4
4 队比赛…
2
k
−
1
2^k-1
2k−1 队与
2
k
2^k
2k 队比赛。
之后的
2
k
−
2
2^{k-2}
2k−2 场比赛,分别为第
1
1
1 场的胜者与第
2
2
2 场比赛的胜者比赛,第
3
3
3 场的胜者与第
4
4
4 场比赛的胜者比赛…第
2
k
−
1
−
1
2^{k-1}-1
2k−1−1 场的胜者与第
2
k
−
1
2^{k-1}
2k−1 场比赛的胜者比赛。
以此类推,直到只剩下一支队伍。
给定一个长度为 2 k − 1 2^k-1 2k−1 的字符串,每个字符表示某一场比赛的结果:
- 若 s i = 0 s_i=0 si=0 ,则表示在第 i i i 场比赛中,编号更小的队伍获胜;
- 若 s i = 1 s_i=1 si=1 ,则表示在第 i i i 场比赛中,编号更大的队伍获胜;
- 若 s i = ? s_i=? si=? ,则表示在第 i i i 场比赛中,两只队伍都可能获胜;
有 q ( 1 ≤ q ≤ 2 ⋅ 1 0 5 ) q(1\leq q \leq 2 \cdot 10^5) q(1≤q≤2⋅105) 次对字符串的修改操作,每次操作如下:
- 输入 p ( 1 ≤ p ≤ 2 k − 1 ) p(1 \leq p \leq 2^k-1) p(1≤p≤2k−1) 和 c ( c ∈ { 0 , 1 , ? } ) c(c \in \{0,1,?\}) c(c∈{0,1,?}) ,将 s i s_i si 修改为 c c c 。
每次修改后,需要输出最终有多少只队伍可能获胜。
题解
不妨先将整个字符串
s
s
s 翻转,并对每场比赛重新编号,那么
s
1
s_1
s1 就代表总决赛,
s
2
,
s
3
s_2,s_3
s2,s3 代表四分之一决赛,
s
4
,
s
5
,
s
6
,
s
7
s_4,s_5,s_6,s_7
s4,s5,s6,s7 代表八分之一决赛…
以此类推,发现第
i
i
i 场比赛的选手,分别是第
2
i
2i
2i 场比赛的胜者和第
2
i
+
1
2i+1
2i+1 场比赛的胜者。这一结构和线段树十分相似,考虑采用类似线段树的方式维护。
设 s u m i sum_i sumi 表示第 i i i 场比赛的胜者有几种可能性。那么将子节点信息汇总到父节点的 p u s h U p pushUp pushUp 函数可以表示为:
- 若 s i = 0 s_i=0 si=0 ,则 s u m i = s u m 2 i + 1 sum_i=sum_{2i+1} sumi=sum2i+1 ;
- 若 s i = 1 s_i=1 si=1 ,则 s u m i = s u m 2 i sum_i=sum_{2i} sumi=sum2i ;
- 若 s i = ? s_i=? si=? ,则 s u m i = s u m 2 i + s u m 2 i + 1 sum_i=sum_{2i}+sum_{2i+1} sumi=sum2i+sum2i+1 ;
每次修改时,逐个用 p u s h U p pushUp pushUp 函数修改从第 p p p 场比赛到总决赛的 s u m sum sum 信息即可。
s u m 1 sum_1 sum1 即是最终可能获胜的队伍数。
参考代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=600600;
char s[MAXN];
int k,mx,sum[MAXN];
void pushUp(int i)
{
if(s[i]=='0')
sum[i]=sum[i<<1|1];
else if(s[i]=='1')
sum[i]=sum[i<<1];
else
sum[i]=sum[i<<1]+sum[i<<1|1];
}
void build(int i)
{
if(i>=mx)
{
sum[i]=1;
return;
}
build(i<<1);
build(i<<1|1);
pushUp(i);
}
void change(int i)
{
if(!i)
return;
pushUp(i);
change(i>>1);
}
int main()
{
int q,p;
char c;
scanf("%d",&k);
mx=(1<<k);
scanf("%s",s+1);
reverse(s+1,s+mx);
build(1);
scanf("%d",&q);
while(q--)
{
scanf("%d %c",&p,&c);
p=mx-p;
s[p]=c;
change(p);
printf("%d\n",sum[1]);
}
}
E. Gold Transfer
题目大意
给定一棵有根树,根节点编号为 0 0 0 。每个节点有 a i ( 1 ≤ a i ≤ 1 0 6 ) a_i(1 \leq a_i \leq 10^6) ai(1≤ai≤106) 吨黄金,每吨黄金价格为 c i ( 1 ≤ c i ≤ 1 0 6 ) c_i(1 \leq c_i \leq 10^6) ci(1≤ci≤106) 。
初始只有 0 0 0 号节点,接下来有 q ( 1 ≤ q ≤ 3 ⋅ 1 0 5 ) q(1 \leq q \leq 3 \cdot 10^5) q(1≤q≤3⋅105) 次操作,每次操作有以下两种类型:
- 1 p i a i c i 1 \; p_i \; a_i \; c_i 1piaici -添加节点 i i i ,其父亲为 p i p_i pi ,有 a i a_i ai 吨黄金,每吨黄金价格为 c i c_i ci ,保证 c i > c p i c_i>c_{p_i} ci>cpi ;
- 2 v i w i 2 \; v_i \; w_i 2viwi -在 v i v_i vi 到根节点的路径上的节点中,用最小的花费购买 w i w_i wi 吨黄金,如果无法买齐 w i w_i wi 吨黄金,则买下所有能购买的黄金。在每个节点购买黄金后,该节点的 a i a_i ai 会减少相应数值。
读入强制在线。
题解
因为
c
i
>
c
p
i
c_i>c_{p_i}
ci>cpi ,因此从
v
i
v_i
vi 到根节点的路径上,黄金是越来越便宜的。
得到贪心策略:
- 对于 v i v_i vi ,寻找 v i v_i vi 到根节点的路径上,最靠近根节点且还有黄金余量的节点 s t st st ,从 s t st st 开始向 v i v_i vi 逐个购买黄金,直到买够 w i w_i wi 吨黄金或到达 v i v_i vi 节点。
由于每次修改只会向树中添加节点,因此可以用倍增的方式维护树。设
p
i
,
j
p_{i,j}
pi,j 表示
i
i
i 节点向上
2
j
2^j
2j 步的节点。
基于
p
i
,
j
p_{i,j}
pi,j ,可以在
O
(
l
o
g
(
q
)
)
O(log(q))
O(log(q)) 的复杂度内找到任意节点向上若干步的祖先节点编号。
寻找
s
t
st
st 时,可以采用二分结合倍增寻找,实现时两者可以结合在一起,减少一个
l
o
g
log
log 的复杂度。
总复杂度 O ( q ∗ l o g ( q ) ) O(q*log(q)) O(q∗log(q)) 。
参考代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=300300;
const int MAXM=20;
int p[MAXN][MAXM];
ll a[MAXN],c[MAXN];
int findfa(int x,int dep)
{
for(int i=0;dep>=(1<<i);i++)
{
if(x==-1)
return -1;
if(dep&(1<<i))
x=p[x][i];
}
return x;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int q,x,y,z,op,i,dis,rt,now;
ll tmp,ans,w,num;
memset(p,-1,sizeof(p));
cin>>q>>a[0]>>c[0];
for(x=1;x<=q;x++)
{
cin>>op;
if(op==1)
{
cin>>p[x][0]>>a[x]>>c[x];
for(i=1;i<MAXM;i++)
{
if(p[x][i-1]==-1)
p[x][i]=-1;
else
p[x][i]=p[p[x][i-1]][i-1];
}
}
else
{
cin>>y>>w;
dis=0;
now=y;
for(i=MAXM-1;i>=0;i--)
{
if(p[now][i]!=-1&&a[p[now][i]])
{
dis+=1<<i;
now=p[now][i];
}
}
num=0;
ans=0;
for(i=dis;i>=0&&(w-num);i--)
{
now=findfa(y,i);
tmp=min(w-num,a[now]);
ans+=tmp*c[now];
num+=tmp;
a[now]-=tmp;
}
if(w-num)
{
tmp=min(w-num,a[y]);
ans+=tmp*c[y];
num+=tmp;
a[y]-=tmp;
}
cout<<num<<" "<<ans<<endl;
}
}
}
F. String Distance
题目大意
对于两个字符串 a a a 和 b b b ,你可以对其进行任意次下述操作:
- 选择字符串 a a a 或 b b b 的任意连续子串并排序
设 f ( a , b ) f(a,b) f(a,b) 表示使得 a = b a=b a=b 的最小操作次数。如果不存在 a = b a=b a=b 的方案,则 f ( a , b ) = 1337 f(a,b)=1337 f(a,b)=1337 。
给定 n ( 1 ≤ n ≤ 2 ⋅ 1 0 5 ) n(1 \leq n \leq 2 \cdot 10^5) n(1≤n≤2⋅105) 个不同的字符串 s 1 , s 2 , . . . , s n ( ∣ s 1 ∣ = ∣ s 2 ∣ = . . . = ∣ s n ∣ , n ⋅ ∣ s 1 ∣ ≤ 2 ⋅ 1 0 5 ) s_1,s_2,...,s_n(|s_1|=|s_2|=...=|s_n|,n \cdot |s_1| \leq 2 \cdot 10^5) s1,s2,...,sn(∣s1∣=∣s2∣=...=∣sn∣,n⋅∣s1∣≤2⋅105) ,求 ∑ i = 1 n ∑ j = i + 1 n f ( s i , s j ) \sum_{i=1}^n{\sum_{j=i+1}^n{f(s_i,s_j)}} ∑i=1n∑j=i+1nf(si,sj) 。
题解
参考了AliShahali1382的代码。
设 m m m 为字符串长度, S [ l , r ] S[l,r] S[l,r] 为字符串 S S S 的从 S l S_l Sl 到 S r S_r Sr 的子串。
对于两个字符串 A , B ( A < B ) A,B(A < B) A,B(A<B) ,有以下几种情况:
- A A A 与 B B B 的组成字符不一样, f ( A , B ) = 1337 f(A,B)=1337 f(A,B)=1337 ;
- 需要对 A A A 和 B B B 都排序后才能得到 A = B A=B A=B , f ( A , B ) = 2 f(A,B)=2 f(A,B)=2 ;
- 对 B B B 的某一子串排序后得到 A = B A=B A=B , f ( A , B ) = 1 f(A,B)=1 f(A,B)=1 ;
对于第一种情况,直接对字符串按不同字符组成分类即可。
对于第二种和第三种情况,不妨先假设都是第二种情况计算贡献,再减去第三种情况的数量。
考虑第三种情况出现的条件。
设
i
n
c
i
inc_i
inci 表示满足字符串
A
A
A 的子串
A
[
i
,
i
n
c
i
]
A[i,inc_i]
A[i,inci] 是连续不下降子串的最大值。
如果字符串
B
B
B 满足
B
[
0
,
i
−
1
]
=
A
[
0
,
i
−
1
]
B[0,i-1]=A[0,i-1]
B[0,i−1]=A[0,i−1] 且
B
[
i
n
c
i
+
1
,
m
−
1
]
=
A
[
i
n
c
i
+
1
,
m
−
1
]
B[inc_i+1,m-1]=A[inc_i+1,m-1]
B[inci+1,m−1]=A[inci+1,m−1] ,则将子串
B
[
i
,
i
n
c
i
]
B[i,inc_i]
B[i,inci] 排序后即可得到
A
=
B
A=B
A=B ,即
f
(
A
,
B
)
=
1
f(A,B)=1
f(A,B)=1 。
因此可以先对同一组成的字符串排序,枚举字符串 A A A ,再从大到小枚举最长公共前缀 l c p lcp lcp ,寻找满足 B [ 0 , l c p − 1 ] = A [ 0 , l c p − 1 ] B[0,lcp-1]=A[0,lcp-1] B[0,lcp−1]=A[0,lcp−1] 且 B [ l c p ] ≠ A [ l c p ] B[lcp] \neq A[lcp] B[lcp]=A[lcp] 且 B [ i n c l c p + 1 , m − 1 ] = A [ i n c l c p + 1 , m − 1 ] B[inc_{lcp}+1,m-1]=A[inc_{lcp}+1,m-1] B[inclcp+1,m−1]=A[inclcp+1,m−1] 的字符串 B B B 的数量。
- 对于 B [ 0 , l c p − 1 ] = A [ 0 , l c p − 1 ] B[0,lcp-1]=A[0,lcp-1] B[0,lcp−1]=A[0,lcp−1] 且 B [ l c p ] ≠ A [ l c p ] B[lcp] \neq A[lcp] B[lcp]=A[lcp] 的条件,由于字符串是排序后的,因此可以基于二分确定字符串 B B B 的范围 B ∈ { s l , s l + 1 , . . . , s r } B \in \{s_l,s_{l+1},...,s_r \} B∈{sl,sl+1,...,sr} ,其中 r r r 通过二分查找确定, l l l 由上一次的 r r r 转移得到。
- 对于 B [ i n c l c p + 1 , m − 1 ] = A [ i n c l c p + 1 , m − 1 ] B[inc_{lcp}+1,m-1]=A[inc_{lcp}+1,m-1] B[inclcp+1,m−1]=A[inclcp+1,m−1] 的条件。由于是求相同后缀的数量,不妨将字符串都倒序存入字典树,并在字典树上的每个节点都创建一个向量,存储字符串的编号。那么求指定后缀的字符串,可以直接在该后缀对应的节点上的向量内二分查找编号在 [ l , r ] [l,r] [l,r] 范围内数量。
为了快速找到后缀 A [ i n c i + 1 , m − 1 ] A[inc_i+1,m-1] A[inci+1,m−1] 对应的节点,可以用倍增的方式,在字典树上查找后缀 A A A 所在的叶节点的向上 i n c i + 1 inc_i+1 inci+1 步的祖先。
总复杂度 O ( N M l o g N ) O(NMlogN) O(NMlogN) , N N N 为字符串数量, M M M 为字符串长度。
参考代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=200200;
const int MAXM=26;
const int MAXG=18;
int n,m;
int p[MAXN][MAXG],tr[MAXN][MAXM],dest[MAXN],inc[MAXN];
string s[MAXN];
vector<int> vec[MAXN];
map<vector<int>,vector<string> > mp;
map<vector<int>,vector<string> >::iterator it;
int getLcp(string &s1,string &s2)
{
int ret=0;
while(ret<m&&s1[ret]==s2[ret])
ret++;
return ret;
}
int getPar(int x,int dis)
{
for(int i=0;(1<<i)<=dis;i++)
{
if(dis&(1<<i))
x=p[x][i];
}
return x;
}
ll cal()
{
if(m==1)
return 1ll*n*(n-1)/2;
sort(s,s+n);
ll ret=0;
int cnt=0,i,j,c,l,r,lef,rig,mid;
memset(tr[0],0,sizeof(tr[0]));
vec[0].clear();
for(i=0;i<n;i++)//倒序存入字典树
{
vec[0].push_back(i);
int v=0;
for(j=m-1;j>=0;j--)
{
c=s[i][j]-'a';
if(!tr[v][c])
{
tr[v][c]=++cnt;
p[cnt][0]=v;
vec[cnt].clear();
memset(tr[cnt],0,sizeof(tr[cnt]));
}
v=tr[v][c];
vec[v].push_back(i);
}
dest[i]=v;
}
for(i=1;i<=cnt;i++)
sort(vec[i].begin(),vec[i].end());
for(j=1;j<MAXG;j++)
for(i=1;i<=cnt;i++)
p[i][j]=p[p[i][j-1]][j-1];
for(i=0;i<n;i++)
{
inc[m-1]=m-1;
for(j=m-2;j>=0;j--)
{
if(s[i][j]<=s[i][j+1])
inc[j]=inc[j+1];
else
inc[j]=j;
}
l=i+1;
for(int lcp=m-1;lcp>=0;lcp--)
{
lef=l;rig=n-1;
while(lef<=rig)
{
mid=(lef+rig)>>1;
int tmp=getLcp(s[i],s[mid]);
if(tmp<lcp)
rig=mid-1;
else
lef=mid+1;
}
r=lef;
if(l==r)
continue;
int p=getPar(dest[i],inc[lcp]+1);
ret+=lower_bound(vec[p].begin(),vec[p].end(),r)
-lower_bound(vec[p].begin(),vec[p].end(),l);
l=r;
}
}
return ret;
}
int main()
{
ios::sync_with_stdio(false);
int i,j;
ll ans;
cin>>n;
for(i=0;i<n;i++)
{
cin>>s[i];
vector<int> tmp(26,0);
for(j=0;j<s[i].length();j++)
tmp[s[i][j]-'a']++;
mp[tmp].push_back(s[i]);
}
m=s[0].length();
ans=1337ll*n*(n-1)/2;
for(it=mp.begin();it!=mp.end();++it)
{
n=0;
for(i=0;i<it->second.size();i++)
s[n++]=(it->second)[i];
ans+=(2ll-1337ll)*n*(n-1)/2;
ans-=cal();
}
cout<<ans<<endl;
}