NUIST OJ 1350-1352 面朝大海,春暖花开【初识线段树】

NUIST OJ 1350-1352 面朝大海,春暖花开

  • NUIST OJ 1350-1352 面朝大海春暖花开
    • NUIST OJ 1350 面朝大海 春暖花开 基础版
    • NUIST OJ 1351 面朝大海 春暖花开 数据增强版
    • NUIST OJ 1352 面朝大海 春暖花开 数据更强版
      • 线段树引入
      • 线段树的储存
      • 线段树的创建
      • 区间修改
      • 区段求和
    • 随便说说
    • 后记

本题组初步认识了线段树及其使用。其中1350-1351的探索过程,引出线段树,并证明其在高效解决连续区间的动态查询问题上的作用。

NUIST OJ 1350 面朝大海 春暖花开 [ 基础版 ]

题目描述

选择那些大晴天的日子,行走在孤单的海岸线,静静地种花给自己看~
我们假设把海岸线分为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;
}

这里写图片描述
这里写图片描述

NUIST OJ 1351 面朝大海 春暖花开 [ 数据增强版 ]

本题在数据上进行调整,扩充测试数据,增强了严苛程度。相关修改如下:

输入描述:

多组输入
对每组输入,第一行有两个整数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 的空间来保存,也不会节省空间甚至某些情况下可能消耗比原来更多。

以上题目的困窘都暗示着有一种高效的数据结构,在高效解决连续区间的动态查询问题上有很强的能力。由此,我们来看今天的主角 ————–> 线段树

NUIST OJ 1352 面朝大海 春暖花开 [ 数据更强版 ]

除了种花之外,看花(查询)的次数也大大增多

题目描述

第三次选择那些大晴天的日子,第三次行走在孤单的海岸线,第三次静静地种更多的花给自己看~
我们假设把海岸线分为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 团队大佬的帮助指正。

你可能感兴趣的:(NUIST,OJ)