欢迎留言![全网最详细讲解]Part1.OpenJudge百练2528:Mayor's posters Part2.线段树从简单题到复杂题详细讲解

Part1.百练2528:Mayor's posters

题目链接:http://bailian.openjudge.cn/p...

思路1:Brute Force,record and keep updating the most outside poster‘s ID. Scan once and count the number of different IDs at last.

Solution 2. Based on Segment-Tree(线段树).

先理解线段树的基础知识。

    建树。每个节点的id值(关键值)存储该区域最外层的海报的id。节点关键值id的设置:将-1 定义为没有海报覆盖,0代表有多个海报覆盖,id=1到m代表被第id个海报覆盖,线段树只要在0时向下深入,其他的直接统计颜色。统计第id个可以考虑使用数组模拟位图。
        查询。在一个路径上不继续往子孙搜索的标志是,这个区域上只被一张海报覆盖或,即遇到1到m,此时更新位图。遇到-1,也停止在路径上继续搜索。
        扫描为位图得到最后结果。

Code for Solution 2 :
注意下面code中,我用-1表示了有多个海报,0表示了没有多个海报覆盖,与上面不一致。

//贴海报,输出没有被覆盖的个数

include<stdio.h>

struct node

{

int l,r; //左右孩子的编号  

int st,mi,en;  

int id;  

}; // 线段树简单一维

const int maxN = 50000002; //线段树的节点个数

const int maxL = 10000020; //台阶的宽度上限

node segment_tree[maxN]; //保存着线段树的所有节点

define tree segment_tree

int root, ptr;

void insert(int cr, int start, int end, int color) //插入到指定区域,同时初始化沿途的所有节点

{

if(start >= end) //不符合输入要求  

    return;  

if(tree[cr].st == start && tree[cr].en == end) //输入区间正好等于节点的表示区间  

{  

    tree[cr].id = color; //这个区间属于该海报  

    return;  

}  

int mid = (tree[cr].st + tree[cr].en) / 2;  

if(tree[cr].l == 0) //意味着还没有初始化孩子结点  

{  

    //ptr代表节点的编号  

    tree[cr].l = ptr++;  

    tree[tree[cr].l].l = tree[tree[cr].l].r = 0;  

    tree[tree[cr].l].id = -1;  

    tree[tree[cr].l].st = tree[cr].st, //这里对左右孩子的范围进行初始化  

    tree[tree[cr].l].en = mid;  

}  

if(tree[cr].r == 0)  

{  

    tree[cr].r = ptr++;  

    tree[tree[cr].r].l = tree[tree[cr].r].r = 0;  

    tree[tree[cr].r].id = -1;  

    tree[tree[cr].r].st = mid,  

    tree[tree[cr].r].en = tree[cr].en;  

}  



if(tree[cr].id != 0) //之后的子区间肯定都属于该海报  

{  

    tree[tree[cr].l].id = tree[tree[cr].r].id = tree[cr].id;  

    tree[cr].id = 0;  

}  



if(start >= mid){  

    insert(tree[cr].r, start, end, color);  

    return;  

}  

if(end <= mid){  

    insert(tree[cr].l, start, end, color);  

    return;  

}  

insert(tree[cr].l, start, mid, color);  

insert(tree[cr].r, mid, end, color);  

}

char exist[10001];

void trail(int cr) //统计可以看见的节点编号

{

if(cr == 0 || tree[cr].id == -1)  

    return;  

exist[tree[cr].id] = 1; //id不为0,意味着只有它可见,但之后的节点都看不见了.  

if(tree[cr].id != 0) //不为0意味着后面的区域都要被覆盖.  

    return;  

trail(tree[cr].l);  

trail(tree[cr].r);  

}

//初始化跟节点

void init()

{

root = 1;  

tree[root].l = tree[root].r = tree[root].id = 0;  

tree[root].st = 1, tree[root].en = maxL, tree[root].mi = (1 + maxL)/2;  

ptr = 2;  

}

int main()

