#2024-03-08 贡献法
1、原理
枚举贡献用于求解一个字符串所有子串的某种性质,或者一个大区间中所有子区间的某种性质。因为子串或者子区间的枚举时间复杂度为O(n^2),所以改变思路,枚举字符串或区间中所有元素对子串或子区间对应性质的贡献,这样的话时间复杂度大概被优化为O(n)。通过把所有元素的贡献求和便可解决问题。
2、例题讲解
大致题意:对于一个全为小写字母组成的字符串S,我们定义S的分值f(S)为S中恰好出现一次的字符个数,求S中所有非空子串的分值和。
*****a*****a********a**** |
以上面的字符串为例,计算红色a(后面用A来表示)的贡献值,同理可以枚举剩下的25个字母。字母要想产生贡献,相应子串必须包含该字母,显然只有三种情况:
1、A是子串的左端点;2、A是子串的右端点;3、A是子串的一部分但不是端点。
1对应的贡献值就是A左侧不是A的字母数,同理2对应的贡献值就是A右侧不是A的字母书。
3对应的贡献值就是包含A的子串数,即在A左侧枚举左端点,在右侧枚举右端点,等于
(A左侧不是A的字母数) * (A右侧不是A的字母数)。
#include <cstdio>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 1e5 + 5;
int num[N], l[30][N], r[30][N];
char str[N];
int main()
{
scanf("%s", str);
int len = strlen(str);
LL res = len; // 所以字母单独出现一次都是分值为1的非空子串
for (int i = 0; i < len; i ++)
num[i] = str[i] - 'a'; // 将字母变成数字处理
for (int i = 0; i < 26; i ++) // 字符串中可能包含26种字母
{
for (int j = 0, cnt = 0; j < len; j ++) // 用cnt动态存储当前字母左侧相同字符的距离
{
if (num[j] == i) l[i][j] = cnt, cnt = 0;
else cnt ++;
}
for (int j = len - 1, cnt = 0; j >= 0; j --) // 用cnt动态存储当前字母右侧相同字符的距离
{
if (num[j] == i) r[i][j] = cnt, cnt = 0;
else cnt ++;
}
}
for (int i = 0; i < 26; i ++)
{
for (int j = 0; j < len; j ++)
{
res += (LL)l[i][j] * r[i][j] + l[i][j] + r[i][j]; // 计算每个字母的贡献
}
}
printf("%lld\n", res);
return 0;
}
后续修改:l和r数组由于是按位置储存,所以不需要开二维数组。
#include <cstdio>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 1e5 + 5;
int num[N], l[N], r[N];
char str[N];
int main()
{
scanf("%s", str);
int len = strlen(str);
LL res = len;
for (int i = 0; i < len; i ++)
num[i] = str[i] - 'a';
for (int i = 0; i < 26; i ++)
{
for (int j = 0, cnt = 0; j < len; j ++)
{
if (num[j] == i) l[j] = cnt, cnt = 0;
else cnt ++;
}
for (int j = len - 1, cnt = 0; j >= 0; j --)
{
if (num[j] == i) r[j] = cnt, cnt = 0;
else cnt ++;
}
}
for (int j = 0; j < len; j ++)
{
res += (LL)l[j] * r[j] + l[j] + r[j];
}
printf("%lld\n", res);
return 0;
}