字符串Hash例题详解

Catalogue

    • 1.Codeforces 898F Restoring the Expression
    • 2.Codeforces 961F k-substrings
    • 3.Codeforces 985F Isomorphic Strings
    • 4.friends

1.Codeforces 898F Restoring the Expression

字符串Hash例题详解_第1张图片
字符串Hash例题详解_第2张图片
题目大意:给定一个字符串,将其划分成三个部分,使其满足一个加法等式。

设计思路:在这里首先我们考虑假定和为    n    \;n\; n位数,那么两个加数的位数肯定都不会超过    n    \;n\; n,并且其中一个加数一定是    n    \;n\; n位或者    n − 1    \;n-1\; n1位。这样我们先求出整个字符串的    H a s h    \;Hash\; Hash函数,这里    b a s e    \;base\; base    10    \;10\; 10便于加法计算,然后枚举等号的位置,判断三个字符串的关系。

Solution:

#include
using namespace std;
typedef long long ll;
const int maxn = 1e6 + 5;
const ll base = 10;
const ll mod1 = 1e9 + 7, mod2 = 1e9 + 9;
int len;
char s[maxn];
ll res1, res2;
ll Hash1[maxn], Hash2[maxn], p1[maxn], p2[maxn];
inline  ll getHash1(int l, int r)
{
	return ((Hash1[r] - Hash1[l - 1] * p1[r - l + 1]) % mod1 + mod1) % mod1;
}
inline ll getHash2(int l, int r)
{
	return ((Hash2[r] - Hash2[l - 1] * p2[r - l + 1]) % mod2 + mod2) % mod2;
}
inline void write(int pos1,int pos2)
{
	for (int i = 1; i <= pos1; i++)
	{
		putchar(s[i]);
	}
	putchar('+');
	for (int i = pos1 + 1; i <= pos2; i++)
	{
		putchar(s[i]);
	}
	putchar('=');
	for (int i = pos2 + 1; i <= len; i++)
	{
		putchar(s[i]);
	}
	putchar(10);
}
inline bool check(int l, int r)
{
	if (r - l > 1)		//b不是1位数,要判断其是否含前导0,如果有则不符合要求
	{
		if (s[l + 1] == '0')
			return false;
	}
	if (len - r > 1)	//x不是1位数,要判断其是否含前导0,如果有则不符合要求
	{
		if (s[r + 1] == '0')
			return false;
	}
	ll a1 = getHash1(1, l), a2 = getHash2(1, l);
	ll b1 = getHash1(l + 1, r), b2 = getHash2(l + 1, r);
	ll c1 = getHash1(r + 1, len), c2 = getHash2(r + 1, len);
	if ((a1 + b1) % mod1 == c1 && (a2 + b2) % mod2 == c2)
	{
		write(l, r);
		return true;
	}
	return false;
}
int main()
{
	cin >> s + 1;
	len = (int)strlen(s + 1);
	if (s[1] == '0')		//如果第一位是0,那么只能是0+b=c的情况
	{
		write(1, (len + 1) / 2);
		return 0;
	}
	p1[0] = p2[0] = 1;
	for (int i = 1; i <= len; i++)
	{
		Hash1[i] = (Hash1[i - 1] * base + s[i] - '0') % mod1;
		Hash2[i] = (Hash2[i - 1] * base + s[i] - '0') % mod2;
		p1[i] = p1[i - 1] * base % mod1;
		p2[i] = p2[i - 1] * base % mod2;
	}
	for (int i = 1, j = len / 2; i <= j; i++)
	{
		if (len - 2 * i <= i)
		{
			if (check(i, len - i))		//a与c等长的情况
			{
				break;
			}
		}
		if (len - 2 * i - 1 <= i + 1)		//a为n-1,c为n的情况
		{
			if (check(i, len - i - 1))
			{
				break;
			}
		}
		if (check(i, (len + i) / 2))		//b与c等差或相差1的情况
		{
			break;
		}		
	}
	return 0;
}

2.Codeforces 961F k-substrings

