CF558E A Simple Task

题目大意

给定一个长度不超过\(10^5\)的,由小写字母构成的字符串、和不超过\(50000\)次操作。每次操作可以选择一个区间\([l,r]\),将这个区间的子串里的所有字母按照字典序非严格递增或非严格递减地排序。
现给定这个长度为\(N\)字符串,和\(M\)次操作,每次指明区间和单调变化的类型。请输出最后得到的字符串。

这道题最暴力的思路显然是每次操作都快排一次,时间复杂度是\(O(MN\log N)\)。当然,由于字符的种类不超过\(26\)个,可以用桶排序优化到\(O(MN)\)

之后,有两位同学提供了两种虽然暴力但可行的思路:

Sol 1: Chtholly Tree

一种前卫的数据结构。其本质是将序列操作改为三元组操作。

还记得在四边形不等式学习过的决策优化的技巧吗?由于最优决策是非严格单调递增的,我们可以用决策三元组\((l,r,p)\)代表在\(l\)阶段到\(r\)阶段之间的最优决策都是\(p\)。这样时间复杂度就从\(O(N^2)\)降到了\(O(N\log N)\)

这道题也是一样。由于字符串的长度远远长于字符表的长度,在随机数据下,整个字符串一定会出现重复的字母。这个时候我们就可以用三元组\((l,r,ch)\)表示字符串str[l]str[r]的字符均为ch

由于每次要修改一个区间\((L,R)\),我们需要支持动态插入,动态查找的数据结构。我们就使用std::set来解决这个问题。

这样一来,我们就不得不提到一个数据结构:珂朵莉树(Chtholly Tree)。它有两种基本的操作,分别是splitassign

首先定义一个三元组节点:

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推平操作,直接将中间所有的块用一个块代替。推平操作直观的意义就是将大区间\((l,r)\)赋值成同一个值。

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 的基本操作,我们可以从中得到启发。对于每个操作的区间,我们可以直接暴力地统计区间里每个字符出现的个数,然后按顺序插入新的块。由于采用了分块的储存方式,这个速度会大大提升。

对于升序和降序操作,我们都可以用一个桶排序来完成,经过分块优化后,时间复杂度下限会降到\(\Omega(\log N)\)

这一部分的代码:

#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

如题。这种解法是从一个子任务上扩展而来的:这个字符串仅由\(a\)\(b\)组成。

当只有这两个字符时,我们可以分别建立2棵线段树,分别维护\(a\)\(b\)在区间\([l, r]\)内的数量。操作和询问的时间复杂度都是\(O(\log N)\)的。

推广到整道题,我们可以建立26棵线段树,分别维护26个字母。其实实现起来也不难,只是它和普通的线段树不同:维护什么值?怎么打延迟标记?最后如何输出?这些问题都比较难想。建议自己一步步实现,可以加深对线段树的理解。这里由于本人技术不行,就不张贴代码了。

不过说实话,这种做法其实是相当暴力的了。在考场上还是建议用第一种方法。

posted @ 2019-08-08 08:23  LinearODE  阅读(263)  评论(0编辑  收藏  举报