BZOJ - 4025 二分图 (LCT维护关于删除时间的最大生成树)

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;
}

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值