首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

扫描线

编程知识
2024年08月27日 18:32

引入

扫描线一般运用在图形上面,它和它的字面意思十分相似,就是一条线在整个图上扫来扫去,它一般被用来解决图形面积,周长,以及二维数点等问题。

Atlantis 问题

题意

在二维坐标系上,给出多个矩形的左下以及右上坐标,求出所有矩形构成的图形的面积。

解法

根据图片可知总面积可以直接暴力即可求出面积,如果数据大了怎么办?这时就需要讲到 扫描线 算法。

过程

现在假设我们有一根线,从下往上开始扫描:

  • 如图所示,我们可以把整个矩形分成如图各个颜色不同的小矩形,那么这个小矩形的高就是我们扫过的距离,那么剩下了一个变量,那就是矩形的长一直在变化。
  • 我们的线段树就是为了维护矩形的长,我们给每一个矩形的上下边进行标记,下面的边标记为 1,上面的边标记为 -1,每遇到一个矩形时,我们知道了标记为 1 的边,我们就加进来这一条矩形的长,等到扫描到 -1 时,证明这一条边需要删除,就删去,利用 1 和 -1 可以轻松的到这种状态。
  • 还要注意这里的线段树指的并不是线段的一个端点,而指的是一个区间,所以我们要计算的是 \(r+1\)\(r-1\)
  • 需要 离散化

实现

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+10,mod=998244353;
typedef long long ll;
typedef pair<int,int> PII;
int T;
int n,m,k,q;
vector<array<int,4>> e;
int ans=0;
vector<int> vx;
struct info
{
    int minx,mincnt;
};
struct node
{
    info val;
    int tag;
}tr[N*8];
info operator+(const info& a, const info& b)
{
    if(a.minx<b.minx)
        return a;
    else if(b.minx<a.minx) return b;
    else
    {
        //cout<<a.mincnt<<" "<<b.mincnt<<endl;
        return (info){a.minx,a.mincnt+b.mincnt};
    }
}
void update(int p)
{
    tr[p].val=tr[2*p].val+tr[2*p+1].val;
    //cout<<p<<" "<<tr[p].val.mincnt<<endl;
}
void settag(int p,int t)
{
    tr[p].tag+=t;
    tr[p].val.minx+=t;
}
void pushdown(int p)
{
    if(tr[p].tag)
    {
        int t=tr[p].tag;
        settag(2*p,t);
        settag(2*p+1,t);
        tr[p].tag=0;
    }
}
void build(int p,int l,int r)
{
    if(l==r)
    {
        //cout<<vx[r]-vx[r-1]<<endl;
        tr[p].val={0,vx[r]-vx[r-1]};
        tr[p].tag=0;
        return ;
    }
    int mid=(l+r)/2;
    build(2*p,l,mid);
    build(2*p+1,mid+1,r);
    update(p);
    //cout<<tr[p].val.mincnt<<endl;
}
void modify(int p,int l,int r,int ql,int qr,int tag)
{
    if(l==ql&&r==qr)
    {
        settag(p,tag);
        return ;
    }
    int mid=(l+r)/2;
    pushdown(p);
    if(qr<=mid) modify(2*p,l,mid,ql,qr,tag);
    else if(ql>mid) modify(2*p+1,mid+1,r,ql,qr,tag);
    else{
        modify(2*p,l,mid,ql,mid,tag);
        modify(2*p+1,mid+1,r,mid+1,qr,tag);
    }
    update(p);
}
info query(int p,int l,int r,int ql,int qr)
{
    if(l==ql&&r==qr)
    {
        return tr[p].val;
    }
    int mid=(l+r)/2;
    pushdown(p);
    if(qr<=mid) return query(2*p,l,mid,ql,qr);
    else if(ql>mid) return query(2*p+1,mid+1,r,ql,qr);
    else return query(2*p,l,mid,ql,mid)+
    query(2*p+1,mid+1,r,mid+1,qr);
    update(p);
}
void solve()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        int x1,x2,y1,y2;
        cin>>x1>>y1>>x2>>y2;
        e.push_back({y1,1,x1,x2});
        e.push_back({y2,-1,x1,x2});
        vx.push_back(x1);
        vx.push_back(x2);
    }
    sort(e.begin(),e.end());
    sort(vx.begin(),vx.end());
    vx.erase(unique(vx.begin(),vx.end()),vx.end());
    m=vx.size()-1;
    build(1,1,m);
    //for(int i=1;i<=10;i++) cout<<tr[i].val.mincnt<<" ";
    int s=tr[1].val.mincnt;
    //cout<<s<<endl;
    int pre=0;
    int now=0;
    for(auto et : e)
    {
        int cnt=s;
        now=et[0];
        int d=tr[1].val.minx;
        //cout<<cnt<<endl;
        if(d==0) cnt-=tr[1].val.mincnt;
        ans+=(now-pre)*cnt;
        //cout<<cnt<<endl;
        pre=now;
        int x1=lower_bound(vx.begin(),vx.end(),et[2])-vx.begin()+1;
        int x2=lower_bound(vx.begin(),vx.end(),et[3])-vx.begin();
        //cout<<x1<<" "<<x2<<endl;
        if(x1>x2) continue;
        modify(1,1,m,x1,x2,et[1]);
    }
    cout<<ans<<endl;
    return ;
}
signed main()
{
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    T=1;
    //cin>>T;
    while(T--)
     {
         solve();
     }
     return 0;
} 

