hash
性质:如果存在一个长度为 len 的相同子串,那么一定存在一个长度为 len - 1 的相同子串
于是可以二分查找,通过哈希判断是否存在
双模太慢过不去,小模数会被卡,只能自然溢出
考虑爆搜,需要记录状态有:哪些队伍打完所有比赛了(通过按顺序搜索可以直接优化掉这一维),当前是哪两只队伍比赛,所有人的分数
考虑优化状态量,发现后面未确定的队伍成绩顺序调换,本质上是一个方案,所以实际上无需按顺序记录所有人的分数,只需要记录的分数集合,这样就消掉了一个 \(o(n!)\) 的状态量,记录的分数集合可以用 hash 维护。
然后还需要加入一些其他剪枝。
堆
很容易能想到将一段连续的砖高度改成中位数是最优的,所以题目就转化成了一个类似滑动窗口的东西,求区间中位数
问题是时间戳没法处理,毕竟堆只能删除堆顶元素。
所以这里使用 <code>std::multiset</code> 维护。
<code>std::set</code> (算数据结构吗?
首先,我们只能枚举一个数组用多少数,且必须在 \(o(\log^2 n),o(\log n)\) 或更短时间内求出另外两个数组的答案。
先对每一个数进行一个转换:设数对 \(t_i\rightarrow (x_i, y_i, z_i)\) 的三个参数分别表示 \(t_i\) 这个数在 \(a_i,b_i,c_i\) 中第一次出现的位置(不存在就是无穷大),那么一个合法的方案 \((u, v, w)\) 即满足 \(\forall t_i,a_i \leq u \text{ or } b_i \leq v \text{ or } c_i \leq w\)。
既然我们已经枚举了第一维的 \(u\),那么通过对数对的第一维进行排序(毕竟我们不关心每个数字的具体大小,我们只关心每个数对有没有满足条件,所以排序完全没有问题),我们可以快速得出哪些数是已经满足条件的,而哪些数没有满足条件。接下来就是对剩下不满足条件的数对(以下均称“数对”)进行求解。
忽略第一维后,考虑将数对看作二维平面上的点 \((x_i,y_i)\)(字母大写和上面的数对定义做区分),那么问题就转化成了求两条直线 \(x = v, y = w\),满足:所有点要么在 \(x = v\) 的左边、要么在 \(y = w\) 的下面。
也就是把第一象限再划分成四个小块,让每个点都不在右上方的小块(可以在边界上)。
图中的当然不是最优方案。(可能的)最优方案一定要满足,固定一条线后,另一条线下降到不能再下降。因为坐标轴可以旋转,所以这里一律固定 \(x = v\) 并让 \(y = w\) 尽可能下降。也就是这样:
(细心的同学可以发现这张图其实是重画了的,大家尽量理解一下)
通俗地说,选定两个相邻的“最外层”的点(类似于一个凸包),以左边点的横坐标作为 \(v\),以右边点的纵坐标作为 \(w\),这样选出来的直线 \(x = v,y = w\) 才有可能成为一个最优的方案,该方案的答案就是 \(v + w\)。
下一个方案、再下一个方案是这样的:
可以看到,它是一个“阶梯型”下降的趋势。这个阶梯图样非常重要,是本题解题关键。
刚刚说到了对第一维进行排序,排完序之后不满足的点就可以一个一个加入这个二维平面,寻找一个能让它满足的方案。一个点加入后就不能删除,那么有些点永远也无法成为“最外层”的点,让我们直接忽略它们!
接下来你有了一堆纵坐标递减的点。这些点能产生的所有答案就是相邻的 \(x_i +y_{i + 1}\),到时候更新答案即是求它的最小值。考虑加入一个新点会对它们有什么影响。
此时加入一个新点:
首先,d 和 e 组成的答案无法满足 j 的条件,这个答案应当被删除。
其次,b、c、d 三个点不再是“最外层”的点,应当把它们和它们产生的答案从图中删去。此时我们也可以看出,一个点是“最外层的点”的充要条件是:不存在横纵坐标都比它大的点。
在做完上述两个处理之后,我们就可以放心添加 \(x_a +y_j, x_j+y_e\) 这两个答案了。
所以,加入一个新点,应当先删除相邻节点不合法的答案,再循环删除所有不在最外层的点,最后再计算它与相邻点产生的答案。
维护这个二维平面的点需要快速插入、查找、删除(按一定顺序的)相邻节点;维护这个二维平面上所有的答案需要快速插入、查找、删除,查询最小值。
这几个操作只需要 <code>std::set</code> 和 <code>std::multiset</code> 即可完成。
01 线段树,维护区间最长空白。一个空白可能在左边、中间、右边,所以要另外维护前缀、后缀空白长度。
正着求不好求,但是出现奇数次的数异或和很好求,所以就反着求,再求一个区间所有不同数异或和,然后俩异或一下就是答案。
发现直接维护区间不好维护,于是选择离线做法,按右端点排序询问,然后维护前缀不同数异或和 \(t_i\)、前缀所有数异或和(其实就是“出现奇数次的数异或和”)\(w_i\),于是答案就是 \(t_r \oplus t_{l - 1}\oplus w_r \oplus w_{l - 1}\)。
每向右移动一次当前的右端点,就是要加入这个数;但是这个数可能在之前出现过,这样再搞一个前缀异或就会把这个数消掉,于是我们要在之前它出现的位置先删掉这个数,再在这个位置加入。
为什么不干脆就不加入了呢?因为 \(t_r \oplus t_{l - 1}\) 的时候,会把所有位置在 \([1, l - 1]\) 的不同数消掉,包括以前出现的那个数。所以这里只能这么搞。
所以我选择了树状数组。
当然也可以直接莫队搞过去,不过需要玄学卡常
先想想只用线段树的区间查询能不能做,此时线段树维护的就是两点间的最短距离。
好像不大好想?线段树是维护区间问题的,所以这里我们把两个节点所在的列看作区间边界,所以线段树维护的信息就是每个区间从一个边界到另一个边界的最短距离。
一个边界是两个格子,两个边界之间会有四个距离,所以我们一个区间要维护四个信息,即:左上 / 左下角的格子 到 右上 / 右下角的格子 的最短距离。如果不能到达就是 -1.
听起来好像挺对的?此时一个区间的答案可以被拆成多个区间的答案合并。
思考如何合并答案。
从左区间往右走,必然要从左区间左边界走到左区间右边界,经过交界处再花费 1 的距离从左区间右边界走到右区间左边界,再从右区间左边界走到右区间右边界。
如下图:
左区间左边界有两个起点,交界处有两个中继点,右区间右边界有两个终点,一共 8 种情况分别转移即可。
<code>dist</code> 数组的第一维代表区间左边界的点,第二维代表区间右边界的点,<code>0</code> 是上边,<code>1</code> 是下边,<code>dist[0][1]</code> 即是从左上角往右下角走的最短路径长度。