线段树 从入门到进阶(超清晰,简单易懂)

article/2025/9/30 2:06:04

目录

  • 第一部 概念引入
  • 第二部 简单(无pushdown)的线段树
    • 1、单点修改,区间查询
    • 2、区间修改,单点查询
  • 第三部 进阶线段树
  • 第四部 乘法(根号)线段树
    • 1、乘法线段树
    • 2、根号线段树
    • 模板题与代码:
    • 单点修改,区间查询:洛谷树状数组模板1
    • 区间修改,单点查询:洛谷树状数组模板2
    • 区间加法,洛谷线段树模板1
    • 区间乘法:洛谷线段树模板2
    • 区间根号,bzoj3211

线段树是什么??线段树怎么写??

如果你在考提高组前一天还在问这个问题,那么你会与一等奖失之交臂;如果你还在冲击普及组一等奖,那么这篇博客会浪费你人生中宝贵的5~20分钟。

上面两句话显而易见,线段树这个数据结构是一个从萌新到正式OI选手的过渡,是一个非常重要的算法,也是一个对于萌新来说较难的算法。不得不说,我学习了这个算法5遍左右才有勇气写的这篇博客。

但是,对于OI正式选手来说,线段树不是算法,应该是一种工具。她能把一些对于区间(或者线段)的修改、维护,从O(N)的时间复杂度变成O(logN)

第一部:线段树概念引入

第二部:简单(无pushdown)的线段树

第三部:区间+/-修改与查询

第四部:区间乘除修改与查询

第一部 概念引入

线段树是一种二叉树,也就是对于一个线段,我们会用一个二叉树来表示。比如说一个长度为4的线段,我们可以表示成这样:

在这里插入图片描述

这是什么意思呢? 如果你要表示线段的和,那么最上面的根节点的权值表示的是这个线段 1 ∼ 4 1\sim 4 14 的和。根的两个儿子分别表示这个线段中 1 ∼ 2 1\sim 2 12的和,与 3 ∼ 4 3\sim 4 34的和。以此类推。

然后我们还可以的到一个性质:节点 i i i的权值 = = = 她的左儿子权值 + + + 她的右儿子权值。因为 1 ∼ 4 1\sim 4 14 的和就是等于 1 ∼ 2 1\sim 2 12 的和与 2 ∼ 3 2\sim 3 23 的和的和。

根据这个思路,我们就可以建树了,我们设一个结构体 treetree[i].l tree[i].r 分别表示这个点代表的线段的左右下标,tree[i].sum 表示这个节点表示的线段和。

我们知道,一颗从 1 1 1 开始编号的二叉树,结点 i i i 的左儿子和右儿子编号分别是 2 × i 2\times i 2×i 2 × i + 1 2\times i + 1 2×i+1

再根据刚才的性质,得到式子: t r e e [ i ] . s u m = t r e e [ i ∗ 2 ] . s u m + t r e e [ i ∗ 2 + 1 ] . s u m ; tree[i].sum=tree[i*2].sum+tree[i*2+1].sum; tree[i].sum=tree[i2].sum+tree[i2+1].sum; 就可以建一颗线段树了!代码如下:

inline void build(int i,int l,int r){//递归建树tree[i].l=l;tree[i].r=r;if(l==r){//如果这个节点是叶子节点tree[i].sum=input[l];return ;}int mid=(l+r)>>1;build(i*2,l,mid);//分别构造左子树和右子树build(i*2+1,mid+1,r);tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;//刚才我们发现的性质return ;
}

嗯,这就是线段树的构建,你可能会问为什么要开好几倍的内存去储存一条线段。这是因为我们还没有让这个过大的数组干一些实事,那么什么是实事呢?让我们进入下一部(在你看懂这一部的情况下)

第二部 简单(无pushdown)的线段树

1、单点修改,区间查询

其实这一章开始才是真正的线段树,我们要用线段树干什么?答案是维护一个线段(或者区间),比如你想求出一个 1 ∼ 100 1\sim 100 1100 区间中, 4 ∼ 67 4\sim 67 467 这些元素的和,你会怎么做?朴素的做法是for(i=4;i<=67;i++) sum+=a[i],这样固然好,但是算得太慢了。

我们想一种新的方法,先想一个比较好画图的数据,比如一个长度为 4 4 4 的区间,分别是 1 、 2 、 3 、 4 1、2、3、4 1234 ,我们想求出第 1 ∼ 3 1\sim 3 13 项的和。按照上一部说的,我们要建出一颗线段树,其中点权(也就是红色)表示和:

在这里插入图片描述

然后我们要求 1 ∼ 3 1\sim 3 13 的和,我们先从根节点开始查询,发现她的左儿子1-2这个区间和答案区间1~3有交集,那么我们跑到左儿子这个区间。

然后,我们发现这个区间 1 ∼ 2 1\sim 2 12 被完全包括在答案区间 1 ∼ 3 1\sim 3 13 这个区间里面,那就把她的值 3 3 3 返回。

我们回到了 1 ∼ 4 1\sim 4 14 区间,发现她的右儿子 3 ∼ 4 3\sim 4 34区间和答案区间 1 ∼ 3 1\sim 3 13 有交集,那么我们走到 3 ∼ 4 3\sim 4 34 区间

到了 3 ∼ 4 3\sim 4 34 区间,我们发现她并没有完全包含在答案区间 1 ∼ 3 1\sim 3 13 里面,但发现她的左儿子 3 ∼ 3 3\sim 3 33 区间和 1 ∼ 3 1\sim 3 13 区间又交集,那么久走到 3 ∼ 3 3\sim 3 33 区间

到了 3 ∼ 3 3\sim 3 33区间,发现其被答案区间完全包含,就返回她的值 3 3 3一直到最开始

3 ∼ 3 3\sim 3 33 区间的 3 3 3 + + + 1 ∼ 2 1\sim 2 12 区间的 3 3 3 = 6 =6 =6,我们知道了 1 ∼ 3 1\sim 3 13 区间和为 6 6 6

有人可能会说你这样是不是疯了,我拿脚都能算出 1 + 2 + 3 = 6 1+2+3=6 1+2+3=6,为什么这么麻烦?!

因为这才几个数,如果一百万个数,这样时间会大大增快。

