小H的循环节(Hash表-字符串)


字符串Hash

最近遇到了很多字符串问题,

可是我既不会后缀DC3,也不会后缀自动机,后缀树……

于是,看到题解用Hash骗,小感动一下……(能看懂题解就是很幸福的事4……求D)

这题用Hash的话可以乱搞,记得答案不能二分

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<functional>
#include<iostream>
#include<cmath>
#include<cctype>
#include<ctime>
using namespace std;
#define For(i,n) for(int i=1;i<=n;i++)
#define Fork(i,k,n) for(int i=k;i<=n;i++)
#define Rep(i,n) for(int i=0;i<n;i++)
#define ForD(i,n) for(int i=n;i;i--)
#define RepD(i,n) for(int i=n;i>=0;i--)
#define Forp(x) for(int p=pre[x];p;p=next[p])
#define Fort(i,k,t,n) for(int i=k;i<=n;i+=t) 
#define Lson (x<<1)
#define Rson ((x<<1)+1)
#define MEM(a) memset(a,0,sizeof(a));
#define MEMI(a) memset(a,127,sizeof(a));
#define MEMi(a) memset(a,128,sizeof(a));
#define INF (2139062143)
#define F (100000007)
#define MAXN (200000+10)
#define MAXT (10+10)
long long mul(long long a,long long b){return (a*b)%F;}
long long add(long long a,long long b){return (a+b)%F;}
long long sub(long long a,long long b){return (a-b+(a-b)/F*F+F)%F;}
typedef long long ll;
typedef unsigned long long ull;
const ull base=10007;
int T,n;
char s[MAXN];
ull bin[MAXN],h[MAXN]={0};
ull hash(int a,int b){if (a>b) return 0;return h[b]-h[a-1]*bin[b-a+1];}
ull link(ull a,int len2,ull b){return a*bin[len2]+b;}
bool check1(int l)
{
	bool b=0;
	Fort(i,l+1,l,n)
	{
		int len=min(l,n-i+1);
		if (h[len]^hash(i,i+len-1))
		{
			if (b) return 0;b=1;
			int l=1,r=len,ans=0;
			while (l<=r)
			{
				int m=l+r>>1;
				if (hash(i,i+m-1)==h[m]) ans=m,l=m+1;
				else r=m-1; 
			}			
			if (i+len>n) len--;
			if (link(hash(i,i+ans-1),len-ans,hash(i+ans+1,i+len))==h[len]) i++;
			else return 0; 		
		}
	}
	return 1;
}
bool check2(int l)
{
	if (l+1>=n) return 1;
	{
		int i=l+2;
		int len=min(l,n-l-1);
		{
			int l=1,r=len,ans=0;
			while (l<=r)
			{
				int m=l+r>>1;
				if (hash(i,i+m-1)==h[m]) ans=m,l=m+1;
				else r=m-1; 
			}
			if (i+ans-1==n) return 1; 			
			if (link(h[ans],len-ans,hash(ans+2,len+1))==hash(i,i+len-1 )) i+=len;
			else return 0;
		}
		for(i;i<=n;)
		{
			int len=min(l,n-i+1);
			if (hash(l+2,l+2+len-1)==hash(i,i+len-1)) i+=len; 
			else return 0;
		}
	}
	return 1;
}
int main()
{
	freopen("h.in","r",stdin);
	freopen("h.out","w",stdout);
	scanf("%d",&T);
	bin[0]=1;
	For(i,MAXN-1) bin[i]=bin[i-1]*base;
	while (T--)
	{
		scanf("%d%s",&n,s+1); h[0]=0;
		For(i,n) h[i]=h[i-1]*base+s[i];
		For(i,n)
		{
		/*	if (i==1)
			{cout<<'d'<<endl;}*/
			if (check1(i)||check2(i)) {cout<<i<<endl;break;	}
		}
	}	
	
	
	return 0;
}



