• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
nannandbk
博客园    首页    新随笔    联系   管理    订阅  订阅
[数据结构]scanning line(扫描线)

scanning line(扫描线)

1.1扫描线的思想以及在几何问题上的应用(eg1,3)

二维数点

平面上有n个点(xi,yi)。

回答q个询问,每个询问给定一个矩形[X1,X2]×[Y1,Y2],询问矩形里面有多少个点。

因为有1e9的范围,我们离散化一下,我们只关心顺序,不关心具体是多少

这里相当于只需要把原来的点的坐标离散化,查询的时候直接二分找到小于等于他的第一个坐标即可。

思想:

①扫描线+容斥

②离线
image

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 201000;
vector<int>vx;
vector<array<int,4>>event;;
int n,q,m;
int a[N],ans[N];
int c[N];
int query(int x){//1...x
	ll s = 0;
	for(;x;x-=x&(-x))
		s += c[x];
	return s;
}
void modify(int x,int s){//a[x]+=s
	for(;x<=m;x+=x&(-x))
		c[x]+=s;
}
int main()
{
	ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	cin>>n>>q;
	for(int i = 1;i<=n;i++)
	{
		int x,y;
		cin>>x>>y;
		vx.push_back(x);
		//y坐标,类型,x坐标
		event.push_back({y,0,x});
	}
	for(int i = 1;i<=q;i++)
	{
		int x1,x2,y1,y2;
		cin>>x1>>x2>>y1>>y2;
		//用容斥
		//y坐标,类型1-,2+,x坐标,哪一个询问
		//0,1,2的设置其实还包含了希望哪个事件先发生,坐标一样的话,我们希望先插入再查询
		event.push_back({y2,2,x2,i});
		event.push_back({y1-1,2,x1-1,i});
		event.push_back({y2,1,x1-1,i});
		event.push_back({y1-1,1,x2,i});
	}
	sort(event.begin(), event.end());
	sort(vx.begin(), vx.end());
	//去重
	vx.erase(unique(vx.begin(), vx.end()),vx.end());
	m = vx.size();
	for(auto evt:event)
	{
		if(evt[1]==0){//插入
			int y = lower_bound(vx.begin(), vx.end(),evt[2])-vx.begin()+1;//树状数组是1base的
			modify(y,1);
		}
		else{//查询<=它的最后一个位置,即第一个>它的位置-1
			int y = upper_bound(vx.begin(), vx.end(),evt[2])-vx.begin()-1+1;//树状数组是1base的
			int tmp = query(y);
			if(evt[1]==1)ans[evt[3]] -= tmp;
			else ans[evt[3]] += tmp;
		}
	}
	for(int i = 1;i<=q;i++)
		cout<<ans[i]<<'\n';
	return 0;
}

矩形面积并

矩形面积并

思路:

x坐标离散化

cnt>0覆盖,cnt=0未被覆盖

用线段树记录cnt的最小值,也就是被覆盖次数的最小的段。
image

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 201000;
vector<int>vx;
vector<array<int,4>>event;;
int n,q,m;

struct info
{
	int minv,mincnt;
};

info operator+(const info &l,const info &r)
{
	info a;
	a.minv = min(l.minv,r.minv);
	if(l.minv==r.minv)a.mincnt = l.mincnt + r.mincnt;
	else if(l.minv<r.minv)a.mincnt = l.mincnt;
	else a.mincnt = r.mincnt;
	return a;
}

struct node{
	int t;
	info val;
}seg[N*8];

void update(int id)
{
	seg[id].val = seg[id*2].val+seg[id*2+1].val;
}

void settag(int id,int t)
{
	seg[id].val.minv = seg[id].val.minv+t;
	seg[id].t = seg[id].t + t;
}

void pushdown(int id)
{
	if(seg[id].t!=0)
	{
		settag(id*2,seg[id].t);
		settag(id*2+1,seg[id].t);
		seg[id].t = 0;
	}
}

void build(int id,int l,int r)
{
	if(l==r)
		seg[id].val = {0,vx[r]-vx[r-1]};
	else 
	{
		int mid = (l+r)>>1;
		build(id*2,l,mid);
		build(id*2+1,mid+1,r);
		update(id);
	}
}


void modify(int id,int l,int r,int x,int y,ll t){
	if(l==x&&r==y)
	{
		settag(id,t);
		return;
	}
	int mid = (l+r)/2;
	pushdown(id);
	if(y<=mid) modify(id*2,l,mid,x,y,t);
	else if(x>mid) modify(id*2+1,mid+1,r,x,y,t);
	else{
		modify(id*2,l,mid,x,mid,t),modify(id*2+1,mid+1,r,mid+1,y,t);
	}
	update(id);
}


