本题组初步认识了线段树及其使用。其中1350-1351的探索过程,引出线段树,并证明其在高效解决连续区间的动态查询问题上的作用。
题目描述
选择那些大晴天的日子,行走在孤单的海岸线,静静地种花给自己看~
我们假设把海岸线分为n块,每块的分别标记为1…n,每块都可以种花,每次种花可以选择某个[left,right]的闭区间,每块种上一朵花.经过m次种花操作后,根据输入的区间,求该区间内花的总数.
输出描述:
对每组测试数据,输出区间[a,b]内花的总数
样例输入
5 2
1 5
1 2
2 3
样例输出
3
仅作为引例,数据较为友好,直接简单的数组暴力求解即可解决,不进行过多说明。
#include
#include
#include
using namespace std;
int main()
{
int n, m, a, b, l, r, num;
int *data = NULL;
while (cin >> n >> m)
{
num = 0;
data = (int*)realloc(data, n * sizeof(int));
memset(data, 0, sizeof(int)*n);
while (m--)
{
cin >> l >> r;
if (l > r)
{
l = l + r;
r = l - r;
l = l - r;
}
for (int i = l - 1; i < r; i++)
data[i] += 1;
}
cin >> a >> b;
if (a > b)
{
a = a + b;
b = a - b;
a = a - b;
}
for (int i = a - 1; i <= b - 1 ; i++)
num += data[i];
cout << num << endl;
}
return 0;
}
本题在数据上进行调整,扩充测试数据,增强了严苛程度。相关修改如下:
输入描述:
多组输入
对每组输入,第一行有两个整数n m,分别代表总块数和种花的次数.(1 <= n, m <= 100000)
接下来的m行, 每行两个整数 L,R 代表[L,R]区间内每块种上一朵花.(1 <= L <= R <= n)
最后一行,输入两个整数 a,b 代表最后要查询的花的总数的区间.(1 <= a <= b <= n)
直接套用1350的代码,不出意外的Runtime Error
个人推断是无法申请这么大的连续内存空间。考虑至此,我决定尝试使用链表的方式储存数据。修改如下:
#include<iostream>
using namespace std;
typedef struct List
{
int data;
List * next;
} *LinkList;
void creatlist(LinkList &L,int n)
{
L = (LinkList)malloc(sizeof(List));
L->next = NULL;
LinkList p, q = L;
while (n--)
{
p = (LinkList)malloc(sizeof(List));
p->data = 0;
p->next = NULL;
q->next = p;
q = q->next;
}
}
void DeleteAll(LinkList head)
{
LinkList p = head;
if (head == NULL) //链表为空无需处理
return;
while (p->next != NULL) //删除链表非首结点元素
{
p = p->next;
free(head);
head = p;
}
free(head); //删除链表首结点元素
}
int main()
{
int n, m, a, b, l, r, num;
LinkList p = NULL;
while (cin >> n >> m)
{
LinkList L;
creatlist(L,n);
num = 0;
while (m--)
{
cin >> l >> r;
p = L->next;
for (int i = 0; i < l - 1; i++)
p = p->next;
for (int i = l - 1; i < r; i++)
{
p->data += 1;
p = p->next;
}
}
cin >> a >> b;
p = L->next;
for (int i = 0; i < a - 1 ; i++)
p = p->next;
for (int i = a - 1; i <= b - 1; i++)
{
num += p->data;
p = p->next;
}
cout << num << endl;
DeleteAll(L);
}
return 0;
}
然而结果很难过
超时了。但是这其实并不意外。链式结构里面,要修改后面节点的,需要从头开始一个一个找过去,查询的时候也是如此,在数据特别多的情况下,链表极长,虽然解决的存储的问题,但是耗时确实不可接受。
鉴于此,我就想是否可以不保存每个节点上的数量、不对每个节点单独维护。这样就可以大大节省修改每个节点的耗费。即只统计操作中对最后结果有用的值并加和。代码如下:
#include
using namespace std;
int main()
{
int **data = NULL, n, m, le, ri, a, b, num;
while (cin >> n >> m)
{
num = 0;
free(data);
data = new int*[m];
for (int i = 0; i < m; i++)
{
data[i] = new int[2];
cin >> data[i][0] >> data[i][1];
}
cin >> a >> b;
for (int i = 0; i < m; i++)
{
if (data[i][0] > b)
continue;
if (data[i][1] < a)
continue;
if (data[i][1] > b)
data[i][1] = b;
if (data[i][0] < a)
data[i][0] = a;
num += data[i][1] - data[i][0] + 1;
}
cout << num << endl;
}
return 0;
}
其实从结果来看,还是挺让人不满意的。细细分析,若是种花操作很多的情况下,对每个操作仍然需要 sizeof(int)*2 的空间来保存,也不会节省空间甚至某些情况下可能消耗比原来更多。
以上题目的困窘都暗示着有一种高效的数据结构,在高效解决连续区间的动态查询问题上有很强的能力。由此,我们来看今天的主角 ————–> 线段树
除了种花之外,看花(查询)的次数也大大增多
题目描述
第三次选择那些大晴天的日子,第三次行走在孤单的海岸线,第三次静静地种更多的花给自己看~
我们假设把海岸线分为n块,每块的分别标记为1…n,每块都可以种花,每次种花可以选择某个[left,right]的闭区间,每块种上一朵花.经过m次种花操作后, 输入t次区间, 根据输入的区间,求该区间内花的总数.
注意这一次,我们要看更多次的花儿,所以在第一行要输入看花的次数t
输入描述:
多组输入
对每组输入,第一行有三个整数n m t,分别代表总块数和种花的次数以及我们希望查询区间的次数.
(1 <= n, m, t<= 100000)
接下来的m行, 每行两个整数 L,R 代表[L,R]区间内每块种上一朵花.(1 <= L <= R <= n)
接下来的t行, 每行输入两个整数 a,b 代表最后要查询的花的总数的区间.(1 <= a <= b <= n)
本题再次套用1351的程序,简单加上多组查询的循环,就会各种超时超内存。很显然,需要新方法。
先贴代码和结果再解释。
#include
#include
#include
using namespace std;
struct SegmentTree
{
int left;
int right;
int mid;
long sum;
long lazy;
}st[330000];
void BuildTree(int x,int y,int num)//建树
{
st[num].left = x;
st[num].right = y;
st[num].mid = (x + y) / 2;
st[num].lazy = 0;
if (x == y)
return;
else
{
BuildTree(x, st[num].mid, num * 2);
BuildTree(st[num].mid + 1, y, num * 2 + 1);
}
return;
}
//void Update(int Pre,int Des,int Val)//单点修改
//{
// if (st[Pre].left == Des&&st[Pre].right == Des)
// st[Pre].sum += Val;
// else
// {
// if (Des <= st[Pre].mid)
// Update(Pre * 2, Des, Val);
// else
// Update( Pre * 2 + 1, Des, Val);
// st[Pre].sum = st[Pre * 2].sum + st[Pre * 2 + 1].sum;
// }
//}
void SecUpdate(int L, int R, int Pre, int val)//区间修改
{
if (L <= st[Pre].left&&R >= st[Pre].right)
st[Pre].lazy += val;
else
{
if (R > st[Pre].mid)
SecUpdate(L, R, Pre * 2 + 1, val);
if (L <= st[Pre].mid)
SecUpdate(L, R, Pre * 2, val);
st[Pre].sum = st[Pre * 2].sum + (st[Pre * 2].right - st[Pre * 2].left + 1)*st[Pre * 2].lazy + st[Pre * 2 + 1].sum + (st[Pre * 2 + 1].right - st[Pre * 2 + 1].left + 1)*st[Pre * 2 + 1].lazy;
}
}
long long GetSum(int x, int y, int Pre)//区段求和
{
long long ans = 0;
if (x <= st[Pre].left&&y >= st[Pre].right)
ans = st[Pre].sum + (st[Pre].right - st[Pre].left + 1)*st[Pre].lazy;
else
{
if (st[Pre].lazy != 0)
{
st[Pre].sum += (st[Pre].right - st[Pre].left + 1)*st[Pre].lazy;
st[Pre * 2].lazy += st[Pre].lazy;
st[Pre * 2 + 1].lazy += st[Pre].lazy;
st[Pre].lazy = 0;
}
if (x <= st[Pre].mid)
ans += GetSum(x, y, Pre * 2);
if (y > st[Pre].mid)
ans += GetSum(x, y, Pre * 2 + 1);
}
return ans;
}
int main()
{
int N, m, t,a,b,l,r;
while (scanf("%d%d%d", &N, &m, &t) != EOF)
{
memset(st, 0, sizeof(st));
BuildTree(1, N, 1);
while (m--)
{
scanf("%d%d", &a, &b);
SecUpdate(a, b, 1, 1);
}
while (t--)
{
scanf("%d%d", &l, &r);
printf("%lld\n",GetSum(l, r, 1));
}
}
return 0;
}
对于线段树的理解贴出个人认为较清楚的链接,我也是初学就不讲的一知半解了
[ CSDN岩之痕 ]
线段树可以使用链表或者数组模拟。简单起见,此处使用数组模拟
struct SegmentTree
{
int left;
int right;
int mid;
long sum;
long lazy;
}st[330000];
其中left和right表示的是本节点及其下属节点维护的数据范围,mid为该范围的中值,便于后续的操作。
sum和lazy用于表示数据总和。类似的还可以定义maximum、minimum等等。
void BuildTree(int x,int y,int num)//建树
{
st[num].left = x;
st[num].right = y;
st[num].mid = (x + y) / 2;
st[num].lazy = 0;
if (x == y)
return;
else
{
BuildTree(x, st[num].mid, num * 2);
BuildTree(st[num].mid + 1, y, num * 2 + 1);
}
return;
}
注意:因为在调用建树函数之前,主函数内有一句
memset(st, 0, sizeof(st));
将所有的值置位0,故此在初始化中不需要考虑。若实际需求中有初始化数据数组,则应该在
if (x == y)
{
/////////求和赋值////////
return;
}
内部给叶子结点赋初始,并且在
else
{
BuildTree(x, st[num].mid, num * 2);
BuildTree(st[num].mid + 1, y, num * 2 + 1);
/////////求和赋值////////
}
给非叶子结点赋值。
void SecUpdate(int L, int R, int Pre, int val)//区间修改
{
if (L <= st[Pre].left&&R >= st[Pre].right)
st[Pre].lazy += val;
else
{
if (R > st[Pre].mid)
SecUpdate(L, R, Pre * 2 + 1, val);
if (L <= st[Pre].mid)
SecUpdate(L, R, Pre * 2, val);
st[Pre].sum = st[Pre * 2].sum + (st[Pre * 2].right - st[Pre * 2].left + 1)*st[Pre * 2].lazy + st[Pre * 2 + 1].sum + (st[Pre * 2 + 1].right - st[Pre * 2 + 1].left + 1)*st[Pre * 2 + 1].lazy;
}
}
注意到,这里使用了一个lazy变量。简要叙述下lazy标签带来的好处。
当需要更新一个区间的值,例如1-5区间,下分1-3,4-5;而1-3又分为1-2,3;4-5分为4、5;1-2再分为1、2。若是对这个区间的每个值进行+1操作,需要一层层往下找并且修改每个节点,太麻烦太耗时了!
此处利用了一个lazy变量,表示对改范围区间内每个值的通用操作,例如+1。那么在后续的区间查询求和操作中,只需要计算范围乘上lazy增量,即可算得该区间的增量,大大减少了时间消耗。
那么lazy标记所表示的值何时传递到下属节点呢?在需要对节点下属子区间操作的时候,例如查询求和时。
long long GetSum(int x, int y, int Pre)//区段求和
{
long long ans = 0;
if (x <= st[Pre].left&&y >= st[Pre].right)
ans = st[Pre].sum + (st[Pre].right - st[Pre].left + 1)*st[Pre].lazy;
else
{
if (st[Pre].lazy != 0)
{
st[Pre].sum += (st[Pre].right - st[Pre].left + 1)*st[Pre].lazy;
st[Pre * 2].lazy += st[Pre].lazy;
st[Pre * 2 + 1].lazy += st[Pre].lazy;
st[Pre].lazy = 0;
}
if (x <= st[Pre].mid)
ans += GetSum(x, y, Pre * 2);
if (y > st[Pre].mid)
ans += GetSum(x, y, Pre * 2 + 1);
}
return ans;
}
这里求和并且返回的时候就考虑到了lazy标记带来的额外增量,并且一次性更新到下属节点。若lazy=5,则原来需要遍历子树5次的操作在此简化为1步。而且,这种更新只是更新到下一层的lazy标记当中,并不是必定延伸到叶子结点,即能偷懒就偷懒。此处配有【手动滑稽】
一开始在没有了解线段树这个知识之前,总是想着在旧的方法上面优化调整,这样有时候只能勉强完成题目要求。例如1351的程序我随便加了个多次检索的for循环就提交到1352,这不可能不出问题的。做题目不能老想着用以前的旧的知识去完成,这样永远学不到新东西。做题的目的,除了练习之外,更多的是开阔视野啊。
这是我第一次在CSDN上面发表拙见,难免有不可预知的错误望谅解与指正。
对于完全没有入门的我来说,所谓的算法路漫漫,好在,还年轻啊。
在此感谢所有CSDN上的前辈写的文字,浅显易懂使人受益匪浅。还有 NUIST ThinkSpirit 团队大佬的帮助指正。