虽然CPP给的标题是线段树,但是由于后面又加了道和线段树无关的题目,然后发现这个扫描线还在二维数点里面有使用,就一并完成了,所以姑且称为
扫描线+图论优化
Part 1 扫描线
oi-wiki如是说:
扫描线一般运用在图形上面,它和它的字面意思十分相似,就是一条线在整个图上扫来扫去,它一般被用来解决图形面积,周长,以及二维数点等问题。
真的是好随意的解释,但是我也不知道具体怎么说(不然我干什么要搜)
计入都说了可以解决图形面积,周长,以及二维数点等问题那么就按照这个顺序讲吧
面积
P5490 【模板】扫描线 & 矩形面积并
只好解决矩形面积了
扫描线处理大部分问题,都是将一维通过扫描线枚举得到,另一维通过数据结构进行维护。
那么具体怎么处理?
输入每一个矩形,我们都可以得到一个矩形的四点,由于范围很大,我们明显需要离散化来处理每一个用数据结构存储的点。
我们通过扫描线的思路,把一个矩形拆成两条线,一个+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|1void 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;
}
周长
Picture
还是按照扫描线做法处理
直接用线段树记录目前这条线上分了几段边,然后计入另一维长度的贡献
但是这样只记录了一边,比较赖的做法是也给另一边跑一次,这样更加简单。
代码:
#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|1void 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;
}
二维数点
P2163 [SHOI2007] 园丁的烦恼
比较常见的类型
也是按照一维扫描,一维数据结构来实现
这一次按照顺序扫描,实际上是类似前缀和的操作,要减去左边的线的贡献
这样就可以得到了
代码:
#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;
}
P1972 [SDOI2009] HH的项链
依然离线
考虑按照顺序处理一维,即扫描线,另一维靠数据结构
我们记录上一次出现的位置,将贡献删除即可达成离线处理的操作’
代码:
#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|1void 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;
}
完结撒花