[POI2015] POD 题解

前言

题目链接:洛谷

题意简述

长度为 \(n\) 的一串项链,每颗珠子是 \(k\) 种颜色之一。第 \(i\) 颗与第 \(i-1, i+1\) 颗珠子相邻,第 \(n\) 颗与第 \(1\) 颗也相邻。切两刀,把项链断成两条链。要求每种颜色的珠子只能出现在其中一条链中。求方案数量(保证至少存在一种),以及切成的两段长度之差绝对值的最小值。

题目分析

据我所知,这道题有如下做法:

  1. 线段树 + 增量法 + 最值 / 二分
  2. 哈希 + 双指针
  3. 分治 + 双指针

写一篇题解以总结并加深印象。

方法 \(1\):线段树 + 增量法 + 最值 / 二分

分别对于每种颜色考虑。对于一种颜色,把这个环分成了若干段,我们要切一定是截取某一段的一部分,要不然就会跨过一个位置,不合法。暴力的想法是枚举一个端点,对每种颜色分别考虑,把合法的位置标记。最后被标记了 \(k\) 次的位置就是合法的位置。

优化也很简单,对于两个相邻的位置,需要更改的地方并不多。考虑增量法,把端点 \(i - 2 \sim i - 1\) 移动到 \(i - 1 \sim i\),只会对 \(i - 1\) 这个位置对应的颜色 \(col_{i - 1}\) 造成影响。我们把上一次出现 \(col_{i - 1}\) 的位置到 \(i - 2 \sim i - 1\)\(1\),再把 \(i - 1 \sim i\) 到下一次出现的位置之间加 \(1\),就完成了更新。

这很线段树啊,先把边权右放,即用 \(i\) 这个位置的值代表 \(i - 1 \sim i\) 这条边,修改变成了区间加。第一个问题,即方案数是求区间最值和区间最值数量,很简单。

对于第二个问题,为了让答案尽量小,即让其中一段尽可能接近 \(\cfrac{n}{2}\),我们先找到那个位置 \(p\),这时候序列被 \(p\)\(i\) 分成了三个部分,对于每个部分求区间最值出现的最靠近 \(p\) 的位置,容易发现就是最左边或最右边的位置,计算下距离,对答案取个 \(\min\) 即可。注意,那个部分的最值必须也要满足要求才能对答案有贡献。当然,也可以线段树上二分出最左边或最右边的位置。

此外,注意有些颜色可能在序列中只出现了一次,我们无论怎么切都不会使它们不合法,所以实现的时候特判一下就好了。以及环上的处理略为繁琐。

时间复杂度:\(\Theta(n \log n)\),瓶颈在线段树,但逝大常数,要卡卡常。

方法 \(2\):哈希 + 双指针

如果进一步分析问题很容易发现,环是一个幌子,我们只需要找到一段区间,对于每种颜色,要么全都在里面出现了,要么一次都没有出现,那么截取出来一定是合法的。因为对于另一半,每种颜色也可能是全都出现了或者一次都没有出现。

我们可以设计出一个哈希函数,对于每一种颜色全部出现和一次都没出现出现是等价的。

异或哈希:如果某种颜色出现了 \(c\) 次,让前 \(c - 1\) 出现的位置的权值设置成随机数,最后一位设置成前 \(c - 1\) 位的权值异或和。这样,如果有一个区间完全包含或完全不包含所有的位置,对于这一种颜色,异或和均为 \(0\)。当然,对于一个区间里的每一个颜色异或和都为 \(0\) 的话,这就是一个合法区间。哈希嘛,不需要考虑不同颜色之间的影响。于是问题变成了统计有多少区间异或和为 \(0\)。搞个前缀异或和,变成了前缀异或和相等的所有位置之间任意取构成的区间。如果某一种前缀和出现了 \(x\) 次,那么方案数增加 \(\cfrac{x(x - 1)}{2}\)。第二个问题,就是最接近 \(\cfrac{n}{2}\) 的两个位置,排序后双指针即可。

