Description
cxt的扑克牌越来越先进了,这回牌面的点数还可以是负数, 这回cxt准备给扑克牌分组,他打算将所有的牌分成若干个堆,每堆的牌面总和和都要大于零。由于扑克牌是按顺序排列的,所以一堆牌在原牌堆里面必须是连续的。请帮助cxt计算一下,存在多少种不同的分牌的方案。由于答案可能很大,只要输出答案除以1,000,000,009的余数即可。
Input Format
第一行:单个整数:N,1 ≤ N ≤ 10^6
第二行到N + 1行:在第i + 1行有一个整数:Ai, 表示第i张牌牌面的值。
Output Format
第一行:单个整数,表示分组方案数除以1,000,000,009的余数
Sample Input
4
2
3
-3
1
Sample Output
4
hint
分别是[2 3 − 3 1], [2 3 − 3] [1], [2] [3 − 3 1], [2] [3 − 3] [1]
注意: 题目描述有误 不是正数 是非负数
1.纯dp O(n^2)
纯dp的方法很简单, dp[i]表示前i个数的分法. 状态转移方程为 dp[i] = sigma(dp[j]) j的条件是 0<= j < i 且 presum[j]<=presum[i]
后者保证了 [j+1,i] 这个段落的和大于等于0
PS: 一个错误的dp策略是 认为如果num[i]>=0 and num[i]+num[i-1] >=0 的话 就 dp[i] = 2*dp[i-1]. 因为对于前i-1中分法的每一个分法的最后一个段落,可以认为num[i]放入还是不放入.这是错误的, 因为第i个数的加入 使得每一个分法的前面会重新组合.原本是负数的段落可能会变为正数
代码如下:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
#include <iostream> #include <cstdlib> #include <cstdio> #include <cstring> #define INF 1000000009 using namespace std; int* nums; int* sums; unsigned int* dp; int main(int argc, char const *argv[]) { int N; cin>>N; //动态数组 nums = new int[N+5]; sums = new int[N+5]; dp = new unsigned int[N+5]; //输入初始化 memset(sums,0,sizeof(int)*(N+5)); for (int i = 1; i <= N; ++i) { scanf("%d",&nums[i]); sums[i] = sums[i-1] + nums[i]; } dp[0] = 1; for (int i = 1; i <= N; ++i) { dp[i] = 0; for (int j = 0; j < i; ++j) if(sums[i]-sums[j]>=0)//表示从j+1到i 的和是整数可以成为一段 { dp[i] += dp[j]; dp[i] %= INF;// 容易写错 } } cout<<dp[N] % INF <<endl; delete[] nums; delete[] sums; delete[] dp; return 0; }
2.dp + 排序 去重 树状数组优化 O(nlgn)
O(n^2)会超时,最重要的原因是每个内层循环去寻找比presum[i]小的presum[j] 然后叠加dp[j]. 想优化这个过程 可以考虑先排序presum[],又因为相同的presum值,后来的presum对应的dp值可以根据之前的presum的dp值算出来,所以可以去重. 说的比较绕 举个例子把过程写出来也许会好一点
比如
5
2 3 -5 2 1
nums: 2 3 -5 2 1
原始presum:2 5 0 2 3
排序presum:0 2 2 3 5
去重uqsum:0 2 3 5 长度为4
树状数组c的下标从1开始一直到4
假设有一个dp数组,这个数组dpk表示的与O(n^2)中的dp[k]不同,这里的dpk指的是
//以下用dpk表示这个算法里的dp; dp[k]表示上一个算法里的dp
令tmpSum = uqsum[k-1] "在uqsum里第k个(下标为k-1)的那个前缀和"
当当前和是tmpSum时的总共的分法数目
比如dp1 表示的是 当前和为0时 分法有多少个 ; dp2 表示的是 当前和为2时 分法有多少个....
//注意 若有重复的当前和uqsum[k-1] dpk表示的是所有的累积 比如Step5的说明
根据树状数组的规则 把c数组和dp数组关联起来, 这样更新和查找都会变快
c1 = dp1
c2 = dp1+dp2
c3 = dp3
c4 = dp1+dp2+dp3+dp4
根据后继节点的定义: c1的后继是c2, c2的后继是c4,c3的后继是c4
根据前驱节点的定义: c3的前驱是c2 其他节点没有前驱节点
每次更新某一个树状数组的节点时,都要循环更新它的后继节点
每次获取某一个树状数组的节点开始的和时,要循环求出它的前驱节点的和 (从后面的例子中能体会到)
以上关于树状数组的理解来源于:http://www.cnblogs.com/xudong-bupt/p/3484080.html
-----------------------------------------
#####
Step1:设置所有的 负前缀和 和 0前缀和 的dp值为1 ,这个例子中没有负前缀和
此步之前
dp : 0 0 0 0
此步之后
dp: 1 0 0 0
相应的
c: 1 1 0 1 // 所以我们只要更新c1+1即可后继节点会跟着更新 update(c1,1)
#####
Step2: 获得第一个当前和 curSum = 0 + num[1] = 2 //因为此时的presum已经排序了不能用,必须重新构造原始的前缀和
去uqsum数组里找比2小的有几个,发现有1个,它对应的分法数为dp1 之前curSum=2 和 分法的数目的和 对应的是dp2 (比较绕,往后看会好一点)
(这里就是之前提到的,为什么可以去重的原因,因为每次更新都是在前一阶段的同一位置 += )
所以 dp2 += (dp1 + dp2)
此时dp是
dp: 1 1 0 0
相应的树状数组的操作就是: update(2,getSum(c2)) 让c2和c2的后继节点都加上上一阶段的c2,所以此后
c2 += c2 ; c4+=c2;
c: 1 2 0 2
#####
Step3: 加入第二个数 curSum = 0 + num[1] + num[2] = 5
在uqsum找到 有3个比5小的,他们分别对应 dp1,dp2,dp3 5对应的是dp4
所以 dp4 +=( dp1 + dp2 + dp3 + dp4 )
此时dp: 1 1 0 2
相应的树状数组操作就是: update(4,getSum(4))
c4 += c4
所以c: 1 2 0 4
#####
Step4: 加入第三个数 curSum = 0 + n1+n2+n3 = 0
在uqsum里找到0个比0小的, 所以只要让dp1 += (dp1)即可
()里面的dp1指的是上一次当前和是0的时候的分法数
dp为: 2 1 0 2 //注意第一个2的含义 是 第一次当前和为0是的dp[0]=1,加上第二次为0的dp[3] = 1
相应的 update(1,getSum(1))
c1 += c1
c为: 2 3 0 5
#####
Step5: 加入第四个数 curSum = 0 + n1+n2+n3+n4 = 2
此时再去uq里面找比2小的 发现有1个 对应dp1 自己是dp2
此时还未更新的dp2指的是上一次Step2中的dp2, 所以 dp2 += (dp1+dp2)
这时很好说明Step4里: dp1=2的作用 因为它记录了两次当前和为0的累积情况所以 +dp1的时候有两个效果
第一个效果是 从num[1]到num[4]这个整体作为一种分法 因为dp[0]=0
第二个效果是 从num[4]到num[4]这一个数作为一段,之前的作为一段的分法 因为dp[3]=0
此时dp为: 2 4 0 2
说回来,对应的树状数组的操作就是
c2 += c2; c4+=c2;
update(2,getSum(2))
此时c为: 2 6 0 8
#####
Step6: 加入最后一个数 curSum = 0 +n1+n2+n3+n4+n5 = 3
去uqsum中找到比3小的有2个 分别对应 dp1 dp2 加上上一阶段的 dp3
所以结果其实就是res = dp1+dp2+dp3 此时输出结果 退出即可.
树状数组的取值是 res = getSum(3);
虽然c3 = dp3 但是因为c3 是有前驱节点c2的
所以getSum(3) = c3 +c2 = dp3 + (dp1 + dp2) = 0 + 6
最终结果为6
------------------------------------
总结一下dp的变化过程
0 2 3 5 : 这个是uqsum
--------
0 0 0 0
1 0 0 0
1 1 0 0
1 1 0 2
2 1 0 2
2 4 0 2
最终输出 2+4+0
总结一下c的变化过程
0 2 3 5: 这个是uqsum
----------
0 0 0 0
1 1 0 1
1 2 0 2
1 2 0 4
2 3 0 5
2 6 0 8
最终输出 6+0
速度变快的原因是 c的加法操作数量比dp的加法操作数量少很多
代码如下:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
#include <iostream> #include <algorithm> using namespace std; int nums[100005] ={0};//存储原始数据 int presum[100005] ={0};//前缀和 后来排序 int uqsum[100005] ={0};//排序且去重的前缀和 int cnt = 0; //表示uq的长度 int c[100005] ={0};//树状数组 存储dp const int MOD = 1000000009; inline int lowbit(int cid){ return cid & (-cid); } inline int getSum(int cid){ //得到树状数组中id是cid的那个节点的值 int sum = 0; for (int i = cid; i >= 1 ; i -= lowbit(i)) //注意i的更新是找前驱节点 sum = (sum + c[i]) % MOD; return sum; } inline void update(int cid, int value){ //更新某一个c[cid]的值 value也是从c里取出来的 所以已经是几个dp的和了 for (int i = cid; i <= cnt; i += lowbit(i)) c[i] = (c[i] + value) % MOD; } inline int find(int key){ //find(key)+1 表示的是 再uqsum中比key小的个数+1 正好是c中对应的cid return lower_bound(uqsum,uqsum+cnt,key) - uqsum; //lower_bound返回的是uqsum中小于cnt的最后一个的指针 } int main(int argc, char const *argv[]) { //输入数据 并且同时计算原始前缀和数组 int N; cin>>N; presum[0]=0; for (int i = 1; i <= N; ++i) { cin>>nums[i]; presum[i] = presum[i-1] + nums[i]; } //对presum排序 注意presum的长度为N+1 sort(presum,presum+N+1); //去重 加入到uqsum中 uqsum[cnt] = presum[0];//因为presum[0]可能是负数 for (int i = 1; i < N+1; ++i) { if(presum[i] != uqsum[cnt]) uqsum[++cnt] = presum[i]; } cnt++;//cnt表示个数 //初始化c数组 update(find(0)+1,1);// 如果前缀和没有负数: 其实是dp1=1的意思 指的是前缀和为uqsum[0]的分法为1 // 如果前缀和里有负数: 就是 dp1 = dp2...= dpk = 1 其中uqsum[k-1] = 0 int res = 0; //记录每次的dp和, 用于更新下一阶段 int curSum = 0; for (int i = 1; i <= N; ++i)//针对每个前缀和 { curSum += nums[i]; int cid = find(curSum) + 1; //得到当前和对应的cid res = getSum(cid); // update(cid,res); // } return 0; }