【[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\) 值就是

\[H(T) = (H(S + T) - H(S) * p^{length(T)})\ mod\ M \]

根据这个性质就可以进行运算。

所以在这个题目中前缀 \(Hash\) 值可以 $$Hash[k] = Hash[i] - Hash[i - k] * power[k]$$

后缀 \(Hash\) 值 $$Hash[k] = Hash[i - k + 1] - Hash[i + 1] * power[k]$$

之后就开始进行对每次分割的计算,对每次的结果进行比较和储存,得出答案,但是,我试了一下,发现 \(TLE\) 了两个点 第七个和第九个 (其实吸氧也能 \(AC\) )。

于是开始了我们的剪枝之路,发现了两个规率 :

  1. 因为存的是正反两个子串,所以再判重的时候只需判断前缀 \(Hash\) 是否相等即可,优化 \(0.1\) 秒。
  2. 对于每一个求出的答案,如果在后面的枚举中可分的段数还比已知答案要小,肯定不是最优解,所以直接排除即可,优化 \(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\) 声一片。

posted @ 2021-01-07 20:07  Ti_Despairy  阅读(75)  评论(0)    收藏  举报