字符串Hash例题详解_第3张图片
字符串Hash例题详解_第4张图片
题目大意:给定一个字符串,对于它的所有子串    k − s u b s t r i n g s    \;k-substrings\; ksubstrings,问其前缀和后缀相同的最大长度,且长度必须为奇数,如果不存在则为    − 1    \;-1\; 1    k − s u b s t r i n g s    \;k-substrings\; ksubstrings是这样定义的:对于每个字符串,每次在开头和结尾删去    i    \;i\; i个字符后得到的字符串。

设计思路:对于这道题目,我们考虑用字符串    H a s h    \;Hash\; Hash来解决。先考虑朴素的枚举思想,即对于一个子串,从大到小枚举所有奇数前缀和后缀,然后根据Hash值来判断二者是否相等,复杂度在最坏情况下会达到    O ( n 2 )    \;O(n^2)\; O(n2),即使    4 s    \;4s\; 4s也不够解决。观察字符串的特点,当在计算    i    \;i\; i的最大公共前缀和后缀时,由于每次子串是两头向中间收缩,所以有这样一个结果,即    r e s [ i ] − 2 ≤ r e s [ i + 1 ]    \;res[i]-2≤res[i+1]\; res[i]2res[i+1],观察下图所示:
字符串Hash例题详解_第5张图片
在计算出第    i    \;i\; i位开始的子串的最大长度,紧接着计算    i + 1    \;i+1\; i+1位的长度,首先去掉首字符和尾字符,由于之前我们已经得到两部分子串长度相同,所以对于    i + 1    \;i+1\; i+1开始的子串的结果至少为上一次结果-2。也就是说对于上一次的前缀和后缀,去掉它们首字符和尾字符,中间部分一定是相同的,所以有    r e s [ i ] − 2 ≤ r e s [ i + 1 ]    \;res[i]-2≤res[i+1]\; res[i]2res[i+1],简单移项后就有    r e s [ i ] ≤ r e s [ i + 1 ] + 2    \;res[i]≤res[i+1]+2\; res[i]res[i+1]+2,问题恰好为求最大值,根据这个等式可以得到较长子串一定不会较短子串的结果+2,所以从中间开始枚举,然后对每个子串从上一次的结果开始,逐次-2,判断    H a s h    \;Hash\; Hash值是否相等。

Solution:

