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”,不含引号。
Sample Input
3 3 3
1 2 0 2
2 3 0 3
1 3 1 2
Sample Output
Yes
No
Yes
HINT
样例说明:
0时刻,出现两条边1-2和2-3。
第1时间段内,这个图是二分图,输出Yes。
1时刻,出现一条边1-3。
第2时间段内,这个图不是二分图,输出No。
2时刻,1-2和1-3两条边消失。
第3时间段内,只有一条边2-3,这个图是二分图,输出Yes。
数据范围:
n<=100000,m<=200000,T<=100000,1<=u,v<=n,0<=start<=end<=T。
解题思路:看了半天题解终于看懂了,做法是用LCT,LCT只能维护树的信息,但是如果他要维护图的话,也不是不可以,这个时候通常的套路是新建一个集合,用来记录在图中,但是不在树中的边。所以这题,我们也可以采取这种思路。我们按照时间扫过去,如果发现加边了,那就加边,删边了,那就删边。但是加边的时候会有一个问题,万一形成了环怎么办?如果形成了环,那么就把这条边插到集合里,不加到LCT里即可,这个时候顺便判断一下那个环大小是奇数还是偶数,来判断当前是否是二分图。删边的时候直接删即可。但是这样会存在一个问题,如果我把边删了,原本之前的环就消失了,那么我应该把集合里的边重新加回到树里,才能保证正确性。但是直接这么做很困难,因为我没有记录哪条边属于哪个环,而且也很难记录,所以我们要保证一个东西,就是树上的边,应该尽可能的晚被删掉,这样就不会出现需要把集合的边重新加回到树里的情况了。也就是说我们要维护一颗树,这颗树上的边都是最晚删除的,就可以了。那么怎么做呢?当加入一条边,它的删除时间比换里的边要小,那么我们就不插入这条边,否则,就查询环里删除时间最小的边,把他删掉,再把当前边加到树里。这些LCT都可以维护。另外这题要拆边成点,直接维护边很困难。
#include <iostream>
#include <string.h>
#include <math.h>
#include <vector>
#include <algorithm>
#include <stdio.h>
using namespace std;
const int MAXN = 400010;
typedef long long ll;
int Q[MAXN];
int ch[MAXN][2];
int fa[MAXN];
int rev[MAXN];
int siz[MAXN];
int N, M, T;
int mn[MAXN];//动态树维护的东西,保存退出时间最小的点的编号
int et[MAXN];//保存每个点的退出时间
struct edge
{
int u, v;
int b, e;
int id;
friend bool operator<(edge a, edge b)
{
return a.b < b.b;
}
} in[MAXN << 2], out[MAXN << 2];
bool cmp(edge a, edge b)
{
return a.e < b.e;
}
bool isroot(int rt)
{
return ch[fa[rt]][0] != rt && ch[fa[rt]][1] != rt;
}
bool get(int x) { return ch[fa[x]][1] == x; }
void pushup(int x)
{
if (!x)
return;
mn[x] = x;
siz[x] = x > N;//由于把边拆成了点,所以要判断一下
if (ch[x][0])
siz[x] += siz[ch[x][0]], mn[x] = et[mn[x]] < et[mn[ch[x][0]]] ? mn[x] : mn[ch[x][0]];
if (ch[x][1])
siz[x] += siz[ch[x][1]], mn[x] = et[mn[x]] < et[mn[ch[x][1]]] ? mn[x] : mn[ch[x][1]];
}
//下推函数
void pushdown(int x)
{
if (rev[x])
{
rev[ch[x][0]] ^= 1;
rev[ch[x][1]] ^= 1;
rev[x] ^= 1;
swap(ch[x][0], ch[x][1]);
}
}
//Splay旋转函数,通常无需修改
void Rotate(int x)
{
int old = fa[x], oldf = fa[old], op = get(x);
if (!isroot(old))
ch[oldf][ch[oldf][1] == old] = x; //这一条一定要放在改变父子关系之前!在纯Splay中是放在后面的,因为直接看oldf是否为0可知old是否为根。
ch[old][op] = ch[x][op ^ 1];
fa[ch[x][op ^ 1]] = old; //但这里使用isroot,改变之后就不能判断了!
ch[x][op ^ 1] = old;
fa[old] = x;
fa[x] = oldf;
pushup(old);
pushup(x);
}
//Splay函数,将rt变为某棵Splay树的根节点,通常无需修改
void Splay(int x)
{
int tp = 1;
Q[1] = x;
for (int i = x; !isroot(i); i = fa[i])
Q[++tp] = fa[i]; //对于LCT的判断是否是根节点,需要使用isroot,在纯Splay中使用的是fa,不要搞混!
for (int i = tp; i; i--)
pushdown(Q[i]);
for (int FA; !isroot(x); Rotate(x))
{
FA = fa[x];
if (!isroot(FA))
Rotate(get(x) == get(FA) ? FA : x);
}
}
//打通x到整个LCT的根的路径,即fa和ch都是正确的
void Access(int x)
{
int t = 0;
while (x)
{
Splay(x);
ch[x][1] = t;
pushup(x);
t = x;
x = fa[x];
}
}
//把x变为整个LCT的根,先打通路径,然后把他变为他的Splay的根即可。
void Makeroot(int x)
{
Access(x);
Splay(x);
rev[x] ^= 1;
}
//链接函数,先把x变为整个LCT的根(这时x才没有父亲,所以不能用Splay),然后再设置一个父亲即可
void Link(int x, int y)
{
Makeroot(x);
Makeroot(y);
fa[x] = y;
pushup(y);
}
//剪断函数,根据Splay原理,可以把x变为y的左节点,然后删除即可
void Cut(int x, int y)
{
Makeroot(x);
Access(y);
Splay(y);
if (ch[y][0] == x)
{
fa[x] = ch[y][0] = 0;
pushup(y);
}
}
//分割函数,即把x到y这条路径变为一颗Splay树,从而把区间查询变为树上节点查询(Splay原理)
void split(int x, int y)
{
Makeroot(y);
Access(x);
Splay(x);
}
//找到原树上的根
//具体实现:首先Access这个点,然后在Splay树中将这个点转到根
//由于Splay树按照深度为关键字排序,所以不断地向左子树寻找,就可以找到深度最小的根。
int find_root(int x)
{
Access(x);
Splay(x);
int now = x;
while (ch[now][0])
now = ch[now][0];
return now;
}
//查询x到y之间是否已经有路径,直接查询所在的LCT的根是否相等即可
int isconnected(int x, int y)
{
int rx, ry;
rx = find_root(x);
ry = find_root(y);
return rx == ry;
}
int query(int x, int y, bool w)
{
Makeroot(x);
Access(y);
Splay(y);
return w ? siz[ch[y][0]] : mn[ch[y][0]];
}
int cnt = 0;//保存奇环数量
bool vis[MAXN];//保存当前边是否是非树边
void insert(edge k)
{
if (k.u == k.v)//自环特殊判断
{
vis[k.id] = 1;
cnt++;
return;
}
et[k.id] = k.e;//初始化退出时间
mn[k.id] = k.id;//假设是自己,后面再更新
if (isconnected(k.u, k.v))//判断是否连通
{
int x = query(k.u, k.v, 0);//查询退出时间最小的点
if (et[x] > et[k.id])//如果当前边退出时间较小,那么要把边加进去
{
if (query(k.u, k.v, 1) & 1 ^ 1)//查询路径长度,看看是不是偶数,是的话加进去会变成奇环
{
vis[k.id] = 1;//标记一下
cnt++;
}
else
return;//否则不用管,因为形成的是偶环
}
else
{
if (query(k.u, k.v, 1) & 1 ^ 1)//如果是奇环
{
vis[x] = 1;//把最小那个变为非树边
cnt++;
}
Cut(in[x - N].u, in[x - N].id);//删掉
Cut(in[x - N].v, in[x - N].id);
Link(k.u, k.id);//把当前边加进去
Link(k.v, k.id);
}
}
else
{
Link(k.u, k.id);//直接连
Link(k.id, k.v);
}
}
void del(edge k)
{
if (vis[k.id])
{
vis[k.id] = 0;
cnt--;
}
else
{
if (isconnected(k.u, k.id))
{
Cut(k.u, k.id);
Cut(k.v, k.id);
}
}
}
int main()
{
scanf("%d%d%d", &N, &M, &T);
//1~N表示非树边,N~N+N表示真正的边
for (int i = 0; i <= N; i++)
et[i] = 1 << 30;//拆边了,所以非树边初始化为最大
for (int i = 1; i <= M; i++)
{
scanf("%d%d%d%d", &in[i].u, &in[i].v, &in[i].b, &in[i].e);
}
sort(in + 1, in + 1 + M);
for (int i = 1; i <= M; i++)
in[i].id = N + i;
for (int i = 1; i <= M; i++)
out[i] = in[i];
sort(out + 1, out + 1 + M, cmp);
int k = 1, j = 1;
for (int i = 1; i <= T; i++)
{
while (in[j].b < i && j <= M)//把该加的边都加了
insert(in[j++]);
while (out[k].e < i && k <= M)//把该删的都删了
del(out[k++]);
if (cnt)
puts("No");
else
puts("Yes");
}
return 0;
}