CF558E A Simple Task
正文
题目大意
给定一个长度不超过
的,由小写字母构成的字符串、和不超过 次操作。每次操作可以选择一个区间 ,将这个区间的子串里的所有字母按照字典序非严格递增或非严格递减地排序。
现给定这个长度为字符串,和 次操作,每次指明区间和单调变化的类型。请输出最后得到的字符串。
这道题最暴力的思路显然是每次操作都快排一次,时间复杂度是
之后,有两位同学提供了两种虽然暴力但可行的思路:
Sol 1: Chtholly Tree
一种前卫的数据结构。其本质是将序列操作改为三元组操作。
还记得在四边形不等式学习过的决策优化的技巧吗?由于最优决策是非严格单调递增的,我们可以用决策三元组
这道题也是一样。由于字符串的长度远远长于字符表的长度,在随机数据下,整个字符串一定会出现重复的字母。这个时候我们就可以用三元组str[l]
到str[r]
的字符均为ch
。
由于每次要修改一个区间std::set
来解决这个问题。
这样一来,我们就不得不提到一个数据结构:珂朵莉树(Chtholly Tree)。它有两种基本的操作,分别是split
和assign
。
首先定义一个三元组节点:
struct triplet{
int l, r, ch;
triplet(int l, int r = -1, int ch = 0): l(l), r(r), ch(ch){}
bool operator < (const triplet& other) const
{
return l < other.l;
}
};
这个节点使得在set
里,所有的区间都是按从左到右的顺序排好序的。
接下来,和分块一样,对于每次修改的一个区间,我们维护区间里面的每一个块,而对边缘的块采用暴力处理方法。想要实现这个操作,就要定义一个split
操作。
#define pointer std::set<triplet>:: iterator
std::set<triplet> triplets;
inline pointer split(int pos)
{
pointer it = triplets.lower_bound(triplet(pos));
if(it != triplets.end() && it->l == pos)
return it;
-- it;
int l = it->l, r = it->r, ch = it->ch;
triplets.erase(it);
triplets.insert(triplet(l, pos - 1, ch));
return triplets.insert(triplet(pos, r, ch)).first;
}//找到pos所属的块,并将其分裂成[l,pos - 1]和[pos, r]两个块。
分裂之后,我们可以用一个assign
推平操作,直接将中间所有的块用一个块代替。推平操作直观的意义就是将大区间
inlien void assignChar(int l, int r, char ch)
{
pointer leftIt = split(l), rightIt = split(r + 1);
triplets.erase(leftIt, rightIt);
triplets.insert(triplet(l, r, ch));
}
根据Chtholly Tree 的基本操作,我们可以从中得到启发。对于每个操作的区间,我们可以直接暴力地统计区间里每个字符出现的个数,然后按顺序插入新的块。由于采用了分块的储存方式,这个速度会大大提升。
对于升序和降序操作,我们都可以用一个桶排序来完成,经过分块优化后,时间复杂度下限会降到
这一部分的代码:
#define INCREASE true
#define DECREASE false
inline void rerange(int l, int r, bool order)
{
memset(countLetter, 0, sizeof(countLetter));//countLetter[26]表示对应字母个数
pointer leftIt = split(l), rightIt = split(r + 1);
for(pointer it = leftIt; it != rightIt; ++ it)
{
countLetter[it->ch] += it->r - it->l + 1;
}//桶排序
triplets.erase(leftIt, rightIt);//删除原值
int left = l;
if(order == DECREASE)
for(rg int ch = 25; ch >= 0; -- ch)
{
if(countLetter[ch])
{
triplets.insert(triplet(left, left + countLetter[ch] - 1, ch));
left = left + countLetter[ch];
}
}
else
for(rg int ch = 0; ch <= 25; ++ ch)
{
if(countLetter[ch])
{
triplets.insert(triplet(left, left + countLetter[ch] - 1, ch));
left = left + countLetter[ch];
}
}
//根据对应的顺序,把新的三元组插入原来的列表里
}
稍微注意一下细节就可以了。
Chtholly Tree 是一种非常直观的结构。实不相瞒,我在考场上就是这个思路,但是由于没学过这些东西,结果自己写,写爆了。
这里之所以取它的英文名,是因为中文名太蠢了。我觉得与其叫“珂朵莉树”,不如叫“动态分块”,即直观又高端。
Sol 2: Multiple Segment Tree
如题。这种解法是从一个子任务上扩展而来的:这个字符串仅由
当只有这两个字符时,我们可以分别建立2棵线段树,分别维护
推广到整道题,我们可以建立26棵线段树,分别维护26个字母。其实实现起来也不难,只是它和普通的线段树不同:维护什么值?怎么打延迟标记?最后如何输出?这些问题都比较难想。建议自己一步步实现,可以加深对线段树的理解。这里由于本人技术不行,就不张贴代码了。
不过说实话,这种做法其实是相当暴力的了。在考场上还是建议用第一种方法。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 深入理解 Mybatis 分库分表执行原理
· 如何打造一个高并发系统?
· .NET Core GC压缩(compact_phase)底层原理浅谈
· 现代计算机视觉入门之:什么是图片特征编码
· .NET 9 new features-C#13新的锁类型和语义
· Sdcb Chats 技术博客:数据库 ID 选型的曲折之路 - 从 Guid 到自增 ID,再到
· 语音处理 开源项目 EchoSharp
· 《HelloGitHub》第 106 期
· Spring AI + Ollama 实现 deepseek-r1 的API服务和调用
· 使用 Dify + LLM 构建精确任务处理应用