#include
using namespace std;
typedef long long ll;
const int maxn = 1e6 + 5;
const ll base = 131;
const ll mod = 1e9 + 7;
ll Hash[maxn], b[maxn];
char ch[maxn];
int n, t, res[maxn / 2];
inline int read()
{
	int t = 0;
	char ch = getchar();
	while (ch < '0' || ch > '9')
	{
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9')
	{
		t = (t << 3) + (t << 1) + (ch ^ 48);
		ch = getchar();
	}
	return t;
}
inline void write(int t)
{
	if (t < 0)
	{
		putchar('-');
		t = -t;
	}
	if (t > 9)
	{
		write(t / 10);
	}
	putchar(t % 10 + '0');
}
inline void init()
{
	b[0] = 1;
	for (int i = 1; i <= n; i++)
	{
		b[i] = (b[i - 1] * base) % mod;
	}
	for (int i = 1; i <= n; i++)
	{
		Hash[i] = (Hash[i - 1] * base + ch[i] - 'a' + 1) % mod;
	}
}
inline ll get_hash(int l, int r)
{
	return ((Hash[r] - Hash[l - 1] * b[r - l + 1]) % mod + mod) % mod;
}
inline void solve(int x)
{
	int y = t + t - x;
	if (!(n & 1))		//还是奇偶问题,偶数的时候右边界+1,简单模拟一下就可以发现
	{
		y++;
	}
	for (int i = res[x + 1] + 2; i >= 1; i -= 2)		//从上一次结果出发开始枚举
	{
		if (get_hash(x, x + i - 1) == get_hash(y - i + 1, y))
		{
			res[x] = i;
			return;
		}
	}
	res[x] = -1;
	return;
}
int main()
{
	n = read();
	scanf("%s", ch + 1);
	init();
	t = n / 2;			//分别处理字符串长度为奇数和偶数的情况
	if (n & 1)
	{
		t++;
		res[t] = -1;		//对于奇数长度的字符串,两端长度是关于一个字符对称的,正中间的字符一定没有前后缀
	}
	else
	{
		if (ch[t] == ch[t + 1])		//偶数长度则比较最中间两个字符即可
		{
			res[t] = 1;
		}
		else
		{
			res[t] = -1;
		}
	}
	for (int i = t - 1; i > 0; i--)
	{
		solve(i);		//从中间开始枚举,依此解决较长子串
	}
	for (int i = 1; i <= t; i++)
	{
		write(res[i]);
		if (i == t)
		{
			putchar(10);
		}
		else
		{
			putchar(' ');
		}
	}
	return 0;
}

3.Codeforces 985F Isomorphic Strings

字符串Hash例题详解_第6张图片
字符串Hash例题详解_第7张图片
题目大意:给定一个字符串,然后每次截取两端相等长度的子串,问这两个子串是否同构。(同构就是两个字符对应位置只有一一映射的关系,即不能多对一也不能一对多)

设计思路:如果简单采取一一映射的方法,对每次询问子串遍历,那么复杂度肯定也是无法满足需求。由于这里的问题是对于两个字符串而言,它们能否构成同构,而字符串    H a s h    \;Hash\; Hash的思想是将字符串映射为一个尽可能唯一的整数,通过比较两个整数来判断两个字符串是否相等,而这里两个字符串相等的关系不是仅仅的对应字符相等,而是要满足同构这种特殊相等关系,所以这时候不再对字符串进行    H a s h    \;Hash\; Hash,而对每个字符的位置    H a s h    \;Hash\; Hash,然后通过    H a s h    \;Hash\; Hash值判断是否同构。

如何理解对位置    H a s h    \;Hash\; Hash是这个问题的难点,首先要知道    H a s h    \;Hash\; Hash函数有一种类似前缀和的性质,每一位字符    H a s h    \;Hash\; Hash值是在上一位的结果基础上乘以进制再加上本位字符的数值大小。现在考虑两个字符串是否同构,那么来看如下一个例子;

比如对于字符串    a b c a b b a c d b c c    \;abcabbacdbcc\; abcabbacdbcc,选取它的两个子串如    b c a b b    \;bcabb\; bcabb    c d b c c    \;cdbcc\; cdbcc,肉眼可以直观看出这两个字符串是同构的,那么如何用计算机来判断呢?在这个例子中    f ( b ) = c    ,    f ( c ) = d , f ( a ) = b    \;f(b)=c\;,\;f(c)=d,f(a)=b\; f(b)=c,f(c)=d,f(a)=b,也就是说,每个字母和另一个有对应关系,有了这个想法,我们考虑将原字符串映射为26个01串,每个01串表示一个字母,该位置为这个字符为1其余为0,那么上述例子中就可以转化为100100100000, 010011000100, 001000010011, 000000001000…这时候问题就转化了,对于    f ( b ) = c    \;f(b)=c\; f(b)=c,在    b    \;b\; b转化的01串中,对应位置的子串为10011,在    c    \;c\; c转化的01串中,对应位置的子串为10011,这两个转化的子串是完全一致的,也就是说,对于    b    \;b\; b    c    \;c\; c,满足一一映射的关系,因为如果子串比如是…a…a…和…c…c…c…,那么对应的01串是…010…010…和…010…010…010…,这两个字符串一定是不相等的。所以判断两个字符串是否同构,只需要得到这两个字符串每个每个字母的    H a s h    \;Hash\; Hash,然后判断两个字符串的26个字母是否都存在一一对应的关系。

Solution:

#include
using namespace std;
typedef long long ll;
const int maxn = 2e5 + 6;
const ll base = 131;
const ll mod = 1e9 + 7;
int n, m;
char ch[maxn];
int x, y, len;
ll Hash[maxn][30], b[maxn];
inline int read()
{
	int t = 0;
	char ch = getchar();
	while (ch < '0' || ch>'9')
	{
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9')
	{
		t = (t << 3) + (t << 1) + (ch ^ 48);
		ch = getchar();
	}
	return t;
}
inline void init()
{
	b[0] = 1;
	for (int i = 1; i <= n; i++)
	{
		b[i] = (b[i - 1] * base) % mod;
	}
}
inline ll get_hash(int l, int r, int t)
{
	return (((Hash[r][t] - Hash[l - 1][t] * b[r - l + 1])) % mod + mod) % mod;
}
inline bool check()
{
	ll res_a[30], res_b[30];
	for (int i = 1; i <= 26; i++)		//得到两个字符串对于的26个Hash值
	{
		res_a[i] = get_hash(x, x + len - 1, i);
		res_b[i] = get_hash(y, y + len - 1, i);
	}
	//排序是为了便于比较
	sort(res_a + 1, res_a + 27);
	sort(res_b + 1, res_b + 27);
	for (int i = 1; i <= 26; i++)
	{
		if (res_a[i] != res_b[i])
		{
			return false;
		}
	}
	return true;
}
int main()
{
	n = read();
	m = read();
	scanf("%s", ch + 1);
	init();
	//将原字符串转换为26个01串
	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= 26; j++)
		{
			Hash[i][j] = (Hash[i - 1][j] * base % mod + (ll)(ch[i] == (j - 1 + 'a'))) % mod;
		}
	}
	while (m--)
	{
		x = read();
		y = read();
		len = read();
		if (check())
		{
			puts("YES");
		}
		else
		{
			puts("NO");
		}
	}
	return 0;
}

