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<iostream>
#include<cstdlib>
#include<cstring>
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<iostream>
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<iostream>
#include<cstdio>
#include<cstring>
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 1350-1352 面朝大海,春暖花开【初识线段树】相关推荐

  1. NUIST OJ 1352 回顾 【差分】

    NUIST OJ 1352 回顾 [差分] NUIST OJ 1352 回顾 差分 题目 原问题 差分是什么 求An数组和Sn数组 快速求区间和 代码呈现 随便说说 后记 题目 题目描述 第三次选择那 ...

  2. 1350:面朝大海 春暖花开 [ 基础版 ]

    1350:面朝大海 春暖花开 [ 基础版 ] 难度: 倔强青铜    时间限制: 1000MS   空间限制: 64MB   提交数: 335   通过数: 27 题目描述: 选择那些大晴天的日子,行 ...

  3. 面朝大海, 春暖花开

    从明天起, 做一个幸福的人 喂马, 劈柴, 周游世界 从明天起, 关心粮食和蔬菜 我有一所房子, 面朝大海, 春暖花开 从明天起, 和每一个亲人通信 告诉他们我的幸福 那幸福的闪电告诉我的 我将告诉每 ...

  4. NUIST OJ 1364 [2017 江苏科技大学 程序设计竞赛]D.重复成绩统计(改编) 【STL-map】

    NUIST OJ 1364 [2017 江苏科技大学 程序设计竞赛]D.重复成绩统计(改编) [STL-map] NUIST OJ 1364 2017 江苏科技大学 程序设计竞赛D重复成绩统计改编 S ...

  5. NUIST OJ 1369 [2017 江苏科技大学 程序设计竞赛] B. Mr.Z 的四因子数 (数据加强版)

    NUIST OJ 1369 [2017 江苏科技大学 程序设计竞赛] B. Mr.Z 的四因子数 (数据加强版) NUIST OJ 1369 2017 江苏科技大学 程序设计竞赛 B MrZ 的四因子 ...

  6. 《面朝大海, 春暖花开》——海子

    <面朝大海, 春暖花开>--海子 从明天起, 做一个幸福的人 喂马, 劈柴, 周游世界 从明天起, 关心粮食和蔬菜 我有一所房子, 面朝大海, 春暖花开 从明天起, 和每一个亲人通信 告诉 ...

  7. ----海子----《面朝大海 春暖花开》

    从明天起,做一个幸福的人 喂马,劈柴,周游世界 从明天起,关心粮食和蔬菜 我有一所房子,面朝大海,春暖花开 从明天起,和每一个亲人通信 告诉他们我的幸福 那幸福的闪电告诉我的 我将告诉每一个人 给每一 ...

  8. 禾穗漫读 | 面朝大海 春暖花开

    主理人 | 謝玢        编辑 | Dave  董雯雪       视觉设计 | 任染 这是 禾穗HERS女性商学院 的第  005 篇禾穗漫读 面朝大海 春暖花开 海子 从明天起, 做一个幸福 ...

  9. [转载]面朝大海 春暖花开

    从明天起 做一个幸福的人 喂马 劈柴 周游世界 从明天起 关心粮食和蔬菜 我有一所房子 面朝大海 春暖花开 从明天起 和每一位亲人通信 告诉他们我的幸福 那幸福的闪电告诉我的 我要告诉每一个人 给每一 ...

最新文章

  1. 论网站长尾关键词优化的六大方法
  2. 【Android NDK 开发】Android.mk 配置动态库 ( Android Studio 配置动态库 | 动态库加载版本限制 | 本章仅做参考推荐使用 CMake 配置动态库 )
  3. 团队开发博客成员介绍(发在个人博客中了)
  4. C#获取当前路径的方法集合
  5. redis实现session共享,哨兵
  6. 使用 BenchmarkDotnet 测试代码性能
  7. 取消android所有动画,android studio 取消BottomNavigationView的动画等
  8. 清华毕业生做保姆,那又如何呢
  9. sweetalert插件的使用
  10. for循环中的参数能不省略
  11. Displaytag 详解
  12. 轻快pdf阅读器 电子书阅读软件电脑版
  13. Linux 用户管理
  14. 两条平滑曲线相乘_对三次贝塞尔曲线过点平滑中尖角和交叉现象的优化
  15. 读书笔记-可靠性工程师前言
  16. Unity实现人物移动和镜头跟随
  17. 密码学---公钥密钥---背包密码体制
  18. 政治冲刺押题所有“黑幕”!只用肖四肖八行吗?
  19. 基于Android平台的监控端和被监控端系统
  20. Linux / Windows系统中安装最新版 ElasticSearch (es)搜索引擎 超详细图文教程【一看就懂】

热门文章

  1. 计算机桌面最小化后找不到,微博桌面最小化后找不到图标了怎么办??
  2. Windows登录类型及安全日志解析
  3. SUMO中车辆需求建模方法(rou.xml)
  4. 华为路由器 静态路由
  5. 同程学生卡怎么开通,开通同程旅行学生会员开通方法
  6. 5G手机即将上市,新的创业风口来了!
  7. 量子计算机不是永动机,几种人类设计的永动机,最后一个是谁设计的!彻底服了!...
  8. 记一次Redis被入侵(被黑)处理过程
  9. Window类似Alfred的搜索软件——Wox
  10. 从财报看康佳、创维、TCL、海信能否守住家电市场的“江湖地位”?