bzoj4025 二分图 线段树分治+带权可回滚并查集+前向星

题目链接:传送门

Description
神犇有一个n个节点的图。因为神犇是神犇,所以在T时间内一些边会出现后消失。神犇要求出每一时间段内这个图是否是二分图。这么简单的问题神犇当然会做了,于是他想考考你。

Input
输入数据的第一行是三个整数n,m,T。
第2行到第m+1行,每行4个整数u,v,start,end。第i+1行的四个整数表示第i条边连接u,v两个点,这条边在start时刻出现,在第end时刻消失。

Output
输出包含T行。在第i行中,如果第i时间段内这个图是二分图,那么输出“Yes”,否则输出“No”,不含引号。

二分图判定

引理:若一个图是二分图,则这个图不存在奇环。

比较好证明,珂以看这篇博客,这里不证了qwq
先不考虑边消失的情况,考虑怎样判二分图:想到用带权并查集,带权并查集上维护点到代表元的距离的奇偶性。
加入一条边 ( u , v ) (u,v) (u,v)时分情况讨论:
如果 u u u v v v本来不在同一个联通块中,则连边也不珂能形成环,所以直接合并就好qwq
合并时是代表元合并,假设 x x x到代表元的距离的奇偶性为 d i s [ x ] dis[x] dis[x] 0 0 0表示偶数, 1 1 1表示奇数。
代表元之间的距离应为 d i s [ u ] dis[u] dis[u] ^ d i s [ v ] dis[v] dis[v] ^ 1 1 1(^表示异或),表示 u u u到代表元的距离加上 v v v到代表元的距离加上1的奇偶性。
如果 u u u v v v在同一个联通块中,且 u u u v v v到代表元距离之和为偶数,则连边之后会形成奇环(加上了新的边,偶数变奇数)

解析

如果边出现的时间段不相交,发现每次消失的边都是最后加入的qwq。
那么珂以用资磁回滚的并查集,搞一个栈,像回滚莫队一样回滚。
但是这里边出现的时间段珂以相♂交,所以考虑把所有线段分成若干不相♂交的线段。
线段?线段树!
首先把每个时间段对应到线段树上的若干区间。
举个栗子,假设 T = 4 T=4 T=4,即所有线段都在 [ 1 , 4 ] [1,4] [1,4]内,然后有一条边出现的时间是 [ 1 , 3 ] [1,3] [1,3]
脑补一下,珂以发现这里会把它映射到 [ 1 , 2 ] [1,2] [1,2] [ 3 , 3 ] [3,3] [3,3]这两个区间上qwq。
意思就是让它在 1 1 1时刻出现,在 2 2 2时刻末消失,再在 3 3 3时刻出现, 3 3 3时刻末消失。
这里用前向星存下线段树上每个节点被哪些边包含(用vector也珂以qwq)
用一次询问处理出所有答案:
每次访问一个线段树节点,先把前向星里的所有储存的边加到图中,然后判是不是二分图。
如果不是,那么再加入边这个图也不会成为二分图,所以这个时间段内图均不是二分图,所以回滚后返回。
如果是,叶子节点的情况就珂以记录答案了,否则递归求解两个孩子。

Talk is cheap, show you the code:

#include<stdio.h>
#include<cstring>
#include<algorithm>
#include<math.h>
#include<vector>
#define re register int
#define rl register ll
#define lc rt<<1
#define rc rt<<1|1
using namespace std;
typedef long long ll;
int read() {
	re x=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9') {
		if(ch=='-')	f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9') {
		x=(x<<1)+(x<<3)+ch-'0';
		ch=getchar();
	}
	return x*f;
}
const int Size=200005;
const int INF=0x3f3f3f3f;
namespace UnionFindSet {

int father[Size],siz[Size],dis[Size];
inline void init(int n) {
	for(re i=1; i<=n; i++) {
		father[i]=i;
		siz[i]=1;
		dis[i]=0;
	}
}
int Find(int x) {
	if(x==father[x])	return x;
	return Find(father[x]);
}
struct Stack {
	int top;
	int u[Size],v[Size],fa[Size],sz[Size],d[Size];
	inline void push(int x,int y) {
		u[++top]=x;
		v[top]=y;
		fa[top]=father[x];
		sz[top]=siz[y];
		d[top]=dis[x];
	}
	inline void pop() {
		father[u[top]]=fa[top];
		siz[v[top]]=sz[top];
		dis[u[top]]=d[top];
		top--;
	}
	void pop(int x) {
		while(top>x) {
			pop();
		}
	}
} S;
void Union(int u,int v) {
	re fu=Find(u);
	re fv=Find(v);
	if(siz[fu]>siz[fv])	swap(fu,fv);
	S.push(fu,fv);
	siz[fv]+=siz[fu];
	dis[fu]=dis[u]^dis[v]^1;
	father[fu]=fv;
}
int dist(int x) {
	if(x==father[x])	return dis[x];
	return dist(father[x])^dis[x];
}

}
using namespace UnionFindSet;
int n,m,T,cnt,head[Size<<2];
struct Edge {		//用前向星存下包括线段树上某个节点的区间的线段 
	int v,next;
} w[Size*25];
void AddEdge(int u,int v) {
	w[++cnt].v=v;
	w[cnt].next=head[u];
	head[u]=cnt;
}
struct node {
	int l,r;
} tree[Size<<2];
void Build(int l,int r,int rt) {
	tree[rt].l=l;
	tree[rt].r=r;
	if(l==r)	return;
	int mid=(l+r)>>1;
	Build(l,mid,lc);
	Build(mid+1,r,rc);
}
void Update(int l,int r,int x,int rt) {
	if(l<=tree[rt].l && tree[rt].r<=r) {
		//如果这条边的编号和线段树节点的编号连边 
		//说明这条边出现的时间包含了这个节点的左右区间 
		AddEdge(rt,x);
		return;
	}
	int mid=(tree[rt].l+tree[rt].r)>>1;
	if(l<=mid)	Update(l,r,x,lc);
	if(r>mid)	Update(l,r,x,rc);
}
int u[Size],v[Size];		//记录每条边 
bool solve(int rt) {
	for(re i=head[rt]; i; i=w[i].next) {
		//遍历所有时间包括当前区间的边 
		int nxt=w[i].v;
		int fu=Find(u[nxt]);
		int fv=Find(v[nxt]);
		if(fu!=fv || (dist(u[nxt])^dist(v[nxt]))) {
			//当u[nxt]和v[nxt]不属于同一个联通块,
			//或者属于同一个联通块且加边不会构成奇环时,这个图仍然是一个二分图 
			//dis[u[nxt]]^dis[v[nxt]]表示u[nxt]到v[nxt]的距离的奇偶性 
			//若这一坨为1,表示距离为奇数,加边后不会形成奇环 
			//注意当u[nxt]和v[nxt]不再同一个联通块时,不用合并qwq 
			if(fu!=fv) {
				Union(u[nxt],v[nxt]);
			}
		} else {
			//如果加边之后形成了奇环就返回false 
			return true;
		}
	}
	return false;
}
bool ans[Size];
void Query(int rt) {
	int pre=S.top;
	bool fail=solve(rt);
	if(fail) {
		S.pop(pre);		//回滚到之前的状态 
		return;
	}
	if(tree[rt].l==tree[rt].r) {
		ans[tree[rt].l]=true;
	} else {
		//前向星里存的是所有出现时间段包含当前线段树节点的区间的边 
		//所以此时还不用回滚 
		Query(lc);
		Query(rc);
	}
	S.pop(pre);
}
int main() {
//	freopen("1.in","r",stdin);
	n=read();
	m=read();
	T=read();
	Build(1,T,1);
	for(re i=1; i<=m; i++) {
		u[i]=read();
		v[i]=read();
		int s=read();
		int e=read();
		Update(s+1,e,i,1);
	}
	init(n);
	Query(1);
	for(re i=1; i<=n; i++) {
		if(ans[i]) {
			puts("Yes");
		} else {
			puts("No");
		}
	}
	return 0;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值