4.friends

题目大意:给定一个字符串,从中删除任一字符后得到的字符串能够分为两个相同的子串,如果不可能输出"NOT POSSIBLE",存在唯一解输出这个字符串,存在多解输出“NOT UNIQUE”。

设计思路:首先对于长度为偶数的字符串一定是不存在解的,删除一个字符后剩下的字符串不能分为两个等长的字符串。然后对于奇数长度的字符串,只需要枚举删除每一位字符后剩下的字符串分为两部分是否相等,如果相等则保存它的    H a s h    \;Hash\; Hash值,用于下次再存在解时判重。同时可以进一步优化,当枚举的字符位于前    n 2    \;\frac{n}{2}\; 2n时,拆分的其中一个字符串一定是原字符串的后    n 2    \;\frac{n}{2}\; 2n位,同样枚举到后一半的字符时其中一个字符串一定是原字符串的前    n 2    \;\frac{n}{2}\; 2n位,预处理这两个值避免每次枚举时重复计算。(    p s :    \;ps:\; ps:这道题卡了 1 e 9 + 7 1e9+7 1e9+7 1 e 9 + 9 1e9+9 1e9+9,所以模数要放大一些)

Solution:

#include
using namespace std;
#include
typedef unsigned long long ull;
const ull base = 131;
const int maxn = 2e6 + 5;
int n, x, pos;
char ch[maxn];
ull Hash[maxn], b[maxn];
ull ans, res, res1, res2;
inline void init()
{
	b[0] = 1;
	for (int i = 1; i <= n; i++)
	{
		b[i] = b[i - 1] * base;
		Hash[i] = Hash[i - 1] * base + (ull)(ch[i] - 'A' + 1);
	}
}
inline ull get_hash(int l, int r)
{
	return Hash[r] - Hash[l - 1] * b[r - l + 1];
}
inline bool check(int t)
{
	if (t >= x)
	{
		ans = get_hash(x, t - 1) * b[n - t] + get_hash(t + 1, n);
		return ans == res1;
	}
	else
	{
		ans = get_hash(1, t - 1) * b[x - t] + get_hash(t + 1, x);
		return ans == res2;
	}
}
int main()
{
	scanf("%d", &n);
	if (n == 1 || !(n & 1))
	{
		puts("NOT POSSIBLE");
		return 0;
	}
	scanf("%s", ch + 1);
	init();
	x = n / 2 + 1;
	res1 = get_hash(1, x - 1);
	res2 = get_hash(x + 1, n);
	for (int i = 1; i <= n; i++)
	{
		if (check(i))
		{
			if (!res)
			{
				res = ans;
			}
			else if (res != ans)
			{
				puts("NOT UNIQUE");
				return 0;
			}
			pos = i;
		}
	}
	if (!pos)
	{
		puts("NOT POSSIBLE");
		return 0;
	}
	if (pos < x)
	{
		for (int i = x + 1; i <= n; i++)
		{
			putchar(ch[i]);
		}
	}
	else
	{
		for (int i = 1; i < x; i++)
		{
			putchar(ch[i]);
		}
	}
	putchar(10);
	return 0;
}

有时间再更新啦 完结撒花!

你可能感兴趣的:(Problemset)