int main()
{
	ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i = 1;i<=n;i++)
	{
		int x1,x2,y1,y2;
		cin>>x1>>x2>>y1>>y2;
		vx.push_back(x1);
		vx.push_back(x2);
		//y坐标,类型0,x坐标
		event.push_back({y1,1,x1,x2});
		event.push_back({y2,-1,x1,x2});
	}
	sort(event.begin(), event.end());
	sort(vx.begin(), vx.end());
	//去重
	vx.erase(unique(vx.begin(), vx.end()),vx.end());
	m = vx.size()-1;//段数=点数-1
	build(1,1,m);
	int prey = 0;
	int totlen = seg[1].val.mincnt;
	ll ans = 0;
	for(auto evt:event)
	{
		int cov = totlen;
		if(seg[1].val.minv==0)
			cov = totlen - seg[1].val.mincnt;
		ans += (ll)(evt[0]-prey)*cov;
		prey = evt[0];
		int x1 = lower_bound(vx.begin(), vx.end(),evt[2])-vx.begin()+1;
		int x2 = lower_bound(vx.begin(), vx.end(),evt[3])-vx.begin();
		if(x1>x2)continue;
		modify(1,1,m,x1,x2,evt[1]);
	}
	cout<<ans<<endl;
	return 0;
}

补充:矩形周长并

矩形周长Picture

思路:和上一题矩形面积并思路差不多,也是用扫描线扫,不过没有面积那么好处理。对于面积,只用有覆盖的地方*高度的加和就是答案了,但是周长的话上边界不能想面积那样不管,周长的话要考虑上边界在哪是要的。

那么做法是:

对于矩形嘛,我们考虑扫两边,竖着扫再横着扫。这里以竖着扫为例。

我们竖着扫,也就是for一遍y,那么只需要对x进行离散化即可。

再建树,用来记录最小值和最小值出现的次数。

注意:我们离散化记录的是点,而线段树我们记录的线段。

也就是说记录的是[L+1,R]这一个线段,那么用lower_bound求到x1要+1。

然后开始扫描,遇到的如果是下边界,对于x1到x2这一段要cnt+1。遇到的如果是下边界的话cnt-1。那么如何统计答案呢?

我们遇到一条与扫描线平行的线段时,答案加上【(上一条覆盖的长度-这一次覆盖的长度)的绝对值】即:ans += abs(precnt-nowcnt)

这句话怎么理解呢?

我们画图理解一下...

image

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 201000;
vector<int>vx,vy;
vector<array<int,4>>event1,event2;
int n;

struct info
{
	int minv,mincnt;
};

info operator+(const info &l,const info &r)
{
	info a;
	a.minv = min(l.minv,r.minv);
	if(l.minv==r.minv)a.mincnt = l.mincnt + r.mincnt;
	else if(l.minv<r.minv)a.mincnt = l.mincnt;
	else a.mincnt = r.mincnt;
	return a;
}

struct node{
	int t;
	info val;
}seg[N*8];

void update(int id)
{
	seg[id].val = seg[id*2].val+seg[id*2+1].val;
}

void settag(int id,int t)
{
	seg[id].val.minv = seg[id].val.minv+t;
	seg[id].t = seg[id].t + t;
}

void pushdown(int id)
{
	if(seg[id].t!=0)
	{
		settag(id*2,seg[id].t);
		settag(id*2+1,seg[id].t);
		seg[id].t = 0;
	}
}

void build(int id,int l,int r)
{
	if(l==r)
		seg[id].val = {0,vx[r]-vx[r-1]};//mincnt就是区间长度,比如对于第一段就是第1个端点-第0个端点
	else 
	{
		int mid = (l+r)>>1;
		build(id*2,l,mid);
		build(id*2+1,mid+1,r);
		update(id);
	}
}

void build2(int id,int l,int r)
{
	if(l==r)
		seg[id].val = {0,vy[r]-vy[r-1]};//mincnt就是区间长度,比如对于第一段就是第1个端点-第0个端点
	else 
	{
		int mid = (l+r)>>1;
		build2(id*2,l,mid);
		build2(id*2+1,mid+1,r);
		update(id);
	}
}


void modify(int id,int l,int r,int x,int y,ll t){
	if(l==x&&r==y)
	{
		settag(id,t);
		return;
	}
	int mid = (l+r)/2;
	pushdown(id);
	if(y<=mid) modify(id*2,l,mid,x,y,t);
	else if(x>mid) modify(id*2+1,mid+1,r,x,y,t);
	else{
		modify(id*2,l,mid,x,mid,t),modify(id*2+1,mid+1,r,mid+1,y,t);
	}
	update(id);
}

