【[POI2010]KOR-Beads】 题解
【思路分析】
都标着 \(Hash\) 的标签,所以一般要用 \(Hash\) 做瞎编中;
此方法极其暴力,但是至少我 \(AC\) 了。
总体思路 :
可以求这 \(n\) 个数的前后缀 \(Hash\)值,之后从每 \(1\) 个分为一段到每 \(n - 1\) 个分为一段,依次进行判断。
因为题目中说 : 子串都是可以反转的 ,所以首先求前后缀 \(Hash\) 值,后缀 \(Hash\) 值就是把前缀 \(Hash\) 值的求法反过来。
【Code】
void prepare()//前后缀Hash值和进制。
{
po[0] = 1;
for(int i = 1;i <= n;i ++) {//进制
po[i] = po[i - 1] * p;
}
for(int i = 1;i <= n;i ++) {
Hash_z[i] = Hash_z[i - 1] * p + s[i];//前缀Hash值
}
for(int i = n;i >= 1;i --) {
Hash_f[i] = Hash_f[i + 1] * p + s[i];//后缀Hash值
}
return;
}
如何来求一个子串的 \(Hash\) 值 :
如果我们已知字符串 \(S\) 的 \(Hash\) 值是 \(H(S)\) ,字符串 \(S + T\) 的 \(Hash\) 值是 \(H(S + T)\) ,那么字符串 \(T\) 的 \(Hash\) 值就是
根据这个性质就可以进行运算。
所以在这个题目中前缀 \(Hash\) 值可以 $$Hash[k] = Hash[i] - Hash[i - k] * power[k]$$
后缀 \(Hash\) 值 $$Hash[k] = Hash[i - k + 1] - Hash[i + 1] * power[k]$$
之后就开始进行对每次分割的计算,对每次的结果进行比较和储存,得出答案,但是,我试了一下,发现 \(TLE\) 了两个点 第七个和第九个 (其实吸氧也能 \(AC\) )。
于是开始了我们的剪枝之路,发现了两个规率 :
- 因为存的是正反两个子串,所以再判重的时候只需判断前缀 \(Hash\) 是否相等即可,优化 \(0.1\) 秒。
- 对于每一个求出的答案,如果在后面的枚举中可分的段数还比已知答案要小,肯定不是最优解,所以直接排除即可,优化 \(0.69\) 秒。
之后我也想不出什么可以优化的方法了,吸了口小氧就过去了,最后不吸氧的情况下是 \(1.5\) 秒(第九个点超时),吸了氧是 \(339ms\) 。( \(STL\) 的常数太大了,用数组来模拟时间要少)。
【Code】
#include<cstdio>
#include<cmath>
#include<iostream>
#include<cstring>
#include<queue>
#include<algorithm>
#include<map>
#include<vector>
#include<set>
#define ull unsigned long long
#define ll long long
#define M 1000010
#define N 1010
#define INF 0x3f3f3f3f
using namespace std;
const int p = 10007;
/*================================================*/
int n;
int s[M];
ull Hash_z[M], Hash_f[M], po[M];;//正Hash值 ,反Hash值,进制
int cnt;//计算有几个最优解
int ans_cnt;//看可分为几段不同的值
vector<ull> qp;//储存每次分割的答案
int maxn = -1e5;//计算最终的可分的最大的段数
vector<int> ans;//储存每个最优解
int m; //剪枝用
/*================================================*/
inline int read()
{
int s = 0, f = 0;char ch = getchar();
while (!isdigit(ch)) f |= ch == '-', ch = getchar();
while (isdigit(ch)) s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
void prepare()//前后缀Hash 值和进制位
{
po[0] = 1;
for(int i = 1;i <= n;i ++) {
po[i] = po[i - 1] * p;
}
for(int i = 1;i <= n;i ++) {
Hash_z[i] = Hash_z[i - 1] * p + s[i];
}
for(int i = n;i >= 1;i --) {
Hash_f[i] = Hash_f[i + 1] * p + s[i];
}
return;
}
bool check(int x)//判断函数,判断是否有这个子串
{
for(int i = 0;i <qp.size(); i ++) {
if(qp[i] == x) return false;
}
return true;
}
void solve(int k)
{
ans_cnt = 0;
for(int i = k;i <= n;i += k) {
int sum_z = Hash_z[i] - Hash_z[i - k] * po[k];
//这个子串前缀Hash值
int sum_f = Hash_f[i - k + 1] - Hash_f[i + 1] * po[k];//后缀
if(check(sum_z)) {//判断
qp.push_back(sum_z);
ans_cnt++;//累加可分的段数
//if(check(sum_f)) //剪枝 1 ,效果如上
qp.push_back(sum_f);
}
}
if(maxn == ans_cnt) {//当已知的最优解和新求出的解相同时
ans.push_back(k);//记录k 值
cnt++;//最优解个数++
} else if(maxn < ans_cnt) {//又新求出更优的解
ans.clear();//清空原来的答案序列
ans.push_back(k);//添加解
maxn = ans_cnt;
cnt = 1;
}
if(ans_cnt != 0) {
m = min(m,n / ans_cnt);//剪枝 2 效果如上
}
qp.clear();//多次分割记得清空
}
/*=================================================*/
signed main()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
n = read();
for(int i = 1;i <= n;i ++) {
s[i] = read();
}
prepare();
m = n;
for(int i = 1;i <= m;i ++) solve(i);
printf("%d %d\n",maxn,cnt);
for(int i = 0;i <ans.size();i ++) printf("%d ",ans[i]);//输出
return 0;
}
小错误
取底数的时候要取个大点的值(或者写双 \(Hash\)),利用 \(unsigned\) \(long\) \(long\) 的自然溢出来判断,否则很容易和我一样,用 \(31\) 来做进制,使得 \(WA\) 声一片。