{

int test,n,i,l,r;  

scanf("%d", &test);  

while(test--)  

{  

    init();  

    scanf("%d",&n);  

    for(i = 1; i <= n; i++)  

    {  

        scanf("%d%d",&l,&r);  

        insert(1, l, r+1, i); //从根节点开始插入  

    }  

    for(i = 1; i <= n; i++)  

        exist[i] = 0;  

    trail(1);  

    int ans = 0;  

    for(i = 1; i <= n; i++)  

        if(exist[i])  

            ans++;  

    printf("%d\n",ans);  

}  

return 0;  

}

Part2.线段树从简单题到复杂题详细讲解

目录

线段树综述
线段树的更新操作的流程
例题:百练OJ-贴海报
本题选择的线段树模型
核心数据结构和算法步骤
c++代码实现

线段树综述

线段树,图例:

图1
这里写图片�述

图1的解读:
A[l,r]表示区间[l,r] , A[t] 表示区间 [t,t]
线段树描述了图中的数组
数组元素个数N是叶子节点的个数
2N - 1 是总节点个数
A[l,r]的取值是对应区间的数值和
叶子节点是单个元素的值
非叶子节点是子节点的和

//一个区间对应一个节点
struct TreeNode
{

 
  int l,r;        // 区间的左边界或右边界
  int data;   // 区间中的最大值、最小值, 区间的极值(最大值减 - 最小值)
                    // 区间最外层覆盖物的id ,  id = 0,表示最外层无覆盖物;  id = -1 表示最外层上并列了多个覆盖物

}node[2*N] //整个线段树用数组表示

node[i] 的孩子是 node[2i+1] node[2i + 2] //i从o开始

[l,r]根据需要设置为(1) [l,mid] , [mid.r] 或 (2) [l.mid],[mid+1,r]。
一般原本离散的模型,如图1中的数组选择模型(2)
后文的贴海报习题将连续模型离散化,需要选择模型(1), 否则[mid,mid+1]这个实际存在的连续段没有表示出来。

非叶子节点的意义;

可以是子节点的和,子节点中的最大、最小值等

线段树类似于二叉搜索树、堆

线段树的节点意义. 提示二:小Hi大讲堂之线段树的节点意义页面中的提示二部分

应用场景模型:
「区间问题」,通过使用额外的空间(非叶子节点,如图1)来记录某个区间的值,通过「平衡二叉树」存储对应的数据,使得查询时间复杂度降低到 O(logn)。
「区间问题」:一个区间的和,区间中的最大值、最小值, 区间的极值(最大值减 - 最小值)

线段树数据结构的实现;
用数组实现,类比堆的数组实现

以上内容的参考文献: 微信文章搜索.超越技术.图解线段树

线段树的创建、查询、更新

https://blog.csdn.net/gl48654...

线段树的更新

将A[4]更新为666
紫色是查找路径,A[4]的所有祖先的值也要更新,查找可以递归实现,祖先的值可以选择在每次递归结束时更新

待整理:
线段树的创建: 微信文章搜索.超越技术.今天不学数据结构,学画画

典型题-求一堆数据某数据区间的极差,极差 = 最大值 - 最小值

题目描述
在紫金山的路两边有好多树,每棵树的高度记作h,h为一个int类的正整数,从路的开头到路的尽头对树依次编号
1,2,3,4…n (n<100,000)。现在求[a,b]从a号树到b号树高度的极差,就是[a,b]中最高树的高度减去最矮树的高度。