和哈希:设计一个哈希函数 \(F(x)\),对于一个局面我们定义其权值为 \(\sum \limits _ {i = 1} ^ {k} yzh[i] \cdot F(cnt[i])\),其中 \(yzh[i]\) 为每一种颜色对应的随机权,\(cnt[i]\) 为颜色 \(i\) 在这个局面出现的次数。\(F(x)\) 应满足对于 \(x = 0\) 和 $x = $ 颜色 \(i\) 在整个序列出现的总次数,返回值相同,对于其他 \(x\) 返回个随机值即可。那么不就和异或哈希后续处理一样了吗?

时间复杂度:\(\Theta(n \log n)\),瓶颈在排序,但挺快的。

方法 \(3\):分治 + 双指针

首先来考虑简化模型。对于相互包含的颜色,如 \(\texttt{A} \cdots \texttt{B} \cdots \texttt{A} \cdots \texttt{B}\),我们既要满足放在一段 \(\texttt{A}\) 中,也要满足放在一段 \(\texttt{B}\) 中,那么综合考虑,\(\texttt{A}\)\(\texttt{B}\) 是没有区别的,那为什么不合并起来呢?发现简化后的模型,一种颜色只会出现在另一种颜色的某一段中。

不妨考虑一个颜色 \(\texttt{A}\),它出现在序列上是形如 \(\texttt{A} \cdots \texttt{A} \cdots \texttt{A}\) 的,不妨记作 \(f(\texttt{A})\),那么我们选取的一段一定是在相邻的一段 \(\texttt{A} \cdots \texttt{A}\) 中。我们分别考虑每一段。

每一段是形如 \(\texttt{A} f(\texttt{B}) f(\texttt{C}) \cdots f(\texttt{K}) \texttt{A}\) 的。显然被分成了若干子问题,递归解决每一个 \(f(\texttt{B} \cdots \texttt{K})\),接下来只用考虑选出的区间是完整的若干连续的子段,如 \(f(\texttt{B})\)\(f(\texttt{B}) f(\texttt{C})\) 等等。设 \(f(\texttt{B}) f(\texttt{C}) \cdots f(\texttt{K})\)\(k\) 个子问题。那么对于当前对方案数的贡献是在 \(k + 1\) 个点中选出两个,其中构成的连续段就是选出段,方案数是 \(\cfrac{(k + 1) k}{2}\)。第二个问题同之前解决方法一样,双指针即可。

分治部分很好处理,那来说说预处理合并颜色怎么搞。不妨设 \(nxt_i\) 表示下一次出现 \(i\) 这个位置的颜色的位置,如果没有则是 \(0\)。那么对于相交的两个颜色,这样一段:\(j \cdots i \cdots nxt_j \cdots nxt_i\),即对于 \(i\)\(\exists j \in [1, i - 1] \land nxt_j \in (i, nxt_i)\)。对于前一个条件,我们从左到右扫过来,对于当前的 \(i\),只用考虑之前被扫过的 \(j\)。对于后一个条件,先考虑一个 \(nxt\) 从顶到底递增的单调栈,每次先把 \(nxt_j \leq i\) 的弹栈弹掉,对于剩下的元素,和 \(< nxt_i\) 的每个合并一下,但是时间复杂度会假。如果要保证时间复杂度,即每个元素只被加进去一次,弹出去一次,我们在访问的时候就要弹掉。

对于 \(i\) 之后的一个 \(k\),如果 \(i\)\(k\) 满足交叉,那么这么做没有什么问题。但是如果有 \(nxt_k \leq nxt_i\) 并且存在一个被弹掉的 \(j\),满足 \(nxt_k > nxt_j\),就会出现问题,可见下图。

图一:处理前

当然,类似红绿关系的可以不止两种,可以继续套。我们用上述单调栈合并后的结果是下图。

图二:处理一遍后

发现,合并后,如果还存在上述遗留情况的交叉,会越过处理后同一种颜色的相邻两段。而在处理前遗留情况是越过某一颜色的最后一次出现的位置。所以再用相同的算法处理一遍即可。

图三:最终形态

至于颜色的合并,使用并查集即可。

至此,我们完成了本题的求解。时间复杂度 \(\Theta(n \alpha (k))\)。瓶颈在于并查集。

代码

均给出了详细注释,供大家参考。

方法 \(1\):线段树 + 增量法 + 最值 / 二分