<think> 我们正在讨论字符串哈希函数参数的设计方法。根据引用材料和之前的讨论,哈希函数设计的关键在于减少冲突、保证分布均匀性和计算高效性。对于字符串哈希,常用的参数设计方法包括: 1. **多项式滚动哈希(Polynomial Rolling Hash)**: - 基本形式:$H(s) = \left( s_0 \cdot p^{n-1} + s_1 \cdot p^{n-2} + \cdots + s_{n-1} \cdot p^0 \right) \mod M$ - 参数选择: - 基数 $p$:通常选择一个质数,大于字符集大小(如ASCII字符集为128,则选131、13331等) - 模数 $M$:大质数(如$10^9+7$、$10^9+9$)或使用无符号整型的自然溢出(相当于模$2^{32}$或$2^{64}$) 2. **参数设计原则**: - **$p$的选择**:应避免与字符集的最大字符值有公因子,通常取质数。例如: - 对于小写字母字符串,$p=31$(或$53$)是常见选择 - 对于包含大写字母、数字的字符串,$p$至少应大于128(如$131$) - **$M$的选择**: - 需要足够大以减少冲突(理想情况是大于所有可能字符串的数量) - 但实际中难以满足,一般取大质数(如$10^9+7$)或利用自然溢出 - 若使用自然溢出,则不需要显式模运算,但可能增加冲突风险 3. **双哈希技术**: - 当需要高可靠性时,使用两个不同的哈希函数($H_1$和$H_2$): - $H_1(s) = \left( \sum s_i \cdot p_1^i \right) \mod M_1$ - $H_2(s) = \left( \sum s_i \cdot p_2^i \right) \mod M_2$ - 最终哈希值可以是$(H_1(s), H_2(s))$或组合形式 - 冲突概率降至$\frac{1}{M_1 \cdot M_2}$[^1] 4. **字符串哈希模板代码(C++)**: ```cpp typedef unsigned long long ull; const ull p = 131; // 基数 const ull M = 1e9+7; // 模数 ull compute_hash(string const& s) { ull hash_value = 0; ull power = 1; for (char c : s) { hash_value = (hash_value + (c - 'a' + 1) * power) % M; power = (power * p) % M; } return hash_value; } ``` 5. **避免哈希碰撞的实践技巧**: - **避免使用小模数**:如引用[2]所述,使用不可变对象作为键,同理,哈希函数本身也应避免可预测的碰撞 - **随机种子**:在哈希函数中加入随机种子(如$H(s) = H(s) \oplus seed$)可防止哈希洪水攻击 - **长度混合**:将字符串长度纳入哈希计算(如$H(s) = H(s) + len(s) \cdot p^{len(s)+1}$) 6. **性能优化**: - **预计算幂次**:对于多次调用的情况,预先计算$p$的各次幂 - **前缀哈希**:计算字符串所有前缀的哈希,可实现$O(1)$的子串哈希计算 7. **参数选择的数学依据**: - 冲突概率:由生日悖论,当哈希值均匀分布时,至少一次冲突的概率约为$1 - e^{-n^2/(2M)}$,其中$n$为键数量 - 因此,若期望冲突概率低于$\epsilon$,则需满足$M > \frac{n^2}{2\ln(1/(1-\epsilon))}$(例如$n=10^6$且$\epsilon=0.001$时$M > 5 \times 10^{11}$) ### 示例分析 假设设计一个处理最多$10^6$个字符串的哈希函数: - 选择$p=131$(经验值,适用于ASCII) - 选择$M=2^{64}$(自然溢出,利用`unsigned long long`自动取模) - 冲突概率:$\approx \frac{(10^6)^2}{2 \times 2^{64}} \approx 2.7 \times 10^{-9}$(接近0) ### 决策建议 ```mermaid graph TD A[字符串哈希设计] --> B{数据规模n} B -->|n < 10^4| C[单哈希+小模数 M=10^9+7] B -->|10^4 ≤ n ≤ 10^6| D[自然溢出或双哈希] B -->|n > 10^6| E[双哈希+大素数 M1,M2>10^9] A --> F{安全性要求} F -->|防攻击| G[随机种子+盐值] F -->|普通| H[固定p和M] ``` ### 常见问题 1. **为什么$p$必须是质数?** 质数$p$确保在模运算下形成循环群,减少周期性导致的碰撞(如$p$整除$M$时会产生规律碰撞)[^2]。 2. **如何处理空字符串?** 通常定义空字符串的哈希值为0(或一个非零常数,但需一致)。 3. **中文字符串如何处理?** 将中文字符转换为Unicode码点(如UTF-8编码后逐字节处理),或直接使用宽字符(但基数$p$需更大)。 --- §§ 相关问题 §§ 1. 如何评估一个字符串哈希函数的实际冲突率? 2. 在分布式系统中如何设计一致性哈希的参数? 3. 有哪些针对字符串哈希的已知攻击方式?如何防御? 4. 如何为特定字符集(如DNA序列:仅含ATCG)优化哈希参数? </think> ### 字符串哈希函数参数设计方法 字符串哈希函数的设计核心在于平衡**冲突率**、**计算效率**和**分布均匀性**。以下是关键设计要点: #### 一、核心参数设计原则 1. **基数 $p$(Base)的选择** - **必须为质数** 避免与字符编码值产生周期性冲突(如 ASCII 值多为偶数) - 推荐值:$p = 31$(小写字母场景)、$p = 131$(通用 ASCII)、$p = 13331$(Unicode) - **经验准则**:$p > \text{字符集大小}$ 例如 ASCII 字符集大小为 128,则 $p > 128$ 2. **模数 $M$ 的选择** - **大质数优先**:$M \geq \text{最大键数量}^2$(由生日悖论推导[^1]) $$\text{冲突概率} \approx 1 - e^{-n^2/(2M)} \quad (n=\text{键数量})$$ - **权衡方案**: - 内存敏感时:使用 $M = 2^k$(位运算优化) ```cpp uint32_t hash = key % (1 << 20); // M=2^{20}, 用 & 运算替代 % ``` - 高性能需求时:自然溢出($M=2^{32}$ 或 $2^{64}$) 3. **初始种子 $seed$** - 防御哈希洪水攻击:$seed = \text{random}() \oplus \text{time}()$ - 示例: ```cpp uint64_t seed = time(NULL) ^ 0xDEADBEEF; ``` #### 二、多项式滚动哈希设计模板 最常用的字符串哈希结构(参考引用[3]的编程思想): $$H(s) = \left( s[0] \cdot p^{k-1} + s[1] \cdot p^{k-2} + \cdots + s[k-1] \cdot p^0 \right) \mod M$$ ```cpp // C++ 实现示例 uint64_t poly_hash(const string& s, uint64_t p=131, uint64_t M=1e9+7) { uint64_t hash_val = 0; uint64_t power = 1; for (char c : s) { hash_val = (hash_val + (c - 'a' + 1) * power) % M; // 字符映射到1-26 power = (power * p) % M; // 更新幂次 } return hash_val; } ``` #### 三、冲突优化技术 1. **双哈希法** $$H_1(s) = \text{poly_hash}(s, p_1, M_1) \\ H_2(s) = \text{poly_hash}(s, p_2, M_2)$$ 最终键位置:$\text{pos} = (H_1 + i \cdot H_2) \mod m$($m$ 为桶数) 2. **长度混合** 解决 "ab" 和 "ba" 冲突: $$H(s) = H_{\text{poly}}(s) + \text{len}(s) \cdot p^{k+1}$$ 3. **边界强化** 强化首尾字符影响: $$H(s) = (s[0] \cdot p^{k} + H_{\text{poly}}(s) + s[k-1] \cdot p) \mod M$$ #### 四、参数选择决策树 ```mermaid graph TD A[设计字符串哈希] --> B{安全要求} B -->|防攻击| C[随机种子+双哈希<br>M1=1e9+7, M2=1e9+9] B -->|高性能| D[自然溢出<br>M=2^64] A --> E{字符集类型} E -->|ASCII| F[p=131] E -->|Unicode| G[p=13331] E -->|DNA/ATCG| H[p=5] A --> I{内存限制} I -->|紧张| J[M=2^20] I -->|宽松| K[M=大质数] ``` #### 五、验证参数有效性的方法 1. **雪崩测试** 修改 1 比特 → 哈希值应有 $\geq 50\%$ 比特翻转 2. **冲突测试** 用 10,000 个真实键值测试,冲突数 $\leq \sqrt{M}$ [^2] 3. **分布测试** 计算桶间方差:$\sigma^2 < \frac{\text{平均负载}}{2}$ > **案例**:Python 的字符串哈希(引用[2]) > 使用 $p=1000003$ 和 $M=2^{61}-1$(Mersenne 质数), > 通过预计算降低计算复杂度[^2]。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值