线段树分治概述

其实很好理解的一个算法:我们把所有操作放到一棵树里,然后像遍历树一样遍历整棵树。有点像把操作放到dfs树里然后做,回溯的时候把操作还原。然而这个算法不一样的是:一个操作可能会覆盖线段树的多个节点,如果一个操作横跨当前两个节点我们可能需要把它拆成两个操作。当然对于线段树熟悉的话,这并不是问题,一个操作最多被分成 log l o g 个。
我们模仿线段树,如果这个操作覆盖整个区间,就操作,否则看它是否横跨两个儿子,如果是,则分成两个,否则直接分给对应儿子。
下面看一道例题:4025: 二分图

Description

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

Input

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

Output

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











解:

我们首先要知道怎么判断二分图。判断连通性?自然想到了并查集。具体怎么做,在网上找了好久居然没有找到?
然后大佬表示:这不是很简单吗?我们对于每个点记录它到它父亲路径长度的奇偶性。如果当前只有一个环的话(如图):
线段树分治概述_第1张图片
观察两个绿色的点,因为两个点到最上面那个点都会走一条共同路径,这条路径走了两次,所以对奇偶性是没有影响的。
然后我们考虑怎么连接两个点?
线段树分治概述_第2张图片

观察两个绿点,我们是要把绿点连起来,但实际上我们是要把并查集两个祖先连起来。两个绿点的路径长应该是奇数,所以我们把蓝箭头的路径求出在异或一下就可以得到这条边的权。
那么我们需不需要更改路径上的权呢?事实上是不需要的。我们观察蓝色绿色黄色组成的环,他们异或起来应该是0,所以我们从下面走和从上面走,两个加在一起是一整个环,所以上下走的奇偶性是相同的。

至于要修改回去的吧,我们不能路径压缩,这样就不能修改回去了。我们并查集要启发式合并一下。
然后按照线段树分治的方法做一下就好了。

这里有一个问题,我们为了好写,把一个区间的修改全部放进一个vector里,直接递归传参vector。感觉空间有点爆炸?
应该是可以记录两个儿子需要的修改,然后这两个会有重叠,应该是可以实现的?但是可能就难写得多。
这里只给出空间 nlog n l o g 的做法。(一直感觉vector不优美)

code:

#include
#include
#include
#include
#include
#include
#include
using namespace std;
struct lxy{
    int s,t,x,y;
    bool operator < (const lxy &QAQ)const{
      if(s!=QAQ.s) return sreturn t>QAQ.t;
    }
}data[200005];
struct lxy1{
    int x,y;
}yyy;

int n,m,T,fa[100005],k;
bool ans[100005],dis[100005];

int findit(int u){
    while(fa[u]!=u){
        k^=dis[u];
        u=fa[u];
    }
    return u;
}

int readit()
{
    int a=0;char ch=getchar();
    while(ch<'0'||ch>'9') ch=getchar();
    while(ch>='0'&&ch<='9'){
        a=a*10+ch-'0';
        ch=getchar();
    }
    return a;
}

void CDQ(int l,int r,vector  &v)
{
    int mid=(l+r)>>1;
    vector  t1,t2;//两儿子需要的操作
    queue  d;//记录操作,用于还原
    for(int i=v.size()-1;i>=0;i--){//包含整个区间就做
        if(v[i].s<=l&&v[i].t>=r){
            k=0;int x=findit(v[i].x),y=findit(v[i].y);
            if(x==y&&k==0){//出现奇环这个儿子下面全是No,可以直接回溯return
                for(int j=l;j<=r;j++) ans[j]=1;
                while(!d.empty()){
                    lxy1 now=d.front();
                    fa[now.x]=now.x;dis[now.x]=0;//开始还原的时候fa赋成0了,还不知道怎么RE的
                    d.pop();
                }
                return;
            }
            else if(x!=y){
                if((rand()&1)==0){
                    fa[x]=y;dis[x]=(k^1);
                    yyy.x=x;yyy.y=y;
                    d.push(yyy);
                }
                else{
                    fa[y]=x;dis[y]=(k^1);
                    yyy.x=y;yyy.y=x;
                    d.push(yyy);
                }
            }
        }
        else if(v[i].t<=mid) t1.push_back(v[i]);//给左儿子
        else if(v[i].s>mid) t2.push_back(v[i]);//给右儿子
        else t1.push_back(v[i]),t2.push_back(v[i]);横跨就两个都给
    }
    if(l!=r) CDQ(l,mid,t1),CDQ(mid+1,r,t2);
    while(!d.empty()){//还是最后要回溯
        lxy1 now=d.front();
        fa[now.x]=now.x;dis[now.x]=0;
        d.pop();
    }
}

int main()
{
    srand(2333);
    scanf("%d%d%d",&n,&m,&T);
    for(int i=1;i<=n;i++) fa[i]=i;
    vector  v;
    for(int i=1;i<=m;i++)
      data[i].x=readit(),data[i].y=readit(),data[i].s=readit(),data[i].t=readit(),data[i].s++;
    for(int i=1;i<=m;i++)
      if(data[i].s<=data[i].t)
        v.push_back(data[i]);
    CDQ(1,T,v);
    for(int i=1;i<=T;i++)
    {
        if(ans[i]==1) printf("No\n");
        else printf("Yes\n");
    }
}

你可能感兴趣的:(数据结构)