【bzoj2342】[Shoi2011]双倍回文

【题意】

定义 双倍回文 为 一个回文串套一个回文串, 问一个串中的最长双倍回文串。


【题解】

首先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;
}

大致跑出来的时间对比:
                算法ProblemResultMemoryTimeCode_Length
暴力剪枝2342Accepted10060 kb92 ms936 B
并查集2342Accepted13968 kb112 ms1111 B
树状数组+二分2342Accepted17872 kb408 ms1614 B
树状数组+倍增2342Accepted17872 kb556 ms1543 B
set2342Accepted25588 kb972 ms1293 B


【技巧总结】

虽然是一道很水的题, 但是感觉思维的过程还是很值得借鉴的。

一般在做一道题的时候, 如果思维的深度不够的话 会想到一些 用稍微复杂一些的东西来维护的方法, 时间复杂度与编程复杂度皆较高(不考虑用STL水过去);反之, 越深入地思考到问题的本质越能发现一些简单高效的精妙方法, 令人大呼机智。

另外, 一些基础的东西要形成模块, 这样在遇到 一类问题的时候都可以很快地 调用起它, 不然很多看似学会了的东西仍 无法自主应用到做题中。

还有一点是在做题的时候的反方向思考。 这道题最后的并查集方法如果 式子列的和上面方法一样的话就要从 大的往小的扫(当然把式子反过来列也可以解决这个问题), 为什么必须反过来呢? 因为并查集实现的是把集合合并, 或者说是把可选的变得越来越少, 在第一组式子的第一个式子中 只有当 x 越来越小, y 的可行解才会越来越少, 这时候才可以维护。 这便又回到了 对算法的深入理解上面,比如并查集, bzoj 1015 就是很显然的要反向处理, 因为这样才可以让联通块越来越少。

再做一些题以后 我要把每个简单算法或数据结构都进行一个系统的总结。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值