题目意思
给出一个数字串,问有多少子段满足,以某种方式将这个子段重新排列,使得这个字段可以分成两个完全相同的部分。
分析
这道题目,如果用
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 1−l每个数字出现次数的奇偶性与 $ 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;
}