003前缀和与差分——算法备赛

子串分值

蓝桥杯2020省赛题

问题描述

对于给定字符串s,s的分值f(s)为s中恰好出现一次的字符的个数。

给定一个字符串s,求该字符串所有非空子串的分值之和。

s的长度n,1<=n<=1^5;

思路分析

首先考虑暴力法,枚举每个子串,计算分数之和,假设计算每个子串的分数时间复杂度为O(1),那总复杂度也得时O(n^2),显然要超时。

看看暴力法有没有重复计算的地方?

例:定右端点遍历“ecacabc”,在求子串“ecacabc”的全部的以‘c’(最后那个‘c’)为右端点的子串分值时,可以发现“ecacabc”只比前面的“ecacab”多了一个‘c’,

  • f(“c”)=0+1
  • f("bc")=f("b")+1
  • f("abc")=f("ab")+1
  • f("cabc")=f("cab")-1
  • f("acabc")=f("acab")-1
  • f("cacabc")=f("cacab")
  • f("ecacabc")=f("ecacab")

记以下标i结尾的字符子串的分数之和为sum[i],对于上面的例子,sum[6]=sum[5]+3-2

进一步分析发现,以当前字符为右端点的所有子串的分值之和前面两个相同字符的位置有关,利用前面的计算结果可以在O(1)时间复杂度内计算以当前字符为右端点的所有子串的分值之和。

具体地,前前个相同的字符位置为l前一个相同的字符位置为rsum[i]=sum[i-1]+(i-r)-(l-r)

代码

#include <bits/stdc++.h>
using namespace std;
int main()
{
    
    
    string str; cin >> str;
    int n = str.size();
    //pre[i]={l,r} l表示str[i]前前个相同的下标,r表示str[i]前一个相同的下标,初始时都为-1
    //例 "ababc"  pre[2]={-1,0}
    vector<pair<int, int>>pre(n, {
    
     -1,-1 });  
    vector<int>d(26, -1);  //d[ch-'a']记录字符ch在str中上一次出现的下标.

    for (int i = 0; i < n; i++) {
    
    
        int& t = d[str[i] - 'a'];
        if (t == -1) pre[i].first = -1;  //前面未出现相同的字符
        else pre[i].first = pre[t].second;  //前面出现相同的字符
        pre[i].second = t;
        t = i;
    }


    long long ans = 0;
    long long sum = 0;
    for (int i = 0; i < n; i++) {
    
    
        long long add = i - pre[i].second;  //添加一个字符str[i]对sum的增量
        long long sub = pre[i].second - pre[i].first;  //添加一个字符str[i]对sum的减量
        sum += add - sub;
        ans += sum;
    }
    cout << ans;
    return 0;
}
统计子矩阵

蓝桥杯2022省赛题

给定N*M的矩阵A,统计多少个子矩阵(最小1*1,最大N*M)的所有数之和小于给定数

思路分析:

考虑枚举i行和j列,其中j>=i, 预先对每一列求前缀和s,s[a][b]表示第b列前a行的前缀和。

对于第i行到第j行,第k列的的和为C[k]=s[j][k]-s[i-1][k], k范围为[1,M]。

下一步问题变成给定数组C,求连续和不超过K的方案数。

对于数组C可以采用双指针维护右边界r时,左边最小为l,此时方案数加r-l+1. 每次右边界加1,通过保持当前不超过K来调整左边界。

时间复杂度O(n^2*M) 相比枚举每个右上角和左下角下标求子矩阵所有数之和的复杂度O(n^4)好多了。

#include <iostream>
#include<vector>
using namespace std;
const int N=502;
int a[N][N]={
    
    0};
int main()
{
    
    
  // 请在此输入您的代码
  ios::sync_with_stdio(false);
  cin.tie(0);
  int n,m,k;
  cin>>n>>m>>k;
  long long ans=0;
  for(int i=1;i<=n;i++){
    
    
    for(int j=1;j<=m;j++){
    
    
      cin>>a[i][j];
      a[i][j]+=a[i-1][j];  //a[i][j]为以i为下边界,第j列的前缀和
    }
  }
  for(int i=1;i<=n;i++){
    
    
    for(int j=i;j<=n;j++){
    
      //扫描线,确定上下边界[i,j]
      int sum=0;
      for(int l=1,r=1;r<=m;r++){
    
      //扫描线,确定左右边界、[l,r]  时间复杂度为O(m)
        sum+=a[j][r]-a[i-1][r];  //a[j][r]-a[i-1][r]为第r列中i行~j行的区间和。
        while(sum>k){
    
    
          sum-=a[j][l]-a[i-1][l];  //a[j][l]-a[i-1][l]为第l列i行~j行的区间和。
          l++;
        }
        /*r-l+1为纵向边界固定[i,j]横向边界[l,r]范围内符合子矩阵和不超过k的最大的横向距离
        横向边界在[l,r]范围内的子矩阵都满足条件,共有r-l+1个子矩阵。
        */
        ans+=r-l+1;  
      }
    }
  }
  cout<<ans;
  
  return 0;
}