练习

B 维正交范围

B 维正交范围指在一个 B 维直角坐标系下,第 \(i\) 维坐标在一个整数范围 \([l_i,r_i]\) 间,内部的点集。

一般来说,一维正交范围简称区间,二维正交范围简称矩形,三维正交范围简称立方体(我们常说的二维数点就是二维正交范围)。

对于一个静态的二维问题,我们可以使用扫描线扫一维,数据结构维护另一维。
在扫描线从左到右扫的过程中,会在数据结构维护的那一维上产生一些修改与查询。
如果查询的信息可差分的话直接使用差分,否则需要使用分治。差分一般用树状数组和线段树维护,但因为树状数组好写而且常数小,所以大部分人会选择用树状数组来维护。分治一般是 CDQ 分治(但是这里不涉及分治)。

另一种比较容易理解的看待问题的角度是站在序列角度,而不站在二维平面角度。如果我们这样看待问题,则扫描线实际上是枚举了右端点 \(r=1\cdots n\),维护一个数据结构,支持查询对于当前的 \(r\),给定一个值 \(l\)\(l\)\(r\) 的答案是什么。即扫描线扫询问右端点,数据结构维护所有左端点的答案,或者说遍历一维,数据结果维护另一维。

复杂度一般为 \(O((n+m)\log n)\)

二维数点

给一个长为 \(n\) 的序列,有 \(m\) 次查询,每次查区间 \([l,r]\) 中值在 \([x,y]\) 内的元素个数。

这个问题就叫做二维数点。我们可以发现等价于我们要查询一个二维平面上矩形内的点的数量和。这里讲一下这个问题最简单的处理方法,扫描线 + 树状数组。

很显然,这个问题是一个静态的二维问题,我们通过扫描线可以将静态的二维问题转换为动态的一维问题。维护动态的一维问题就使用数据结构维护序列,这里可以使用树状数组。

先将所有的询问离散化,用树状数组维护权值,对于每次询问的 \(l\)\(r\),我们在枚举到 \(l-1\) 时统计当前位于区间 \([x,y]\) 内的数的数量 \(a\),继续向后枚举,枚举到 \(r\) 时统计当前位于区间 \([x,y]\) 内的数的数量 \(b\)\(b-a\) 即为该次询问的答案。

例题

洛谷 P2163[SHOI2007] 园丁的烦恼

题意:求\([x1,x2],[y1,y2]\)区间内有多少个点
思路,可以用扫描线+树状数组来做,按y轴从下到上扫描,同是将x坐标离散化,计算一个差分即可得出答案
代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+10,mod=998244353;
typedef long long ll;
typedef pair<int,int> PII;
int T;
int n,m,k,q;
int a[N];
int c[N];
vector<array<int,4>> event;
int lowbit(int x)
{
    return x&(-x);
}
void modify(int p)
{
    for(;p<=N;p+=lowbit(p))
    {
        c[p]++;
    }
}
int query(int x)
{
    int res=0;
    for(;x>0;x-=lowbit(x))
    {
        res+=c[x];
    }
    return res;
}
vector<int> ans(N+1);
vector<int> vx;
void solve()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        int x,y;
        cin>>x>>y;
        vx.push_back(x);
        event.push_back({y,0,x,i});
    }
    for(int i=1;i<=m;i++)
    {
        int a,b,c,d;
        cin>>a>>b>>c>>d;
        event.push_back({d,2,c,i});
        event.push_back({d,1,a-1,i});
        event.push_back({b-1,1,c,i});
        event.push_back({b-1,2,a-1,i});
    }
    sort(event.begin(),event.end());
    sort(vx.begin(),vx.end());
    vx.erase(unique(vx.begin(),vx.end()),vx.end());
    for(auto ets : event)
    {
        if(ets[1]==0)
        {
            int id=lower_bound(vx.begin(),vx.end(),ets[2])-vx.begin()+1;
            modify(id);
        }
        else
        {
            int id=upper_bound(vx.begin(),vx.end(),ets[2])-vx.begin();
            int s=query(id);
            if(ets[1]==2) ans[ets[3]]+=s;
            else ans[ets[3]]-=s;
        }
    }
    for(int i=1;i<=m;i++) cout<<ans[i]<<"\n";
    return ;
}
signed main()
{
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    T=1;
    //cin>>T;
    while(T--)
     {
         solve();
     }
     return 0;
} 

