【题意】
定义 双倍回文 为 一个回文串套一个回文串, 问一个串中的最长双倍回文串。
【题解】
首先manacher扫一遍是必须的,(注意如果把原串每两个字符中间加一个字符那样扫manacher一定要扫够2 * n 啊, 注意边界 s[1] 和 s[2 * n + 1] 都要赋值为'*', 开始因此WA了一次)。
接下来的做法就很多啦,,
下面看看要求的是什么:
显然要枚举每一个 对称轴 x,然后对于每个 x, 要找到一个最大的 y,满足
y - f[y] <= x &&
y <= x + f[x] / 2
考虑从小到大地枚举 x , 这时候只要把 y 按照 y - f[y] 排序, 就可以单调地加入 y 了!
接下来要做的就是 在当前满足第一个条件的 y 的集合里 找到一个最大的 y , 使得它 <= x + f[x] / 2。
丢到set里当然就可以了。
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <set>
#define MAXN 1000005
using namespace std;
int n, p[MAXN], f[MAXN], ans, now, q[MAXN];
char s[MAXN];
set <int> la;
bool cmp(int a, int b){return a - f[a] < b - f[b];}
int main()
{
scanf("%d", &n);
scanf("%s", s + 1);
for(int i = n; i; i --)s[i * 2] = s[i], s[i * 2 - 1] = '*';
s[0] = '#'; s[2 * n + 1] = '*';
int k = 0;
for(int i = 2; i <= 2 * n + 1; i ++){
if(k + p[k] - 1 < i)p[i] = 1;
else p[i] = min(p[2 * k - i], k + p[k] - i);
while(s[i + p[i]] == s[i - p[i]])p[i] ++;
if(i + p[i] > k + p[k])k = i;
}
for(int i = 1; i <= n; i ++)f[i] = (p[i * 2 + 1] - 1) / 2;
for(int i = 1; i <= n; i ++)q[i] = i;
sort(q + 1, q + n + 1, cmp);
int lala = 1;
for(int i = 1; i <= n; i ++){
while(lala <= n && q[lala] - f[q[lala]] <= i)la.insert(q[lala]), lala ++;
set <int> :: iterator tt = la.upper_bound(i + f[i] / 2);
if(tt != la.begin())ans = max(ans, (*--tt - i) * 4);
}cout << ans << endl;
return 0;
}
考虑我们是要维护一个数列, 每次询问小于等于一个值 的 最大值是多少。
可不可以不开平衡树?
考虑用树状数组, 只可以维护小于一个值的数有多少个, 知道了这个以后二分一下不就可以确定最大值的位置了吗, 或者用类似倍增一样的方法也可以搞上去。
但是复杂度是 n log² n 的。 网上有人说由于某些奇特性质可以做到 n log n, 似乎很玄妙我没有懂。
倍增版
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
#define lowbit(x) x & (-x)
#define MAXN 1000005
using namespace std;
int n, p[MAXN], f[MAXN], ans, now, q[MAXN], a[MAXN];
char s[MAXN];
void insert(int x){for(; x <= n; x += lowbit(x))a[x] ++;}
int find(int x){int ans = 0; for(; x; x -= lowbit(x))ans += a[x]; return ans;}
int ask(int x){
int t = 0;
for(int i = 19; i >= 0; i --)if(t + (1<<i) <= n && a[t + (1<<i)] < x)
t += (1<<i), x -= a[t];
return t + 1;
}
int Ask(int x){return ask(find(x));}
bool cmp(int a, int b){return a - f[a] < b - f[b];}
int main()
{
scanf("%d", &n);
scanf("%s", s + 1);
for(int i = n; i; i --)s[i * 2] = s[i], s[i * 2 - 1] = '*';
s[0] = '#'; s[2 * n + 1] = '*';
int k = 0;
for(int i = 2; i <= 2 * n + 1; i ++){
if(k + p[k] - 1 < i)p[i] = 1;
else p[i] = min(p[2 * k - i], k + p[k] - i);
while(s[i + p[i]] == s[i - p[i]])p[i] ++;
if(i + p[i] > k + p[k])k = i;
}
for(int i = 1; i <= n; i ++)f[i] = (p[i * 2 + 1] - 1) / 2;
for(int i = 1; i <= n; i ++)printf("%d ", f[i]);
for(int i = 1; i <= n; i ++)q[i] = i;
sort(q + 1, q + n + 1, cmp);
int lala = 1;
for(int i = 1; i <= n; i ++){
while(lala <= n && q[lala] - f[q[lala]] <= i)insert(q[lala ++]);
ans = max(ans, Ask(i + f[i] / 2) - i);
}cout << ans * 4 << endl;
return 0;
}
二分版
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
#define lowbit(x) x & (-x)
#define MAXN 1000005
using namespace std;
int n, p[MAXN], f[MAXN], ans, now, q[MAXN], a[MAXN];
char s[MAXN];
void insert(int x){for(; x <= n; x += lowbit(x))a[x] ++;}
int find(int x){int ans = 0; for(; x; x -= lowbit(x))ans += a[x]; return ans;}
int Ask(int x){
int l = find(x), r = x, f = l;
while(l + 1 < r){
int mid = l + r >> 1;
if(find(mid) < f)l = mid + 1;
else r = mid;
}if(find(l) == f)return l; return r;
}
bool cmp(int a, int b){return a - f[a] < b - f[b];}
int main()
{
scanf("%d", &n);
scanf("%s", s + 1);
for(int i = n; i; i --)s[i * 2] = s[i], s[i * 2 - 1] = '*';
s[0] = '#'; s[2 * n + 1] = '*';
int k = 0;
for(int i = 2; i <= 2 * n + 1; i ++){
if(k + p[k] - 1 < i)p[i] = 1;
else p[i] = min(p[2 * k - i], k + p[k] - i);
while(s[i + p[i]] == s[i - p[i]])p[i] ++;
if(i + p[i] > k + p[k])k = i;
}
for(int i = 1; i <= n; i ++)f[i] = (p[i * 2 + 1] - 1) / 2;
for(int i = 1; i <= n; i ++)q[i] = i;
sort(q + 1, q + n + 1, cmp);
int lala = 1;
for(int i = 1; i <= n; i ++){
while(lala <= n && q[lala] - f[q[lala]] <= i)insert(q[lala ++]);
ans = max(ans, Ask(i + f[i] / 2) - i);
}cout << ans * 4 << endl;
return 0;
}
嗯, 这样跑出来的确可以比 set 快一点不过 log² 的复杂度还是不太优美啊,,
换一种思路呢? 再回顾一下要求的东西:
最大的y , 满足:
y + f[y] >= x &&
y >= x - f[x] / 2
或者等同于:
y - f[y] <= x &&
y <= x + f[x] / 2
刚才一直是在考虑在保证条件 1 的情况下 条件 2 的最大值, 如果来保证条件 2 呢?
鉴于上面两组式子实质是一样的, 这里统一考虑第二组。保证条件2 即 当 y 的上界确定后, 再看第一个条件, 如果从小到大地扫一遍 x, y可以取的值是越来越少的, 而且如果 y 在之前x = i 时 且满足条件 2 的时候不可取, 那么 y 在 x = i + t 且满足条件2 的时候一定也是不可取的(显然)。 于是? 用一个简单的并查集维护一下就好啦!!
复杂度 n log n
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
#define MAXN 1000005
using namespace std;
int n, p[MAXN], f[MAXN], ans, now, fat[MAXN];
char s[MAXN];
int find(int x){return fat[x] == x ? x : fat[x] = find(fat[x]);}
int main()
{
scanf("%d", &n);
scanf("%s", s + 1);
for(int i = n; i; i --)s[i * 2] = s[i], s[i * 2 - 1] = '*';
s[0] = '#'; s[2 * n + 1] = '*';
int k = 0;
for(int i = 2; i <= 2 * n + 1; i ++){
if(k + p[k] - 1 < i)p[i] = 1;
else p[i] = min(p[2 * k - i], k + p[k] - i);
while(s[i + p[i]] == s[i - p[i]])p[i] ++;
if(i + p[i] > k + p[k])k = i;
}
for(int i = 1; i <= n; i ++)f[i] = (p[i * 2 + 1] - 1) / 2;
for(int i = 1; i <= n; i ++)fat[i] = i;
for(int i = 1; i <= n; i ++){
for(now = find(i - f[i] / 2); now + f[now] < i; now = fat[now])
fat[now] = find(now + 1);
ans = max(ans, i - now);
}
cout << ans * 4<<endl;
return 0;
}
最后, status里那些 100ms -- 的是怎么做到的呢?
n² 暴力 加一个最优性剪枝 !! 就可以做到 100ms 一下了。
不过这个是很容易出数据卡掉的, 所有的字母都一样就好了。
不过这不失为一个骗分的很好方法啊。
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
#define MAXN 1000005
using namespace std;
int n, p[MAXN], f[MAXN], ans;
char s[MAXN];
int main()
{
scanf("%d", &n);
scanf("%s", s + 1);
for(int i = n; i; i --)s[i * 2] = s[i], s[i * 2 - 1] = '*';
s[0] = '#'; s[2 * n + 1] = '*';
int k = 0;
for(int i = 2; i <= 2 * n + 1; i ++){
if(k + p[k] - 1 < i)p[i] = 1;
else p[i] = min(p[2 * k - i], k + p[k] - i);
while(s[i + p[i]] == s[i - p[i]])p[i] ++;
if(i + p[i] > k + p[k])k = i;
}
for(int i = 1; i <= n; i ++)f[i] = (p[i * 2 + 1] - 1) / 2;
for(int i = 1; i <= n; i ++)
for(int j = f[i] / 2; j && j * 4 > ans; j --)
if(f[i - j] >= j && f[i + j] >= j)ans = max(ans, j * 4);
cout << ans << endl;
return 0;
}
大致跑出来的时间对比:
算法 | Problem | Result | Memory | Time | Code_Length |
暴力剪枝 | 2342 | Accepted | 10060 kb | 92 ms | 936 B |
并查集 | 2342 | Accepted | 13968 kb | 112 ms | 1111 B |
树状数组+二分 | 2342 | Accepted | 17872 kb | 408 ms | 1614 B |
树状数组+倍增 | 2342 | Accepted | 17872 kb | 556 ms | 1543 B |
set | 2342 | Accepted | 25588 kb | 972 ms | 1293 B |
【技巧总结】
虽然是一道很水的题, 但是感觉思维的过程还是很值得借鉴的。
一般在做一道题的时候, 如果思维的深度不够的话 会想到一些 用稍微复杂一些的东西来维护的方法, 时间复杂度与编程复杂度皆较高(不考虑用STL水过去);反之, 越深入地思考到问题的本质越能发现一些简单高效的精妙方法, 令人大呼机智。
另外, 一些基础的东西要形成模块, 这样在遇到 一类问题的时候都可以很快地 调用起它, 不然很多看似学会了的东西仍 无法自主应用到做题中。
还有一点是在做题的时候的反方向思考。 这道题最后的并查集方法如果 式子列的和上面方法一样的话就要从 大的往小的扫(当然把式子反过来列也可以解决这个问题), 为什么必须反过来呢? 因为并查集实现的是把集合合并, 或者说是把可选的变得越来越少, 在第一组式子的第一个式子中 只有当 x 越来越小, y 的可行解才会越来越少, 这时候才可以维护。 这便又回到了 对算法的深入理解上面,比如并查集, bzoj 1015 就是很显然的要反向处理, 因为这样才可以让联通块越来越少。
再做一些题以后 我要把每个简单算法或数据结构都进行一个系统的总结。