题面 :
题意:
将一个数组拆成n个不为0的小段,问拆分的方法数。
思路:
开始很容易直接想到一个O(n^2) 的动态规划。
令dp[i]表示i个数不同种划分数,sum[j,i]表示数组下标从j到i的和。
从划分思想,比较容易想到:
dp[i]=∑dp[j] (j<i&&sum[j,i]!=0)
其中sum[j,i]可以由前缀数组O(1)的得到,具体见下面代码。
但明显这题N<1e5,复杂度不够。
继续优化。
对于dp[i]求和这一步我们同样用前缀数组优化。
定义: dp_sum[i] = ∑ dp[i]
那么对于dp[i] = dp_sum[i-1] - 数量(∑ sum[j+1,i]=0)
。
对于sum[j+1,i]=0
,我们两边同时加上sum[0,j],就变成:sum[0,i] = sum[0,j]
。问题变成减去出现sum[0,i] = sum[0,j]
的数量,也就是减去前缀和为sum[0,i]
的数量。
``于是我们只需要一个Hash表去保存前缀和sum[0,i]
出现的个数。时间复杂度O(1)求出sum[0,i]
的数量。
则转移方程:dp[i] = dp_sum[i-1] - Hash[sum[0,i]]
,具体见代码。
代码
O(n^2) TLE代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+10;
const int mod= 1e9+7;
ll dp[N],sum[N];
int main(){
int n;
while(~scanf("%d",&n)){
memset(dp,0,sizeof(dp));
memset(sum,0,sizeof(sum));
sum[0] = 0;
for(int i=1;i<=n;i++){
int x;scanf("%d",&x);
sum[i] = sum[i-1] + x;
}
dp[0] = 1;
for(int i=1;i<=n;i++){
for(int j=0;j<i;j++){
if(sum[i]-sum[j]!=0){
dp[i]+=dp[j];
}
}
//cout<<dp[i]<<" ";
}
//cout<<endl;
printf("%d\n",dp[n]);
}
}
O(n)正解代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+10;
const int mod= 1e9+7;
ll dp[N],sum[N],sum_dp[N];
map<ll,ll> Hash;
int main(){
int n;
while(~scanf("%d",&n)){
memset(dp,0,sizeof(dp));
sum[0] = 0;
for(int i=1;i<=n;i++){
int x;scanf("%d",&x);
sum[i] = sum[i-1] + x;
}
dp[0] = 1;
sum_dp[0] = 1;
Hash[0] = 1;
for(int i=1;i<=n;i++){
dp[i] = (sum_dp[i-1] - Hash[sum[i]] + mod)%mod;
sum_dp[i] = (sum_dp[i-1] + dp[i] + mod)%mod;
Hash[sum[i]] = (Hash[sum[i]] +dp[i] + mod)%mod;
//cout<<dp[i]<<" ";
}
//cout<<endl;
printf("%lld\n",dp[n]);
}
}