int main()
{
	ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i = 1;i<=n;i++)
	{
		int x1,y1,x2,y2;
		cin>>x1>>y1>>x2>>y2;
		vx.push_back(x1),vx.push_back(x2);
		event1.push_back({y1,-1,x1,x2});
		event1.push_back({y2,1,x1,x2});

		vy.push_back(y1),vy.push_back(y2);
		event2.push_back({x1,-1,y1,y2});
		event2.push_back({x2,1,y1,y2});
	}

	sort(event1.begin(),event1.end());
	sort(vx.begin(),vx.end());
	vx.erase(unique(vx.begin(),vx.end()),vx.end());
	sort(event2.begin(),event2.end());
	sort(vy.begin(),vy.end());
	vy.erase(unique(vy.begin(),vy.end()),vy.end());
	int m = vx.size()-1;
	build(1,1,m);
	int precnt = 0,nowcnt = 0;
	int totlen = seg[1].val.mincnt;
	ll ans = 0;
	for(auto evt:event1)
	{
		int x1 = lower_bound(vx.begin(),vx.end(),evt[2])-vx.begin()+1;
		int x2 = lower_bound(vx.begin(),vx.end(),evt[3])-vx.begin();
		if(x1>x2)continue;
		modify(1,1,m,x1,x2,-evt[1]);
		nowcnt = totlen;
		if(seg[1].val.minv==0)
			nowcnt -= seg[1].val.mincnt;
		//cout<<"now = "<<nowcnt<<" pre = "<<precnt<<endl;
		ans += abs(nowcnt-precnt);
		precnt = nowcnt;
	}
	//cout<<ans<<endl;
	m = vy.size()-1;
	memset(seg,0,sizeof(seg));
	build2(1,1,m);
	precnt = 0;
	totlen = seg[1].val.mincnt;
	for(auto evt:event2)
	{
		int y1 = lower_bound(vy.begin(),vy.end(),evt[2])-vy.begin()+1;
		int y2 = lower_bound(vy.begin(),vy.end(),evt[3])-vy.begin();
		if(y1>y2)continue;
		modify(1,1,m,y1,y2,-evt[1]);
		nowcnt = totlen;
		if(seg[1].val.minv==0)
			nowcnt -= seg[1].val.mincnt;
		ans += abs(nowcnt-precnt);
		precnt = nowcnt;
	}
	cout<<ans<<endl;
	return 0;
}


/*
heck 数据
2
0 0 4 4
0 4 4 8

注意顺序,如果有重边应该先加再减!
*/

1.2 扫描线在序列问题上的应用(eg2)

区间不同数之和

有\(n\)个数\(a1,a2,…,an\)。

有\(q\)组询问,每次给一个区间\([l,r]\),求区间里不同的数字之和,也就是说同一个数字出现多次只算一次。

思路:

①思路一:\(pri[i]\)表示\(a[i]\)上一次出现的位置(只统计第一次出现)

\(pri[i]<l\)和\(l<=i<=r\)满足这两个条件的\(ai\)之和。

转化为二维数点问题,所有限制是两维的都可以看成二维数点。

把一个点坐标看成\((i,pri[i])\),$$\begin{cases} pri[i]<l\\ l<=i<=r \end{cases} \tag{1} $$满足这两个条件的\(ai\)之和。
因为我们对pre[]进行扫描,按pre[]的大小排序,for这pre[]这一维度进行扫描。
image
事件:
0. 插入:modify(i,a[i])

  1. 查询:query(r)-query(l-1)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int long long
const int N = 201000;
vector<int>vx;
vector<array<int,4>>event;;
int n,q,m;
int a[N],ans[N];
int c[N];
int pre[N],last[N];
int query(int x){//1...x
	ll s = 0;
	for(;x;x-=x&(-x))
		s += c[x];
	return s;
}
void modify(int x,int s){//a[x]+=s
	for(;x<=m;x+=x&(-x))
		c[x]+=s;
}
signed main()
{
	ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	cin>>n>>q;
	m = n;
	for(int i = 1;i<=n;i++)
	{
		cin>>a[i];
		pre[i] = last[a[i]];//记录一个数上一次出现的位置
		last[a[i]] = i;
		event.push_back({pre[i],0,i,a[i]});
	}
	/*
		对于一个区间[l,r]
		满足条件的点需要:
		l<=i<=r
		pre[i]<l
	*/
	for(int i = 1;i<=q;i++)
	{
		int l,r;
		cin>>l>>r;
		event.push_back({l-1,1,l,i});
		event.push_back({l-1,2,r,i});
	}
	sort(event.begin(), event.end());//按pre[i]排序
	for(auto evt:event)
	{
		if(evt[1]==0){//插入
			modify(evt[2],evt[3]);
		}
		else{
			if(evt[1]==1)
				ans[evt[3]] -= query(evt[2]-1);
			else
				ans[evt[3]] += query(evt[2]);
		}
	}
	for(int i = 1;i<=q;i++)
		cout<<ans[i]<<'\n';
	 0;
}