我们总结一下,线段树的查询方法:

  1. 如果这个区间被完全包括在目标区间里面,直接返回这个区间的值

  2. 如果这个区间的左儿子和目标区间有交集,那么搜索左儿子

  3. 如果这个区间的右儿子和目标区间有交集,那么搜索右儿子

写成代码,就会变成这样:

inline int search(int i,int l,int r){if(tree[i].l>=l && tree[i].r<=r)//如果这个区间被完全包括在目标区间里面,直接返回这个区间的值return tree[i].sum;if(tree[i].r<l || tree[i].l>r)  return 0;//如果这个区间和目标区间毫不相干,返回0int s=0;if(tree[i*2].r>=l)  s+=search(i*2,l,r);//如果这个区间的左儿子和目标区间又交集,那么搜索左儿子if(tree[i*2+1].l<=r)  s+=search(i*2+1,l,r);//如果这个区间的右儿子和目标区间又交集,那么搜索右儿子return s;
}

关于那几个if的条件一定要看清楚,最好背下来,以防考场上脑抽推错。

然后,我们怎么修改这个区间的单点,其实这个相对简单很多,你要把区间的第dis位加上k。

那么你从根节点开始,看这个dis是在左儿子还是在右儿子,在哪往哪跑,

然后返回的时候,还是按照tree[i].sum=tree[i*2].sum+tree[i*2+1].sum的原则,更新所有路过的点

如果不理解,我还是画个图吧,其中深蓝色是去的路径,浅蓝色是返回的路径,回来时候红色的+标记就是把这个点加上这个值。

在这里插入图片描述

把这个过程变成代码,就是这个样子:

