扫描线+图论优化学习笔记
虽然CPP给的标题是线段树,但是由于后面又加了道和线段树无关的题目,然后发现这个扫描线还在二维数点里面有使用,就一并完成了,所以姑且称为
扫描线+图论优化
Part 1 扫描线
oi-wiki如是说:
扫描线一般运用在图形上面,它和它的字面意思十分相似,就是一条线在整个图上扫来扫去,它一般被用来解决图形面积,周长,以及二维数点等问题。
真的是好随意的解释,但是我也不知道具体怎么说(不然我干什么要搜)
计入都说了可以解决图形面积,周长,以及二维数点等问题那么就按照这个顺序讲吧
面积
只好解决矩形面积了
扫描线处理大部分问题,都是将一维通过扫描线枚举得到,另一维通过数据结构进行维护。
那么具体怎么处理?
输入每一个矩形,我们都可以得到一个矩形的四点,由于范围很大,我们明显需要离散化来处理每一个用数据结构存储的点。
我们通过扫描线的思路,把一个矩形拆成两条线,一个+1,一个-1,分别表示添加一条边或者减少一条边即可。
我们按照扫描线处理的一维排序,按照顺序加入线段树,处理即可。
需要注意的点是我们在线段树里面记录的不是点,而是两点之间的区间,这样才可以得到每一组的答案。
代码:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<string.h>
#include<string>
#include<vector>
#define int long long
using namespace std;
const int N=1e5+5;
int n,tmp[N*2],cnt;
struct P{
int l,r,y,v;
}li[N*2];
bool cmp(P a,P b){
return a.y<b.y;
}
struct ST{
int c[N<<4],tag[N<<4],len[N<<4];
#define ls p<<1
#define rs p<<1|1
void pushup(int p){
if(tag[p])c[p]=len[p];
else c[p]=c[ls]+c[rs];
}
void build(int p,int l,int r){
len[p]=tmp[r+1]-tmp[l];
if(l==r)return;
int mid=l+r>>1;
build(ls,l,mid),build(rs,mid+1,r);
}
void change(int p,int l,int r,int L,int R,int v){
if(l>=L&&r<=R){
tag[p]+=v;
pushup(p);
return;
}
int mid=l+r>>1;
if(mid>=L)change(ls,l,mid,L,R,v);
if(mid<R)change(rs,mid+1,r,L,R,v);
pushup(p);
}
}tr;
signed main(){
cin>>n;
cnt=0;
for(int i=1,x,y,xx,yy;i<=n;i++){
scanf("%lld%lld%lld%lld",&x,&y,&xx,&yy);
li[i]={x,xx,y,1};
li[i+n]={x,xx,yy,-1};
tmp[++cnt]=x;
tmp[++cnt]=xx;
}
sort(li+1,li+2*n+1,cmp);
sort(tmp+1,tmp+cnt+1);
cnt=unique(tmp+1,tmp+cnt+1)-tmp-1;
tr.build(1,1,cnt-1);
for(int i=1;i<=2*n;i++){
li[i].l=lower_bound(tmp+1,tmp+cnt+1,li[i].l)-tmp;
li[i].r=lower_bound(tmp+1,tmp+cnt+1,li[i].r)-tmp;
}
int ans=0;
for(int i=1;i<=2*n;i++){
if(i!=1)ans+=tr.c[1]*(li[i].y-li[i-1].y);
tr.change(1,1,cnt-1,li[i].l,li[i].r-1,li[i].v);
}
cout<<ans;
return 0;
}
周长
还是按照扫描线做法处理
直接用线段树记录目前这条线上分了几段边,然后计入另一维长度的贡献
但是这样只记录了一边,比较赖的做法是也给另一边跑一次,这样更加简单。
代码:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<string.h>
#include<string>
#include<vector>
#define int long long
using namespace std;
int n,ma;
struct L{
int l,r,x,f;
}a[200005],b[200005];
bool cmp(L a,L b){
return a.x<b.x;
}
struct ST{
struct P{
int s,l,r;
}c[100005];
int tag[100005],len[100005];
#define ls p<<1
#define rs p<<1|1
void pushup(int p){
if(tag[p]){
c[p]={1,1,1};
}
else if(len[p]==1){
c[p]={0,0,0};
}
else {
c[p].s=c[ls].s+c[rs].s;
if(c[ls].r&&c[rs].l)c[p].s--;
c[p].l=c[ls].l,c[p].r=c[rs].r;
}
}
void build(int p,int l,int r){
tag[p]=0;
len[p]=r-l+1;
c[p]={0,0,0};
if(l==r)return;
int mid=l+r>>1;
build(ls,l,mid),build(rs,mid+1,r);
}
void change(int p,int l,int r,int L,int R,int v){
if(l>=L&&r<=R){
tag[p]+=v;
pushup(p);
return ;
}
int mid=l+r>>1;
if(mid>=L)change(ls,l,mid,L,R,v);
if(mid<R)change(rs,mid+1,r,L,R,v);
pushup(p);
}
}tr;
int solve(L p[]){
int ans=0;
sort(p+1,p+2*n+1,cmp);
tr.build(1,1,20005);
for(int i=1;i<=2*n;i++){
if(i!=1)ans+=(p[i].x-p[i-1].x)*tr.c[1].s;
tr.change(1,1,20005,p[i].l,p[i].r-1,p[i].f);
}
return ans;
}
signed main(){
scanf("%lld",&n);
if(n==0)printf("0");
else {
for(int i=1,x,y,xx,yy;i<=n;i++){
scanf("%lld%lld%lld%lld",&x,&y,&xx,&yy);
x+=10001;
y+=10001;
xx+=10001;
yy+=10001;
a[i]={x,xx,y,1};
b[i]={y,yy,x,1};
a[i+n]={x,xx,yy,-1};
b[i+n]={y,yy,xx,-1};
}
}
cout<<2*(solve(a)+solve(b));
return 0;
}
二维数点
比较常见的类型
也是按照一维扫描,一维数据结构来实现
这一次按照顺序扫描,实际上是类似前缀和的操作,要减去左边的线的贡献
这样就可以得到了
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+5;
int n,m,x[N],y[N],tmpx[N*3],tmpy[N*3],cntx,cnty,ans[N];
vector<int> e1[N],e2[N],e3[N];
int lowbit(int x){
return x&-x;
}
struct BT{
int c[3*N];
void add(int x,int y){
for(int i=x;i<=cnty;i+=lowbit(i))c[i]+=y;
}
int query(int x){
int ans=0;
for(int i=x;i>=1;i-=lowbit(i))ans+=c[i];
return ans;
}
}bit;
struct P{
int x,y,xx,yy;
}a[N];
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>x[i]>>y[i];
for(int i=1;i<=n;i++)tmpx[++cntx]=x[i],tmpy[++cnty]=y[i];
for(int i=1;i<=m;i++){
cin>>a[i].x>>a[i].y>>a[i].xx>>a[i].yy;
a[i].x--,a[i].y--;
tmpx[++cntx]=a[i].x;
tmpx[++cntx]=a[i].xx;
tmpy[++cnty]=a[i].y;
tmpy[++cnty]=a[i].yy;
}
int tmp;
sort(tmpx+1,tmpx+cntx+1);
tmp=unique(tmpx+1,tmpx+cntx+1)-tmpx-1;
cntx=tmp;
sort(tmpy+1,tmpy+cnty+1);
tmp=unique(tmpy+1,tmpy+cnty+1)-tmpy-1;
cnty=tmp;
for(int i=1;i<=n;i++)x[i]=lower_bound(tmpx+1,tmpx+cntx+1,x[i])-tmpx;
for(int i=1;i<=n;i++)y[i]=lower_bound(tmpy+1,tmpy+cnty+1,y[i])-tmpy;
for(int i=1;i<=n;i++)e1[x[i]].push_back(y[i]);
for(int i=1;i<=m;i++)a[i].x=lower_bound(tmpx+1,tmpx+cntx+1,a[i].x)-tmpx;
for(int i=1;i<=m;i++)a[i].xx=lower_bound(tmpx+1,tmpx+cntx+1,a[i].xx)-tmpx;
for(int i=1;i<=m;i++)a[i].y=lower_bound(tmpy+1,tmpy+cnty+1,a[i].y)-tmpy;
for(int i=1;i<=m;i++)a[i].yy=lower_bound(tmpy+1,tmpy+cnty+1,a[i].yy)-tmpy;
for(int i=1;i<=m;i++)e2[a[i].x].push_back(i);
for(int i=1;i<=m;i++)e3[a[i].xx].push_back(i);
for(int i=1;i<=cntx;i++){
for(int j=0;j<e1[i].size();j++)bit.add(e1[i][j],1);
for(int j=0;j<e2[i].size();j++)ans[e2[i][j]]-=bit.query(a[e2[i][j]].yy)-bit.query(a[e2[i][j]].y);
for(int j=0;j<e3[i].size();j++)ans[e3[i][j]]+=bit.query(a[e3[i][j]].yy)-bit.query(a[e3[i][j]].y);
}
for(int i=1;i<=m;i++){
cout<<ans[i]<<endl;
}
return 0;
}
依然离线
考虑按照顺序处理一维,即扫描线,另一维靠数据结构
我们记录上一次出现的位置,将贡献删除即可达成离线处理的操作’
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,a[1000005],pre[1000005],q,ans[1000005];
int read(){
char c=getchar();
while(c>'9'||c<'0')c=getchar();
int x=0;
while(c<='9'&&c>='0')x=x*10+c-48,c=getchar();
return x;
}
int lowbit(int x){
return x&-x;
}
struct P{
int l,r,id;
bool friend operator<(P a,P b){
return a.r<b.r;
}
}b[1000005];
struct BT{
int c[1000005];
void add(int x,int y){
for(int i=x;i<=n;i+=lowbit(i))c[i]+=y;
}
int query(int x){
int ans=0;
for(int i=x;i>=1;i-=lowbit(i))ans+=c[i];
return ans;
}
int query(int l,int r){
return query(r)-query(l-1);
}
}bit;
signed main(){
n=read();
for(int i=1;i<=n;i++)a[i]=read();
q=read();
for(int i=1;i<=q;i++){
b[i].l=read();
b[i].r=read();
b[i].id=i;
}
sort(b+1,b+q+1);
int w=0;
for(int i=1;i<=q;i++){
while(w<b[i].r){
w++;
if(pre[a[w]])bit.add(pre[a[w]],-1);
bit.add(w,1);
pre[a[w]]=w;
}
ans[b[i].id]=bit.query(b[i].l,b[i].r);
}
for(int i=1;i<=q;i++){
printf("%lld\n",ans[i]);
}
return 0;
}
这样扫描线就弄完了
Part 2 图论优化
常规的建图方式可能并不能满足所有需求,比如跑最短路时,时间复杂度与边数是息息相关的。
但是有一些比较毒瘤的题目,建边存在规律
线段树优化建图
一些需要一个点连多个点以及多个点连一个点,连的次数太多明显会有问题,所有可以考虑线段树优化建图。
原理很简单,把几个节点放在一起,搞一个线段树即可。
每个非叶子节点有两个点,一个向上连接,一个向下连接,这样就可以保证树的单向性,根据不同情况可以选择不同的方案连接,并在此之前提前处理出线段树建边。
例题: CF786B Legacy
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,q,s,cnt,in[400005],out[400005],dis[400005],hhq;
vector<pair<int,int>> e[400005];
bool vis[400005];
struct ST{
#define ls p<<1
#define rs p<<1|1
void build(int p,int l,int r){
if(l==r){
in[p]=out[p]=l;
return;
}
in[p]=++cnt,out[p]=++cnt;
int mid=l+r>>1;
build(ls,l,mid),build(rs,mid+1,r);
e[out[p]].push_back({out[ls],0});
e[out[p]].push_back({out[rs],0});
e[in[ls]].push_back({in[p],0});
e[in[rs]].push_back({in[p],0});
}
void change(int p,int l,int r,int L,int R,int v,int w,bool f){
if(l>=L&&r<=R){
if(f)e[in[p]].push_back({v,w});
else e[v].push_back({out[p],w});
return;
}
int mid=l+r>>1;
if(mid>=L)change(ls,l,mid,L,R,v,w,f);
if(mid<R)change(rs,mid+1,r,L,R,v,w,f);
}
}seg;
struct P{
int x,dis;
bool friend operator<(P a,P b){
return a.dis>b.dis;
}
};
void dij(){
memset(dis,0x3f,sizeof(dis));
priority_queue<P> q;
q.push({s,0});
dis[s]=0;
while(!q.empty()){
P u=q.top();
q.pop();
if(vis[u.x])continue;
vis[u.x]=1;
for(auto tmp:e[u.x]){
int v=tmp.first,w=tmp.second;
if(w+u.dis<dis[v]){
dis[v]=w+u.dis;
q.push({v,dis[v]});
}
}
}
}
signed main(){
cin>>n>>q>>s;
cnt=n;
seg.build(1,1,n);
for(int i=1,op,v,l,r,w;i<=q;i++){
cin>>op>>v>>l;
if(op!=1)cin>>r;
cin>>w;
if(op==1)e[v].push_back({l,w});
else if(op==2)seg.change(1,1,n,l,r,v,w,0);
else seg.change(1,1,n,l,r,v,w,1);
}
dij();
for(int i=1;i<=n;i++){
if(dis[i]>=1e18)cout<<-1<<' ';
else cout<<dis[i]<<' ';
}
return 0;
}
前后缀优化建图
有一些题目依然毒瘤,像是有 \(n\) 个点,每一个都能够互相到达,然后还要求最短路径
那么如果建边有明显规律,可以考虑前后缀建图,比较套路的,如果长度为两点之差,那么直接相邻建差值长度边(只是栗子)
例题: AT_abc232_g [ABC232G] Modulo Shortest Path
我们跳出来需要a,进去需要b
因为要模,所有不好直接搞
我们发现每次建边有两种情况,一种是模了,一种是没有模
我们发现把b排序以后就可以对每个点二分出带模不带模的分界线
然后考虑建边,直接分三层,一层为原始点,一层向左,一层向右,然后按照二分的点连边,返回时再记录b
这样图就建完了
但是有问题,这题卡spfa,而a-m是负边权,跑不了
只能把b前移作差,由于已经排序,所有一定正,非常巧
代码:
#include<bits/stdc++.h>
using namespace std;
int n,mod,tmp[200005],dis[600005];
bool vis[600005];
struct P{
int a,b,id;
}c[200005];
bool cmp(P a,P b){
return a.b<b.b;
}
struct Q{
int x,dis;
bool friend operator<(Q a,Q b){
return a.dis>b.dis;
}
};
vector<pair<int,int>> e[600005];
int dij(int s,int en){
priority_queue<Q> q;
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
dis[s]=0;
q.push({s,dis[s]});
while(!q.empty()){
Q u=q.top();
q.pop();
if(vis[u.x])continue;
if(u.x==en)return u.dis;
vis[u.x]=1;
for(auto tmp:e[u.x]){
int v=tmp.first,w=tmp.second;
if(dis[v]>u.dis+w){
dis[v]=u.dis+w;
q.push({v,dis[v]});
}
}
}
}
signed main(){
cin>>n>>mod;
for(int i=1;i<=n;i++)cin>>c[i].a;
for(int i=1;i<=n;i++)cin>>c[i].b;
for(int i=1;i<=n;i++)c[i].id=i;
sort(c+1,c+n+1,cmp);
for(int i=1;i<=n;i++){
e[i+n].push_back({i,c[i].b});
e[i+2*n].push_back({i,0});
tmp[c[i].id]=i;
}
for(int i=2;i<=n;i++)e[i+n].push_back({i+n-1,0});
for(int i=1;i<n;i++)e[i+2*n].push_back({i+2*n+1,c[i+1].b-c[i].b});
for(int i=1;i<=n;i++){
int l=1,r=n;
while(l<=r){
int mid=l+r>>1;
if(c[mid].b+c[i].a>=mod)r=mid-1;
else l=mid+1;
}
r++;
if(r!=1)e[i].push_back({r-1+n,c[i].a});
if(r!=n+1)e[i].push_back({r+2*n,c[i].a+c[r].b-mod});
}
cout<<dij(tmp[1],tmp[n]);
return 0;
}
完结撒花

浙公网安备 33010602011771号