目录
梦开始的地方:
一道very easy的程设题
- 我的提交
论一个人是怎么偷懒的:
#include<stdio.h>
int main()
{
int maxn = -10000,minn = 10000;
int x;
while(scanf("%d",&x)!=EOF)
{
maxn = maxn > x ? maxn : x;
minn = minn < x ? minn : x;
}
printf("%d,%d",minn,maxn);
}
- 可能的正解(c++)
#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
const int N = 1e5+10,M = 2 * N,INF = 0x3f3f3f3f,mod = 1e9+7;
int w[N];
int max(int l,int r)
{
if(l==r)
{
return w[r];
}
else
{
int mid = l + r >> 1;
return std::max(max(l,mid),max(mid+1,r));
}
}
int min(int l,int r)
{
if(l==r)
{
return w[r];
}
else
{
int mid = l + r >> 1;
return std::min(min(l,mid),min(mid+1,r));
}
}
int main()
{
// std::ios::sync_with_stdio(false);
// std::cin.tie(nullptr);
// std::cout.tie(nullptr);
int n = 0;
while(std::cin>>w[++n]);
std::cout<<min(1,n)<<','<<max(1,n);
return 0;
}
- 关于我突发奇想的代码(线段树):
#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
const int N = 1e5+10,M = 2 * N,INF = 0x3f3f3f3f,mod = 1e9+7;
struct Node{
int l,r;
int max,min;
}tr[N*4];
int w[N];
void pushup(int u)
{
tr[u].max = std::max(tr[u<<1].max,tr[u<<1|1].max);
tr[u].min = std::min(tr[u<<1].min,tr[u<<1|1].min);
}
void build(int u,int l,int r)
{
if(l==r)
{
tr[u] = {l,r,w[r],w[r]};
}
else
{
tr[u] = {l,r};
int mid = l + r >> 1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
pushup(u);
}
}
int main()
{
// std::ios::sync_with_stdio(false);
// std::cin.tie(nullptr);
// std::cout.tie(nullptr);
int n = 0;
while(std::cin>>w[++n]);
build(1,1,n);
std::cout<<tr[1].min<<','<<tr[1].max;
return 0;
}
- 本题与线段树之间的联系:
我们可以发现,程设中寻找最大最小值的时间复杂度都是O(nlogn)
但是这题显然有一个地球人都知道的O(n)做法
但是假如我们把递归的过程中每个区间的最大最小值存起来,那么我们惊奇地发现,这个过程就是线段树的初始化,那么具体线段树是什么呢,且听我娓娓道来。
线段树有什么用:
可以处理very多的区间问题,而且一次操作的时间都是O(logn)
对于这一道程设题,确实是大炮打苍蝇。
但是如果当这道题目的查询次数不是一次,而是几千次甚至几万次的时候,O(logn)的查询就显得相当重要了。
这里我先摆一道洛谷上的模板题:
虽然题目为树状数组,但是这题也可以用线段树来做。
(事实上线段树在功能上几乎可以代替树状数组)
因为洛谷中的线段树模板题需要懒标记,我们在这里不会介绍懒标记,所以这里采用洛谷的这一题作为模板。
线段树是什么:
线段树就是一种特殊的二叉树,不要想得太复杂
其中的每一个结点存储的信息是一个区间,故名线段树
那么线段树代码怎么实现呢:
我们先看看线段树的一个图
大概线段树博客都会有这样的图吧:
一般来说,一个节点可以用一个结构体表示。
struct Node{ int l,r; }tr[N * 4];
这样就准备好了
以本题为例,因为我们要求区间和,所以我们要在结构体中多维护一个区间和
struct Node{ int l,r,sum; }tr[N * 4];
特别声明:线段树所需开的节点个数应该是区间长度的4倍(不作证明)
下面的代码实现会使用几种位运算:
- x<<1 x * 2
- x>> 1 x / 2
- x<<1|1 x * 2 + 1
建树:
首先:
因为是二叉树,所以我们把 1 号点 作为 根(root)节点。
当父节点为 点 u 时,左右儿子分别为:
- 左儿子 = u * 2 ----> u << 1
- 右儿子 = u * 2 + 1 ----> u << 1 | 1
tr[u<<1],tr[u<<1|1]
先上代码:
pushup函数:用儿子节点的信息更新父亲节点的信息。
void pushup(int u) { tr[u].sum = tr[u<<1].sum + tr[u<<1|1].sum; }
void build(int u,int l,int r) { if(l==r) { tr[u] = {l,r,w[r]};//w[r]是这个点的值 } else { tr[u] = {l,r}; int mid = l + r >> 1; build(u<<1,l,mid),build(u<<1|1,mid+1,r);//递归建树 pushup(u);//用儿子节点更新父节点 } }
最终的建树效果见上面的图。
单点修改操作:
如果更新了原区间中的某一个点,那么我们显然要把所有含有这个点的节点全部修改。
代码如下:
void modify(int u,int x,int v) { if(tr[u].l==x&&tr[u].r==x) { tr[u].sum += v; } else { int mid = tr[u].l + tr[u].r >> 1; if(x <= mid)modify(u<<1,x,v); else modify(u<<1|1,x,v); pushup(u); } }
修改过程中,我们查看左儿子和右儿子,所更新的点在哪个儿子内,就修改哪个儿子。
要注意更新完后使用pushup函数更新父节点。
区间查询操作 :
这里我们先放代码,再进行解释:
int query(int u,int l,int r) { if(tr[u].l >= l && tr[u].r <= r) { return tr[u].sum; } else { int mid = tr[u].l + tr[u].r >> 1; int res = 0; if(l <= mid)res += query(u<<1,l,r); if(r > mid)res += query(u<<1|1,l,r); return res; } }
- 如果询问区间包含了某个节点的区间,返回这个节点的和即可。
- 如果询问区间与左儿子相交,就去递归询问左儿子
- 如果询问区间与右儿子相交,就去递归询问右儿子
例子:询问[4,9]的区间和
至此,在本题中的基本操作都完成了,把上面这些核心操作合并,加上main函数,那么这道题的AC代码就完成了。
#include<bits/stdc++.h> typedef long long ll; typedef unsigned long long ull; const int N = 5e5+10,M = 2 * N,INF = 0x3f3f3f3f,mod = 1e9+7; struct Node{ int l,r,sum; }tr[N * 4]; int w[N]; void pushup(int u) { tr[u].sum = tr[u<<1].sum + tr[u<<1|1].sum; } void build(int u,int l,int r) { if(l==r) { tr[u] = {l,r,w[r]}; } else { tr[u] = {l,r}; int mid = l + r >> 1; build(u<<1,l,mid),build(u<<1|1,mid+1,r); pushup(u); } } void modify(int u,int x,int v) { if(tr[u].l==x&&tr[u].r==x) { tr[u].sum += v; } else { int mid = tr[u].l + tr[u].r >> 1; if(x <= mid)modify(u<<1,x,v); else modify(u<<1|1,x,v); pushup(u); } } int query(int u,int l,int r) { if(tr[u].l >= l && tr[u].r <= r) { return tr[u].sum; } else { int mid = tr[u].l + tr[u].r >> 1; int res = 0; if(l <= mid)res += query(u<<1,l,r); if(r > mid)res += query(u<<1|1,l,r); return res; } } int main() { int n,m; std::cin>>n>>m; for(int i = 1 ; i <= n ; i++)std::cin>>w[i]; build(1,1,n); while(m--) { int op,x,y; std::cin>>op>>x>>y; if(op==1) { modify(1,x,y); } else { std::cout<<query(1,x,y)<<'\n'; } } return 0; }
最后关于线段树的几点说明:
时间复杂度:
建树过程:O(nlogn)
修改操作:O(logn)
查询操作:O(logn)
总复杂度(操作数很大):O(nlogn)
如何维护更多的信息:
我们只需要在结构体中加上我们所需维护的信息,并在各个函数中稍作修改即可。
具体的例子可以看开头的代码(维护最大值和最小值)。
可以维护哪些信息:
我们可以发现,线段树的信息是需要由左右儿子(左右区间)推出的。
所以,我们所维护的信息必须要具有结合性,如:
- 区间和
- 最大值
- 最小值
- 异或和
pushup函数如下:
tr[u].sum = tr[u<<1].sum + tr[u<<1|1].sum; tr[u].min = std::min(tr[u<<1].min + tr[u<<1|1].min); tr[u].max = std::max(tr[u<<1].max + tr[u<<1|1].max); tr[u].v = tr[u<<1].v ^ tr[u<<1|1].v;
关于线段树的更多拓展:
- 区间修改
- 动态开点
挖坑行为,本博客中不涉及区间修改:我们容易发现,如果我们像单点修改一样去区间修改,那么时间复杂度将不再是O(logn),所以我们会采用一个操作——懒标记
动态开点:在普通线段树中,区间长度一旦给定,就是无法修改的,也不能增加新的点,我们可以将线段树可持久化,也就是大名鼎鼎的动态开点线段树——主席树。