inline void add(int i,int dis,int k){if(tree[i].l==tree[i].r){//如果是叶子节点,那么说明找到了tree[i].sum+=k;return ;}if(dis<=tree[i*2].r)  add(i*2,dis,k);//在哪往哪跑else  add(i*2+1,dis,k);tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;//返回更新return ;
}

2、区间修改,单点查询

区间修改和单点查询,我们的思路就变为:如果把这个区间加上 k k k ,相当于把这个区间涂上一个 k k k 的标记,然后单点查询的时候,就从上跑到下,把沿路的标记加起来就好。

这里面给区间贴标记的方式与上面的区间查找类似,原则还是那三条,只不过第一条:如果这个区间被完全包括在目标区间里面,直接返回这个区间的值变为了如果这个区间如果这个区间被完全包括在目标区间里面,讲这个区间标记 k k k

具体做法很像,这里贴上代码:

void modify(int p, int l, int r, int k) 
{if(tr[p].l >= l && tr[p].r <= r) {tr[p].num += k;return ;}int mid = tr[p].l + tr[p].r >> 1;if(l <= mid) modify(p << 1, l, r, k);if(r > mid) modify(p << 1 | 1, l, r, k);
}
/*
inline void add(int i,int l,int r,int k){if(tree[i].l>=l && tree[i].r<=r){//如果这个区间被完全包括在目标区间里面,讲这个区间标记ktree[i].sum+=k;return ;}if(tree[i*2].r>=l)add(i*2,l,r,k);if(tree[i*2+1].l<=r)add(i*2+1,l,r,k);
}
*/

然后就是单点查询了,这个更好理解了,就是dis在哪往哪跑,把路径上所有的标价加上就好了:

void query(int p, int x)
{ans += tr[p].num;//一路加起来if(tr[p].l == tr[p].r) return;int mid = tr[p].l + tr[p].r >> 1;if(x <= mid) query(p << 1, x);else query(p << 1 | 1, x); 
}
/*
void search(int i,int dis){ans+=tree[i].sum;//一路加起来if(tree[i].l==tree[i].r)return ;if(dis<=tree[i*2].r)search(i*2,dis);if(dis>=tree[i*2+1].l)search(i*2+1,dis);
}
*/

区间修改,单点查询完整测试代码:

#include <bits/stdc++.h>
using namespace std;
const int maxn = 5e5 + 7;int n, m, s, t;
int ans;
int a[maxn];
struct segment_tree
{	struct node{int l, r;int num;}tr[maxn * 4];void build(int p, int l, int r){tr[p] = {l, r, 0};if(l == r) {tr[p].num = a[l];return ;}int mid = l + r >> 1;build(p << 1, l, mid);build(p << 1 | 1, mid + 1, r);}		void modify(int p, int l, int r, int k) {if(tr[p].l >= l && tr[p].r <= r) {tr[p].num += k;return ;}int mid = tr[p].l + tr[p].r >> 1;if(l <= mid) modify(p << 1, l, r, k);if(r > mid) modify(p << 1 | 1, l, r, k);}void query(int p, int x){ans += tr[p].num;if(tr[p].l == tr[p].r) return;int mid = tr[p].l + tr[p].r >> 1;if(x <= mid) query(p << 1, x);else query(p << 1 | 1, x); }
}ST;int main()
{cin >> n >> m;for (int i = 1; i <= n; ++ i) {scanf("%d", &a[i]);}ST.build(1, 1, n);for (int i = 1; i <= m; ++ i) {int c;scanf("%d", &c);if(c == 1) {int x, y, z;scanf("%d%d%d", &x, &y, &z);ST.modify(1, x, y, z);}else {ans = 0;int x;scanf("%d", &x);ST.query(1, x);printf("%d\n", ans);}}return 0;
}
/*
int main()
{n = 100;for (int i = 1; i <= n; ++ i) {a[i] = i;}ST.build(1, 1, n);m = 10;for (int i = 1; i <= m; ++ i) {int l = 1, r = 100;ST.modify(1, l, r, 10000);ans = 0;// query(p, x), p = 1, x 为想要查询的点的下标ST.query(1, 50); // 单点查询 下标为 50 的点的值,ans = 50 + 10000 * icout << i << " " << ans << '\n';ans = 0;ST.query(1, 100); // 单点查询 ans = 100 + 10000 * icout << i << " " << ans << '\n'; }return 0;
}
*/

这里区间修改,单点查询的完整代码没有问题,已 AC 洛谷 P3368 【模板】树状数组 2:
在这里插入图片描述
❑ ❑\,\, 为什么需要把路上的 num \text{num} num 加起来:

因为我们在进行区间修改的时候,若当前区间已经被完全包含在目标区间 [ l , r ] [l,r] [l,r] 里,直接将该区间 tree[i].num += k,不需要再继续往下走了,类似 lazy 标记,所以单点查询的时候要再加上路径上的值(即原本的权值再加上经过的若干次修改的值才是这个单点的值)。


不知不觉,这第二章已经结束。这样的简单(原谅我用这个词)线段树,还可除了求和,还可以求区间最小最大值,还可以区间染色。

但是!这样的线段树展现不出来她的魅力,因为区间求和,树状数组比她少了一个很大的常数。二区间最值,ST表的那神乎其技 O ( 1 ) O(1) O(1) 查询也能完爆她。这是为什么?因为线段树的魅力还没有展现出来,她最美丽的地方:pushdown还未展现于世,如果你已经对这一章充足的了解,并且能不看博客把洛谷上树状数组模板1、2都能写出来,那么请你进入下一部。

第三部 进阶线段树

区间修改、区间查询,你可能会认为,把上一章里面的这两个模块加在一起就好了,然后你就会发现你大错特错。

因为如果对于 1 ∼ 4 1\sim 4 14 这个区间,你把 1 ∼ 3 1\sim 3 13 区间 + 1 +1 +1 ,相当于把节点 1 ∼ 2 1\sim 2 12 3 3 3标记,但是如果你查询 2 ∼ 4 2\sim 4 24 时,你会发现你加的时没有标记的 2 2 2节点和没有标记的 3 ∼ 4 3\sim 4 34 节点加上去,结果当然是错的。

那么我们应该怎么办?这时候 pushdown 的作用就显现出来了。

你会想到,我们只需要在查询的时候,如果我们要查的 2 2 2节点在 1 ∼ 2 1\sim 2 12 区间的里面,那我们就可以把 1 ∼ 2 1\sim 2 12区间标记的那个 + 1 +1 +1 给推下去这样就能顺利地加上了。
怎么记录这个标记呢?我们需要记录一个“懒标记” lazytage,来记录这个区间

区间修改的时候,我们按照如下原则:

  1. 如果当前区间被完全覆盖在目标区间里,讲这个区间的 sum+k*(tree[i].r-tree[i].l+1)
  2. 如果没有完全覆盖,则先下传懒标记
  3. 如果这个区间的左儿子和目标区间有交集,那么搜索左儿子
  4. 如果这个区间的右儿子和目标区间有交集,那么搜索右儿子

然后查询的时候,将这个懒标记下传就好了,下面图解一下:

如图,区间 1 ∼ 4 1\sim 4 14 分别是 1 、 2 、 3 、 4 1、2、3、4 1234,我们要把 1 ∼ 3 1\sim 3 13 区间 + 1 +1 +1 。因为 1 ∼ 2 1\sim 2 12 区间被完全覆盖,所以将其 + 2 +2 +2,并将紫色的 lazytage+1, 3 3 3 区间同理

在这里插入图片描述

注意我们处理完这些以后,还是要按照tree[i].sum=tree[i*2].sum+tree[i*2+1].sum的原则返回,代码如下:

void add(int i,int l,int r,int k)
{if(tree[i].r<=r && tree[i].l>=l)//如果当前区间被完全覆盖在目标区间里,讲这个区间的sum+k*(tree[i].r-tree[i].l+1){tree[i].sum+=k*(tree[i].r-tree[i].l+1);tree[i].lz+=k;//记录lazytagereturn ;}push_down(i);//向下传递if(tree[i*2].r>=l)add(i*2,l,r,k);if(tree[i*2+1].l<=r)add(i*2+1,l,r,k);tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;return ;
}

其中的pushdown,就是把自己的lazytage归零,并给自己的儿子加上,并让自己的儿子加上k*(r-l+1)

void push_down(int i)
{if(tree[i].lz!=0){tree[i*2].lz+=tree[i].lz;//左右儿子分别加上父亲的lztree[i*2+1].lz+=tree[i].lz;int mid=(tree[i].l+tree[i].r)/2;tree[i*2].sum+=tree[i].lz*(mid-tree[i*2].l+1);//左右分别求和加起来tree[i*2+1].sum+=tree[i].lz*(tree[i*2+1].r-mid);tree[i].lz=0;//父亲lz归零}return ;
}

查询的时候,和上一章的几乎一样,就是也要像修改一样加入pushdown,这里用图模拟一下。我们要查询 2 ∼ 4 2\sim 4 24区间的和,这是查询前的情况,所有紫色的代表lazytage

在这里插入图片描述

然后,我们查到区间 1 ∼ 2 1\sim 2 12 时,发现这个区间并没有被完全包括在目标区间里,于是我们就pushdown,lazytage下传,并让每个区间 sum 加上 ( r − l ) × l a z y t a g e (r-l)\times lazytage (rl)×lazytage

在这里插入图片描述

然后查到 2 ∼ 2 2\sim 2 22 区间,发现被完全包含,所以就返 3 3 3,再搜索到 3 ∼ 4 3\sim 4 34 区间,发现被完全包含,那么直接返回 8 8 8 ,最后 3 + 8 = 11 3+8=11 3+8=11 就是答案

这里是代码实现:

inline int search(int i,int l,int r){if(tree[i].l>=l && tree[i].r<=r)return tree[i].sum;if(tree[i].r<l || tree[i].l>r)  return 0;push_down(i);int s=0;if(tree[i*2].r>=l)  s+=search(i*2,l,r);if(tree[i*2+1].l<=r)  s+=search(i*2+1,l,r);return s;
}

好了,到了这里,我们就学会了用线段树进行区间加减操作,大家可以完成洛谷的线段树模板1.

第四部 乘法(根号)线段树

1、乘法线段树

如果这个线段树只有乘法,那么直接加入lazytage变成乘,然后tree[i].sum*=k就好了。但是,如果我们是又加又乘,那就不一样了。

当lazytage下标传递的时候,我们需要考虑,是先加再乘还是先乘再加。我们只需要对lazytage做这样一个处理。

lazytage分为两种,分别是加法的plz和乘法的mlz。

mlz很简单处理,pushdown时直接*父亲的就可以了,那么加法呢?

我们需要把原先的plz*父亲的mlz再加上父亲的plz.

inline void pushdown(long long i){//注意这种级别的数据一定要开long longlong long k1=tree[i].mlz,k2=tree[i].plz;tree[i<<1].sum=(tree[i<<1].sum*k1+k2*(tree[i<<1].r-tree[i<<1].l+1))%p;//tree[i<<1|1].sum=(tree[i<<1|1].sum*k1+k2*(tree[i<<1|1].r-tree[i<<1|1].l+1))%p;tree[i<<1].mlz=(tree[i<<1].mlz*k1)%p;tree[i<<1|1].mlz=(tree[i<<1|1].mlz*k1)%p;tree[i<<1].plz=(tree[i<<1].plz*k1+k2)%p;tree[i<<1|1].plz=(tree[i<<1|1].plz*k1+k2)%p;tree[i].plz=0;tree[i].mlz=1;return ;
}

然后加法和减法的函数同理,维护lazytage的时候加法标记一定要记得现乘再加。

值得一提的是,计算*2时一定要改成i<<1这样能解决很多时间,还有要开long long,还有,函数前面要加inline 我在其他OJ交这道题时,就因为没加inline 就被卡了,交了就过了。

2、根号线段树

其实,根号线段树和除法线段树一样。她们乍眼一看感觉直接用lazytage标记除了多少,但是实际上,会出现精度问题。

c++的除法是向下取整,很明显,(a+b)/k!=a/k+b/k(在向下取整的情况下),而根号,很明显根号(a)+根号(b)!=根号(a+b)那么怎么办?

第一个想法就是暴力,对于每个要改动的区间l~r,把里面的每个点都单独除,但这样就会把时间复杂度卡得比大暴力都慢(因为多个常数),所以怎么优化?

我们对于每个区间,维护她的最大值和最小值,然后每次修改时,如果这个区间的最大值根号和最小值的根号一样,说明这个区间整体根号不会产生误差,就直接修改(除法同理)

其中,lazytage把除法当成减法,记录的是这个区间里每个元素减去的值。

下面是根号线段树的修改过程:

inline void Sqrt(int i,int l,int r){if(tree[i].l>=l && tree[i].r<=r && (tree[i].minn-(long long)sqrt(tree[i].minn))==(tree[i].maxx-(long long)sqrt(tree[i].maxx))){//如果这个区间的最大值最小值一样long long u=tree[i].minn-(long long)sqrt(tree[i].minn);//计算区间中每个元素需要减去的tree[i].lz+=u;tree[i].sum-=(tree[i].r-tree[i].l+1)*u;tree[i].minn-=u;tree[i].maxx-=u;//cout<<"i"<<i<<" "<<tree[i].sum<<endl;return ;}if(tree[i].r<l || tree[i].l>r)  return ;push_down(i);if(tree[i*2].r>=l)  Sqrt(i*2,l,r);if(tree[i*2+1].l<=r)  Sqrt(i*2+1,l,r);tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;tree[i].minn=min(tree[i*2].minn,tree[i*2+1].minn);//维护最大值和最小值tree[i].maxx=max(tree[i*2].maxx,tree[i*2+1].maxx);//cout<<"i"<<i<<" "<<tree[i].sum<<endl;return ;
}

然后pushdown没什么变化,就是要记得tree[i].minn、tree[i].maxx也要记得-lazytage。


附赠一份支持单点修改,区间取相反数,区间最小值,区间最大值的线段树代码

题目链接:https://www.luogu.com.cn/problem/P1505

题解:BZOJ 2157 「国家集训队」旅游(树链剖分,线段树,边权转点权)

struct Segment_Tree
{struct node {int l, r;int sum;
//		int laz; int tag;//相反数的tagint maxx;int minn;}tr[maxn << 2];void pushup(int p){node &l = tr[p << 1], &r = tr[p << 1 | 1], &rt = tr[p];rt.sum = l.sum + r.sum;rt.maxx = max(l.maxx, r.maxx);rt.minn = min(l.minn, r.minn);}void pushdown(int p){node &l = tr[p << 1], &r = tr[p << 1 | 1], &rt = tr[p];if(rt.tag == 0) return ;l.tag ^= 1, r.tag ^= 1;l.sum = -l.sum, r.sum = -r.sum;l.maxx = -l.maxx, r.maxx = -r.maxx;l.minn = -l.minn, r.minn = -r.minn;swap(l.maxx, l.minn), swap(r.maxx, r.minn);rt.tag = 0;}void build(int p, int l, int r){tr[p].l = l, tr[p].r = r;if(l == r) {tr[p].sum = tr[p].minn = tr[p].maxx = a_after[l];return ;}	int mid = l + r >> 1;build(p << 1, l, mid);build(p << 1 | 1, mid + 1, r);pushup(p);}void modify(int p, int x, int k){if(tr[p].l == tr[p].r) {tr[p].sum = tr[p].minn = tr[p].maxx = k;return ;}if(tr[p].tag) pushdown(p);int mid = tr[p].l + tr[p].r >> 1;if(x <= mid) modify(p << 1, x, k);if(x > mid) modify(p << 1 | 1, x, k);pushup(p);}void _reverse(int p, int l, int r){if(tr[p].l >= l && tr[p].r <= r) {tr[p].tag ^= 1;tr[p].sum = -tr[p].sum;tr[p].minn = -tr[p].minn;tr[p].maxx = -tr[p].maxx;swap(tr[p].minn, tr[p].maxx);return ;}if(tr[p].tag) pushdown(p);int mid = tr[p].l + tr[p].r >> 1;if(l <= mid) _reverse(p << 1, l, r);if(r > mid) _reverse(p << 1 | 1, l, r);pushup(p);}int query_sum(int p, int l, int r){int res = 0;if(tr[p].l >= l && tr[p].r <= r) return tr[p].sum;if(tr[p].tag) pushdown(p);int mid = tr[p].l + tr[p].r >> 1;if(l <= mid) res += query_sum(p << 1, l, r);if(r > mid) res += query_sum(p << 1 | 1, l, r);pushup(p);return res;}int query_max(int p, int l, int r){int res = -INF;if(tr[p].l >= l && tr[p].r <= r) return tr[p].maxx;if(tr[p].tag) pushdown(p);int mid = tr[p].l + tr[p].r >> 1;if(l <= mid) res = max(res, query_max(p << 1, l, r));if(r > mid) res = max(res, query_max(p << 1 | 1, l, r));pushup(p);return res;}int query_min(int p, int l, int r){int res = INF;if(tr[p].l >= l && tr[p].r <= r) return tr[p].minn;if(tr[p].tag) pushdown(p);int mid = tr[p].l + tr[p].r >> 1;if(l <= mid) res = min(res, query_min(p << 1, l, r));if(r > mid) res = min(res, query_min(p << 1 | 1, l, r));pushup(p);return res;} 
}ST; 

模板题与代码:

最后,我们给几道模板题,再贴上代码:

单点修改,区间查询:洛谷树状数组模板1

P3374 【模板】树状数组 1

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <queue>
#include <stack>
#include <vector>
using namespace std;
#define MAXN 100010
#define INF 10000009
#define MOD 10000007
#define LL long long
#define in(a) a=read()
#define REP(i,k,n) for(long long i=k;i<=n;i++)
#define DREP(i,k,n) for(long long i=k;i>=n;i--)
#define cl(a) memset(a,0,sizeof(a))
inline long long read(){long long x=0,f=1;char ch=getchar();for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=-1;for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0';return x*f;
}
inline void out(long long x){if(x<0) putchar('-'),x=-x;if(x>9) out(x/10);putchar(x%10+'0');
}
long long n,m,p;
long long input[MAXN];
struct node{long long l,r;long long sum,mlz,plz;
}tree[4*MAXN];
inline void build(long long i,long long l,long long r){tree[i].l=l;tree[i].r=r;tree[i].mlz=1;if(l==r){tree[i].sum=input[l]%p;return ;}long long mid=(l+r)>>1;build(i<<1,l,mid);build(i<<1|1,mid+1,r);tree[i].sum=(tree[i<<1].sum+tree[i<<1|1].sum)%p;return ;
}
inline void pushdown(long long i){long long k1=tree[i].mlz,k2=tree[i].plz;tree[i<<1].sum=(tree[i<<1].sum*k1+k2*(tree[i<<1].r-tree[i<<1].l+1))%p;tree[i<<1|1].sum=(tree[i<<1|1].sum*k1+k2*(tree[i<<1|1].r-tree[i<<1|1].l+1))%p;tree[i<<1].mlz=(tree[i<<1].mlz*k1)%p;tree[i<<1|1].mlz=(tree[i<<1|1].mlz*k1)%p;tree[i<<1].plz=(tree[i<<1].plz*k1+k2)%p;tree[i<<1|1].plz=(tree[i<<1|1].plz*k1+k2)%p;tree[i].plz=0;tree[i].mlz=1;return ;
}
inline void mul(long long i,long long l,long long r,long long k){if(tree[i].r<l || tree[i].l>r)  return ;if(tree[i].l>=l && tree[i].r<=r){tree[i].sum=(tree[i].sum*k)%p;tree[i].mlz=(tree[i].mlz*k)%p;tree[i].plz=(tree[i].plz*k)%p;return ;}pushdown(i);if(tree[i<<1].r>=l)  mul(i<<1,l,r,k);if(tree[i<<1|1].l<=r)  mul(i<<1|1,l,r,k);tree[i].sum=(tree[i<<1].sum+tree[i<<1|1].sum)%p;return ;
}
inline void add(long long i,long long l,long long r,long long k){if(tree[i].r<l || tree[i].l>r)  return ;if(tree[i].l>=l && tree[i].r<=r){tree[i].sum+=((tree[i].r-tree[i].l+1)*k)%p;tree[i].plz=(tree[i].plz+k)%p;return ;}pushdown(i);if(tree[i<<1].r>=l)  add(i<<1,l,r,k);if(tree[i<<1|1].l<=r)  add(i<<1|1,l,r,k);tree[i].sum=(tree[i<<1].sum+tree[i<<1|1].sum)%p;return ;
}
inline long long search(long long i,long long l,long long r){if(tree[i].r<l || tree[i].l>r)  return 0;if(tree[i].l>=l && tree[i].r<=r)return tree[i].sum;pushdown(i);long long sum=0;if(tree[i<<1].r>=l)  sum+=search(i<<1,l,r)%p;if(tree[i<<1|1].l<=r)  sum+=search(i<<1|1,l,r)%p;return sum%p;
}
int main(){in(n);    in(m);in(p);REP(i,1,n)  in(input[i]);build(1,1,n); REP(i,1,m){long long fl,a,b,c;in(fl);if(fl==1){in(a);in(b);in(c);c%=p;mul(1,a,b,c);}if(fl==2){in(a);in(b);in(c);c%=p;add(1,a,b,c);}if(fl==3){in(a);in(b);printf("%lld\n",search(1,a,b));}}return 0;
}
/*
5 4 1000
1 2 3 4 5
3 1 5
2 1 5 1
1 1 5 23 1 5
*/ 

区间修改,单点查询:洛谷树状数组模板2

P3368 【模板】树状数组 2

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <queue>
using namespace std;
int n,m;
int ans;
int input[500010];
struct node
{int left,right;int num;
}tree[2000010];
void build(int left,int right,int index)
{tree[index].num=0;tree[index].left=left;tree[index].right=right;if(left==right)return ;int mid=(right+left)/2;build(left,mid,index*2);build(mid+1,right,index*2+1);
}
void pls(int index,int l,int r,int k)
{if(tree[index].left>=l && tree[index].right<=r){tree[index].num+=k;return ;}if(tree[index*2].right>=l)pls(index*2,l,r,k);if(tree[index*2+1].left<=r)pls(index*2+1,l,r,k);
}
void search(int index,int dis)
{ans+=tree[index].num;if(tree[index].left==tree[index].right)return ;if(dis<=tree[index*2].right)search(index*2,dis);if(dis>=tree[index*2+1].left)search(index*2+1,dis);
}
int main()
{int n,m;cin>>n>>m;build(1,n,1);for(int i=1;i<=n;i++)scanf("%d",&input[i]);for(int i=1;i<=m;i++){int a;scanf("%d",&a);if(a==1){int x,y,z;scanf("%d%d%d",&x,&y,&z);pls(1,x,y,z);}if(a==2){ans=0;int x;scanf("%d",&x);search(1,x);printf("%d\n",ans+input[x]);}}
}

区间加法,洛谷线段树模板1

P3372 【模板】线段树 1

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=1e6+7;
const ll mod=2147483647;
ll n,m;
struct node
{ll l,r,sum,lz;
}tree[N];
ll arr[N];
void build(ll i,ll l,ll r,ll arr[])
{tree[i].lz=0;//初始化的时候肯定都是0tree[i].l=l;tree[i].r=r;if(l==r){tree[i].sum=arr[l];//到达底部单节点才把输入的值赋给你return ;}ll mid=(l+r)/2;build(i*2,l,mid,arr);build(i*2+1,mid+1,r,arr);tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;//树已经全部建完了,再从下往上+++,使得上层的树都有了值return ;
}
inline void push_down(ll i)
{if(tree[i].lz!=0){tree[i*2].lz+=tree[i].lz;tree[i*2+1].lz+=tree[i].lz;ll mid=(tree[i].l+tree[i].r)/2;tree[i*2].sum+=tree[i].lz*(mid-tree[i*2].l+1);tree[i*2+1].sum+=tree[i].lz*(tree[i*2+1].r-mid);tree[i].lz=0;}return ;
}
inline void add(ll i,ll l,ll r,ll k)
{if(tree[i].l>=l&&tree[i].r<=r){tree[i].sum+=k*(tree[i].r-tree[i].l+1);tree[i].lz+=k;return ;}push_down(i);if(tree[i*2].r>=l)add(i*2,l,r,k);if(tree[i*2+1].l<=r)add(i*2+1,l,r,k);tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;return ;
}
inline ll searchs(ll i,ll l, ll r)
{if(tree[i].l>=l&&tree[i].r<=r)return tree[i].sum;push_down(i);ll num=0;if(tree[i*2].r>=l)num+=searchs(i*2,l,r);if(tree[i*2+1].l<=r)num+=searchs(i*2+1,l,r);return num;
}
int main()
{ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);cin>>n>>m;for(int i=1;i<=n;++i)cin>>arr[i];build(1,1,n,arr);//从根节点开始建树for(int i=1;i<=m;++i){ll tmp;cin>>tmp;if(tmp==1){ll a,b,c;cin>>a>>b>>c;add(1,a,b,c);//每次修改都是从编号为1开始的,因为编号1是树的顶端,往下分叉}if(tmp==2){ll a,b;cin>>a>>b;printf("%lld\n",searchs(1,a,b));//编号i的话,每次都是从1开始}}return 0;
}

区间乘法:洛谷线段树模板2

P3373 【模板】线段树 2

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=1e6+7;
template<typename T>void read(T &x)
{x=0;char ch=getchar();ll f=1;while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}x*=f;
}
ll n,m,arr[N],mod,flag,cn,cm,cw;
struct node
{ll l,r,sum,mul,add;//有乘有加,先乘后加
}tree[N];inline void build(ll i,ll l,ll r)
{tree[i].l=l;tree[i].r=r;tree[i].mul=1;if(l==r){tree[i].sum=arr[l]%mod;return ;}ll mid=(l+r)>>1;build(i*2,l,mid);build(i*2+1,mid+1,r);tree[i].sum=(tree[i*2].sum+tree[i*2+1].sum)%mod;
}
inline void push_down(ll i)
{tree[i*2].sum=(ll)(tree[i].mul*tree[i*2].sum+((tree[i*2].r-tree[i*2].l+1)*tree[i].add)%mod)%mod;tree[i*2+1].sum=(ll)(tree[i].mul*tree[i*2+1].sum+((tree[i*2+1].r-tree[i*2+1].l+1)*tree[i].add)%mod)%mod;tree[i*2].mul=(ll)(tree[i*2].mul*tree[i].mul)%mod;tree[i*2+1].mul=(ll)(tree[i*2+1].mul*tree[i].mul)%mod;tree[i*2].add=(ll)(tree[i*2].add*tree[i].mul+tree[i].add)%mod;tree[i*2+1].add=(ll)(tree[i*2+1].add*tree[i].mul+tree[i].add)%mod;tree[i].mul=1;tree[i].add=0;
}
inline void add(ll i,ll l,ll r,ll k)
{if(tree[i].l>=l&&tree[i].r<=r){tree[i].add=(ll)(tree[i].add+k)%mod;tree[i].sum=(ll)(tree[i].sum+k*(tree[i].r-tree[i].l+1))%mod;return ;}push_down(i);if(tree[i*2].r>=l)add(i*2,l,r,k);if(tree[i*2+1].l<=r)add(i*2+1,l,r,k);//ll mid=(tree[i].l+tree[i].r)>>1;//if(l<=mid)add(i*2,l,r,k);//if(mid<r)add(i*2+1,l,r,k);tree[i].sum=(tree[i*2].sum+tree[i*2+1].sum)%mod;
}
inline void mult(ll i,ll l,ll r,ll k)
{if(tree[i].l>=l&&tree[i].r<=r){tree[i].mul=(tree[i].mul*k)%mod;tree[i].add=(tree[i].add*k)%mod;tree[i].sum=(tree[i].sum*k)%mod;return ;}push_down(i);if(tree[i*2].r>=l)mult(i*2,l,r,k);if(tree[i*2+1].l<=r)mult(i*2+1,l,r,k);//ll mid=(tree[i].l+tree[i].r)>>1;//if(l<=mid)mult(i*2,l,r,k);//if(mid<r)mult(i*2+1,l,r,k);tree[i].sum=(tree[i*2].sum+tree[i*2+1].sum)%mod;
}
inline ll query(ll i,ll l,ll r)
{if(tree[i].l>=l&&tree[i].r<=r)return tree[i].sum;push_down(i);ll num=0;if(tree[i*2].r>=l)num=(num+query(i*2,l,r))%mod;if(tree[i*2+1].l<=r)num=(num+query(i*2+1,l,r))%mod;return num;//ll val=0;//ll mid=(tree[i].l+tree[i].r)>>1;//if(l<=mid)val=(val+query(i*2,l,r))%mod;//if(mid<r)val=(val+query(i*2+1,l,r))%mod;//return val;
}
int main()
{read(n),read(m),read(mod);for(int i=1;i<=n;++i)read(arr[i]);build(1,1,n);for(int i=1;i<=m;++i){read(flag);if(flag==1){read(cn),read(cm),read(cw);mult(1,cn,cm,cw);}else if(flag==2){read(cn),read(cm),read(cw);add(1,cn,cm,cw);}else {read(cn),read(cm);cout<<query(1,cn,cm)<<endl;}}
}
/*
5 4 1000
1 2 3 4 5
3 1 5
2 1 5 1
1 1 5 23 1 5
*/ 

区间根号,bzoj3211

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#define MAXN 1000010
#define REP(i,k,n) for(int i=k;i<=n;i++)
#define in(a) a=read()
using namespace std;
int read(){int x=0,f=1;char ch=getchar();for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';return x*f;
}
struct node{int l,r;long long lz,sum,maxx,minn;
}tree[MAXN<<2];
int n,m,input[MAXN];
inline void build(int i,int l,int r){tree[i].l=l;tree[i].r=r;if(l==r){tree[i].sum=tree[i].minn=tree[i].maxx=input[l];return ;}int mid=(l+r)>>1;build(i*2,l,mid);build(i*2+1,mid+1,r);tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;tree[i].minn=min(tree[i*2].minn,tree[i*2+1].minn);tree[i].maxx=max(tree[i*2].maxx,tree[i*2+1].maxx);return ;
}
inline void push_down(int i){if(!tree[i].lz)  return ;long long k=tree[i].lz;tree[i*2].lz+=k;tree[i*2+1].lz+=k;tree[i*2].sum-=(tree[i*2].r-tree[i*2].l+1)*k;tree[i*2+1].sum-=(tree[i*2+1].r-tree[i*2+1].l+1)*k;tree[i*2].minn-=k;tree[i*2+1].minn-=k;tree[i*2].maxx-=k;tree[i*2+1].maxx-=k;tree[i].lz=0;return ;
}
inline void Sqrt(int i,int l,int r){if(tree[i].l>=l && tree[i].r<=r && (tree[i].minn-(long long)sqrt(tree[i].minn))==(tree[i].maxx-(long long)sqrt(tree[i].maxx))){long long u=tree[i].minn-(long long)sqrt(tree[i].minn);tree[i].lz+=u;tree[i].sum-=(tree[i].r-tree[i].l+1)*u;tree[i].minn-=u;tree[i].maxx-=u;//cout<<"i"<<i<<" "<<tree[i].sum<<endl;return ;}if(tree[i].r<l || tree[i].l>r)  return ;push_down(i);if(tree[i*2].r>=l)  Sqrt(i*2,l,r);if(tree[i*2+1].l<=r)  Sqrt(i*2+1,l,r);tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;tree[i].minn=min(tree[i*2].minn,tree[i*2+1].minn);tree[i].maxx=max(tree[i*2].maxx,tree[i*2+1].maxx);//cout<<"i"<<i<<" "<<tree[i].sum<<endl;return ;
}
inline long long search(int i,int l,int r){if(tree[i].l>=l && tree[i].r<=r)return tree[i].sum;if(tree[i].r<l || tree[i].l>r)  return 0;push_down(i);long long s=0;if(tree[i*2].r>=l)  s+=search(i*2,l,r);if(tree[i*2+1].l<=r)  s+=search(i*2+1,l,r);return s;
}
int main(){in(n);REP(i,1,n)  in(input[i]);build(1,1,n);in(m);int a,b,c;REP(i,1,m){in(a);in(b);in(c);if(a==1)printf("%lld\n",search(1,b,c));if(a==2){Sqrt(1,b,c);//for(int i=1;i<=7;i++)//    cout<<tree[i].sum<<" ";// cout<<endl;}}
}

这一篇文章讲解的非常详细易懂,所以存下来用来复习。
对原作者的图进行了重画,更加清晰,对排版也做了优化。
后面的模板代码也用的是我自己AC的代码。

注:如果您通过本文,有(qi)用(guai)的知识增加了,请您点个赞再离开,如果不嫌弃的话,点个关注再走吧 ! 当然,也欢迎在讨论区指出此文的不足处,作者会及时对文章加以修正 !


http://chatgpt.dhexx.cn/article/s4DingdH.shtml

相关文章

Navicat Premium 数据库开发工具

官网地址&#xff1a;https://www.navicat.com.cn/ Navicat Premium 是一套数据库开发工具&#xff0c;让你从单一应用程序中同时连接 MySQL、MariaDB、MongoDB、SQL Server、Oracle、PostgreSQL 和 SQLite 数据库。它与 Amazon RDS、Amazon Aurora、Amazon Redshift、Microso…

Navicat 一款数据库开发软件

Navicat premium是一款数据库管理工具&#xff0c;非常好用&#xff0c;能复制表结构等&#xff0c;大大简化了后端开发的工程量&#xff0c;其软件示意图如下 Navicat Premium 15 已经发布了&#xff0c;但是价格昂贵&#xff0c;所以将 no-money version 的双手奉上。 使用…

Java学习中的数据库和数据库开发工具

一、数据库 1、数据库&#xff0c;通常是一个戒一组文件&#xff0c;保存了一些符合特定规格的数据&#xff0c;数据库对应的英询单词是DataBase&#xff0c;简称DB&#xff1b;数据库软件称为数据库管理系统&#xff0c;英文简称DBMS&#xff0c;全称为DataBase Management S…

五大主流数据库深度对比!数据库开发、管理看这篇就够了

开发数据库应用&#xff0c;选择一个好的数据库是非常重要的。目前&#xff0c;商品化的数据库管理系统以关系型数据库为主导产品&#xff0c; 技术比较成熟。面向对象的数据库管理系统技术先进&#xff0c;数据库易于开发、维护。无论是关系型数据库还是非关系型数据库&#x…

mysql数据库开发环境_MySQL数据库教程-环境与集成开发工具

MySQL数据库基础教程 数据库是存储数据与管理数据的基础,也是目前绝大多数动态网站开发所必备的基础知识。MySQL数据库教程系列文章主要以MySQL数据库管理系统为例对关系型数据库相关定义与操作等进行讲解。并结合php与PDO等讲解,为初学者提供快速学习PHP动态网站开发所需的知…

C/C++ MySQL数据库开发

C/C++ MySQL数据库开发笔记 MySQL启动方式手动启动MySQL数据库:命令行方式启动MySQL数据库:配置环境变量MySQL在Linux下安装MySQL的配置文件重要参数设置C/C++ 访问MySQL数据库(MySQL开发环境的配置(MySQL开发头文件和库文件)MySQL 数据库的连接MySQL 数据类型以及对应的 …

1、 数据库开发规范

第1章 数据库开发规范的制定 数据库设计步骤&#xff1a; 1、数据结构设计&#xff1a;逻辑设计-》物理设计 2、实际工作中&#xff1a;逻辑设计物理设计 3、物理设计&#xff1a;表名字段名字段类型 数据库设计几个规范&#xff1a; 数据库命名规范、数据库基本设计规范…

数据库开发-2-开发数据库的要点

Lec2-开发数据库的要点 1. 开发成功数据库应用的特点 需要理解数据库体系结构需要理解锁和并发控制特性&#xff1a;每个数据库都以不同的方式实现最重要的是不要把数据库当"黑盒" 要了解每一个数据库的体系结构和特征最常见的问题就是因为我们对数据库本身了解不足…

数据库开发(Sqlite)

1、数据库开发 1.1 数据与数据管理 什么是信息&#xff1f;   信息是指对现实世界存在方式或运动状态的反应。 什么是数据&#xff1f;   数据是指存储在某一媒体上&#xff0c;能够被识别的物理符号&#xff1b;   数据的概念在数据处理领域已经被大为拓宽&#xff0c…

《软件测试的艺术》第4章:测试用例的设计-白盒测试

写在前面&#xff1a;原书中包含白盒测试、黑盒测试、错误猜测、测试策略四个小节&#xff0c;涵盖内容较多&#xff0c;因此按章节拆分叙述。 《软件测试的艺术》&#xff1a; 白盒测试--语句覆盖 语句覆盖的用例设计原则&#xff1a;将程序中的每条语句至少执行一次。 白盒…

《软件测试的艺术》读后感 Or 读书笔记

《软件测试的艺术》读后感 Or 读书笔记 第一章 一次自评价测试第二章 软件测试的心理学和经济学第三章 代码检查、走查与评审第四章 测试用例的设计第五章 模块&#xff08;单元&#xff09;测试第六章 更高级别的测试第七章 可用性&#xff08;或用户体验&#xff09;测试第八…

软件测试的艺术 学习笔记

文章目录 4.2黑盒测试4.2.1 等价划分4.2.2 边界值分析4.2.3 因果图 4.3 错误猜测4.4 测试策略 5. 模块(单元&#xff09;测试5.1 测试用例设计5.2 增量测试5.3 自顶向下测试与自底向上测试5.3.1 自顶向下的测试5.3.2 自底向上的测试5.3.3 比较 5.4 执行测试 6 更高级别的测试6.…

《软件测试的艺术》第六章 更高级别的测试

《软件测试的艺术》第六章 更高级别的测试 6.0 前言软件开发过程模型 6.1 功能测试6.2 系统测试6.2.1 能力测试6.2.2 容量测试6.2.3 强度测试6.2.4 可用性测试6.2.5 安全性测试6.2.6 性能测试6.2.7 存储测试6.2.8 配置设置6.2.9 兼容性/转换测试6.2.10 安装测试6.2.11 可靠性测…

模块测试(单元测试)——软件测试的艺术

是大型程序测试的第一个步骤【大型程序即超过500条语句的程序】 了解 模块测试是对程序中的单个程序、子程序/过程进行测试的过程【并非对整个程序】&#xff1a; 关注点在较小单元&#xff0c;是一种管理组合的测试元素的手段减轻调试的难度&#xff0c;把错误定位到一个小…

《软件测试的艺术》第1章:一次自评价测试

写在前面&#xff1a; 相比于芯片验证&#xff0c;软件测试有着悠久的历史沉淀和更为完整的生态&#xff0c;和芯片验证在某些方面上几乎有着相同的思路和方法。因此从软件测试的视角出发&#xff0c;重新思考芯片验证的方方面面。第一个系列为《软件测试的艺术》学习。 第一…

9年测试老鸟:Glenford J编写《软件测试的艺术》PDF,高清中文版

内容简介 本书以一次自评价测试开篇&#xff0c;从软件测试的心理学和经济学人手&#xff0c;探讨了代码检查、走查与评审、测试用例的设计、模块(单元)测试、系统测试、调试等主题&#xff0c;以及极限测试、因特网应用系统测试等高级主题&#xff0c;全面展现了作者的软件测…

系统测试——软件测试的艺术

系统测试有着特定的目的&#xff1a;将系统或程序与其初始目标进行比较&#xff0c;给定目标后有两含义&#xff1a; 系统测试不局限于系统&#xff0c;若产品是一个程序&#xff1a;系统测试就是试图说明程序作为一个整体是如何不满足其目标的过程根据定义&#xff0c;若产品…

《软件测试的艺术》重点记录

----定义---- 测试是为发现错误而执行程序的过程。 测试提高了程序的可靠性或质量。 ----测试方法---- 黑盒测试&#xff1a;又称之为数据驱动的测试或输入/输出驱动的测试。 白盒测试&#xff1a;对程序的逻辑结构进行检查&#xff0c;从中获取测试数据。 ----测试的原则…

软件测试的艺术(测试工程师必备基本知识与概念)

目录&#xff1a; 一、黑盒测试与白盒测试&#xff1a; 等价类划分&#xff1a; 一、确定等价类 确定等价类是选取每一个输入条件&#xff08;通常是规格说明中的一个句子或短语&#xff09;并 将其划分为两个或更多的组。可以使用图 4-3 中的表格来进行划分。注意&#xff0…

《软件测试的艺术》第五章 模块(单元)测试

目录 5.0 前言 5.1 测试用例设计 5.2 增量测试 5.3 自顶向下测试和自底向上测试 5.4 执行测试 5.5 小结 5.0 前言 大型的软件程序需要特别的测试对策。在本章中我们会探讨构建大型程序测试的第一个步骤&#xff1a;模块测试&#xff08;单元测试&#xff09;&#xff0c…