题解:P5787 二分图 /【模板】线段树分治
题解:P5787 线段树分治单 \(\log\) 做法
前言:
题目传送门,本文同步来自本人洛谷题解。
本题解不应被视为一篇模板题题解。
本题解中,视 \(n\)、\(m\)、\(k\) 同阶。
本题解的主要目的是提供一种时间复杂度为 \(O(n\log n)\) 的线段树分治做法,
但是,这种做法的常数大,本题实测慢于 \(O(n\log^2 n)\) 的做法,甚至需要重视常数才能通过,在数据范围更大并且时空限制开大的情况下,才有可能体现其优势。
所以,本题解主要意义为扩展思路而非算法教学。
建议理解线段树分治并实现了 \(O(n\log^2 n)\) 的做法后阅读,在此,推荐阅读 WeLikeStudying 大佬的题解。
具体实现:
\(O(n\log^2 n)\) 的线段树分治做法需要使用扩展域并查集,
然而,对于静态地判断二分图,我们有一种 BFS 的做法(染色法),考虑将这种算法推广到本题中。
类似于扩展域并查集,我们需要向线段树中加入连边的操作,
此外,我们还要在树上加入时给所有访问到的线段树结点中加入操作所涉及的点(下文称之为有效点),
根据线段树的特点,一次树上加入最多访问到 \(O(\log n)\) 个线段树结点,
在树上加入操作的 左右边界的最近公共祖先处 分裂(即分别向左右儿子加入)之后,后续的分裂,至少有一个儿子不存在或打上懒标记,由此,复杂度能够保证。
又因为每次连边操作涉及到的点是 \(O(1)\) 的,总共有 \(O(n)\) 次操作,那么,整棵线段树中,有效点的总数是 \(O(n\log n)\) 。
那么,在线段树分治的过程中,我们只需使用染色法的思路去对有效点的颜色进行再染色即可,时间复杂度 \(O(n\log n)\) 。
染色的实现:对于结点 \(i\) ,认为它的初始颜色为 \(i\) ,初始对立色为 \(i+n\) ,访问一个线段树结点时,取出所有有关信息,暴力 BFS ,同时更新颜色,细节部分见代码注释。
算法本质:
对于每一个线段树结点,所涉及到的有效点的个数有限,只对这些有效点进行操作即可降低复杂度。
代码:
注意:有效点不能排序去重,这样只会劣化。
提示:这种做法可能需要用链式前向星代替动态数组等方式降低常数,代码中将部分动态数组进行了替换。
code
#include<bits/stdc++.h>
using namespace std;
#define N 100005
#define F first
#define S second
//以下数组空间复杂度是 O(nlogn) 的
struct ope{//用于存储线段树中结点所涉及的操作(加边)
pair<int,int>a;
int nxt;
}op[N*40];//类似链式前向星方式存储
int head_op[N<<2],sum_op=0;
vector<int>e[N<<2];//用于存储线段树中结点所涉及的图中的点
vector<pair<int,int>>change[2][20];
//change 表示变化数组,用于结束后还原
//以下数组空间复杂度是 O(n) 的
vector<int>g[N];//对于每一层建图使用
vector<int>S[2];//bfs 是存储访问到的点
pair<int,int>qu[N];//手写队列
int col[2][N];//col 的分层:0层代表本色,1层代表对立色,对原始点存
int vis[N];//用于 bfs
int new_col[N<<1],op_col[N<<1];//对颜色点存,opposite
inline int read(){
int a=0;char ch=getchar();
while(!isdigit(ch)) ch=getchar();
while(isdigit(ch)) a=10*a+(ch^'0'),ch=getchar();
return a;
}
void adds(int p,int l,int r,int ql,int qr,int a,int b){
e[p].push_back(a),e[p].push_back(b);
if(ql<=l&&r<=qr){
op[++sum_op]={{a,b},head_op[p]};
head_op[p]=sum_op;return;
}
int mid=(l+r)>>1;
if(ql<=mid) adds(p<<1,l,mid,ql,qr,a,b);
if(mid<qr) adds(p<<1|1,mid+1,r,ql,qr,a,b);
}
bool bfs(int p,int dep){//p是颜色,这是在颜色图中的BFS
S[0].clear(),S[1].clear();
int head=0,tail=0;
S[0].push_back(p),vis[p]=1,qu[tail++]={p,1};
while(head!=tail){//op==1 访问到的都是对立点
pair<int,int> pi=qu[head++];
for(auto &v:g[pi.F]){
if(!vis[v]){
S[pi.S].push_back(v),
vis[v]=pi.S+1,qu[tail++]={v,pi.S^1};
}
else if(vis[v]!=pi.S+1) return 1;
}
}
if(S[1].empty()) return 0;//说明是孤立点
int Nc=S[0][0],Oc=S[1][0];//Nc->new_col,Oc->op_col
//检查颜色是否合法,即检验是否是二分图
for(auto &v:S[0]){
if(v!=Oc&&op_col[v]!=Nc){
new_col[op_col[v]]=Oc;
new_col[v]=Nc,op_col[v]=Oc;
}
else return 1;
}
for(auto &v:S[1]){
if(v!=Nc&&op_col[v]!=Oc){
new_col[op_col[v]]=Nc;
new_col[v]=Oc,op_col[v]=Nc;
}
else return 1;
}
return 0;
}
void Get(int p,int l,int r,int dep){
//每一层的图可以暴力建,只需要建关于颜色的图即可,其他限制无影响
change[0][dep].clear(),change[1][dep].clear();
for(auto &v:e[p]){
g[col[0][v]].clear(),vis[col[0][v]]=0;//清空,设置标记
new_col[col[0][v]]=col[0][v],op_col[col[0][v]]=col[1][v];
}
for(int i=head_op[p];i;i=op[i].nxt){//加入本层的边
pair<int,int>pi=op[i].a;
g[col[0][pi.F]].push_back(col[0][pi.S]);
g[col[0][pi.S]].push_back(col[0][pi.F]);
}
for(auto &v:e[p]){
if(!vis[col[0][v]]){
if(bfs(col[0][v],dep)){
for(int i=l;i<=r;i++) puts("No");
return;
}
}
}
if(l==r) puts("Yes");
else{
for(auto &v:e[p]){//重新染色,向下传递
if(op_col[col[0][v]]!=col[1][v]){
change[1][dep].push_back({v,col[1][v]});
col[1][v]=op_col[col[0][v]];
}
if(new_col[col[0][v]]!=col[0][v]){
change[0][dep].push_back({v,col[0][v]});
col[0][v]=new_col[col[0][v]];
}
}
int mid=(l+r)>>1;
Get(p<<1,l,mid,dep+1),Get(p<<1|1,mid+1,r,dep+1);
for(const auto &pi:change[0][dep]) col[0][pi.F]=pi.S;//恢复颜色
for(const auto &pi:change[1][dep]) col[1][pi.F]=pi.S;//恢复对立色
}
}
int main(){
int n=read(),m=read(),k=read();
for(int i=1;i<=m;i++){
int x=read(),y=read(),l=read()+1,r=read();
if(l<=r) adds(1,1,k,l,r,x,y);
}
for(int i=1;i<=n;i++) col[0][i]=i,col[1][i]=i+n;
//初始时对立色类似扩展域并查集
//但是本做法不能使用扩展域并查集,实测会 MLE
Get(1,1,k,0);
return 0;
}
复杂度:
时空复杂度都是 \(O(n\log n)\) ,在具体实现部分已分析。
结语:
在此鸣谢:
MyDearJellyfish 老师讲解此方法,
MZMTab 同学提供降常思路。
提交记录,完结!

浙公网安备 33010602011771号