BZOJ4025:二分图 ((CDQ分治+并查集)/LCT)

9 篇文章 0 订阅
9 篇文章 0 订阅

题目传送门:http://www.lydsy.com/JudgeOnline/problem.php?id=4025


题目分析:这是一道LCT好题,同时一道CDQ分治好题啊,做了我整整一天……

先说一下LCT的做法:
首先我们可以发现一个结论:对于一个偶环,如果不保存它的其中一条边,不会影响新的边加进来时对奇环的判断,但如果不保存两条边就会影响:

那就是说如果新加进来的边形成了一个偶环,我就不用把这条边加进LCT里了?当然不是,如果我们没有将新边加进LCT里,接下来偶环上的另一条边消失了,LCT就变成了上图右边的样子,它有可能无法判断出奇环,然而整个图实际上的样子应该是左边那样,存在奇环。那么我们应该在LCT中不保留哪条边呢?从上面我们可以看出,如果删除消失时间最早的边,就不会影响正确性(自己YY一下即可知道)。因为该边存在的时候偶环上的其它边都还存在,所以没有它也能判断奇环。
还有一个问题:如果出现了奇环怎么办?除非奇环上的一条边消失,否则整个图一直都不是二分图。那么我们在LCT中删除掉奇环上的消失时间最早的边,然后在当前时间到其消失时间段打个标记即可。其实归纳起来还是LCT的老套路:出现环则考虑删掉环上的哪条边会对答案没有影响。LCT的思路我想了几十分钟就想到了。


CODE(LCT):

#include<iostream>
#include<string>
#include<cstring>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<stdio.h>
#include<algorithm>
using namespace std;

const int maxn=100100;

struct data
{
    int obj;
    bool tp;
    data *Next;
} e[maxn<<2];
data *head[maxn];
int cur=-1;

struct Tnode
{
    int val,Size,L,R,id,path_parent;
    bool flip;
    Tnode *fa,*son[2],*minv;
    int Get_d() { return fa->son[1]==this; }
    void Connect(Tnode *P,int d) { (son[d]=P)->fa=this; }
    void Push_down()
    {
        if (flip)
        {
            swap(son[0],son[1]);
            if (son[0]) son[0]->flip^=1;
            if (son[1]) son[1]->flip^=1;
            flip=false;
        }
    }
    void Up()
    {
        Size=1;
        minv=this;
        if (son[0])
        {
            Size+=son[0]->Size;
            if (son[0]->minv->val<minv->val) minv=son[0]->minv;
        }
        if (son[1])
        {
            Size+=son[1]->Size;
            if (son[1]->minv->val<minv->val) minv=son[1]->minv;
        }
    }
} tree[maxn*3];
Tnode *Node[maxn*3];
int point=-1;

bool on_tree[maxn<<1];
bool odd[maxn<<1];
int num=0;

int u[maxn<<1];
int v[maxn<<1];
int ed[maxn<<1];

int n,m,t;

void Add(int x,int y,bool z)
{
    cur++;
    e[cur].obj=y;
    e[cur].tp=z;
    e[cur].Next=head[x];
    head[x]=e+cur;
}

Tnode *New_node(int i,int v)
{
    point++;
    tree[point].val=v;
    tree[point].id=i;
    tree[point].Size=1;
    tree[point].fa=tree[point].son[0]=tree[point].son[1]=NULL;
    tree[point].minv=tree+point;
    return tree+point;
}

void Push(Tnode *P)
{
    if (!P) return;
    Push(P->fa);
    P->Push_down();
}

void Zig(Tnode *P)
{
    Tnode *F=P->fa;
    int d=P->Get_d();
    if (P->son[!d]) F->Connect(P->son[!d],d);
    else F->son[d]=NULL;
    if (F->fa) F->fa->Connect(P, F->Get_d() );
    else P->fa=NULL;
    P->Connect(F,!d);
    F->Up();
    P->path_parent=F->path_parent;
    F->path_parent=0;
}

void Splay(Tnode *P)
{
    Push(P);
    while (P->fa)
    {
        Tnode *F=P->fa;
        if (F->fa) ( P->Get_d()^F->Get_d() )? Zig(P):Zig(F);
        Zig(P);
    }
    P->Up();
}

void Down(int x)
{
    Splay(Node[x]);
    if (Node[x]->son[1])
    {
        Node[x]->son[1]->path_parent=x;
        Node[x]->son[1]->fa=NULL;
        Node[x]->son[1]=NULL;
        Node[x]->Up();
    }
}

