【算法学习笔记】40.树状数组 动态规划 SJTU OJ 1289 扑克牌分组

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个数的加入 使得每一个分法的前面会重新组合.原本是负数的段落可能会变为正数

代码如下:

#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;
}

O(n^2)

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的加法操作数量少很多

代码如下:

#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;
}

树状数组

时间: 2024-10-08 02:17:59

【算法学习笔记】40.树状数组 动态规划 SJTU OJ 1289 扑克牌分组的相关文章

【算法学习笔记】83.排序辅助 动态规划 SJTU OJ 1282 修路

此题倒是能用贪心骗点分... 其实对于每一个位置 , 我们知道最后的改善结果一定是原数列中的数 . (因为要尽量减少消耗, 可以考虑减小至和相邻的相同) 有了这个结论之后, 我们就考虑用dp来做这件事情 首先 存下所有数据于 data[] 排序data 得到 data_sort[] 然后用dp[i][j]来表示 前i个元素 以data_sort[j]为结尾 转换成 递增序列的耗费. 那么我们可以知道 dp[i][j] = min({dp[i-1][k]}) + | data[i]-  data_

【算法学习笔记】89. 序列型动态规划 SJTU OJ 4020 数列游戏

http://acm.sjtu.edu.cn/OnlineJudge/problem/4020 一上手就来了一个删点 排序+DFS.... 虽然正确性没问题 但是超时 只有60分. 主要在于不知道怎么减少搜索量 思路就是删除一些肯定不能在的点, 然后经过条件判断 DFS地去搜索最长的路径 #include <iostream> #include <vector> #include <algorithm> #include <cstring> #include

【算法学习笔记】77.双线棋盘 动态规划 SJTU OJ 1263 纸来纸去

dp[i][j][k][l] 表示同时从(1,1)到(i,j)和从(1,1)到(k,l) 的 最大热心程度. (= = 三维的优化 有时间在搞..) 注意这里有个地方和别人不太一样,我是判断如果终点重复的时候,直接减去一次那个点得好心度,表示有一条经过时该位置的人的好心度是0: #include <iostream> using namespace std; int map[55][55]={0},dp[55][55][55][55]={0}; int max(int a,int b){ret

【算法学习笔记】91.简单森林计数 SJTU OJ 1045 二哥的家族

其实巨水...然而 不用scanf prinf 根本过不了.....真无聊 第一版代码有点问题 效率不高 主要是考虑到这个家族有可能一开始就是个森林 不是从树里分出去的 实际上数据点还是一棵树 然后变成的森林 这样的话只要三个数组就可以了 alive记录是否活着 sons记录每个人的子节点个数 father记录每个人的父节点 可以根据alive[father[x]]判断x是否是一个树的根节点 cnt 维护家族数目 每次死人的时候 判断死的人是某个树的根节点还是只是一个叶子节点 就可以了 #inc

【算法学习笔记】46.拓扑排序 优先队列 SJTU OJ 3010 Complicated Buttons

Description 凯恩在遗迹探险时遇到了n个按钮,刚开始所有按钮都处于开状态,凯恩的经验告诉他把所有按钮都关上会有“好事”发生,可是有些按钮按下时会让其他一些已经闭合的按钮弹开,经过凯恩研究,每个按钮都对应着一个固定的弹开集合,这个按钮按下时,弹开集合中所有的按钮都会变为开状态.现在小k想知道是否能让所有的按钮变为闭状态.如果能,打印最少步数以及方案,否则,打印“no solution”. Input Format 第一行一个整数n,表示n个按钮. 接下来n行,表示编号为1到n个按钮的弹开

【算法学习笔记】67.状态压缩 DP SJTU OJ 1383 畅畅的牙签袋

思想来自:http://blog.pureisle.net/archives/475.html 主要思想是用1和0来表示是否被填,然后根据两行之间的状态关系来构建DP方程. 1.首先初始化第一行 计算第一行可以被横着填的方案数.此时cnt是1 所以其实合法的dp[1][j]都是1 2.然后开始构建第二行至最后一行 构建每行时,枚举上一行的可行状态,cnt += 达到该状态的方法数,从而计算dp值. 对上一行的该状态进行取反操作,得到上一行是0的位置,把它们变成1,模拟竖着填. 然后和全集按位与操

【算法学习笔记】61.回溯法 DFS SJTU OJ 1106 sudoku

虽然DLX可以提高效率....但是对于NPC问题也不用太追求效率了,而且还只有一个测试点. 所以 只要DFS不断的填入,直到空格全部被填满:要注意的是DFS中全局变量的更新和恢复. 至于存储的方法,只要考虑每一行每一列每一个小块的不重复即可. #include <iostream> #include <cstring> using namespace std; int cnt = 0 ;//表示剩余的要填的空格的数目 struct point { int x,y; }; point

【算法学习笔记】62.状态压缩 DP SJTU OJ 1088 邮递员小F

状态压缩,当我们的状态太多时可以考虑用bit来存储,用二进制来表示集合,用&来取交集,用^来异或. DP过程很简单,遍历所有情况取最短路径就行,因为最短哈密顿回路本身就是一个NPC问题,效率不高. #include <vector> #include <iostream> using namespace std; //最短哈密顿回路问题 NP完全问题... int map[16][16]={0}; int n=0; const int INF=768000;//3000*1

【算法学习笔记】86.栈 中缀表达式 SJTU OJ 1033 表达式计算

...被输入给坑了 应该先把所有的空格删掉再玩  还有就是测试点里好像根本就没有关于后结合的事情...不过后结合也很简单 控制一下优先级的判断即可. 中缀表达式的处理核心就是两个堆栈的维护 一个是 操作符 一个是 操作数 只有当 当前正在处理的操作符的优先级大于(不考虑后结合时) 栈顶操作符的时候, 才进行计算.(或者出现右括号时 一直计算到左括号出现) 代码比较长 用了struct来存储token 比较清晰. #include <iostream> #include <stack>