zoukankan      html  css  js  c++  java
  • 虚拟化构建二分图(BZOJ2080 题解+浅谈几道双栈排序思想的题)

    虚拟化构建二分图

    ------BZOJ2080 题解+浅谈几道双栈排序思想的题

    本题的题解在最下面↓↓↓

    不得不说,第一次接触类似于双栈排序的这种题,是在BZOJ的五月月赛上。

    【BZOJ4881】[Lydsy2017年5月月赛]线段游戏

    传送门

    简洁的题面:给你一个1到n(n<=100000)的排列,问你能否将这个排列分成两个升序的子序列,如果能,求方案数。(本人强行将问题拆成两个子任务,原因你一会就会知道~)

    P.S.:比赛时除太空猫外唯一想出来的题,其实我已经写过一篇题解了,不过当时比赛还没结束,发的比较仓促,只说了怎么做的而没说为什么要这么做。这里填个小坑。

    分析:先考虑第一问,如果你从正面来想这道题,那么你有很多种方法从原序列中找出一个升序的子序列,但是你无法保证剩下的那个子序列也是升序的。所以我们不妨换个角度,考虑什么样的子序列是不能选的。

    容易发现,一个子序列是升序的=这个子序列中不存在逆序对。那么一旦原序列中存在逆序对(a,b),我们就需要将他们分到不同的序列中去。如果你善于抽象模型的话,你会发现这其实是一个二分图的染色问题。(重点!)也就是说,你如果将原序列看成一张图,那么对于所有逆序对(a,b),那么我们在a,b间连一条边,如果我们将这个二分图染色,那么如果其中一个点是黑色另一个点一定是白色。也就对应了:我们必须将它们分到不同的序列中去。

    那么我们该如何构建这个二分图呢?(神犇GXZ告诉我们:可以用并查集。(其实不能,不过思想很好。))我们采用带权并查集的思想,如果某两个点之间的关系是确定的,我们就将它们放到同一个并查集中去。我们从左到右扫一遍原序列,每扫到一个点,就把之前所有比它大的数都拿出来,并合并二者所在的连通块(当然,你需要记录并查集中的每个点与父亲的关系)。在合并的时候,如果(a,b)已经在同一个并查集中,我们可以顺便判断一下二者的关系,如果出现矛盾,则输出不能。那么第二问的结果呢?答案就是(2^连通块的数量)。(这个需要自己yy一下了。)

    不幸的是,单纯的并查集还是无法解决此题,我们应该想一种办法来优化并查集的实现过程。考虑,假设之前有一个连通块A,我们当前枚举到的数是b,如果b需要与A合并,只需要满足A中存在一个数a>b,也就是说A中最大的数amax>b。换句话说,我们将A中其它的数都忽视掉,只考虑A中最大的数amax,就让amax来代表整个并查集。于是我们想到用set来模拟这个过程,将set中所有比b大的数都取出来,再将这些数中最大的数扔回到set里,答案就是(2^set的大小)。但是第一问该怎么办呢?用set显然是处理不了的。发现如果原序列中存在矛盾,就是存在a-b,b-c,c-a(或者a-b-c-d-e-a。。。)之间都存在冲突,如果他们出现的顺序是a,b,c,那么有a>b>c。也就是说我们只需要求出原序列中最长下降子序列的长度,如果长度>=3,则输出无解。这个可以用树状数组搞定。

    本人代码:

    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <set>
    using namespace std;
    int n,ans,v[100010],tr[100010];
    set<int> s;
    void updata(int x,int val)
    {
    	for(int i=x;i<=n;i+=i&-i)	tr[i]=max(val,tr[i]);
    }
    int query(int x)
    {
    	int i=x,ret=0;
    	for(i=x;i;i-=i&-i)	ret=max(ret,tr[i]);
    	return ret;
    }
    int main()
    {
    	scanf("%d",&n);
    	int i,a,b;
    	set<int>::iterator it;
    	for(i=1;i<=n;i++)	scanf("%d",&v[i]);
    	for(i=n;i>=1;i--)
    	{
    		a=query(v[i]-1)+1;
    		if(a>=3)
    		{
    			printf("0");
    			return 0;
    		}
    		updata(v[i],a);
    	}
    	for(i=1;i<=n;i++)
    	{
    		b=v[i];
    		while(!s.empty())
    		{
    			it=s.upper_bound(b);
    			if(it==s.end())	break;
    			b=*it,s.erase(b);
    		}
    		s.insert(b);
    	}
    	b=s.size(),ans=1;
    	while(b--)	ans=(ans*2)%998244353;
    	printf("%d",ans);
    	return 0;
    }

    说了这么多,重点还是在于二分图的抽象那块,以及并查集的应用与优化。但是如果上面那题你已经觉得太简单了,那么看下一道吧。

    【由于版权原因,题目名称已屏蔽】

    简洁的题面:你有两个栈和n(n<=100000)个物品,每个物品都有一个给定的进栈时间和出栈时间,这2n个时间组成了一个1到2n的全排列,要求在一个物品进栈时,它可以被放在任意一个栈的顶端,出栈时,它也必须在某个栈的顶端。问是否存在满足要求的进出栈方案,如果有,输出方案数。

    P.S.:PoPoQQQ大爷不要打我~

    分析:其实,这道题看起来已经是双栈排序的加强版了,但是由于入栈出栈的具体时间都已经给出,所以我觉得这其实还是双栈排序的简化版。

    还记得上一题用到的带权并查集吗?没错,我们依然可以用并查集来解决这道题。现在我们思考,如何将此题转化成一个二分图模型。

    如果我们将每个物品都看成一条线段,线段的端点分别是入栈时间和出栈时间,那么当出现下面的情况时,两个物品不能放在同一个栈中。

    即,对于线段(x1,y1)和(x2,y2),如果x1<x2<y1<y2或x2<x1<y2<y1,那么他们不能在同一个栈中。我们在所有冲突的点对之间连边,这样就又构成了一个二分图,如果这个二分图中存在奇环,则说明无法将这个二分图染色。我们依旧试着采用并查集,但是这个二分图的边数是n2级别的,我们该如何连边呢?

    我们只考虑x1<x2<y1<y2的情况,我们先将所有线段按y从大到小排序,然后我们枚举x1,y1,然后对于之前所有的x2∈(x1,y1),我们都要在二者之间连边(或者说将二者所在并查集合并)。这时,姜神犇说:可以采用线段树来优化并查集。没错,我们只需要让左端点在(x1,y1)中所有线段都合并到当前的并查集中,再把x1放入线段树就行了。具体实现略有些复杂,不过姜神毕竟是神,吾等蒟蒻也只有%姜神的代码的份了~

    姜神的代码:

    #include<iostream>
    #include<cstring>
    #include<ctime>
    #include<algorithm>
    #include<iomanip>
    #include<cstdlib>
    #include<cstdio>
    #include<cmath>
    #include<vector>
    #include<queue>
    #include<stack>
    #include<bitset>
    #include<set>
    #include<map>
    using namespace std;
    #define MAXN 100010
    #define MAXM 1010
    #define eps 1e-8
    #define ll long long
    #define MOD 1000000007
    #define INF 1000000000
    struct line{
    	int x;
    	int y;
    	friend bool operator <(const line &x,const line &y){
    		return x.y<y.y;
    	}
    };
    int n;
    line a[MAXN];
    int sum[MAXN<<3];
    int ch[MAXN<<3];
    int bel[MAXN],f[MAXN],fv[MAXN];
    int fa(int x){
    	if(f[x]==x){
    		return x;
    	}
    	fa(f[x]);
    	fv[x]*=fv[f[x]];
    	f[x]=f[f[x]];
    	return f[x];
    }
    void add(int x,int y,int z,int p,int av){
    	sum[x]+=av;
    	if(y==z){
    		return;
    	}
    	int mid=y+z>>1;
    	if(p<=mid){
    		add(x<<1,y,mid,p,av);
    	}else{
    		add(x<<1|1,mid+1,z,p,av);
    	}
    }
    inline void toch(int x,int y){
    	if(!sum[x]){
    		return ;
    	}
    	if(!ch[x]){
    		ch[x]=y;
    	}else{
    		int t=ch[x];
    		int tt=fa(abs(t)),ty=fa(abs(y));
    		int t1=fv[abs(t)],t2=fv[abs(y)];
    		if(t<0){
    			t1*=-1;
    		}
    		if(y<0){
    			t2*=-1;
    		}
    		if(tt!=ty){
    			f[tt]=ty;
    			if(t1!=t2){
    				fv[tt]=-1;
    			}else{
    				fv[tt]=1;
    			}
    		}else{
    			if(t1!=t2){
    				printf("0
    ");
    				exit(0);
    			}
    		}
    	}
    }
    inline void pd(int x){
    	if(ch[x]){
    		toch(x<<1,ch[x]);
    		toch(x<<1|1,ch[x]);
    		ch[x]=0;
    	}
    }
    void change(int x,int y,int z,int l,int r,int cv){
    	if(!sum[x]){
    		return ;
    	}
    	if(y==l&&z==r){
    		toch(x,cv);
    		return ;
    	}
    	pd(x);
    	int mid=y+z>>1;
    	if(r<=mid){
    		change(x<<1,y,mid,l,r,cv);
    	}else if(l>mid){
    		change(x<<1|1,mid+1,z,l,r,cv);
    	}else{
    		change(x<<1,y,mid,l,mid,cv);
    		change(x<<1|1,mid+1,z,mid+1,r,cv);
    	}
    }
    int ask(int x,int y,int z,int p){
    	if(y==z){
    		if(!ch[x]){
    			return 0;
    		}
    		int t=fa(abs(ch[x]));
    		if(ch[x]>0){
    			return t*fv[abs(ch[x])];
    		}else{
    			return t*fv[abs(ch[x])]*-1;
    		}
    	}
    	pd(x);
    	int mid=y+z>>1;
    	if(p<=mid){
    		return ask(x<<1,y,mid,p);
    	}else{
    		return ask(x<<1|1,mid+1,z,p);
    	}
    }
    int main(){
    	int i;
    	scanf("%d",&n);
    	for(i=1;i<=n;i++){
    		scanf("%d%d",&a[i].x,&a[i].y);
    	}
    	sort(a+1,a+n+1);
    	for(i=1;i<=n;i++){
    		add(1,1,n<<1,a[i].x,1);
    	}
    	for(i=1;i<=n;i++){
    		int t=ask(1,1,n<<1,a[i].x);
    		add(1,1,n<<1,a[i].x,-1);
    		if(!t){
    			f[i]=i;
    			fv[i]=1;
    		}else{
    			f[i]=abs(t);
    			fv[i]=abs(t)/t;
    		}
    		change(1,1,n<<1,a[i].x,a[i].y,i*-1);
    	}
    	int ans=1;
    	for(i=1;i<=n;i++){
    		if(f[i]==i){
    			(ans*=2)%=MOD;
    		}
    	}
    	printf("%d
    ",ans);
    	return 0;
    }

    本人的代码:

    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <utility>
    #include <algorithm>
    #define mp(A,B) make_pair(A,B)
    #define lson x<<1
    #define rson x<<1|1
    #define F first
    #define S second
    using namespace std;
    typedef pair<int,int> pii;
    const int maxn=200010;
    int n,ans;
    int fa[maxn],d[maxn],vis[maxn],sum[maxn<<2];
    pii s[maxn<<2],p[maxn];	//有必要说明一下s的意义,只有叶子节点的s记录的是区间中那个位置的线段与父亲的关系,其余的都是lazy标记
    bool cmp(pii a,pii b)
    {
    	return a.S>b.S;
    }
    int find(int x)
    {
    	if(fa[x]!=x)
    	{
    		int tmp=fa[x];
    		fa[x]=find(fa[x]);
    		d[x]^=d[tmp];
    	}
    	return fa[x];
    }
    void link(int x,pii y)	//修改函数,也是用线段树维护并查集的核心函数
    {
    	if(!sum[x])	return ;
    	if(!s[x].F)	//如果没有标记,打一个
    	{
    		s[x].F=y.F,s[x].S=y.S;
    		return ;
    	}
    	int tmp;
    	tmp=s[x].F,s[x].F=find(s[x].F),s[x].S^=d[tmp];	//注意:只有在pushdown或访问到目标节点时才进行路径压缩!
    	tmp=y.F,y.F=find(y.F),y.S^=d[tmp];
    	if(s[x].F==y.F&&s[x].S==y.S)	return ;
    	if(s[x].F==y.F&&s[x].S!=y.S)	//如果产生冲突,exit
    	{
    		printf("0
    ");
    		exit(0);
    	}
    	if(s[x].F!=y.F)	//否则合并并查集
    	{
    		d[s[x].F]=s[x].S^y.S,fa[s[x].F]=y.F;
    		s[x].F=y.F,s[x].S=y.S;
    	}
    }
    void pushdown(int x)
    {
    	if(s[x].F)
    	{
    		link(lson,s[x]),link(rson,s[x]);
    		s[x]=mp(0,0);
    	}
    }
    void add(int l,int r,int x,int a,pii y)
    {
    	sum[x]++;	//sum用来判断当前位置是否有值,如果没有的话显然就不用打标记了
    	if(l==r)
    	{
    		link(x,y);
    		return ;
    	}
    	pushdown(x);
    	int mid=l+r>>1;
    	if(a<=mid)	add(l,mid,lson,a,y);
    	else	add(mid+1,r,rson,a,y);
    }
    void updata(int l,int r,int x,int a,int b,pii y)
    {
    	if(!sum[x])	return ;
    	if(a<=l&&r<=b)
    	{
    		link(x,y);
    		return ;
    	}
    	pushdown(x);
    	int mid=l+r>>1;
    	if(a<=mid)	updata(l,mid,lson,a,b,y);
    	if(b>mid)	updata(mid+1,r,rson,a,b,y);
    }
    void dfs(int l,int r,int x)	//最后要整体pushdown一次
    {
    	if(l==r)	return ;
    	pushdown(x);
    	int mid=l+r>>1;
    	dfs(l,mid,lson),dfs(mid+1,r,rson);
    }
    int main()
    {
    	scanf("%d",&n);
    	int i,t;
    	pii tmp;
    	for(i=1;i<=n;i++)	scanf("%d%d",&p[i].F,&p[i].S);
    	sort(p+1,p+n+1,cmp);
    	for(i=1;i<=n;i++)	fa[i]=i;
    	for(i=1;i<=n;i++)
    	{
    		updata(1,n<<1,1,p[i].F,p[i].S,mp(i,1));
    		add(1,n<<1,1,p[i].F,mp(i,0));
    	}
    	dfs(1,n<<1,1);
    	for(ans=i=1;i<=n;i++)	if(find(i)==i)	ans=(ans<<1)%1000000007;
    	printf("%d",ans);
    	return 0;
    }

    到这里,我们构建二分图的思路就又多了一个:用线段树维护带权并查集。虽说实现过程十分困难,但是二者的核心思想是差不多的:用各种优化,将二分图构建出来。不过,看了大爷的题解才发现还有另一种做法:

    “对于每个区间,线段树上将Yj的位置设为Xj,查询时广搜,在线段树上每次找[Xi,Yi]区间的最小值,若这个最小值小于Xi则将其在线段树上删除并加入队列。

    “将区间按照X排序,依次在线段树上将Y位置涂上自己的颜色,查询时若[Xi,Yi]中有与自己同色的位置则不是二分图。”

    如果你能看懂的话,你会发现其实上面的做法已经很接近真正的双栈排序的做法了;如果看不懂的话,请再看下一道题:

    【BZOJ2080】[Poi2010]Railway

    传送门

    简洁的原题面一个铁路包含两个侧线1和2,左边边由A进入,右边由B出去(看下面的图片)

    有n个车厢在通道A上,编号为1到n,它们被安排按照要求的顺序(a1,a2,a3,a4....an)进入侧线,进去还要出来,它们要按照编号顺序(1,2,3,4,5。。。。n)从通道B出去。他们从A到1或2,然后经过一系列转移从B出去,不用考虑容量问题。如果可以按照编号顺序到通道B,则输出两行,第一行为TAK,第二行为n个由空格隔开的整数,表示每个车厢进入的侧线编号(1,2)。要求输出字典序最小的方案。否则输出NIE。

    P.S.:一开始我以为这道题用线段树优化并查集搞搞也能过,结果naive了~

    分析:这道题看似限制条件变少了,但是用并查集却不那么容易了。因为并查集的核心依旧是:利用各种优化,将二分图建出来。但是我们不妨思考一下,这个二分图真的有建出来的必要吗?

    答案是:没有必要。因为如果我们能够快速判断出两个点在二分图中是否连有边,那么我们就没必要 真的 在图中连上这一条边。(难以理解?)因为你可以想象,我们判断二分图是否能染色的方法是找奇环,但是我们并不需要n2那么多的边才能找出这样的奇环,我们只需要找出一个判定条件,使得满足这个条件的点对之间都连有边就行了。下面我们来寻找这个判定条件。

    思考:当i,j满足什么条件时,i,j不能在同一个栈中?

    1.假设i<j,若Ai<Aj,那么i必须先进先出,j后进后出,但是如果存在一个k,满足i<j<k且Ak<Ai<Aj,那么显然会产生冲突,所以i,j不能在同一个栈中;反之,若不存在这样的k,那么易证i,j一定是可以在同一个栈中的。

    2.若i<j且Ai>Aj,那么我们完全可以让i先进后出,j后进先出。但这种情况下是否我们也能通过找出一些k,使得i,j不能在同一个栈中呢?如果仔细想想的话,发现这样的k一定满足了假设1中的条件。这里不必重复讨论。

    所以,i,j不再同一个栈中的充要条件就是:存在i<j<k且Ak<Ai<Aj,那么我们能否找出一种快速的方法,使我们能够快速知道:对于每个i,有哪些j与它连有边呢?

    这时,我们就要换一个角度了,我们对于每个i,可以贪心的找出最右面的k,使得Ak<Ai。我们设这样的k为Ci。那么对于(i,Ci)中的所有满足Aj>Ai的j,它们与i肯定都连有一条边。我们不妨将这样的边称之为后向边。

    但是从i连出去的边不只有后向边,肯定还存在一些j,并且j有一条后向边连向了i。即j<i且存在k使得j<i<k且Ak<Aj<Ai。我们依旧采用贪心的思路,我们找出ak最小的k,满足i<k,设这样的k为Bi。那么,对于所有的Aj∈(Bi,Ai)且满足j<i的j,它们与i肯定也都连有一条边。我们称这样的边为前向边。

    (前向边,后向边的定义十分重要,没看懂的请先动动笔列一下式子理解清楚!)

    说到这里,我们自然而然的就得出了本题的标准算法(或许你看上面大爷的题解就已经看出来标算了)。标算的核心就是用线段树快速找出前向边和后向边,然后利用BFS不断给所有节点进行染色。

    1.若Ai=j,设Pj=i。先O(n)扫两遍,求出每个i对应的Bi,Ci,然后构建两棵线段树,一棵为位置(P)的线段树,维护权值(A)的最大值;一棵为权值(A)的线段树,维护位置(P)的最小值。

    2.我们从1到n枚举所有的点,若当前点i没有被访问过,从i开始进行BFS,每次找出当前队首元素的所有的前向边和后向边,将这些边指向的点全都删除(在两棵线段树上都要删除),并将其染色,再压入队列。

    找前向边的方法:在权值线段树上不断找出[Bi,Ai]中编号最小的点j,并且满足j<i
    找后向边的方法:在位置线段树上不断找出[i,Ci]中权值最大的点j,并且满足Aj>Ai

    3.当所有点都染完色后,手动模拟一下双栈排序的过程,判断此方案是否可行。

    代码:

    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <queue>
    #define lson x<<1
    #define rson x<<1|1
    using namespace std;
    const int maxn=100010;
    const int inf=0x3f3f3f3f;
    struct sag
    {
    	int s[maxn<<2],flag;
    	int btr(int a,int b)
    	{
    		if(flag)	return max(a,b);
    		else	return min(a,b);
    	}
    	void updata(int l,int r,int x,int a,int b)
    	{
    		if(l==r)
    		{
    			s[x]=b;
    			return ;
    		}
    		int mid=l+r>>1;
    		if(a<=mid)	updata(l,mid,lson,a,b);
    		else	updata(mid+1,r,rson,a,b);
    		s[x]=btr(s[lson],s[rson]);
    	}
    	int query(int l,int r,int x,int a,int b)
    	{
    		if(a>b)	return (flag^1)*inf;
    		if(a<=l&&r<=b)	return s[x];
    		int mid=l+r>>1;
    		if(b<=mid)	return query(l,mid,lson,a,b);
    		if(a>mid)	return query(mid+1,r,rson,a,b);
    		return btr(query(l,mid,lson,a,b),query(mid+1,r,rson,a,b));
    	}
    }s1,s2;
    int n;
    int A[maxn],B[maxn],C[maxn],pos[maxn],st[maxn][2],top[2],vis[maxn];
    queue<int> q;
    void bfs(int x)
    {
    	q.push(x),vis[x]=0,s1.updata(1,n,1,x,0),s2.updata(1,n,1,A[x],inf);
    	int t;
    	while(!q.empty())
    	{
    		x=q.front(),q.pop();
    		while(1)
    		{
    			t=s1.query(1,n,1,x,C[A[x]]-1);
    			if(t>A[x])
    			{
    				vis[pos[t]]=vis[x]^1,q.push(pos[t]);
    				s1.updata(1,n,1,pos[t],0),s2.updata(1,n,1,t,inf);
    			}
    			else	break;
    		}
    		while(1)
    		{
    			t=s2.query(1,n,1,B[x]+1,A[x]);
    			if(t<x)
    			{
    				vis[t]=vis[x]^1,q.push(t);
    				s1.updata(1,n,1,t,0),s2.updata(1,n,1,A[t],inf);
    			}
    			else	break;
    		}
    	}
    }
    int main()
    {
    	scanf("%d",&n);
    	s1.flag=1,s2.flag=0;
    	int i,j,t;
    	for(i=1;i<=n;i++)
    	{
    		scanf("%d",&A[i]),pos[A[i]]=i;
    		s1.updata(1,n,1,i,A[i]),s2.updata(1,n,1,A[i],i);
    	}
    	for(B[n]=n,i=n-1;i>=1;i--)	B[i]=min(A[i+1],B[i+1]);
    	for(C[1]=1,i=2;i<=n;i++)	C[i]=max(pos[i-1],C[i-1]);
    	memset(vis,-1,sizeof(vis));
    	for(i=1;i<=n;i++)	if(vis[i]==-1)	bfs(i);
    	for(i=j=1;i<=n;i++)
    	{
    		t=vis[i],st[++top[t]][t]=A[i];
    		for(;st[top[0]][0]==j||st[top[1]][1]==j&&j<=n;j++)
    		{
    			if(st[top[0]][0]==j)	top[0]--;
    			else	top[1]--;
    		}
    	}
    	if(j<=n)
    	{
    		printf("NIE");
    		return 0;
    	}
    	printf("TAK
    ");
    	for(i=1;i<n;i++)	printf("%d ",vis[i]+1);
    	printf("%d",vis[n]+1);
    	return 0;
    }

    总结:本文通过三道题,逐渐介绍了如何将一张现实的二分图通过各种优化(以至于到最后我们根本都没有建出这个图)来达到虚拟化的目的。或许略有繁琐,或许有些地方说的不到位,望见谅!

    感谢曾经给过我灵感的人:神犇GXZ,姜神犇,Po大爷

    By:CQzhangyu

  • 相关阅读:
    IP地址
    ACL访问控制列表
    DHCP原理及配置
    VRRP原理及配置
    ASP.NET CORE RAZOR :向 Razor 页面应用添加模型
    HBuilder + PHP开发环境配置
    添加相关功能
    基于stm32的水质监测系统项目基础部分详细记录
    表单数据验证方法(二)——ASP.NET后台验证
    表单数据验证方法(一)—— 使用validate.js实现表单数据验证
  • 原文地址:https://www.cnblogs.com/CQzhangyu/p/7079577.html
Copyright © 2011-2022 走看看