树状数组系列题目总结
树状数组详解
第01讲什么是树状数组?树状数组用来求区间元素和,求一次区间元素和的时间效率为O(logn)。
有些同学会觉得很奇怪。
用一个数组S[i]保存序列A[]的前i个元素和,那么求区间i,j的元素和不就为S[j]-S[i-1],那么时间效率为O(1),岂不是更快?但是,如果题目的A[]会改变呢?例如:我们来定义下列问题:我们有n个盒子。
可能的操作为1.向盒子k添加石块2.查询从盒子i到盒子j总的石块数自然的解法带有对操作1为O(1)而对操作2为O(n)的时间复杂度。
但是用树状数组,对操作1和2的时间复杂度都为O(logn)。
第02讲图解树状数组C[]现在来说明下树状数组是什么东西?假设序列为A[1]~A[8]网络上面都有这个图,但是我将这个图做了2点改进。
(1)图中有一棵满二叉树,满二叉树的每一个结点对应A[]中的一个元素。
(2)C[i]为A[i]对应的那一列的最高的节点。
现在告诉你:序列C[]就是树状数组。
那么C[]如何求得?C[1]=A[1];C[2]=A[1]+A[2];C[3]=A[3];C[4]=A[1]+A[2]+A[3]+A[4];C[5]=A[5];C[6]=A[5]+A[6];C[7]=A[7];C[8]= A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];以上只是枚举了所有的情况,那么推广到一般情况,得到一个C[i]的抽象定义:因为A[]中的每个元素对应满二叉树的每个叶子,所以我们干脆把A[]中的每个元素当成叶子,那么:C[i]=C[i]的所有叶子的和。
现在不得不引出关于二进制的一个规律:先仔细看下图:将十进制化成二进制,然后观察这些二进制数最右边1的位置:1 --> 000000012 --> 000000103 --> 000000114 --> 000001005 --> 000001016 --> 000001107 --> 000001118 --> 000010001的位置其实从我画的满二叉树中就可以看出来。
线段树以及数组数组类型各题总结-acm
线段树之hdu4027Can you answer these queries?Time Limit: 4000/2000 MS (Java/Others) Memory Limit: 65768/65768 K (Java/Others)Total Submission(s): 4014 Accepted Submission(s): 979Problem DescriptionA lot of battleships of evil are arranged in a line before the battle. Our commander decides to use our secret weapon to eliminate the battleships. Each of the battleships can be marked a value of endurance. For every attack of our secret weapon, it could decrease the endurance of a consecutive part of battleships by make their endurance to the square root of it original value of endurance. During the series of attack of our secret weapon, the commander wants to evaluate the effect of the weapon, so he asks you for help.Y ou are asked to answer the queries that the sum of the endurance of a consecutive part of the battleship line.Notice that the square root operation should be rounded down to integer.InputThe input contains several test cases, terminated by EOF.For each test case, the first line contains a single integer N, denoting there are N battleships of evil in a line. (1 <= N <= 100000)The second line contains N integers Ei, indicating the endurance value of each battleship from the beginning of the line to the end. Y ou can assume that the sum of all endurance value is less than 263.The next line contains an integer M, denoting the number of actions and queries. (1 <= M <= 100000)For the following M lines, each line contains three integers T, X and Y. The T=0 denoting the action of the secret weapon, which will decrease the endurance value of the battleships between the X-th and Y-th battleship, inclusive. The T=1 denoting the query of the commander which ask for the sum of the endurance value of the battleship between X-th and Y-th, inclusive.OutputFor each test case, print the case number at the first line. Then print one line for each query. And remember follow a blank line after each test case.Sample Input101 2 3 4 5 6 7 8 9 1050 1 101 1 101 1 50 5 81 4 8Sample OutputCase #1:1976Source/*一个10^5的序列,有10^5个操作,每个操作为a,b,ca=0时将b到c区间内的数都开根号下取整,a=1时求b到c段的和其中所有数的和不超过2^63。
树状数组
2 区间更新,单点求和
举个具体的栗子。。。
这道题目就是
意思挺明显的。。。 就是更改,查询。。。。
再举个特别的栗子。。。
求逆序数~
例如在序列 { 2, 4, 3, 1 } 中,逆序依次为 (2,1), (4,3), (4,1), (3,1),因此该序列的逆序数为 4。
所以如何求一个数组中逆序数的个数呢。。
//而且是用树状数组的方法。。。
例如 1 5 2 4 3
从输入顺序考虑: 输入1 ,无逆序;
输入 5, 无逆序;
输入 2,有逆序,因为前面有个5,有一个逆序对,这个逆序 对也可以这样考虑,此刻2所在的位置为3,而此刻小于等于2 的数有2 个,所以3-2=1; 输入 4,有逆序,同上,4-3=1; 输入 3 ,有逆序,同上,5-3=2; 所以~ ans=1+1+2~~~
3 树状数组的两类操作 1 单点更新,区间求和 1 一维树状数组,单点更新,区间求和 比如要更新点x ,x点的值加上val即调用add(x , val) , 求区间[1 , x]的和即为getSum(x) int lowbit(int x) { return x&(-x); } int getSum(int x){ int sum = 0; while(x){ sum += treeNum[x]; x -= lowbit(x); } return sum; } void add(int x , int val){ while(x <=N){ treeNum[x] += val; x += lowbit(x); } }
问题的提出
有一个一维数组,长度为n。
树状数组
树状数组武钢三中 吴豪【引言】在解题过程中,我们有时需要维护一个数组的前缀和S[i]=A[1]+A[2]+...+A[i]。
但是不难发现,如果我们修改了任意一个A[i],S[i]、S[i+1]...S[n]都会发生变化。
可以说,每次修改A[i]后,调整前缀和S[]在最坏情况下会需要O(n)的时间。
当n非常大时,程序会运行得非常缓慢。
因此,这里我们引入“树状数组”,它的修改与求和都是O(logn)的,效率非常高。
【理论】为了对树状数组有个形 象的认识,我们先看下面这张图。
如图所示,红色矩形表示的数组C[]就是树状数组。
这里,C[i]表示A[i-2^k+1]到A[i]的和,而k则是i在二进制时末尾0的个数,或者说是i用2的幂方和表示时的最小指数。
( 当然,利用位运算,我们可以直接计算出2^k=i&(i^(i-1)) )同时,我们也不难发现,这个k就是该节点在树中的高度,因而这个树的高度不会超过logn。
所以,当我们修改A[i]的值时,可以从C[i]往根节点一路上溯,调整这条路上的所有C[]即可,这个操作的复杂度在最坏情况下就是树的高度即O(logn)。
另外,对于求数列的前n项和,只需找到n以前的所有最大子树,把其根节点的C加起来即可。
不难发现,这些子树的数目是n在二进制时1的个数,或者说是把n展开成2的幂方和时的项数,因此,求和操作的复杂度也是O(logn)。
接着,我们考察这两种操作下标变化的规律:首先看修改操作:已知下标i,求其父节点的下标。
我们可以考虑对树从逻辑上转化:如图,我们将子树向右对称翻折,虚拟出一些空白结点(图中白色),将原树转化成完全二叉树。
有图可知,对于节点i,其父节点的下标与翻折出的空白节点下标相同。
因而父节点下标 p=i+2^k (2^k是i用2的幂方和展开式中的最小幂,即i为根节点子树的规模)即 p = i + i&(i^(i-1)) 。
接着对于求和操作:因为每棵子树覆盖的范围都是2的幂,所以我们要求子树i的前一棵树,只需让i减去2的最小幂即可。
树常见题目总结
树常见题⽬总结1.树的前序遍历递归:class Solution {public:vector<int> preorderTraversal(TreeNode* root) {vector<int> ans;preorder(ans,root);return ans;}void preorder(vector<int> &ans,TreeNode* root){if(root == NULL)return;ans.push_back(root->val);preorder(ans,root->left);preorder(ans,root->right);}};⾮递归:class Solution {public:vector<int> preorderTraversal(TreeNode* root){vector<int> res;stack<TreeNode*> tmp;if(root != NULL){tmp.push(root);}while(!tmp.empty()){res.push_back(tmp.top() -> val);root = tmp.top();tmp.pop();if(root -> right != NULL){tmp.push(root -> right);}if(root -> left != NULL){tmp.push(root -> left);}}return res;}};2.中序遍历递归:class Solution {public:vector<int>inorderTraversal(TreeNode* root){vector<int>res;inorder(root,res);return res;}void inorder(TreeNode* root,vector<int>& res){if(root == NULL){return;}inorder(root -> left,res);res.push_back(root -> val);inorder(root -> right,res);}};⾮递归:class Solution {public:vector<int> inorderTraversal(TreeNode* root) { vector<int> res;stack<TreeNode*>tmp;while(root != NULL || !tmp.empty()){if(root != NULL){tmp.push(root);root = root -> left;}else{root = tmp.top();res.push_back(root -> val);tmp.pop();root = root -> right;}}return res;}};3.后序遍历递归:class Solution {public:vector<int> postorderTraversal(TreeNode* root){ vector<int>res;postorder(res,root);return res;}void postorder(vector<int> &res,TreeNode* root){ if(root == NULL){return;}postorder(res,root -> left);postorder(res,root -> right);res.push_back(root -> val);}};⾮递归:class Solution {public:vector<int> postorderTraversal(TreeNode* root) { vector<int> res;stack<TreeNode*> tmp1;stack<TreeNode*> tmp2;if(root != NULL){tmp1.push(root);while(!tmp1.empty()){root = tmp1.top();tmp2.push(tmp1.top());tmp1.pop();if(root -> left != NULL)tmp1.push(root -> left);if(root -> right != NULL)tmp2.push(root -> right);}}while(!tmp2.empty()){res.push_back(tmp2.top() -> val);tmp2.pop();}return res;}};4.重建⼆叉树题⽬描述:输⼊某的前序遍历和中序遍历的结果,请重建出该⼆叉树。
数据结构树形结构章节练习含答案
数据结构…树形结构章节练习一.单项选择题1,如图所示的4棵二叉树中,_c_不是完全二叉树。
(A) (B) (C) (D)2.如图所示的4棵二叉树,_b_是平衡二叉树。
(A) (B) (C) (D)在线索化二叉树中,t所指结点没有左子树的充要条件是_b—。
A) t->left=NULL B) t->ltag= 1C) t->Itag=l且t->lcft=NULL D)以上都有不对二叉树按某种顺序线索化后,任一结点均有指向其前驱和后继的线索,这种说法_b—oA)正确B)错误二叉树的前序遍历序列中,任意一个结点均处在其子女结点的前而,这种说法_a—°A)正确B)错误由于二叉树中每个结点的度最大为2,所以二叉树是一种特殊的树,这种说法_b—o A)正确B)错误设高度为h的二叉树上只有度为0和度为2的结点,则此类二叉树中所包含的结点数至少为_b—。
A) 2hB)2h-l C) 2h+l D)h+1如图所示二叉树的中序遍历序列是_b —。
A) abcdgef B) dfebage C) dbaefcg D) defbagc9•已知某二叉树的后序適历序列是dabec,中序遍历序列是debac,它的前序遍历序列是_d—°A) acbed B) decab C) deabc D) cedba如果T2是由有序树T转换而来的二叉树,那么T中结点的前序就是T2中结点的—。
A)前序B)中序C)后序D)层次序如果T2是由有序树T转换而来的二叉结,那么T中结点的后序就是T2中结点的_b—。
A)前序B )中序C)后序D)层次序某二叉树的前序遍历结点访问顺序是abdgcefh, 中序遍历的结点访问顺序是dgbaechf,则其后序遍历的结点访问顺序是_d _____________________ 。
A) bdgcefha B) gdbecflia C) bdgaechf D) gdbehfca二叉树为二叉排序树的充分必要条件是其任一结点的值均大于英左孩子的值、小于其右孩子的值。
树状数组
树状数组是一个查询和修改复杂度都为log(n)的数据结构,假设数组a[1..n],那么查询a[1]+...+a[n]的时间是log级别的,而且是一个在线的数据结构,支持随时修改某个元素的值,复杂度也为log级别。
来观察这个图:令这棵树的结点编号为C1,。
令每个结点的值为这棵树的值的总和,那么容易发现:C1 = A1C2 = A1 + A2C3 = A3 C4 = A1 + A2 + A3 + A4C5 = A5C6 = A5 + A6C7 = A7C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8...C16 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8 + A9 + A10 + A11 + A12 + A13 + A14 + A15 + A16这里有一个有趣的性质:设节点编号为x,那么这个节点管辖的区间为2^k(其中k为x二进制末尾0的个数)个元素。
因为这个区间最后一个元素必然为Ax,所以很明显:Cn = A(n –2^k + 1) + ... + An算这个2^k有一个快捷的办法,定义一个函数如下即可:int lowbit(int x){return x&(x^(x–1));}当想要查询一个SUM(n)(求a[n]的和),可以依据如下算法即可:step1:令sum = 0,转第二步;step2:假如n <= 0,算法结束,返回sum值,否则sum = sum + Cn,转第三步;step3: 令n = n –lowbit(n),转第二步。
可以看出,这个算法就是将这一个个区间的和全部加起来,为什么是效率是log(n)的呢?以下给出证明:n = n –lowbit(n)这一步实际上等价于将n的二进制的最后一个1减去。
而n的二进制里最多有log(n)个1,所以查询效率是log(n)的。
那么修改呢,修改一个节点,必须修改其所有祖先,最坏情况下为修改第一个元素,最多有log(n)的祖先。
树状数组
树 状
围内的数据之和.
修改一个位置上数字的值,就是修改一个 叶子结点的值,而当程序由叶子结点返回
数 组
根节点的同时顺便修改掉路径上的结点的 a数组的值.
对于询问的回答,可以直接查找i..j范围
的 运
内的值,遇到分叉时就兵分两路,最后在合 起来.也可以先找出0..i-1的值和0..j的 值,两个值减一减就行了.后者的实际操作
树
每个星星有个坐标。 如果一个星星的左下方(包含正左和正下)有k颗
状
星星,就说这颗星星是k级的.比如,在下面的例
数
图中,星星5是3级的(1,2,4在它左下)。 星星2,4是1级的。例图中有1个0级,2个1级,1
组
个2级,1个3级的星。
的
运
用
求出各个级别的星星的个数
树 状
算法有很多种,最实用的是树状数 组
状
如果直接做的话,修改的复杂度
数
是O(1),询问的复杂度是O(N),M
组
次询问的复杂度是M*N.M,N的
的
范围可以有100000以上
运
用
用线段树可以这样解:树源自若要维护的序列范围是0..5,先构造下面
状
的一棵线段树:
数
组
的
运
用
可以看出,这棵树的构造用二分便可以实 现.复杂度是2*N.
每个结点用数组a来表示该结点所表示范
的
while k<=n do
运
begin
用
C[k]:=C[k]+delta; k:=k+lowbit(k);
End;
End;
若要求I..j的元素和的值,则求出
1..I-1的值和1..j的值.
树状数组维护区间和的模型及其拓广的简单总结
树状数组维护区间和的模型及其拓广的简单总结by wyl8899树状数组的基本知识已经被讲到烂了,我就不多说了,下面直接给出基本操作的代码。
假定原数组为a[1..n],树状数组b[1..n],考虑灵活性的需要,代码使用int*a传数组。
#define lowbit(x)((x)&(-(x)))int sum(int*a,int x){int s=0;for(;x;x-=lowbit(x))s+=a[x];return s;}void update(int*a,int x,int w){for(;x<=n;x+=lowbit(x))a[x]+=w;}sum(x)返回原数组[1,x]的区间和,update(x,w)将原数组下标为x的数加上w。
这两个函数使用O(操作数*logn)的时间和O(n)的空间完成单点加减,区间求和的功能。
接下来做一些升级,让树状数组完成区间加减,单点查询的功能。
直接做的话很困难,需要对问题做一些转化。
考虑将原数组差分,即令d[i]=a[i]-a[i-1],特别地,d[1]=a[1]。
此时a[i]=d[1]+..+d[i],所以单点查询a[i]实际上就是在求d数组的[1..i]区间和。
而区间[l,r]整体加上k的操作,可以简单地使用d[l]+=k和d[r+1]-=k来完成。
于是,我们用树状数组来维护d[],就可以解决问题了。
下面再升级一次,完成区间加减,区间求和的功能。
仍然沿用d数组,考虑a数组[1,x]区间和的计算。
d[1]被累加了x次,d[2]被累加了x-1次,...,d[x]被累加了1次。
因此得到sigma(a[i])=sigma{d[i]*(x-i+1)}=sigma{d[i]*(x+1)-d[i]*i}=(x+1)*sigma(d[i])-sigma(d[i]*i)所以我们再用树状数组维护一个数组d2[i]=d[i]*i,即可完成任务。
POJ3468就是这个功能的裸题,下面给出代码。
数据结构之树状数组
如果,我们要分析的是“改段求点”型,即
修改某一个区间,使其每个值都加上值val,然后
再去询问你某个点的值是多少。
问题的引入:
给定n个数,a[1]..a[n]。每次我们可能有
两种操作:
(1)将a[i]..a[j]的每个数都加上一个值;
(2)查询a[x]的值。
n的规模如果比较大(约100000)
该如何高效的实现?
令del[i]表示i..MN段被加了多少。则区间修
改[a,b]时,我们让del[a]++,del[b+1]--即可。
(此时树状数组是建立在del数组基础上的)
这个时候,要求某一个位置x的数的值时,用
Getsum(x)来表示。
(即a[x]=sigma(del[1]..del[x]))
问题的深入: 现在,我们如果可以对原数组中,进行区间 的修改,想了解区间的和,该如何去做? del[i]依旧表示i..MN段被加了多少。我们 想知道区间[L,R]的和是多少。这个可以用减法, 用[1,R]的和减去[1,L-1]的和。 提示:建立两棵树状数组
x+lowbit(x)为x的直接父亲编号 void update(int x,int v){ //在x处增加v for (; x<=MN; x+=x&(-x)) C[x]+=v; } //MN为树状数组C的上限
上面为更新C数组的函数
于是乎,最前面提到的问题就完美解决了。 初始化C数组全为0,然后针对每次操作, 操作(1):update(x, val); 操作(2):getsum(x)。 树状数组时间复杂度O(nlgn) ,简洁高效! 学完了树状数组的基本操作,下面来看看
树状数组可以发挥的强大作用吧!
树状数组是一个查询和修改复杂度都为logn级别的区间统计
树状数组是一个查询和修改复杂度都为log(n)级别的区间统计的数据结构,在思想上类似于线段树。
相比线段树,树状数组需要的空间较少,编程复杂度也较低,但适用范围比线段树小。
来观察一下这个图:令这棵树的结点编号为C1,。
令每个结点的值为这棵树的值的总和,那么容易发现:C1 = A1C2 = A1 + A2C3 = A3C4 = A1 + A2 + A3 + A4C5 = A5C6 = A5 + A6C7 = A7C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8...C16 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8 + A9 + A10 + A11 + A12 + A13 + A14 + A15 + A16这里有一个有趣的性质,下午推了一下发现:设节点编号为x,那么这个节点管辖的区间为2^k(其中k为x二进制末尾0的个数)个元素。
因为这个区间最后一个元素必然为Ax,所以很明显:Cn = A(n – 2^k + 1) + ... + An算这个2^k有一个快捷的办法,定义一个函数如下即可:int lowbit(int x){return x&(x^(x–1));}利用机器补码的特点,这个函数可以改得更方便int lowbit(int i){return i&(-i);}如果要把a[n]增加m,可以通过调用如下函数实现void add(int i,int v){while (i<=n){a[i]+=v;i+=lowbit(i);}}如果要统计a[1]到a[n]之间的和,可以通过调用如下函数实现int sum(int i){int s=0;while (i>0){s+=a[i];i-=lowbit(i);}return s;}这是一维的情况,很容易能推广到二维。
POJ上用到树状数组的题目:poj2352 Starspoj2481 Cowspoj1195 Mobile phonespoj3067 Japanpoj2155 Matrixpoj2464 Brownie Points II(以下是经典)树状数组是一种非常优雅的数据结构.当要频繁的对数组元素进行修改,同时又要频繁的查询数组内任一区间元素之和的时候,可以考虑使用树状数组.最直接的算法可以在O(1)时间内完成一次修改,但是需要O(n)时间来进行一次查询.而树状数组的修改和查询均可在O(log(n))的时间内完成.假设a[1...N]为原数组,定义c[1...N]为对应的树状数组:c[i] = a[i - 2^k + 1] + a[i - 2^k + 2] + ... + a[i]其中k为i的二进制表示末尾0的个数,所以2^k即为i的二进制表示的最后一个1的权值. 所以2^k可以表示为n&(n^(n-1))或更简单的n&(-n):int lowbit(int n){return n& (-n);}对a[n]进行修改后,需要相应的修改c数组中的p1, p2, p3...等一系列元素其中p1= n,p i+1= p i+ lowbit(p i)所以修改原数组中的第n个元素可以实现为:void Modify(int n, int delta){while(n <= N){ c[n] += delta; n += lowbit(n);}}当要查询a[1],a[2]...a[n]的元素之和时,需要累加c数组中的q1, q2, q3...等一系列元素其中q1 = n,q i+1= q i - lowbit(q i)所以计算a[1] + a[2] + .. a[n]可以实现为:int Sum(int n){int result = 0;while(n != 0){ result += c[n]; n -= lowbit(n); }return result;}树状数组可以扩充到二维。
用树状解决高中数学树形问题方法全解析
用树状解决高中数学树形问题方法全解析高中数学中的树形问题是指那些需要利用树状图进行求解的问题。
这种问题通常需要我们建立一棵树,并对其进行一系列的操作。
本文将介绍如何用树状解决高中数学树形问题,包括建立树状图、进行递归求解、优化算法等方面的内容。
下文将一一阐述。
1. 建立树状图树状图是树形问题的基础。
对于一个给定的问题,我们需要首先确定树状图的结构。
具体来说,我们需要确定每个节点代表的含义以及节点之间的关系。
通常情况下,我们可以将树状图分为以下几类:(1)有根树:有根树是指一棵树中有一个节点被指定为根节点。
根节点没有父节点,而其他节点都有唯一的父节点。
有根树可以用于解决许多问题,例如二叉树、树状数组等。
(2)无根树:无根树是指一棵树中没有被指定为根节点的节点。
每个节点都有若干个子节点和若干个父节点。
无根树通常用于解决一些比较基础的问题,例如最长上升子序列、最长公共子序列等。
(3)有向无环图(DAG):DAG 是指一类图,其中每条边都是有向的,并且不存在环。
DAG 通常用于解决一些比较复杂的问题,例如最长路问题、拓扑排序等。
2. 递归求解递归是解决树形问题的常用方法。
具体来说,我们可以利用递归的方式对树进行遍历,同时在遍历的过程中计算出问题的答案。
对于每个节点,我们可以假设其已经知道了所有子节点的答案,然后利用这些答案计算出当前节点的答案。
最终,我们可以利用递归的方式计算出整个树的答案。
3. 优化算法在实际应用中,我们经常需要对树形问题进行优化。
下面介绍一些常用的优化算法。
(1)记忆化搜索:记忆化搜索是指在递归求解的过程中,将已经求解过的子问题的答案进行记录,从而避免重复计算。
记忆化搜索常用于一些比较复杂的树形问题,例如最长路问题、最大权闭合子图等。
(2)动态规划:动态规划是一种常用的优化算法,它通常用于求解一些具有重叠子问题的问题。
在树形问题中,我们可以将整个树看作一棵 DAG,然后利用动态规划的方式求解问题。
树状数组习题(一本通)
树状数组习题(⼀本通)1535:【例 1】数列操作模板题#include<iostream>#include<cstring>#include<cmath>#include<algorithm>#include<stack>#include<cstdio>#include<queue>#include<map>#include<vector>#include<set>using namespace std;const int maxn=101000;const int INF=0x3fffffff;typedef long long LL;#define lowbit(x) ((x)&(-x))int tree[maxn];int n,m;void add(int x,int d){while(x<=n){tree[x]+=d;x+=lowbit(x);}}LL getsu(int x){LL ans=0;while(x>0){ans+=tree[x];x-=lowbit(x);}return ans;}int main(){scanf("%d %d",&n,&m);for(int i=1;i<=n;i++) {int x;scanf("%d",&x);add(i,x);}int k,a,b;while(m--){scanf("%d %d %d",&k,&a,&b);if(k==0) printf("%lld\n",getsu(b)-getsu(a-1));else add(a,b);}return 0;}1536:【例 2】数星星 Stars//⼆维的树状数组bushi//认真读题呀看给出数据的特征,是按照纵坐标从⼩到⼤排序的,纵坐标相同的是横坐标从⼩到⼤给出的//也就是说,我们可以不管纵坐标,按照它给出的横坐标依次插⼊,并统计当前星星之前的横坐标⼩于它的星星个数。
树状数组三道模板题小结
树状数组三道模板题⼩结今天写了⼀天北京信息科技⼤学,难度不⼤,跟西电校赛风格类似,⼤部分为数学题与规律题。
(改⽇更)其中的I题为树状数组/线段树模板题,发现之前没有在博客总结,⼀时也找不到好的模板,于是重新深⼊学习了⼀番。
⾸先引⼊洛⾕上的模板题:P3374 【模板】树状数组 1题⽬描述如题,已知⼀个数列,你需要进⾏下⾯两种操作:1.将某⼀个数加上x2.求出某区间每⼀个数的和输⼊输出格式输⼊格式:第⼀⾏包含两个整数N、M,分别表⽰该数列数字的个数和操作的总个数。
第⼆⾏包含N个⽤空格分隔的整数,其中第i个数字表⽰数列第i项的初始值。
接下来M⾏每⾏包含3个整数,表⽰⼀个操作,具体如下:操作1:格式:1 x k 含义:将第x个数加上k操作2:格式:2 x y 含义:输出区间[x,y]内每个数的和输出格式:输出包含若⼲⾏整数,即为所有操作2的结果。
说明时空限制:1000ms,128M数据规模:对于30%的数据:N<=8,M<=10对于70%的数据:N<=10000,M<=10000对于100%的数据:N<=500000,M<=500000题解由于是【单点修改,区间查询】裸题,就直接上AC代码了:#include<iostream>#include<cstdio>using namespace std;#define lowbit(x) ((x)&(-x))int n, m;int C[500010]; // 树状数组只需要开⼀倍内存空间int sum(int x) {int res = 0;while(x) {res += C[x];x -= lowbit(x);}return res;}void add(int x, int d) {while(x<=n) {C[x] += d;x += lowbit(x);}}int main() {cin>>n>>m;for(int i=1;i<=n;i++) {int ai;scanf("%d", &ai);add(i, ai);}while(m--) {int q, x, y;scanf("%d %d %d", &q, &x, &y);if(q==1) {add(x, y);}else if(q==2) {printf("%d\n", sum(y)-sum(x-1));}}return0;}P3368 【模板】树状数组 2题⽬描述如题,已知⼀个数列,你需要进⾏下⾯两种操作:1.将某区间每⼀个数数加上x2.求出某⼀个数的值输⼊输出格式输⼊格式:第⼀⾏包含两个整数N、M,分别表⽰该数列数字的个数和操作的总个数。
树状数组4
逆序对设A[1…n]是一个包含n个不同数的数组。
如果在i<j的情况下,有A[i]>A[j],则(i,j)就称为A中的一个逆序对(inversion)【《算法导论》2-4】现给出一个数列,求该数列中的逆序对数(逆序数)。
本节给出三种方法:方法一是最直接的暴力方法;方法二是基于归并分治的思想;方法三是基于线段树的。
【解法一】暴力方法最直接,也最容易想到,两层for循环就可以算出来逆序数:每遇到一个元素回头遍历寻找比其大的元素个数即可,当然向后寻找比其小的元素个数也可以,复杂度为O(n^2),代码:[cpp]view plaincopy1.int sum = 0;2.for(int i = 0; i < size; ++i)3.{4. for(int j = i+1; j < size; ++j)5. {6. if(arr[i] > arr[j])7. {8. ++sum;9. }10. }11.}12.return sum;【解法二】这种方法最初见于《算法导论》,这里先附上《算法导论》2-4关于逆序对的几点讨论:1)如果数组的元素取自集合[1,2,…,n],那么,怎样的数组含有最多的逆序对?它包含多少个逆序对?析:很容易想到,当一个数列为逆序时,即[n,n-1,n-2,…,1],逆序数是最多的,是多少?当 n=1,inversion=0;n=2,inversion在原来基础上增加1;n=3,inversion增加2…n=n时,inversion增加 n-1,所以总的inversion为等差求和n(n-1)/2;也可以这样理解,数列[n,n-1,n-2,…,1]中任取两个数皆为逆序2,即n(n-1)/2。
对,故逆序数为Cn2)插入排序的运行时间与输入数组中逆序对的数量之间有怎样的关系?析:我们知道插入排序的程序如下[cpp]view plaincopy1.for(int i = 1; i < n; ++i)2.{3. int key = arr[i];4. for(int j = i; j > 0 && arr[j-1] > key; --j)5. {6. arr[j] = arr[j-1];7. }8. arr[j] = key;9.}可以看出,每次内层循环都尝试将当前元素放在“合适”的位置上,所以对于内层循环的每一次迭代,都是在消除当前元素相对之前元素所对应的一系列逆序对,故总的循环便是消除数列所有的逆序对;3)给出一个算法,它能用O(nlgn)的最坏运行时间,确定n个元素的任何排列中逆序对的数目。
树状数组的模板题
树状数组的模板题【如果你不知道什么是树状数组:这是⼀道模板题。
给定数列a1,a2,…,a n,你需要进⾏m各操作,操作有两类:1 i x:给定i,x,将a i加上x;2 l r:给定l.r,求Σr i=l a i的值(换⾔之,求a l+a l+1+⋯+a r的值)。
输⼊格式第⼀⾏包含2个正整数n,m,表⽰数列长度和询问个数。
保证1≤n,m≤106。
第⼆⾏n个整数a1,a2,…,a n,表⽰初始数列。
保证|a i|≤106。
接下来m⾏,每⾏⼀个操作,为以下两种之⼀:1 i x:给定i,x,将a i加上x;2 l r:给定l.r,求Σr i=l a i的值。
保证1≤l≤r≤n,|x|≤106。
输出格式对于每个2 l r操作输出⼀⾏,每⾏有⼀个整数,表⽰所求的结果。
思路:这是⼀道简单的⼀维树状数组的模板题#include <bits/stdc++.h>using namespace std;typedef long long ll;#define endl '\n'#define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);#define _for(i, a, b) for (int i=(a); i<=(b); i++)const int INF = 0x7fffffff;const int MAXN = 1e6 + 10;ll tree[MAXN], n, m;inline ll lowbit(ll x){return x & (-x);}void updata(ll x, ll d){while(x <= n){tree[x] += d;x += lowbit(x);}}ll getsum(ll x){ll res = 0;while(x > 0){res += tree[x];x -= lowbit(x);}return res;}int main(){IOScin >> n >> m;_for(i, 1, n){ll a;cin >> a;updata(i,a);}while(m--){ll id, x, y;cin >> id >> x >> y;if(id == 1){updata(x,y);}else if(id == 2){cout << getsum(y) - getsum(x-1) << endl;}}return 0;}这是⼀道模板题。
树状数组题目
树状数组题目
1.单点修改,区间查询
给定一个长度为n的数组a,支持单点修改和查询区间和,即: - void modify(int i, int x):将a[i]修改为x。
- int query(int l, int r):返回a[l]+a[l+1]+...+a[r]。
使用树状数组(或线段树)实现该数据结构。
2. 区间修改,单点查询
给定一个长度为n的数组a,支持区间修改和查询单个元素,即: - void modify(int l, int r, int x):将a[l]~a[r]都加上x。
- int query(int i):返回a[i]的值。
使用树状数组(或线段树)实现该数据结构。
3. 数组前缀和
给定一个长度为n的数组a,支持查询前缀和,即:
- int query(int i):返回a[0]+a[1]+...+a[i]。
使用树状数组(或前缀和数组)实现该数据结构。
4. 数组后缀和
给定一个长度为n的数组a,支持查询后缀和,即:
- int query(int i):返回a[i]+a[i+1]+...+a[n-1]。
使用树状数组(或前缀和数组)实现该数据结构。
- 1 -。
利用树状数组解决几类问题
利用树状数组解决几类问题树状数组作为一种实现简单、应用较广的高级数据结构,在OI 界的地位越来越重要,下面我来简单介绍一下树状数组和它的简单应用。
一、树状数组简介树状数组(Binary Indexed Trees ,简称BIT)是一种特殊的数据结构,这种数据结构的时空复杂度和线段树相似,但是它的系数要小得多。
它可以方便地查询出一段区间中的数字之和。
其查询和修改的时间复杂度均为O(lbN),并且是一个在线的数据结构,可以随时修改并查询。
我接下来用一道题目来介绍树状数组的几个基本操作。
【引例】假设有一列数{A i }(1<=i<=n ),支持如下两种操作: 1. 将A k 的值加D 。
(k ,D 是输入的数)2. 输出∑=tsi i A (s ,t 都是输入的数,s<=t )这道题目用线段树很容易可以解决,我就不多说了,那么如何用树状数组来解决呢?我们新增一个数组C[],其中C[i]=a[i-2k+1]+……+a[i](k 为i 在二进制形式下末尾0的个数)。
低位技术,也叫Lowbit(k)。
对于Lowbit 这里我提三种不同的写法(这三种形式是等价的,读者有兴趣可以去证明一下):1.Lowbit(k)=k and (k or (k-1))2.Lowbit(k)=k and not (k-1)3.Lowbit(x)=k and –k然后我来分析引例的树状数组解法,为了可以更好地理解这种方法,读者可以根据以下这幅图来加以理解。
【操作2】修改操作:将A k 的值加D可以从C[i]往根节点一路上溯,调整这条路上的所有C[]即可,这个操作的复杂度在最坏情况下就是树的高度即O(lbN)。
定理1:若A[k]所牵动的序列为C[p 1],C[p 2]……C[p m ], 则p 1=k ,而ili i p p 21+=+(l i 为p i 在二进制中末尾0的个数)。
例如A[1]……A[8]中,a[3] 添加x ; p 1=k=3 p 2=3+20=4 p 3=4+22=8 p 4=8+23=16>8由此得出,c[3]、c[4]、c[8]亦应该添加x 。
线段树和树状数组题目总结(未完
线段树和树状数组题目总结(未完)1.Minimum Inversion Number首先要知道如何求一个序列的逆序数,比如2514 :2之前没有数大于它,所以为0,5之前也没有数大于它,所以为0,1之前2,5都大于它,所以为2,4之前只有5大于它,所以为1,因此2514的逆序数为:0 + 0 + 2 + 1 = 3;从前面的描述中,我们可以发现,只要依次累计前面有几个数大于当前数即可。
于是我们可以用一个数组b[n](初始化为0),然后每读取一个数a[i] 就标识b[a[i]] += 1 , 但是在标识之前我们先要统计有多少个数大于a[i],即累计b[a[i]+1] + ... +b[n-1],很显然累计的时间复杂度为O(N),我们能不能在快点呢? 当然,树状数组就可以,它求和的效率是log级别的。
那么如何求最小逆序数呢?(我这里假设一个序列中每个数字都不同)若abcde...的逆序数为k,那么bcde...a的逆序数是多少?我们假设abcde...中小于a的个数为t-1 , 那么大于a的个数就是n - t,当把a移动左移一位时,原来比a大的现在都成了a的逆序对,即逆序数增加n - t,但是原来比a小的构成逆序对的数,现在都变成了顺序,因此逆序对减少t - 1,所以新序列的逆序数为k += n - t - t + 1,即k += n + 1 - 2 * t , 于是我们只要不断移位(n次)然后更新最小值就可以了代码int a[N+1] , b[N+1];int Lowbit(int x){return x & (-x);}void Update(int x, int c){int i;for(i = x; i < N ; i += Lowbit(i)) {b[i] += c;}}int Getsum(int x){int i , k = 0;for(i = x; i >= 1 ; i -= Lowbit(i)) {k += b[i];}}int main(void){int n , i , k , min;while(scanf("%d",&n) != EOF){for(i = 0 ; i < n ; i++){scanf("%d", a + i);a[i]++;}k = 0;memset(b ,0 ,sizeof(b));for(i = 0 ; i < n ; i++){k += i - Getsum(a[i]); // 总数 - 小于(等于)a[i]的个数 = 大于a[i]的个数Update(a[i],1);}for(i = 0 ; i < n ; i++){//Update mink += 1 + n - 2 * Getsum(a[i]);min = min < k ? min : k ;}printf("%d\n",min);}return0;}2.Stars , CowsStars这题就是求一颗星星的左下方有多少星星。
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
M n 2i n lg n (前面两种均为Θ(n) ) ,而且在实际编程中,线段树通常是使
i 0
lg n
用指针进行实现的, 这样的递归和调用就为我们的时间复杂度填上了一个不小的常数 (虽然 在实际分析中常常省去常数的大小,但是在此处它的确是一个不可忽略的因素) 。更要命的 是,线段树的编程难度比较大,稍一疏忽就容易写错,也会消耗很多编程的时间。 在分析了几种不是很乐观的方案之后,沮丧之余,树状数组的出现就像救世主一样拯 救了我们的思路。它可以在Θ(lg n) 的时间内完成一次查询或是修改操作,且只占用Θ(n) 大小的空间(也就是一个数组) ,而且它的代码十分短小精悍,只要牢记它的基本思想就可 以在很短的时间内写出准确无误的代码。
Input
The first line of the input file contains a number of stars N (1<=N<=15000). The following N lines describe coordinates of stars (two integers X and Y per line separated by a space, 0<=X,Y<=32000). There can be only one star at one point of the plane. Stars are listed in ascending order of Y coordinate. Stars with equal Y coordinates are listed in ascending order of X coordinate.
-1-
树状数组系列题目总结 南开中学 莫凡
S16
S8
S4 S2 S1 S3 S5 S6 S7 S9 S10 S11
S12 S14 S13 S15
A1
A2
A3
A4
A5
A6
A7
A8
A9
A10 A11
A12
A13
A14
A15
A16
关于这个图应该多说几句,否则不是很好理解。我们用 A 来表示原来给定的数组,S 表示我们构造出的树状数组。图中两点之间的连线表示一种相加关系,根据这张图,我们是 这样定义这个 S 数组的:S[n]的值等于所有在 S 的下方且与 S 有连线的点的值的和。例如, S[1]=A[1], S[2]=S[1]+A[2], 以及 S[12]=S[10]+S[11]+A[12]等等。 字面上 S 数组的准确定义并 不是那么容易描述, 但是这个图为我们呈现了一种显而易见却又难以描述的规律 (使自己明 白这种规律的最好方法就是照猫画虎重新画一个 32 节点的树) 。 我们现在要做的是仔细研究 分析出它的规律和定义方式,然后对它的效率进行简单的分析。 一步登天总是妄想,对一件事情着手分析时,应该尽可能从最简单的情况入手(但有 时候情况过于简单,什么结论都无法得到) 。假设现在,我们需要将 A[6]到 A[14]的和用 S 数组来表示,我们可以得到这样的结果:sum = S[14] + S[12] + S[8] - S[4] - S[5]。这时候可 以回过头去看一眼上一节所说的方案二, sum = B[14] – B[5]。 很显然的, B[14] = S[14] + S[12] + S[8], B[5] = S[5] + S[4]。在查询的时候我们依旧采用方案二中的思路,就将这个问题转化 成了使用 S 数组来表示 B 数组的问题了,由于符号单一而且更容易发现规律,我们很快可 以得出这件事(接下来的伪代码中我们尝试使用 S 数组来表示 B[n]): ans←0; while(n > 0) begin ans←ans + S[n]; if(n 是 2^x 的倍数而不是 2^(x + 1)的倍数) //x 是一个唯一符合要求的自然数 then n←n-2^x; end; 现在问题的关键转变为了如何去计算这个 x,或是 2^x,潜意识中 2^x 总是和 x 位二进 制有关系——2^x 在二进制中是 1 后面加上 x 个 0。 我们需要了解一个显而易见的引理: 引理 1 一个数被 2^x 整除的充要条件是它的二进制形式的后 x 位均为 0 由于 n 是 2^x 的倍数,所以 n 的后 x 位一定都是 0,而且 n 不是 2^(x+1)的倍数,所以 n 的倒数第 x 位一定为 1,也就是说,我们只需要找出 x 的二进制中最后一个是 1 的位连同它 之后的 0 构成一个新的二进制值, 我们将其记做 lowbit(n) (我们将在下一节中讨论它的计算 方法) 。这样一来,计算 B[n]的详细代码就可以写出(code in C++):
Sample Input
5 1 1 5 1
-4-
树状数组系列题目总结 南开中学 莫凡
7 1 3 3 5 5
Sample Output
1 2 1 1 0
B 题意分析
题目中给出一些(n 个)星星的坐标,将每颗星星的等级定义为纵坐标小于等于它且横 坐标小于等于它(不包括自己)的星星数,而且不会出现两颗星星坐标相同的情况,要求统 计出每一等级(0 ~ n-1)的星星总数。特别地,输入数据按照纵坐标的升序排序,在纵坐标 相等时按照横坐标的升序排序给出
-2-
树状数组系列题目总结 南开中学 莫凡
int B(int n) { int ans = 0; while(n > 0){ ans += S[n]; n -= lowbit(n); } return ans; } 当然,像下面这样写会更简洁: ans = 0; for(int i = n; i > 0; i -= lowbit(i)) ans += S[i]; 通过延续方法二的思路使查询变得简单起来,通过使用 S 数组现场计算 B 数组可以缩 短修改 B 数组的时间。修改的操作和查询几乎如出一辙,通过观察,只需要进行以下操作 即可完成修改: void change(int x, int V) //x 是要修改的位置,V 是改变量 { for(int i = 1; i <= Array_size; i +=lowbit(i)) S[i] += V }
树状数组系列题目总结 南开中学 莫凡
树状数组系列题目总结
第一部分 树状数组数据结构浅析
一、我们面对的问题
假设有这样一个问题:现在我们有一个给定的初始数列 A,它支持两种操作:修改和查 询。修改操作是这样的:在第 a 个数上加上 b(当然,也有可能是减去) 。查询操作要求输 出第 i 个数到第 j 个数之间的总和。 如果直接去处理一个数组的话就可以在Θ(1)的时间内完成一次修改操作,但是它需 要话费Ο(n)的时间完成一次查询操作,这样的效率非常的低,在查询操作比较多而且查 询的区间比较长的话会花费极大的时间。 在统计一段数字的和的时候我们会想到使用一个数组 B[n]来存储 1~n 的部分和,然后 查询时只需要输出 B[j] – B[i – 1]的值即可,这样的优化就将查询的时间减小到了Θ(1) ,但 是很不幸,在修改第 i 个数字的值的时候,B[i]及之后的每一个部分和都需要被修改,这样 就使得每一次修改的时间变为了Ο(n) ,仍然不是高效的方法。 到这时我们不由得想到处理连续区间上问题的必杀神器线段树了,当然,线段树可以 有效地平衡查找和修改的时间消耗,使得问题得以解决,但是线段树的空间复杂度比较大
For example, look at the map shown on the figure above. Level of the star number 5 is equal to 3 (it's formed by three stars with a numbers 1, 2 and 4). And the levels of the stars numbered by 2 and 4 are 1. At this map there are only one star of the level 0, two stars of the level 1, one star of the level 2, and one star of the level 3. You are to write a program that will count the amounts of the stars of each level on a given map.
C 算法分析
这题属于树状数组的基础题。 根据观察题目中给出的输入数据的性质就可以发现, 在每 颗星星的坐标被输入时,它的等级已经确定(所有满足条件的星星都已经在它之前给出) 。 而且它保证了之前的每一颗星星都满足纵坐标的条件, 所以我们只需要统计当前横坐标小于 等于 Xi 的星星个数就可以计算出 Stari 的等级了,所以通过使用一个树状数组统计每个横坐 标位置上有多少颗星星即可了。 需要注意的是为了使树状数组的下标从一开始, 我们在读入 之后应该将横坐标+1 之后在进行统计。
Output
The output should contain N lines, one number per line. The first line contains amount of stars of the level 0, the second does amount of stars of the level 1 and so on, the last line contains amount of stars of the level N-1.
三、巧妙的 lowbit 运算
lowbit 可谓是树状数组这个数据结构的精华思想所在,它决定了树状数组的定义、修改 和查询。可想而知,lowbit 的运算应该是在常数时间内完成的,否则树状数组的时间效率绝 不可能如此之高。很显然,既然是取最后一位,工作自然是通过位运算完成的。在此,我们 不再去探究它的探索过程,而是直接给出结论在对其进行说明。 lowbit(x) = x and (-x) 由于计算机中使用补码的存储方式,-x 的值实际上是 x 按位取反再+1。首先抛开位数 的限制我们要明白一个问题,那就是 10000……(x 个 0)的取反是 01111……(x 个 1) ,再 加一结果依然是 10000……(x 个 0)而无论倒数第一个 1 的左侧是什么,在取反之后都不 会受到+1 的影响,也就是说,x 与-x 在补码的存储方式中有且仅有倒数第 x+1(x 为右侧 0 的个数)位同为 1,用它们进行按位与运算即可得出 lowbit 的正确结果。