②思路二:for(r = 1~n)

维护\(ans[l] :[l,r]\)的答案

\(r-1——>r\) , \(ans[l]:[l,r-1]——>[l,r]\)

\(a_r\)在\([l,r-1]\)未出现,\(ans[l]+=a_r\),否则不变
比如对于\([l,r]\)一段,在\([l,p]\)上\(a_r\)出现过,在\([p+1,r]\)上没出现过,那么\(ans[p+1\)~\(r]+=a[r]\)

综上所述,我们要实现:
①区间加
②单点查询
image

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 201000;
ll n,q,a[N],pos[N],ans[N];
vector<array<int,3>>qu[N];
ll c[N];
ll query(int x){//1...x
	ll s = 0;
	for(;x;x-=x&(-x))
		s += c[x];
	return s;
}
void modify(int x,ll s){//a[x]+=s
	for(;x<=n;x+=x&(-x))
		c[x]+=s;
}
int main()
{
	ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	cin>>n>>q;
	for(int i = 1;i<=n;i++)
		cin>>a[i];

	for(int i = 1;i<=q;i++)
	{
		int l,r;
		cin>>l>>r;
		qu[r].push_back({l,i});
	}
	for(int r = 1;r<=n;r++)
	{
		int p = pos[a[r]];
		modify(p+1,a[r]);
		modify(r+1,-a[r]);
		pos[a[r]] = r;
		for(auto que:qu[r])
		{
			//ans[l]:[l,r]的答案
			ans[que[1]] = query(que[0]);
		}
	}
	for(int i = 1;i<=q;i++)
		cout<<ans[i]<<'\n';
	return 0;
}

2.权值线段树之字典树(eg4)

异或第k小

给\(n\)个数字\(a1,a2,…,an\)。

你要回答\(m\)个询问,每次给定两个数\(x,k\)询问a1 xor x,a2 xor x,…,an xor x中从小到大排序中第\(k\)小的元素。

image

#include<bits/stdc++.h>
using namespace std;
const int N = 201000;
const int M = 30;//0~2^30-1
int n,m,a[N];

struct node
{
	int s[2];
	int sz;
}seg[N*32];
int tot = 0,root;

int main()
{
	cin>>n>>m;
	root = ++tot;
	for(int i = 0;i<n;i++)
	{
		int x;
		cin>>x;
		int p = root;
		for(int j = M-1;j>=0;j--)
		{
			seg[p].sz+=1;
			int w = (x>>j)&1;
			if(seg[p].s[w]==0)seg[p].s[w] = ++tot;
			p = seg[p].s[w];
		}
		seg[p].sz+=1;
	}
	for(int i = 0;i<m;i++)
	{
		int x,k;
		cin>>x>>k;
		int ans = 0;
		int p = root;
		for(int j = M-1;j>=0;j--)
		{
			int w = (x>>j)&1;
			if(seg[seg[p].s[w]].sz>=k)
			{
				p = seg[p].s[w];
			}else{
				k -= seg[seg[p].s[w]].sz;
				ans^=1<<j;
				p = seg[p].s[w^1];
			}
		}
		cout<<ans<<endl;
	}
	return 0;
}

3.扫描线与权值线段树的总和运用(eg5)

mex

题意:

有一个长度为\(n\)的数组 \(a1,a2,…,an\)。

你要回答\(q\)个询问,每次给一个区间\([l,r]\),询问这个区间内最小没有出现过的自然数。

思路:

权值线段树+扫描线+线段树上二分

我们利用权值线段树,从\(1\)到\(n\)开始不断地加\(a[i]\)进去

权值线段树维护某个数最后一次出现在数组中的位置,初始所有数都没出现。

for(r = 1~n)

\(posx:\)x最后一次出现的位置,找到最小的x满足pos[x]<l

对于所有\(R = i\)的询问,我们利用\(L\)进行查询

去找到最后一次出现位置小于\(L\)的最小的数。