void Access(int x)
{
    Down(x);
    int y=Node[x]->path_parent;
    while (y)
    {
        Down(y);
        Node[y]->Connect(Node[x],1);
        Node[y]->Up();
        Node[x]->path_parent=0;
        x=y;
        y=Node[x]->path_parent;
    }
}

Tnode *Find(Tnode *P)
{
    if (P->son[0]) return Find(P->son[0]);
    return P;
}

Tnode *Find_root(int x)
{
    Access(x);
    Splay(Node[x]);
    return Find(Node[x]);
}

void Evert(int x)
{
    Access(x);
    Splay(Node[x]);
    Node[x]->flip^=1;
}

void Link(int x,int y)
{
    Evert(x);
    Node[x]->path_parent=y;
}

void Cut(int x,int y)
{
    Evert(x);
    Access(y);
    Down(x);
    Splay(Node[y]);
    Node[y]->path_parent=0;
}

int main()
{
    freopen("4025.in","r",stdin);
    freopen("4025.out","w",stdout);

    scanf("%d%d%d",&n,&m,&t);
    for (int i=1; i<=m; i++)
    {
        int st;
        scanf("%d%d%d%d",&u[i],&v[i],&st,&ed[i]);
        Add(ed[i],i,false);
        Add(st,i,true);
    }

    for (int i=1; i<=n; i++) Node[i]=New_node(0,maxn);
    for (int i=1; i<=m; i++) Node[n+i]=New_node(n+i,ed[i]);
    for (int i=1; i<=t; i++)
    {
        for (data *p=head[i-1]; p; p=p->Next)
        {
            int x=p->obj;
            if (p->tp)
                if ( Find_root(u[x])!=Find_root(v[x]) )
                {
                    Link(n+x,u[x]);
                    Link(n+x,v[x]);

                    Node[n+x]->L=u[x];
                    Node[n+x]->R=v[x];
                    on_tree[x]=true;
                }
                else
                {
                    Evert(u[x]);
                    Access(v[x]);
                    Splay(Node[ u[x] ]);

                    if ( Node[ u[x] ]->minv->val<ed[x] )
                    {
                        int y=Node[ u[x] ]->minv->id;
                        Cut(y,Node[y]->L);
                        Cut(y,Node[y]->R);

                        Link(n+x,u[x]);
                        Link(n+x,v[x]);

                        Node[n+x]->L=u[x];
                        Node[n+x]->R=v[x];
                        on_tree[x]=true;
                        on_tree[y-n]=false;
                        x=y-n;
                    }

                    Evert(u[x]);
                    Access(v[x]);
                    Splay(Node[ u[x] ]);

                    if ( (Node[ u[x] ]->Size&3)==1 )
                    {
                        odd[x]=true;
                        num++;
                    }
                }
            else
                if (on_tree[x])
                {
                    Cut(n+x,u[x]);
                    Cut(n+x,v[x]);
                    on_tree[x]=false;
                }
                else
                    if (odd[x]) odd[x]=false,num--;
        }

        if (num) printf("No\n");
        else printf("Yes\n");
    }

    return 0;
}

接下来CDQ分治的做法就比较神奇了。我从PoPo姐的文章标题里看到这是个分治+并查集,然后作为一个CDQ分治只学过矩阵操作,最长不下降子序列和三维偏序的蒟蒻,本人想了一个晚上……还是想不出。然后第二天用了40min看网上各路大神的题解,终于领悟了怎么用CDQ分治做QAQ。
首先我们要知道,一个使用了启发式合并的并查集可以在log(n)的时间内完成按顺序的加边删边操作(注意,这里不能使用路径压缩)。然后我们考虑对所有的边进行分治。假设当前处理的时间区间是[L,R],令所有出现时间(st,ed]属于[L,R]的边的集合为E,则对于所有(st,ed]=[L,R]的边,我们先将其加进并查集里。如果出现了奇环,就意味着[L,R]时间段,这个图都不是二分图。否则我们再将E中的其它边分到左右两边递归处理,只在左半段时间出现的边归到左边,只在右半段时间出现的边归到右边,横跨两段的边,我们拆成两条边分别处理,直到处理到L=R或E为空集。注意递归返回的时候,要先将递归开始时加进并查集里的边删去,再退出。
这样时间复杂度是多少呢?在之前学习区间修改区间查询的可持久化线段树时,我曾得到一个结论:一段区间可以用不超过2log(n)的线段树上的节点表示出来。那么假设我们将这里的递归树画出来,就会发现一条边顶多拆成2log(T)条边。由于每一次加边删边时对并查集的操作需要log(n)的时间,总的时间复杂度为 O(mlog(T)log(n))

