【题解】[POI2005]SZA-Template

链接
感觉写了个爽题(
关于 KMP 的 \(next\) 数组(下记 \(nx\) )又有了有趣的看法

首先考虑,如果已知印章为前 \(x\) 个字符,我们怎么判断其是否符合题意。
对于前 \(i\) 个字符(以下为与长度对应,均从 1 开始编号),其公共前/后缀长度分别为 \(nx[i], nx[nx[i]], ...\) ,如果其中有一个等于 \(x\) ,意味着可以以 \(i\) 为末位置盖一个印章
对于整个串,如果我们把所有这样的 \(i\) 都找出来,相邻两者距离不超过 \(x\) ,意味着能全部覆盖,就成了

考虑到 \(nx\) 数组的特点,我们\(nx[i]\to i\) 为边建一棵树
(P.S. 关于这玩意想着多少出个板子题吧,结果已经有一模一样的板子题了 【模板】失配树
我们发现,节点 \(x\) 的子树的节点就是符合上述条件的所有位置 \(i\)。(当然,0 作为根节点是没有意义的)
好啊!然后呢?
哪些是可能为答案的 \(x\) ?显然是 \(nx[N], nx[nx[N]], ...\) ,那么从节点 \(N\) (是个叶节点)开始,顺次往上爬,依次添加以 沿路节点 \(x\) 为根的子树的所有节点,每次按上面的思路判断当前 \(x\) 能不能符合题意。怎么做?开个树状数组记录当前添加的所有节点,再开个树状数组维护所有相邻点对的距离,这样做是 \(O(nlgnlgn)\)

#include <cstdio>
#include <cstring>
using namespace std;
const int MAXN = 500005;

int _(int x) { return x+1; }
int max(int x, int y) { return x > y ? x : y; }
int min(int x, int y) { return x < y ? x : y; }
struct treeArray {
	#define lbt (x&-x)
	int C[MAXN];
	treeArray() { memset(C, 0, sizeof(0)); }
	void add(int x, int k) { for (; x< MAXN; x+=lbt) C[x]+=k; }
	int sum(int x) { int re=0; for (; x; x-=lbt) re+=C[x]; return re; }
} TA, dis;

int N, next[MAXN]; char S[MAXN];
int fst[MAXN], vis[MAXN], ans;
struct edge { int v, pre; } e[MAXN];
void adde(int a, int b, int k) { e[k] = (edge) {b, fst[a]}, fst[a] = k; }
void findEdge(int i, int &_l, int &_r) {
	int l, r, t=TA.sum(_(i));
	l = -1, r = i-1; // (l, r]
	while (r - l > 1) {
		int mid = (l + r) >> 1;
		if (TA.sum(_(mid))<t) l = mid;
		else r = mid;
	} _l = r;
	l = i, r = N; // (l, r]
	while (r - l > 1) {
		int mid = (l + r) >> 1;
		if (TA.sum(_(mid))>t) r = mid;
		else l = mid;
	} _r = r;
}
void insert(int x) {
	int l, r; vis[x] = 1;
	findEdge(x, l, r); TA.add(_(x), 1); // find then add
	//printf("add %d, l=%d, r=%d\n", x, l, r);
	dis.add(_(r-l), -1), dis.add(_(x-l), 1), dis.add(_(r-x), 1);
	for (int o=fst[x]; o; o=e[o].pre) if (!vis[e[o].v]) insert(e[o].v);
}
int main()
{
	scanf("%s", S); N = strlen(S);
	next[0] = 0, next[1] = 0;
	for (int i=1; i< N; i++) {
		int j = next[i];
		while (j && S[i]!=S[j]) j = next[j];
		next[i+1] = S[i]==S[j] ? j+1 : 0;
	}
	for (int i=1; i<=N; i++) adde(next[i], i, i);
	
	ans = N, vis[N] = 1;
	dis.add(_(N), 1), TA.add(_(0), 1), TA.add(_(N), 1);
	for (int i=next[N]; i; i=next[i]) {
		insert(i); if (dis.sum(_(N))==dis.sum(_(i))) ans = i;
	}
	printf("%d", ans);
}

分割线


好了,这个解法比谁的都差(
首先我们之所以要维护所有相邻点对的距离,是因为要求最大的那个距离,而每次新出现的距离不一定是最大的
我们可以反过来从根走到 \(N\) ,然后沿途删掉无关的节点,同时维护每个现存节点的前驱和后继,这时就能更新距离的最大值了,成了 \(O(n)\)
可能在别人眼中这才是常规做法吧...我太菜了
想想也是,链表的删除操作一般比较好维护


另外,还有复杂度和代码长度都吊打我的解法((
\(f[i]\) 表示前 \(i\) 个字符的答案,下文一部分 “\(f[i]\)”、“\(nx[i]\)” 等为形如“前 \(f[i]\) 个字符组成子串”的简写,具体联系上下文
可以证明,\(f[i]\) 只有两种可能取值:\(f[nx[i]]\)\(i\) ,证明:
\(i > f[i] > nx[i]\) ,那么至少 \(f[i]\) 才是 \(nx[i]\) ,矛盾,故 \(f[i]\leq nx[i]\)
既然如此,那么 \(f[i]\) 就能覆盖 \(nx[i]\) :因为“覆盖”是一个向右位移一段就覆盖一次的过程,\(f[i]\) 由定义是前 \(i\) 个字符的后缀,也就是 \(nx[i]\) 的后缀,故此
得证
\(f[nx[i]]\) 能取到,还需要满足 \(f[nx[i]]\) 不仅能覆盖前后两段 \(nx[i]\) ,还要能覆盖中间空出来的一段(如果有的话),我们换一种思考方式(最开始就是卡在这了)
考虑递推的过程,假设 \(i\) 之前的都推出来了,而我们已知能用 \(f[nx[i]]\) 够覆盖 \(i\) 的后缀 \(nx[i]\),我们只要考虑之前部分能不能用同样的东西覆盖到
等价于是否存在 \(j\) ,满足 \(i - nx[i]\leq j\)\(f[j] = f[nx[i]]\) ,这玩意开个桶,位置 \(p\) 储存\(f[j]=p\) 的最大的 \(j\)(就和 LIS 一样!)
就能 \(O(n)\)

实在是吊打我...

posted @ 2021-02-23 21:13  zrkc  阅读(62)  评论(0)    收藏  举报