2022ICPC杭州区域赛K. Master of Both详解(字典树)

碎碎念和一些体会:

2023.9.2跟队友开了这场训练赛,当时纯英文题目看不太懂,认为是求 字符串与字符串之间的逆序对 + 字符串内的逆序对,结果复杂度怎么都优化不了。

后来赛后看了题解,读懂了题,甚至拿别人的题解交代码总是wa in test 3,没考虑到字符串被包含的情况,最后参考到了一篇大佬的题解幡然醒悟。2022ICPC杭州站_AC__dream的博客-CSDN博客

一个星期才补题是因为经历了9.3月赛的重创,经历了一段时间的自我激励和思想挣扎还是决定坚持下去。

目录

题目描述:

思路

problem1:如何统计造成字符串之间出现逆序的某两个字母对的个数。

problem2:为什么把字母映射到1~26而不是0~25呢

AC代码:


题目描述:

Hui-Bot教授是弦论和高级数据结构的大师,所以他提出了一个有趣的问题。给定一个仅由小写英文字母组成的n字符串序列,当按字典顺序比较字符串时,该序列中有多少个反转?

作为Hui-Bot最出色的学生,Putata和Budada分别掌握了高超的弦理论和先进的数据结构技能,他们轻松地一起解决了这个问题。然而,存在q个不同的平行宇宙,其中字母表中的字符并不按原始顺序出现。

形式上,每个宇宙中的字母都是一个字符串,它是26小写英文字母的排列,表示每个字符出现的顺序。

当且仅当以下条件之一成立时,字符串a按字典顺序小于字符串b:

  • a是b的前缀,但a≠b;
  • 在a和b不同的第一个位置,字符串a有一个字母在字母表中出现的时间早于b中对应的字母。

长度n的序列a中的反转次数是满足1≤i<j≤n、aj<ai的有序对(i,j)的数量。

请帮助各个宇宙的普塔塔和布达达解决问题。

输入1≤n≤5×1e5, 1≤q≤5×1e4

1≤|si|≤1e6  |si|的总和不大于1e6
 

简化:求按照每个宇宙字母表顺序,字符串与字符串之间的逆序对数。

比如
 

2 1
abd
abc
abcdefghijklmnopqrstuvwxyz

输出结果为1,因为出现位置abd<abc,但是比较字符串字典序abd>abc,满足逆序要求。

思路

直接对比两两字符串时间复杂度过高。我们可以利用字典树预处理出造成字符串之间出现字典序不同的某两个字母有序对的个数,记为rel[x][y](按插入顺序统计)。再对照宇宙字母表,当字典序x>y时累加当前产生rel[x][y]个逆序的贡献,最终得出结果。

problem1:如何统计造成字符串之间出现逆序的某两个字母对的个数。

我们可以边在字典树中插入边统计,如上面例子,

      a 

      b

d        (c)

我们在已经插入abd后,再插入abc,此时有共同前缀ab,在第三层出现不同并且d先于c插入,我们记录rel['d' - 'a' + 1]['c' - 'a' + 1] += cnt[son[p]['d' - 'a' + 1]]。p为字典树当前遍历到的结点,例子中在第二层位置,son[p]['d' - 'a' + 1]为第三层的d位置,cnt为之前插入的字符串具有abd公共前缀的个数。那么有序对(d,c)的个数增加cnt个。

某一层最多可能有26个字母,故对上面第三层d字母的操作进行25次,自己与自己本身不需要统计。

void insert(string s)
{
	int p = 0;
	for(int i = 0; i < s.size(); i ++)
	{
		int u = s[i] - 'a' + 1;
		if(!son[p][u]) son[p][u] = ++ idx;
		for(int j = 0; j <= 26; j ++)//当前插入字母与该层其他25个字母比较,有的话就记录
		{
			if(j == u) continue;
			rel[j][u] += cnt[son[p][j]]; 
		}
        //细节,按照此存储方式下面两行顺序不能交换。
        //因为idx从0结点开始,该结点不为任何字母,因此从0的子节点字母开始统计
		p = son[p][u];
		cnt[p] ++;
	}
}

problem2:为什么把字母映射到1~26而不是0~25呢

这个细节处理极其关键,我们必须注意,problem1只能解决当公共前缀相同,当前位置字母不同的情况,如abd  abc 中的(d,c)。而像abc ab这种位置靠前的字符串包含位置靠后的字符串的逆序情况,直接被跳过了。

这时可以在插入字符串s时对其进行一个处理,即在尾部加上一个字典序比26个字母都小的字符,这里我们为了方便观察暂且记为#。

对于abc#  ab#,c与#不同,直接可以归为problem1中的情况。所以for循环中处理时可以从0处理到26。

那么可能会考虑到一个问题,#的加入,会不会影响到非包含情况的结果,显然地非包含情况字母不同的位置更靠前,公共前缀在#之前的位置戛然而止,不会产生影响。而对于ab# abc# 的情况,第三个位置#与c的记录rel(#,c)个数不会影响结果,因为(#,x)的有序对永远无法有宇宙字母表满足字典序#>x。

处理部分和统计部分代码:

	while(n --)
	{
		string s; cin >> s;
		s += 'a' - 1;
		insert(s);
	}
	while(q --)
	{
		string s; cin >> s;
		int res = 0;
		for(int i = 0; i < 26; i ++)
		{
			res += rel[s[i] - 'a' + 1][0];//这里是后面字符串包含前面字符串的情况
			for(int j = 0; j < i; j ++)
			{
				res += rel[s[i] - 'a' + 1][s[j] - 'a' + 1];
			}
		}
		cout << res << '\n';
	}

AC代码:

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e6 + 10;
int son[N][30], idx, cnt[N], rel[30][30];
void insert(string s)
{
	int p = 0;
	for(int i = 0; i < s.size(); i ++)
	{
		int u = s[i] - 'a' + 1;
		if(!son[p][u]) son[p][u] = ++ idx;
		for(int j = 0; j <= 26; j ++)
		{
			if(j == u) continue;
			rel[j][u] += cnt[son[p][j]]; 
		}
		p = son[p][u];
		cnt[p] ++;
	}
}
/*
3 1
abcd
abc
ab
abcdefghijklmnopqrstuvwxyz
*/
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	int n, q; cin >> n >> q;
	while(n --)
	{
		string s; cin >> s;
		s += 'a' - 1;
		insert(s);
	}
	while(q --)
	{
		string s; cin >> s;
		int res = 0;
		for(int i = 0; i < 26; i ++)
		{
			res += rel[s[i] - 'a' + 1][0];
			for(int j = 0; j < i; j ++)
			{
				res += rel[s[i] - 'a' + 1][s[j] - 'a' + 1];
			}
		}
		cout << res << '\n';
	}
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值