CODE(CDQ分治+并查集):

#include<iostream>
#include<string>
#include<cstring>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<stdio.h>
#include<algorithm>
using namespace std;

const int maxn=100100;
const int maxm=maxn<<1;
const int maxe=85000000;

int u[maxm];
int v[maxm];
int st[maxm];
int ed[maxm];

int e[maxe];
short int id[maxe];
int cur=-1;

int fa[maxn];
int Size[maxn];
int d[maxn];

bool ans[maxn];
int n,m,T;

int Find(int x)
{
    if (!fa[x]) return x;
    return Find(fa[x]);
}

int Get(int x)
{
    if (!fa[x]) return 0;
    return d[x]^Get(fa[x]);
}

bool Add(int i)
{
    int x=e[i];
    int fu=Find(u[x]),fv=Find(v[x]);
    int y=Get(u[x]),z=Get(v[x]);
    if (fu!=fv)
    {
        if (Size[fu]<=Size[fv]) fa[fu]=fv,d[fu]=y^z^1,Size[fv]+=Size[fu],id[i]=1;
        else fa[fv]=fu,d[fv]=y^z^1,Size[fu]+=Size[fv],id[i]=2;
        return true;
    }
    else return (y^z);
}

int Find_last(int x)
{
    if (!fa[ fa[x] ]) return x;
    return Find_last(fa[x]);
}

void Delete(int i)
{
    int x=e[i];
    if (id[i]==1)
    {
        int fu=Find_last(u[x]),fv=Find(v[x]);
        Size[fv]-=Size[fu],fa[fu]=0,d[fu]=0;
    }
    else
    {
        int fu=Find(u[x]),fv=Find_last(v[x]);
        Size[fu]-=Size[fv],fa[fv]=0,d[fv]=0;
    }
    id[i]=0;
}

void CDQ(int L,int R,int x,int y)
{
    if (L==R)
    {
        bool f=true;
        for (int i=x; i<=y; i++) f&=Add(i);
        ans[L]=f;
        for (int i=y; i>=x; i--) if (id[i]) Delete(i);
        return;
    }

    bool f=true;
    for (int i=x; i<=y; i++)
        if ( st[ e[i] ]<=L && R<=ed[ e[i] ] ) f&=Add(i);

    if (f)
    {
        int mid=(L+R)>>1;
        int l=cur+1;
        for (int i=x; i<=y; i++)
        {
            int j=e[i];
            if (id[i]) continue;
            int Left=max(st[j],L),Right=min(ed[j],mid);
            if (Left<=Right) e[++cur]=j;
        }
        if (l<=cur) CDQ(L,mid,l,cur);
        else
            for (int i=L; i<=mid; i++) ans[i]=true;

        l=cur+1;
        for (int i=x; i<=y; i++)
        {
            int j=e[i];
            if (id[i]) continue;
            int Left=max(st[j],mid+1),Right=min(ed[j],R);
            if (Left<=Right) e[++cur]=j;
        }
        if (l<=cur) CDQ(mid+1,R,l,cur);
        else
            for (int i=mid+1; i<=R; i++) ans[i]=true;
    }
    else
        for (int i=L; i<=R; i++) ans[i]=false;

    for (int i=y; i>=x; i--) if (id[i]) Delete(i);
}

int main()
{
    freopen("4025.in","r",stdin);
    freopen("4025.out","w",stdout);

    //printf("%d\n",sizeof(e)/1024/1024+sizeof(id)/1024/1024);

    scanf("%d%d%d",&n,&m,&T);
    for (int i=1; i<=m; i++)
    {
        scanf("%d%d%d%d",&u[i],&v[i],&st[i],&ed[i]),st[i]++;
        if (st[i]<=ed[i]) e[++cur]=i;
    }
    for (int i=1; i<=n; i++) Size[i]=1;

    CDQ(1,T,0,cur);

    for (int i=1; i<=T; i++) if (ans[i]) printf("Yes\n"); else printf("No\n");
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值