【题解】[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)\) 了
实在是吊打我...

被各种吊打...关于 KMP 的 next 数组的建树
浙公网安备 33010602011771号