https://codechef_shared.s3.amazonaws.com/download/translated/MARCH15/mandarin/STRSUB.pdf
先预处理一个数组pre[],pre[i]表示i这个位置,往前最多能找到哪个位置是满足0和1都不大于k的。
然后以每个位置i为左区间的长度就可以计算出,为 (r - pre[i] + 1)利用这个在预处理一个前缀和数组dp[]。
dp[i]表示:从1到i有多少个子串是满足的题目所给的条件。
注意:pre[]这个数组肯定是递增的 这个很容易证明。dp[i] = dp[i-1] + (i - pre[i] + 1);
(i - pre[i] + 1)为左区间的长度然后每次询问一个区间[l, r]
利用二分在pre[]数组找到一个位置pos,表示pos到r之间的位置的pre[i]都是大于l的,其中pre[]是一个单调递增的序列,这个可以证明。那么这部分的个数就可以计算出来,为dp[r] - dp[pos - 1]。
到了这里你可能会问,为什么不直接计算 dp[r] - dp[l-1]
因为 l~pos 中dp[i]可能会把 小于 l 的部分也计算进去,从而多计算。那么对于l到pos之间的个数, 由于这部分的pre全都是大于l的,
所以这部分每个位置的个数就是 (i - l + 1)
那么这部分的计算方式就是一个等差数列前n项目和。
1 + 2 + 3 + 4 + … + pos - l等差数列可以预处理出来 或者利用公式直接计算
两部分和加起来就是每次询问的答案了
AC代码
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <cstdlib>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int N = 1e5 + 10;
int pre[N];
char str[N];
ll S[N], dp[N];
void getS() {
S[0] = 0;
for(int i = 1; i < N; i++)
S[i] = S[i-1] + i;
}
//二分求出第一个满足 pre[pos] 大于L的位置
int find(int L, int R) {
int lo = L;
R++;
while(L < R) {
int M = L + (R-L)/2;
if(pre[M] < lo) L = M+1;
else R = M;
}
return L;
}
int main() {
getS();
int T;
int n, k, q;
scanf("%d", &T);
while(T--) {
scanf("%d%d%d", &n, &k, &q);
scanf("%s", str+1);
int one = 0, zero = 0, left = n+1;
for(int i = n; i >= 1; i--) { //求出i的左区间
while(one <= k && zero <= k && left > 0) {
left--;
if(str[left] == ‘0‘) zero++; else one++;
}
pre[i] = left+1;
if(str[i] == ‘0‘) zero--; else one--;
}
//dp[i]表示到当前i满足的所有子区间的个数
dp[0] = 0;
for(int i = 1; i <= n; i++)
dp[i] = dp[i-1] + (i-pre[i]+1);
int L, R;
while(q--) {
scanf("%d%d", &L, &R);
int pos = find(L, R);
ll ans = dp[R] - dp[pos-1];
ans += S[pos-L];
printf("%lld\n", ans);
}
}
return 0;
}
原文地址:http://blog.csdn.net/helloworld10086/article/details/44158675