解答要求时间限制:2000ms, 内存限制:64MB
输入
第二行正整数 N,Q,分别表示有 N 棵树,Q 表示有Q(Q<=10000) 次询问。
第三行有 N 个正整数。
下面 Q 行每行有 a,b。分别表示第 a 棵树第 b 棵树(a

输出
对于每次询问输出高树的高度。

样例
输入样例 1 复制

6 3
1 7 3 4 2 5
1 5
4 6
2 2
输出样例 1

6
3
0

//一个区间对应一个节点
struct TreeNode
{

  int l,r;        // 区间的左边界或右边界
  int maxV;   // 区间中的最大值、最小值, 区间的极值(最大值减 - 最小值)
  int minV;

}node[2*N] //整个线段树用数组表示

// 一个区间对应一个节点
class TreeNode {
public:

int l, r;      //  区间的左边界或右边界
int maxV;   //  区间中的最大值、最小值, 区间的极值(最大值减 - 最小值)
int minV;

TreeNode(int l, int r) {
    this->l = l;
    this->r = r;
}

TreeNode() {

}

};

TreeNode g_node[3 * N]; // 整个线段树用数组表示

// g_node[i] 的孩子是 g_node[2i] g_node[2i + 1] // i从1开始
// [l,r]的孩子根据需要设置为 (1 )[l,mid] , [mid.r] 或 (2) [l.mid],[mid+1,r],
// 此处选(2),除了后面的“贴海报例题”,一般选(2), 贴海报是连续区间离散化,[mid, mid +1]
// 是实际存在的,不能丢失

int g_A[N];

/ 建树/
TreeNode build(int i, int l, int r) {

 g_node[i].l = l;
g_node[i].r = r;
if (l == r) {
    g_node[i].maxV = g_A[l];
    g_node[i].minV = g_A[l];
    return g_node[i];
}

int mid = l + (r - l) / 2;

build(2 * i, l, mid);
build(2 * i + 1, mid + 1, r);

g_node[i].maxV = max(g_node[2 * i].maxV, g_node[2 * i + 1].maxV);
g_node[i].minV = min(g_node[2 * i].minV, g_node[2 * i + 1].minV);

return g_node[i];

}

/ Query Min /
int queryMin(int i, int l, int r) { // i is the index of a g_node

if (l < g_node[i].l || r > g_node[i].r)//  wrong parameters
    return -1000;
if (l == g_node[i].l && r == g_node[i].r) return g_node[i].minV;

if (g_node[i].l == g_node[i].r) return g_node[i].minV;

// 这里不能写成 if(l == r),否则在 i=1 时就直接返回node[i].minV了。
// if(l == r) return g_node[i].minV; // 错误

int mid = g_node[i].l + (g_node[i].r - g_node[i].l) / 2;
if (r <= mid) return queryMin(2 * i, l, r);
if (l >= mid + 1) return queryMin(2 * i + 1, l, r);
return min(queryMin(2 * i, l, mid), queryMin(2 * i + 1, mid + 1, r));

}

/ Query Max/
int queryMax(int i, int l, int r) { // i is the index of a g_node

if (l < g_node[i].l || r > g_node[i].r)//  wrong parameters
    return -1000;

if (l == g_node[i].l && r == g_node[i].r) return g_node[i].maxV;

if (g_node[i].l == g_node[i].r) return g_node[i].maxV; 

// 这里不能写成 if(l == r),否则在 i=1 时就直接返回node[i].maxV了。
// if(l == r) return g_node[i].maxV; // 错误

int mid = g_node[i].l + (g_node[i].r - g_node[i].l) / 2;
if (r <= mid) return queryMax(2 * i, l, r);
if (l >= mid + 1) return queryMax(2 * i + 1, l, r);
return max(queryMax(2 * i, l, mid), queryMax(2 * i + 1, mid + 1, r));

}

int main() {

int n, q, a, b;

cin >> n >> q;
for (int i = 1; i < n + 1; ++i) {
    scanf("%d", g_A + i);
}
build(1, 1, n);

while (q--) {
    cin >> a >> b;
    if (a == b) {
        cout << 0 << endl;
        break;
    }
    int tmp = a;
    a = min(a, b);
    b = max(tmp, b);
    cout << queryMax(1, a, b) - queryMin(1, a, b) << endl;
}
return 0;

}

例题(贴海报)的线段树建模

暴力法:标记每个单元上最外层海报的序号,最后进行统计。 时间O( lgN*K) 空间O(N+K) 总长度N,海报个数K
优化方法:线段树方法

本题的线段树建模:

上图线段树中离散的区间是[l,mid],[mid,r] 而不是[l,mid],[mid+1,r]:

至于为什么这样 可以用反证法~~如果[mid,mid+1]之间有一个海报怎么存贮?

叶子节点个数是N,则节点总数是2N

核心数据结构和算法步骤:

node{ // a segment

int st; //开始坐标
int en; //结束坐标
int  id; //该节点上最外面一层海报的序号。 id = 0 表示没有海报覆盖,  -1 表示有多个海报覆盖

}

typedef segment node;

1.建树,输入所有海报信息,如果当前node.id >0 则信息无须继续传递给子节点。 时间O(Klog2N)
2.统计, A[i] 标记第i个线段出现在最外层, 从根节点开始递归遍历更新A[i]信息,如果当前node.id <= 0, 则无须遍历子节点。 时间(log2N)
3.扫描A[i]统计最后的结果, O(K)

贴海报的代码实现:

//贴海报,输出没有被覆盖的个数

include<stdio.h>

struct node

{

int l,r; //左右孩子的编号  

int st,mi,en;  

int id;  

}; // 线段树简单一维

const int maxN = 50000002; //线段树的节点个数

const int maxL = 10000020; //台阶的宽度上限

node segment_tree[maxN]; //保存着线段树的所有节点

define tree segment_tree

int root, ptr;

void insert(int cr, int start, int end, int color) //插入到指定区域,同时初始化沿途的所有节点

{

if(start >= end) //不符合输入要求  

    return;  

if(tree[cr].st == start && tree[cr].en == end) //输入区间正好等于节点的表示区间  

{  

    tree[cr].id = color; //这个区间属于该海报  

    return;  

}  

int mid = (tree[cr].st + tree[cr].en) / 2;  

if(tree[cr].l == 0) //意味着还没有初始化孩子结点  

{  

    //ptr代表节点的编号  

    tree[cr].l = ptr++;  

    tree[tree[cr].l].l = tree[tree[cr].l].r = 0;  

    tree[tree[cr].l].id = -1;  

    tree[tree[cr].l].st = tree[cr].st, //这里对左右孩子的范围进行初始化  

    tree[tree[cr].l].en = mid;  

}  

if(tree[cr].r == 0)  

{  

    tree[cr].r = ptr++;  

    tree[tree[cr].r].l = tree[tree[cr].r].r = 0;  

    tree[tree[cr].r].id = -1;  

    tree[tree[cr].r].st = mid,  

    tree[tree[cr].r].en = tree[cr].en;  

}  



if(tree[cr].id != 0) //之后的子区间肯定都属于该海报  

{  

    tree[tree[cr].l].id = tree[tree[cr].r].id = tree[cr].id;  

    tree[cr].id = 0;  

}  



if(start >= mid){  

    insert(tree[cr].r, start, end, color);  

    return;  

}  

if(end <= mid){  

    insert(tree[cr].l, start, end, color);  

    return;  

}  

insert(tree[cr].l, start, mid, color);  

insert(tree[cr].r, mid, end, color);  

}

char exist[10001];

void trail(int cr) //统计可以看见的节点编号

{

if(cr == 0 || tree[cr].id == -1)  

    return;  

exist[tree[cr].id] = 1; //id不为0,意味着只有它可见,但之后的节点都看不见了.  

if(tree[cr].id != 0) //不为0意味着后面的区域都要被覆盖.  

    return;  

trail(tree[cr].l);  

trail(tree[cr].r);  

}

//初始化跟节点

void init()

{

root = 1;  

tree[root].l = tree[root].r = tree[root].id = 0;  

tree[root].st = 1, tree[root].en = maxL, tree[root].mi = (1 + maxL)/2;  

ptr = 2;  

}

int main()

{

int test,n,i,l,r;  

scanf("%d", &test);  

while(test--)  

{  

    init();  

    scanf("%d",&n);  

    for(i = 1; i <= n; i++)  

    {  

        scanf("%d%d",&l,&r);  

        insert(1, l, r+1, i); //从根节点开始插入  

    }  

    for(i = 1; i <= n; i++)  

        exist[i] = 0;  

    trail(1);  

    int ans = 0;  

    for(i = 1; i <= n; i++)  

        if(exist[i])  

            ans++;  

    printf("%d\n",ans);  

}  

return 0;  

}

例题-计算线段覆盖区域的总长度

微信搜索.信息学竞赛.OI省选算法之线段树(1)


Ocean
1.6k 声望74 粉丝

Mobaxterm