注意:这里权值是\(1e9\)但是\(n\)的范围是\(2e5\),那我们的\(mex\)肯定是小于\(n+1\)的,我们要对\(a[i]\)个\(n+1\)取\(min\).

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 201000;
ll n,q,a[N],pos[N],ans[N];
vector<array<int,3>>qu[N];
struct node{
	int val;
}seg[N*4];

void update(int id)
{
	seg[id].val = min(seg[id*2].val,seg[id*2+1].val);
}


void change(int id,int l,int r,int pos,int val){
	if(l==r)
	{
		seg[id].val = val;
		return;
	}
	int mid = (l+r)/2;
	if(pos<=mid)change(id*2,l,mid,pos,val);
	else change(id*2+1,mid+1,r,pos,val);
	update(id);
}

int search(int id,int l,int r,int d)
{	
	if(l==r)return l;
	int mid = (l+r)>>1;
	if(seg[id*2].val<d)return search(id*2,l,mid,d);
	else return search(id*2+1,mid+1,r,d);
}

int main()
{
	ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	cin>>n>>q;
	for(int i = 1;i<=n;i++)
		cin>>a[i],a[i] = min(a[i],n+1);

	for(int i = 1;i<=q;i++)
	{
		int l,r;
		cin>>l>>r;
		qu[r].push_back({l,i});
	}
	for(int r = 1;r<=n;r++)
	{
		//posx :x最后一次出现的位置,
		//最小的x满足pos[x]<l
		//pos[a[r]] = r;
		change(1,0,n+1,a[r],r);
		for(auto que:qu[r])
		{
			ans[que[1]] = search(1,0,n+1,que[0]);
		}
	}
	for(int i = 1;i<=q;i++)
		cout<<ans[i]<<'\n';
	return 0;
}

4.从权值角度考虑

之前的题目大多数以下标的角度思考,本题考虑从权值角度考虑。

例题:区间lower bound查询

题意:

给你\(n\)个数\(a_1,a_2,…,a_n\)和\(q\)个询问:

  • l r x,询问\(a_l,a_{l+1},…,a_r\)中大于等于\(x\)的最小的数。

思路:

考虑用扫描线。先按权值为第一优先级排序,值一样的话先插入再查询。

因为先插入的是大的数,那再查小的数时,比它大的已经全部插入了,只需要查询区间最小值即可。那么我们需要用线段树实现区间最小值查询和单点修改。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 201000;
const int inf = 1<<30;
int n,q;
int a[N],ans[N];

struct node{
	int minv;
}seg[N*4];

void update(int id)
{
	seg[id].minv = min(seg[id*2].minv,seg[id*2+1].minv);
}

void build(int id,int l,int r)
{
	if(l==r)
		seg[id].minv = inf;
	else 
	{
		int mid = (l+r)>>1;
		build(id*2,l,mid);
		build(id*2+1,mid+1,r);
		update(id);
	}
}


void change(int id,int l,int r,int pos,int val){
	if(l==r)
	{
		seg[id].minv= val;
	}
	else
	{
		int mid = (l+r)/2;
		if(pos<=mid)change(id*2,l,mid,pos,val);
		else change(id*2+1,mid+1,r,pos,val);
		//重要!改完之后记得更新节点的信息
		update(id);
	}
}

//O(logn)
int query(int id,int l,int r,int x,int y)
{
	if(l==x&&r==y)return seg[id].minv; 
	int mid = (l+r)/2;
	if(y<=mid)return query(id*2,l,mid,x,y);
	else if(x>mid)return query(id*2+1,mid+1,r,x,y);
	else{
		return min(query(id*2,l,mid,x,mid),query(id*2+1,mid+1,r,mid+1,y));
	}

}

vector<array<int,4>> event;

int main()
{
	ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	cin>>n>>q;
	for(int i = 1;i<=n;i++)
	{
		cin>>a[i];
		event.push_back({-a[i],0,i});//我们希望从大到小for,那加个-号,,直接sort就是从大到小
						//   设0,保证相同值情况下先插入
	}

	for(int i = 1;i<=q;i++)
	{
		int l,r,x;
		cin>>l>>r>>x;
		event.push_back({-x,i,l,r});
	}
	sort(event.begin(),event.end());
	build(1,1,n);
	for(auto evt:event)
	{
		if(evt[1] == 0)change(1,1,n,evt[2],-evt[0]);
		else ans[evt[1]] = query(1,1,n,evt[2],evt[3]);
	}
	for(int i = 1;i<=q;i++)
	{
		if(ans[i]==inf)ans[i] = -1;
		cout<<ans[i]<<"\n";
	}
	return 0;
}
posted on 2023-06-25 20:58  nannandbk  阅读(87)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3