【Codeforces #632 Div2】 C. Eugene and an array 动态规划 详解

Eugene likes working with arrays. And today he needs your help in solving one challenging task.

An array c is a subarray of an array b if c can be obtained from b by deletion of several (possibly, zero or all) elements from the beginning and several (possibly, zero or all) elements from the end.

Let’s call a nonempty array good if for every nonempty subarray of this array, sum of the elements of this subarray is nonzero. For example, array [−1,2,−3] is good, as all arrays [−1], [−1,2], [−1,2,−3], [2], [2,−3], [−3] have nonzero sums of elements. However, array [−1,2,−1,−3] isn’t good, as his subarray [−1,2,−1] has sum of elements equal to 0.

Help Eugene to calculate the number of nonempty good subarrays of a given array a.

Input
The first line of the input contains a single integer n (1≤n≤2×105) — the length of array a.

The second line of the input contains n integers a1,a2,…,an (−109≤ai≤109) — the elements of a.

Output
Output a single integer — the number of good subarrays of a.

Examples
inputCopy
3
1 2 -3
outputCopy
5
inputCopy
3
41 -41 41
outputCopy
3
Note
In the first sample, the following subarrays are good: [1], [1,2], [2], [2,−3], [−3]. However, the subarray [1,2,−3] isn’t good, as its subarray [1,2,−3] has sum of elements equal to 0.

In the second sample, three subarrays of size 1 are the only good subarrays. At the same time, the subarray [41,−41,41] isn’t good, as its subarray [41,−41] has sum of elements equal to 0.

题意:给一个数组,它有若干个子串,如果一个子串A满足它的所有子串集合都是和不为0的,那么这个A为一个good子串,问这个数组有多少个这样的子串

思路(DP):

比赛的时候,根本没想到,看着师兄20分钟切完C题,感慨自己还是太菜了
赛后对着师兄就几行AC的代码反复琢磨,理解后属实叹以为妙绝(这就是和dalao的差距
PS:题解较长,dalao可选择性跳过部分。
一看这个数据规模,基本上可以说是要O(n)的时间复杂度内解决的。但是,难点在于枚举所有子串的话,总是绕不开循环嵌套。不过,我们想,如果这个数组,没有任何一个子串和为0,那么累加以i位置结尾的子串数量即可。比如a = {3,3,3,3}, 它的数量和就是1+2+3+4=10。

顺着这个思路,如果以i位置结尾的所有子串中,含有和为0的子串,那么对应它就要减去这些个子串。举个例子,3 -3 1 1,下标从1开始 ,那么i=3为结尾的子串个数就要减去包含(-3,3)的,即是这个位置上满足题意的子串数量是3-1=2 个。换言之,如果有一个子串的和为0,那么所有以它为子串的序列都不能选。

OK,前面都是铺垫,有了前面的角度切入,我们不妨设dp[i]为以i位置结尾的子串中,不能选的子串个数,然后这个位置对答案的贡献就是i-dp[i],如上述例子,dp[3]等于1,代表它前面有1个子串不能选(含有和为0的子串),同理,dp[4] = 1,代表他不能选(-3,3,1,1)这个子串,因为含有(-3,-3)。

那么怎么给dp建立状态转移方程呢?我们发现,我们从前向后递推的时候,如果当前这个子串不能选,那么以i+1为结尾的子串中,也不能包含当前这个子串,很简单的例子,还是上面那个,所有i=2位置以后的都不能选包含(-3,3)的,也就是说,dp[i]可以由dp[i-1]转化过来,因为dp[i]存放的就是当前要剔除的子串数量,而当前的串又被后面的串所包含,所以当前要剔除的子串,后面一样要剔除,这就是为什么可以把dp[i]往后丢的原因。这是一种情况,可以从dp[i-1]转化过来,还有一种情况,就是当前这个位置的出现产生了一段和为0的子串!举个例子,3,-3,1,2在i=4时,产生了从i=2到i=4,且和为0的子串,这样的话,所有包含这个子串的串都要舍去,而我们又是以i=4结尾,那么当前位置长度超过3的子串都要舍去(也就是说以i=4结尾,{3,-3,1,2}这个串就不能选了,因为它包含了上述为0子串)。
图示就是这样:
在这里插入图片描述

针对上面说的第二种情况,要知道一个是否存在区间内出现和为0的子串,我们就可以先用map记录一下前缀和是否出现过,因为我们是从前往后读入,那么如果出现两个相同的前缀和,那么说明这一段之间的和为0,也就是sum[j]-sum[i] = 0,即是区间内【i+1,j】的和为0 。那上面做例子,sum[1]=3,sum[4]=3,说明【2,4】之间和为0 。所以,我们只需要用map来记录当前前缀和前一次出现位置,那么我们就可以得到当前“不能选”的区间长度了。然后综上这两种情况不能选的数量取个最大值,再累加当前i-dp[i]即可。所以状态转移方程就是

dp[i] = max(dp[i-1],pre_sum[sum[i]]+1);
其中pre_sum记录当前前缀和前一次出现位置。

综上,思路就说完了。相信你再看代码就应该能完全理解了。

#include <iostream>
#include <cstdio>
#include <vector>
#include <cmath>
#include <cstring>
#include <string>
#include <map>
#include <algorithm>
#define maxn 200000+500
using namespace std;
typedef long long ll;
ll dp[maxn];
ll sum[maxn];
ll a[maxn];
map<ll,ll> pre_sum;
int main()
{
    ll n;
    cin>>n;
    sum[0] = 0;;
    pre_sum[0] = 0;
    dp[0] = 0;
    ll ans = 0;
    for(ll i=1;i<=n;i++)
    {
          scanf("%lld",&a[i]);
          sum[i] = sum[i-1] + a[i];
          if(sum[i]!=0&&pre_sum[sum[i]]==0)  pre_sum[sum[i]] = -1;
          dp[i] = max(dp[i-1],pre_sum[sum[i]]+1);
          ans += i - dp[i];
          pre_sum[sum[i]] = i;
    }
    cout<<ans<<endl;
    return 0;
}

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值