只写了最值版本的,代码,跑得很慢

方法 \(2\):哈希 + 双指针

异或哈希

代码\(528\) 毫秒

和哈希

代码\(1.43\)

方法 \(3\):分治 + 双指针

最优解,才 \(200\) 毫秒左右。

#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;

const int MAX = 1 << 25;
char buf[MAX], *p = buf;

#define isdigit(x) ('0' <= x && x <= '9')
#define getchar() *p++
#define check(x) ans = min(ans, abs(n - ((x) << 1)))

inline void read(int &x) {
	char ch = 0; x = 0;
	for (; !isdigit(ch); ch = getchar());
	for (;  isdigit(ch); x = (x << 3) + (x << 1) + (ch ^ 48), ch = getchar());
}

const int N = 1e6 + 10;
const int inf = 0x3f3f3f3f;

int n, k, val[N], ans = inf;
long long cnt;

int fa[N], rk[N];
int get(int x) { return fa[x] == x ? x : fa[x] = get(fa[x]); }
void merge(int a, int b) {
	if ((a = get(a)) == (b = get(b))) return;
	if (rk[a] < rk[b]) swap(a, b);
	fa[b] = a;
	if (rk[a] == rk[b]) ++rk[a];
}
// 并查集 

int fir[N], lst[N], nxt[N], pre[N];

inline int trans(int x) { return x ^ n ? x + 1 : 1; }  // x 的下一个位置 
inline int snart(int x) { return x ^ 1 ? x - 1 : n; }  // x 的上一个位置 
inline int dis(int x, int y) { return x - y + 1; }     // y ~ x 这样一段的距离 

void solve(int x) {
	for (int i = trans(x); i != nxt[x]; i = trans(pre[i]))  // i 是这种颜色出现的第一个位置,pre[i] 就是最后一个位置 
	for (int j = i; nxt[j] != i; j = nxt[j])  // 把这种颜色每一段分治解决 
		solve(j);
	int cnt = 0;
	for (int i = trans(x), j = i; i != nxt[x]; ++cnt, i = trans(pre[i])) {  // 双指针找接近 n/2 的方案 
		while (j != nxt[x] && dis(pre[i], j) > n >> 1) j = trans(pre[j]);
		check(dis(pre[i], j));
		check(dis(pre[i], nxt[snart(j)]));
	}
	::cnt += 1ll * cnt * (cnt + 1);
}

int stack[N], top;
inline void init() {
	memset(lst, 0, sizeof (int) * (k + 1)), top = 0;
	for (int i = 1; i <= n; ++i) nxt[i] = 0, nxt[lst[val[i]]] = i, lst[val[i]] = i;
	for (int i = 1; i <= n; ++i) if (nxt[i]) {
		while (top && nxt[stack[top]] <= i) --top;
		while (top && nxt[stack[top]] <= nxt[i]) merge(val[i], val[stack[top--]]);
		stack[++top] = i;
	}  // 单调栈操作 
	for (int i = 1; i <= n; ++i) val[i] = get(val[i]);  // 直接设置成合并后的颜色 
}

signed main() {
	fread(buf, 1, MAX, stdin);
	read(n), read(k);
	for (int i = 1; i <= n; ++i) read(val[i]);
	for (int i = 1; i <= k; ++i) fa[i] = i;
	init(), init();  // 预处理两遍 
	memset(lst, 0, sizeof (int) * (k + 1));
	for (int i = 1; i <= n; ++i) {
		if (!fir[val[i]]) fir[val[i]] = i;
		else nxt[lst[val[i]]] = i;
		lst[val[i]] = i;
	}
	for (int i = 1; i <= n; ++i) {
		if (!nxt[i]) nxt[i] = fir[val[i]];
		pre[nxt[i]] = i;
	}  // 处理前驱后继 
	int i = 1; do solve(i), i = nxt[i]; while (i != 1);  // 最外层调用 
	printf("%lld %d", cnt >> 1, ans);
	return 0;
}

后记

希望本文能够帮助你熟悉每一种方法。也希望我以后遇到这种题别寄了啊!

posted @ 2024-08-05 16:28  XuYueming  阅读(8)  评论(0编辑  收藏  举报