1. 程式人生 > 實用技巧 >P3498 [POI2010]KOR-Beads 題解

P3498 [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;
}
  • 之後就開始進行對每次分割的計算,對每次的結果進行比較和儲存,得出答案,但是,我試了一下,發現 \(TLE\) 了兩個點 第七個和第九個 (其實吸氧也能 \(AC\) )。

  • 於是開始了我們的剪枝之路,發現了兩個規率 :

  1. 因為存的是正反兩個子串,所以再判重的時候只需判斷字首 \(Hash\) 是否相等即可,優化 \(0.1\) 秒。
  2. 對於每一個求出的答案,如果在後面的列舉中可分的段數還比已知答案要小,肯定不是最優解,所以直接排除即可,優化 \(0.69\) 秒。

之後我也想不出什麼可以優化的方法了,吸了口小氧就過去了,不吸氧的情況下是 \(1.5\) 秒,吸了氧是 \(339ms\)


【Code】

#include<cstdio>
#include<cmath>
#include<iostream>
#include<cstring>
#include<queue>
#include<algorithm>
#include<stdlib.h>
#include<time.h>
#include<map>
#include<vector>
#include<set>
#define ull unsigned long long
#define ll long long
#define M 1000010
#define N 1010
#define qaq cout<<"可行QAQ"<<endl
#define INF 0x3f3f3f3f
using namespace std;
const int mod1 = 19260817;
const int mod2 = 19660813;
const ull mod3 = 212370440130137957ll;
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\) 聲一片。