AtCoder [ABC295D] Three Days Ago

题目意思

给出一个数字串,问有多少子段满足,以某种方式将这个子段重新排列,使得这个字段可以分成两个完全相同的部分。

分析

这道题目,如果用 O ( n 2 ) O(n^2) O(n2) 那么直接就TLE了,线性的求法好像一时之间也想不出来,那么就可以考虑 O ( n O(n O(n l o g log log n ) n) n)
O ( n O(n O(n l o g log log n ) n) n) 这种时间复杂度第一个想到的应该就是二分法了,那么二分谁呢?

重点部分

如果想要一个字段可以重新排列分成前后两个完全相同的部分,首先肯定得有偶数个数字,其次,每种数字都必须有偶数个。换言之,对于一个有偶数个数字每种数字都有偶数个的子段,必然可以通过重新排列分成两个完全相同的部分。

想知道一个子段中每种数字有几个,就自然而然的想到了前缀和。但是前缀和对二分法没有任何用处,甚至对整道题都是一个没有用的操作。

我们再进一步优化,我们只需要知道一个子段的每种数字是否有偶数个,并不需要知道具体数值,所以我们的前缀和只需要记录前 i i i 个数中是否有偶数个 j j j

这时候,我们会注意到,数字一共就十个,而每个数字只有两种情况:是奇数还是偶数。这个就是一个很典型的状态压缩

不过并不需要用二进制那么麻烦,直接十进制一位代表一个数字就可以。

这时候还是有一个麻烦的问题,状态压缩和二分有什么关系?

我们现在来想一想,前缀和是怎么求出一个子段中数字有多少个的,减法。那么这道题其实可以直接求出数字有多少个再 ÷ 2 \div 2 ÷2求出余数,但这样做就是典型的 O ( n 2 ) O(n ^ 2) O(n2) 做法

这时候需要用到一点数论知识,差的余数 = = = 余数的差,也就是说,如果我们求的每种数字的有奇数个还是偶数个的前缀和相减,如果差为 0 0 0,那么就是一个符合要求的子段。

说的有点抽象,其实很简单

                   0 3 4 2 1 3 3 1 3 1 2 
1有奇数个还是偶数个:  0 0 0 0 1 0 0 0 0 1 0
3有奇数个还是偶数个:  0 1 0 0 0 0 1 0 1 0 0 

由上图我们可以发现,在一个满足要求的子段 [ l , r ] [l, r] [l,r]中,如果 1 − l 1 - l 1l每个数字出现次数的奇偶性与 $ 1 - r $ 的一致,那么就是一个满足要求的子段。

那么就意味着如果两个状态压缩后的数相同, 必然有一个满足要求的子段

所以这就是需要二分的变量,其他的细节就看代码吧!

代码

#include <iostream>
#include <algorithm>
using namespace std;
const int NR = 500010;
int cnt[20][NR], a[NR]; // cnt[j][i]表示数字j在1 ~ i中出现的次数的奇偶性,如果出现了奇数次,那么它的值为1,否则为0
long long b[NR]; // 每个位置的状态
long long ans, n;
int Lower_bound(int l, int r, long long x)
{
	while (l <= r)
	{
		int mid = l + r >> 1;
		if (b[mid] >= x) r = mid - 1;
		else l = mid + 1;
	}
	return l;
}
int Upper_bound(int l, int r, long long x)
{
	while (l <= r)
	{
		int mid = l + r >> 1;
		if (b[mid] > x) r = mid - 1;
		else l = mid + 1;
	}
	return l;
}
int main()
{
	string s;
	cin >> s;
	n = s.length();
	for (int i = 0; i < n; i++)
		a[i+1] = s[i] - '0'; // 转化为数字
	for (int i = 1; i <= n; i++)
	{
		for (int j = 0; j <= 9; j++)
			cnt[j][i] = cnt[j][i-1];
		cnt[a[i]][i] = (cnt[a[i]][i - 1] + 1) % 2; // 求奇偶性
		for (int j = 0; j <= 9; j++)
			b[i] = b[i] * 10 + cnt[j][i]; // 状态
	}
	sort(b + 1, b + n + 1); // 切记要排序
	for (int i = 0; i <= n; i++) // 这里需要从0开始,就是从头开始到第 i 个时一个满足要求的子串
	{
		int pos1 = Lower_bound(i, n, b[i]);
		int pos2 = Upper_bound(i, n, b[i]); // 这里有个细节,就是在求的时候不能整个求,要从 i 到 n 中求与i相同的状态,避免重复。
		// cout << pos1 << " " << pos2 << '\n';
		ans += pos2 - pos1 - 1; // 求个数
	}
	cout << ans << '\n';
	return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值