洛谷 P1908 逆序对

没错,逆序对也可以用扫描线的思维来做。考虑将求逆序对的个数转化为从后向前枚举每个位置 \(i\),求在区间 \([i+1,n]\) 中,大小在区间 \([0,a_i]\) 中的点的个数。题目中数据范围为 \(10^9\),很显然要先进行离散化,我们可以考虑从后向前遍历数组,每次遍历到一个数时更新树状数组(线段树),之后统计当前一共有多少个数小于当前枚举的数,因为我们是从后向前遍历的,所以比当前值小的数的个数就是他的逆序对的个数,可以用树状数组或线段树进行单点修改和区间查询。

洛谷 P1972 [SDOI2009] HH 的项链

简要题意:给定一个序列,多次询问区间 \([l,r]\) 中有多少种不同的数。

这类问题我们可以考虑推导性质,之后使用扫描线枚举所有右端点,数据结构维护每个左端点的答案的方法来实现,我们也可以将问题转换到二维平面上,变为一个矩形查询信息的问题。

在本题中,我们设序列中 \(a_i\) 上一次出现的位置为 \(pre_i\),如果 \(a_i\) 没有出现过,则 \(pre_i = 0\)。根据题意,如果一种数在区间中出现多次,只会产生一次贡献。不妨认为每种数产生贡献的位置是区间中第一次出现的位置,这时可以发现,产生的总贡献即为 \(pre_x \le l - 1\) 的个数,反证法易证。

现在问题即为:给定一个序列 \(pre\),多次查询区间 \([l,r]\) 中有多少个 \(pre_i \le l - 1\)

我们可以把 \(pre_i\) 看成二维平面的点:\(i\) 是横坐标,\(pre_i\) 是纵坐标,问题就转化为了二维数点问题:每次询问左下角为 \((l,0)\),右上角为 \((r,l - 1)\) 的矩形中有几个点。

注意到这个询问是可差分的,我们可以将询问差分为左下角为 \((0,0)\),右上角为 \((r,l - 1)\) 的矩形减去左下角为 \((0,0)\),右上角为 \((l - 1,l - 1)\) 的矩形有几个点,这样方便我们使用扫描线思想。

单次操作复杂度 \(O(\log n)\),共有 \(n\) 次加点操作和 \(2m\) 次查询操作,总时间复杂度 \(O((n + m) \log n)\)

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+10,mod=998244353;
typedef long long ll;
typedef pair<int,int> PII;
int T;
int n,m,k,q;
vector<array<int,4>> e;
int c[N];
int a[N];
int pre[N];
int id[N];
int lowbit(int x)
{
  return x&(-x);
}
void modify(int p,int x)
{
  for(;p<=n;p+=lowbit(p))
  {
      c[p]+=x;
  }
}
int query(int x)
{
  int res=0;
  for(;x>0;x-=lowbit(x))
  {
      res+=c[x];
  }
  return res;
}
int ans[N];
void solve()
{
  cin>>n;
  for(int i=1;i<=n;i++)
  {
      cin>>a[i];
      pre[a[i]]=id[a[i]];
      e.push_back({pre[a[i]],0,i,i});
      id[a[i]]=i;
  }
  cin>>q;
  for(int i=1;i<=q;i++)
  {
      int l,r;
      cin>>l>>r;
      e.push_back({l-1,1,r,i});
      e.push_back({l-1,2,l-1,i});
  }
  sort(e.begin(),e.end());
  for(auto et : e)
  {
      if(et[1]==0)
      {
          modify(et[2],1);
      }
      else
      {
          int s=query(et[2]);
          if(et[1]==1) ans[et[3]]+=s;
          else ans[et[3]]-=s;
      }
  }
  for(int i=1;i<=q;i++) cout<<ans[i]<<endl;
  return ;
}
signed main()
{
  ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
  T=1;
  //cin>>T;
  while(T--)
   {
       solve();
   }
   return 0;
} 

