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也是一个不错的选择。
若有不正确或是不清晰的地方,欢迎评论区指正~