前言
今天给大家带来两道贡献法的问题,先来讲一下什么是贡献法。
贡献法,与其说是一种算法,不如说是一种数学方法,是一种思维方式。
先来给大家举个例子,假设现在有个问题,需要你在一个只有小写字母的字符串中查找所有仅包含一个字符a
的子串数量。
暴力的话是枚举所有子串,O(n^2)
显然不符合我们的预期,那么有什么方法优化呢?
这就要用到我们今天要讲的贡献法了,这一种横看成岭侧成峰的算法,我们来观察下面的样例。
bbbbbacccc
可以发现在中间位置出现了a
,我们将a
的左右两侧划分出来。
bbbbb|a|cccc
问题就转化成了在a
的左边选一个字母,同时在a
的右边选一个字母的所有选法。
我们设左边的字符数量为left
,右边的字符数量为right
,那么根据排列组合的知识可以得到子串数量为:
(left + 1) * (right + 1)
注意这里要加一因为a
也要算进去。
以上就是贡献法的大致思路,所谓横看成岭侧成峰,就是从结果出发,逆推出有效的结论。
可能有的小伙伴发现了我们这个题目有一个要求,即——仅包含一个字符a
。
那我们就来想一下,如果没有这个条件,贡献法可不可行呢?
答案是否定的,我们来观察下面的序列
bbbbabbabbb
可以明显的发现最长的串是同时包含两个a
的,而我们使用贡献法就需要分别求出包含指定a
的所有子串,我们将这些子串视为一个集合。
在计算完所有包含指定的a
的所有子串后我们要进行集合合并,而根据容斥原理,集合合并时需要减去两个集合的交集,显然这个交集我们是不好求出来的。
造成这种情况的原因就是两个集合不是互斥的,放在概率论中就可以说两个条件不是独立的。
所以在判断能否使用贡献法之前需要先判断一下给定的条件能否使每个元素的贡献皆相互独立。
反之,如果发现条件能够使整个结果的集合划分为几个部分,那么就可以使用贡献法。(反应在题目中一般就是“唯一”,“单个”等词)
纸上学来终得钱,接下来我们就根据具体的题目分析,一起来看看所谓的“贡献法”到底有何魔力。
孤独的照片
分析
暴力枚举所有区间的话时间复杂度是O(n^2)
,显然行不通,但是我们发现所有的孤独的照片都有一个特点,即——同时包含两种牛,并且其中有一种牛的数量为1。
很敏锐的发现题目中有唯一的条件,那么可不可以用我们上面讲的贡献法来写呢?
使用贡献法需要满足一个条件,即——我们选定的贡献的对象可以将要求的集合划分。
很显然是可以的,因为两头可能孤独的牛不会出现在同一张“孤独的照片”内,所以任意两头牛产生的贡献的交集为空集,所以我们可以使用贡献法。
那么接下来我们就来思考一下如何使用贡献法。
根据上面的思路,我们需要通过排列组合来每头牛的贡献就要保证当前区间只有一头此类牛。
这个简单,我们预处理出每头牛左边和右边的同类牛的位置(使用扫描法),就可以计算出对于每头牛的最长的“孤独的照片”,随后进行排列组合就好了。
这道题排列组合有一个小技巧,因为有一个要求最短区间长度为3,所以我们需要分类讨论该位置在区间两边和在区间中间的情况。
代码
/*贡献法
*/
#include<iostream>
#include<cstring>
using namespace std;
typedef long long LL;
const int N = 500010;
char s[N];
int n;
int l[N], r[N];
LL ans;LL find(char x)
{int m = 0;memset(l, 0, sizeof(l)); memset(r, 0, sizeof(r));for(int i = 1; i <= n; i++)if(s[i] == x)l[i] = m, m = i;m = n + 1;for(int i = n; i >= 1; i--)if(s[i] == x)r[i] = m, m = i;// 预处理LL ls = 0;for(int i = 1; i <= n; i++){if(s[i] == x) //匹配了{int left = i - l[i] - 1, right = r[i] - i - 1; //左右长度//printf("%d %d %d ", i, left, right);ls += max((LL)0, (LL)left * right);ls += max(0, left - 1);ls += max(0, right - 1);}}return ls;
}int main()
{scanf("%d%s", &n, s + 1);ans += find('G');ans += find('H');printf("%lld", ans);return 0;
}
子串分值
分析
可以发现题目中有唯一的要求,所以我们考虑使用贡献法。
那么,接下来我们要如何来划分呢?
这个简单,我们通过26
个英文字母来划分,因为f
函数是求贡献和,所以我们每次只求单个字符的贡献,最后加起来就一定是贡献和。
但是如果这道题是要我们判断区间内存在个数为1
的字符的区间的数量,显然就无法直接用贡献法求解了,需要进一步的分析。
代码
// 贡献法,枚举26个字母,贡献求和
#include<iostream>
using namespace std;
typedef long long LL;
const int N = 100010;
char s[N];
int n, l[N], r[N];
LL idx;
LL find(char x)
{int m = 0; LL idx = 0;for(int i = 1; i <= n; i++)if(s[i] == x)l[i] = m, m = i;m = n + 1;for(int i = n; i >= 1; i--)if(s[i] == x)r[i] = m, m = i;for(int i = 1; i <= n; i++){if(s[i] == x){int left = i - l[i] - 1, right = r[i] - i - 1;idx += ((LL)left + 1) * (right + 1);}}return idx;
}int main()
{scanf("%s", s + 1);for(int i = 1; s[i]; n = i++);for(char x = 'a'; x <= 'z'; x++)idx += find(x);printf("%lld", idx);return 0;
}