例题

总而言之,二维数点的主要思路就是数据结构维护一维,然后枚举另一维。

参考资料

From:https://www.cnblogs.com/prioritymygirl/p/18383366
本文地址: http://www.shuzixingkong.net/article/1497
0评论
提交 加载更多评论
其他文章 CMake构建学习笔记8-OpenSceneGraph库的构建
1. 概论 在连续构建了zlib、libpng、libjpeg、libtiff、giflib以及freetype这几个库之后,接下来我们就要来一个大的,构建OpenSceneGraph这样大型库。OpenSceneGraph(简称 OSG)是一个高性能、跨平台的三维图形应用程序框架,广泛应用于科学可
Win11如何找回熟悉的开始菜单、任务栏和右键菜单
背景 公司政策满3年可以换新电脑,前段时间申请了下,到手后发现是Win11系统,配置翻倍,欣然接受,把一些常用的软件都安装上,但是,用了一段时间后,发现右键刷新要点击2次,开始菜单找东西也完全靠搜索,任务栏不可定义了,和以前常用的右下角日历小工具不兼容,如果要和这些用惯好多年的操作say goodb
Win11如何找回熟悉的开始菜单、任务栏和右键菜单 Win11如何找回熟悉的开始菜单、任务栏和右键菜单 Win11如何找回熟悉的开始菜单、任务栏和右键菜单
CANopen学习笔记(二)通讯对象PDO和SDO等
通讯对象 PDO 我的观点:一个 CANopen 设备可以拥有最多 512 个 RPDO 和 512 个 TPDO,总共最多 1024 个 PDO。(得到GPT4o的肯定) CiA协议栈观点:一个只有一个逻辑设备的 CANopen 设备最多有 512 个 PDO。 PDO的两种用法: TPDO:生产
CANopen学习笔记(二)通讯对象PDO和SDO等 CANopen学习笔记(二)通讯对象PDO和SDO等 CANopen学习笔记(二)通讯对象PDO和SDO等
Modbus ASCII 获取数据
根据银河高低温试验箱协议读取数据 1.协议内容 8.1:通讯协议介绍 8.1.5 通讯设置 本通讯协议使用异步串行通讯方式,1 个起始位、8 个数据位、2 个停止 位、无奇偶校验数据通讯格式,其中数据位8 位,第1 位为最高位(MSB),第8 位为最低位(LSB)。 8.1.6 数据格式 通讯采用直
Modbus ASCII 获取数据 Modbus ASCII 获取数据
windows权限维持汇总
Windows 权限维持 一、文件层面 1)attrib 使用 Attrib +s +a +h +r 命令 s:设置系统属性(System) a:设置存档属性(Archive) h:设置隐藏属性(Hidden) r:设置只读属性(Read-only) attrib +s +a +h +r c:\te
windows权限维持汇总 windows权限维持汇总 windows权限维持汇总
k8s网络原理之Calico
之前整理总结过有关flannel的相关原理以及详细的传输过程,一直都想总结一篇通俗易懂的有关calico的相关原理和传输过程的,因为平常事情较多,没想到一拖就是这么久,这回借着复习的机会,将calico的原理,传输过程,以及各组件的作用还有两种模式的对比进行了细致的讲解和分析对比,也是对自己之前学习
k8s网络原理之Calico k8s网络原理之Calico k8s网络原理之Calico
生产者消费者模式,以及基于BlockingQueue的快速实现
生产者消费者模式,以及基于BlockingQueue的快速实现什么是生产者消费者模式,简单来说就是有两个角色,一个角色主要负责生产数据,一个角色主要负责消费(使用)数据。那么生产者直接依赖消费者,然后直接调用是否可以?答案是可以的,但是有些场景无法及时解决,典型的就是生产者消费者的速度无法同步,导致
生产者消费者模式,以及基于BlockingQueue的快速实现 生产者消费者模式,以及基于BlockingQueue的快速实现 生产者消费者模式,以及基于BlockingQueue的快速实现
注解是如何实现的?
注解是否支持继承 不支持继承 不能使用关键字extends来继承某个@interface,但注解在编译后,编译器会自动继承java.lang.annotation.Annotation接口. 虽然反编译后发现注解继承了Annotation接口,但即使Java的接口可以实现多继承,但定义注解时依然无法