EOJ 3486 二分+字符串Hash

EOJ 3486 二分+字符串Hash

题目大意

对一个字符串,找出它的字典序最大的子串,如果这个子串有后缀0,则去掉后缀0。题目在此

思路

利用贪心思想,能很简单地想到这个子串必定是去掉后缀0之后的原串的后缀之一。因为如果某个子串字典序最大,但是它不是原串的后缀,我们倘若将它后面的字符添加在这个串后面,那么该子串字典序将会变得更大。

实现方法之一

遍历原串的所有后缀,维护一个字典序最大的起始位置(类似数列中维护最大值)。然后将维护的串与其他串两两比较,将最大字典序的串输出即可。传统方法比较的复杂度为O(n),总体复杂度为O(n2)。倘若使用 二分+字符串Hash 的方法,字符串比较的复杂度为O(logn),总体复杂度为O(n * logn)。

AC代码

#include <bits/stdc++.h>

using namespace std;

typedef unsigned long long ull;

const int NUM = 30000;

vector<int> pos[10];

ull ha[NUM], fac[NUM];
ull seed = 131;
char str[NUM];
int len;

/**
 *获取哈希值
 */
ull getStr(int l, int r) {
    if (l == 0)
        return ha[r];
    l--;
    return ha[r] - ha[l] * fac[r - l];
}

bool cmp(int x, int y) {
    int l = y, r = len;
    int mid;
    int res = -1;
    while (l <= r) {
        mid = (l + r) / 2;
        if (getStr(x, x + (mid - y)) == getStr(y, mid)) {
            res = mid - y;
            l = mid + 1;
        } else
            r = mid - 1;
    }

    res++;
    return str[x + res] < str[y + res];
}

int main() {
#ifdef ACM_LOCAL
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);
#endif
    fac[0] = 1;
    for (int i = 1; i < NUM; i++)
        fac[i] = fac[i - 1] * seed;

    cin >> str;
    len = strlen(str);
    while (str[len - 1] == '0') {
        str[len - 1] = 0;
        len--;
    }

    ha[0] = str[0];
    for (int i = 1; i < len; i++)
        ha[i] = ha[i - 1] * seed + str[i];

    for (int i = 0; i < len; i++)
        pos[str[i] - '0'].push_back(i);

    int big = -1;
    for (int i = 9; i >= 0; i--) {
        if (pos[i].size() > 0) {
            big = i;
            break;
        }
    }

    if (big == -1) {
        cout << "0" << endl;
        return 0;
    }
    int size = pos[big].size();
    int ans = pos[big][0];
    len--;
    for (int i = 1; i < size; i++) {
        if (cmp(ans, pos[big][i]))
            ans = pos[big][i];
    }

    for (int i = ans; i <= len; i++)
        cout << str[i];

    cout << endl;

    return 0;
}

实现方法之二

使用后缀数组对整个串字典序排序,直接输出最后的串即可(傻瓜式操作~,需注意后缀0)

#include <bits/stdc++.h>

using namespace std;

const int MAXN = 2e5 + 10;

int SA[MAXN], myRank[MAXN], height[MAXN], sum[MAXN], tp[MAXN];
//rank[i] 第i个后缀的排名, SA[i] 排名为i的后缀的位置, Height[i] 排名为i的后缀与排名为(i-1)的后缀的LCP
//sum[i] 基数排序辅助数组, 存储小于i的元素有多少个, tp[i] rank的辅助数组(按第二关键字排序的结果),与SA意义一样

bool cmp(const int *f, int x, int y, int w) {
    return f[x] == f[y] && f[x + w] == f[y + w];
}

void get_SA(const char *s, int n, int m) {
    //先预处理长度为1的情况
    for (int i = 0; i < m; i++) sum[i] = 0;//清0
    for (int i = 0; i < n; i++) sum[myRank[i] = s[i]]++;//统计每个字符出现的次数
    for (int i = 1; i < m; i++) sum[i] += sum[i - 1];//sum[i]为小于等于i的元素的数目
    for (int i = n - 1; i >= 0; i--) SA[--sum[myRank[i]]] = i;//下标从0开始,所以先自减
    //SA[i]存储排名第i的后缀下标,SA[--sum[rank[i]]] = i 即下标为i的后缀排名为--sum[rank[i]],这很显然
    for (int len = 1; len <= n; len *= 2) {
        int p = 0;
        //直接用SA数组对第二关键字排序
        for (int i = n - len; i < n; i++) tp[p++] = i;//后面i个数没有第二关键字,即第二关键字为空,所以最小
        for (int i = 0; i < n; i++) {
            if (SA[i] >= len) tp[p++] = SA[i] - len;
        }
        //tp[i]存储按第二关键字排序第i的下标
        //对第二关键字排序的结果再按第一关键字排序,和长度为1的情况类似
        for (int i = 0; i < m; i++) sum[i] = 0;
        for (int i = 0; i < n; i++) sum[myRank[tp[i]]]++;
        for (int i = 1; i < m; i++) sum[i] += sum[i - 1];
        for (int i = n - 1; i >= 0; i--) SA[--sum[myRank[tp[i]]]] = tp[i];
        //根据SA和rank数组重新计算rank数组
        swap(myRank, tp);//交换后tp指向旧的rank数组
        p = 1;
        myRank[SA[0]] = 0;
        for (int i = 1; i < n; i++) {
            myRank[SA[i]] = cmp(tp, SA[i - 1], SA[i], len) ? p - 1 : p++;//注意判定rank[i]和rank[i-1]是否相等
        }
        if (p >= n) break;
        m = p;//下次基数排序的最大值
    }
    //求height
    int k = 0;
    n--;
    for (int i = 0; i <= n; i++) myRank[SA[i]] = i;
    for (int i = 0; i < n; i++) {
        if (k) k--;
        int j = SA[myRank[i] - 1];
        while (s[i + k] == s[j + k]) k++;
        height[myRank[i]] = k;
    }
}

int main() {
#ifdef ACM_LOCAL
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);
#endif

    char str[30000];
    cin >> str;
    int len = strlen(str);
    while (str[len - 1] == '0')
        len--;
    str[len++] = 0;
    get_SA(str, len, 128);
    bool flag = false;
    for (int i = SA[len - 1]; i < len; i++) {
        if (str[i] <= '9' && str[i] >= '0'){
            cout << str[i];
            flag = true;
        }
    }
    if (!flag)
        cout << "0";
    cout << endl;

    return 0;
}

总结

本人分别使用了两种方法AC,总体感觉如果掌握后缀数组的话这个题确实就是个板子题;但是用来练字符串Hash也是一个不错的选择。
若有不正确或是不清晰的地方,欢迎评论区指正~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值