【EA的练习赛2】【洛谷P7274】草地(单调栈,LCT维护最小生成树)

学到了很多。

我们分步走。

首先在做这道题前先观察到几个小性质:

  1. 操作顺序不同不影响结果

    发现对于每一个黑点,一通操作过后它扩展出的区域是一个矩形,而操作顺序是不影响这个矩形的大小和位置的。

  2. 最后的要求 “任意两个黑色格子八连通” 等价于 “一开始图中的黑色格子八连通”。

    这是显然的,因为对于一个黑点,它扩展出来的格子是个矩形,肯定八连通,所以我们只需要求一开始图中给定的黑点在最后八连通就好了。

    那么对于两个黑点,操作它们同时往左扩展一格,可以看作是让这两个黑点的水平距离减 \(1\)

    不妨把八连通分为直接八连通和间接八连通两种,那么这两个黑点直接八连通就可以看成当两个黑点的水平和竖直距离都小于等于 \(1\) 时,间接八连通就是有一个黑点同时与它们八连通时。

  3. 往左/右是等效的,往上/下同理

    对于两个黑点,你操作它们同时往左扩展或同时往右扩展,出现的结果都是这两个黑点的水平间距减 \(1\),所以是等效的。

    我们不妨设 \(L\) 表示左/右操作的个数,\(U\) 表示上/下操作的个数,那么在这 \(L+U\) 次操作后,所有点的水平距离减小 \(L\),竖直距离减小 \(U\),现在要求使得所有黑点八连通的 \(L+U\) 的最小值。

考虑两个黑点 \((x_1,y_1)\)\((x_2,y_2)\) 什么时候直接八连通。显然,当 \(|x_1-x_2|\leq L+1\)\(|y_1-y_2|\leq U+1\) 时,它们才直接八连通。

不妨将黑点间两两建边,其中一条边包含两个关键字,第一关键字为 \(a=|x_1-x_2|\),第二关键字为 \(b=|y_1-y_2|\)。不妨将这个图命名为 \(A\)

假设 \(L\) 已经固定了,要求最小的 \(U\)。那么我们现在只能保留第一关键字 \(a\leq L+1\) 的边,建成一个图,不妨称其为 \(B\)。然后我们现在要找到一个最小的 \(U\),使得在 \(B\) 图的基础上,只保留第二关键字 \(b\leq U+1\) 的边也能使整个图联通。

也就是说让你在 \(B\) 图中选出一些边,使得只保留这些边的图仍然联通而且这些边的第二关键字 \(b\) 的最大值最小。

容易发现这就是求这个图在边以 \(b\) 为权值的情况下的最小生成树的最大边权,因为最小生成树不仅满足边之和最小,而且满足最大边也是最小的。(可以联系 kruskal 的实现过程简单证明)

但是你发现如果这样连,边数可能达到 \((nm)^4\),是无法接受的。

于是考虑如何如何简化连边。

由于边的权值定义只与点的坐标有关,所以会发现下面这么一种情况:

在这里插入图片描述

明显地,我们连不连 \((A,C)\) 这条边都没有关系,因为边 \((A,B)\) 和边 \((B,C)\) 的第一关键字(水平距离)和第二关键字(竖直距离)都比边 \((A,C)\) 的小,而且连了边 \((A,B)\)\((B,C)\) 就已经能使 \(A\)\(C\) 八连通。

因此,一般地,对于任意两个黑点 \(A\)\(C\),以 \(A\)\(C\) 为对角作矩形(矩形的边水平或竖直),如果这个矩形中(包含矩形的边)包含另一个黑点 \(B\),那么 \((A,C)\) 这条边我们就可以不用连,因为连 \((A,B)\)\((B,C)\) 就够了。

那么简化过这个图后,边数最多能达到多少呢?

考虑构造最大的边数。

不妨设当前图中已经有若干黑点了,这些黑点构成的集合为 \(S\),那么如果我再往这个集合里加入一个黑点 \(B\),那么 \(B\) 肯定要和其它黑点连边,或者说满足了上面的化简条件,把某条边 \((A,C)\) 去掉,增加边 \((A,B)\)\((B,C)\)。但不论怎样总边数都是会增加的。

那么我们得到一个结论:总点数越多,总边数越多。

那么当整个网格图都是黑点时,总边数取到最大值,此时总边数为 \((n-1)m+(m-1)n=2nm-n-m\),是 \(nm\) 级别的。

发现这样是可行的,但简化过程如何实现?

如果把图建出来再简化,边的级别是 \((nm)^2\) 级别的,不能接受。

所以考虑直接找简化后剩下的边。

不妨把简化后的边分成两类:斜右向下的和斜左向下的(可以把横着的和竖着的归到其中一类去)。

以找斜右向下的边为例:

对于一个点 \(A\),我们找以它为起点的斜右向下的边的终点有哪些。

发现其实就是要维护 \(A\) 右下角的一段以黑点构成的最高的纵坐标单增点集,类似维护一个反比例函数 \(y=\dfrac{k}{x}(k<0)\) 的第四象限“”部分:

在这里插入图片描述

(其中红点为 \(A\),蓝点是我们要维护的点)

那么终点就是这些蓝色的点。

所以我们用一个单调栈来维护这些点就好了,具体实现过程详见代码。

那么斜左向下的边也同理。

简化的时间复杂度是 \(O(nm)\) 的,可以接受。

接下来的问题就是如何找到合适的 \(L\)\(U\) 了。

首先蹦出来的想法是二分 \(L\),但 \(L\) 越大,能考虑的边就越多,\(U\) 就越小,而我们要求的是 \(L+U\) 的最小值,所以不能二分。

所以只能枚举 \(L\),那么我们就需要找出第一关键字 \(a\leq L\) 的边,然后求它们以第二关键字 \(b\) 为权值的最小生成树。

如果直接跑 kruskal 的时间是 \(O(nm\log (nm))\),再乘上个枚举 \(L\) 的时间就是 \(O(n^2m\log(nm))\) 了,不能接受。

假设你现在枚举到 \(L\),你发现从 \(L-1\)\(L\) 过程中需要考虑的边只是多了几条而已,而 \(L\)\(1\)\(m\) 的过程中边数最多只会增加 \(nm\) 条,所以考虑动态维护最小生成树。

于是自然而然地就联想到 LCT 了。

用 LCT 动态维护最小生成树的具体过程是:假设当前生成树为 \(T\),新加入的边为 \((u,v)\),边权为 \(x\)。那么在当前生成树 \(T\)\(u\)\(v\) 的路径上,找到边权最大的边,设其为 \((a,b)\),边权为 \(y\)。如果 \(x<y\),我们就把边 \((a,b)\) 删掉,加入边 \((u,v)\)

但是还有两个细节的地方:

  1. LCT 如何维护边权?

    最简单也最直接的方法就是把每一条边拆成一个点,然后维护点权。

    还有一种神奇的方法,具体可以参考这篇博客

    当然无论哪种方法,时间复杂度都不会变大。

  2. 如何维护全局边权最大值?

    用 LCT 维护也不是不可以,具体做法可以参考上面的那篇讲维护边权的博客,它也有讲如何维护子树信息。

    但更暴力的做法是直接用一个 multiset维护全局的边权,LCT 删边的时候在 multiset里面 \(\operatorname{erase}\),加边的时候直接在 multiset里面 \(\operatorname{insert}\)

    总感觉让 LCT 和 set 同时跑有点浪费。

所有的细节也就处理完了,最后的时间复杂度是 \(O(nm \log (nm))\)

感觉这道题涉及的面挺广的,也很毒瘤。

具体代码如下:

#include<bits/stdc++.h>

#define N 1010
#define INF 0x7fffffff
#define lc(u) (t[u].ch[0])
#define rc(u) (t[u].ch[1])

using namespace std;

struct Point
{
	int x,y;
	Point(){};
	Point(int xx,int yy){x=xx,y=yy;}
}sta[N];

struct Edge
{
	int u,v,x,y;//x表示第二关键字,y表示第一关键字
	Edge(){};
	Edge(int uu,int vv,int xx,int yy){u=uu,v=vv,x=xx,y=yy;}
}e[N*N*2];

struct data
{
	int l,u,r;
	data(){};
	data(int a,int b,int c){l=a,u=b,r=c;}
};

struct Splay
{
	int ch[2],fa,val,maxn;
	data p,maxp;//maxp存的是最大边对应点的编号,以及这条边的两个端点的编号
	bool rev;
}t[N*N*3];

int st[N*N*3];
int n,m,node,top,tot,sumb;
int added,ans=INF;
int id[N][N],nxtl[N][N],nxtr[N][N];
bool vis[N][N];

multiset<int>s;

bool cmp(Edge vis,Edge b)
{
	return vis.y<b.y;
}

bool notroot(int u)
{
	return lc(t[u].fa)==u||rc(t[u].fa)==u;
}

bool get(int u)
{
	return rc(t[u].fa)==u;
}

void up(int u)
{
	t[u].maxn=max(t[u].val,max(t[lc(u)].maxn,t[rc(u)].maxn));
	if(t[u].maxn==t[u].val) t[u].maxp=t[u].p;
	else if(t[u].maxn==t[lc(u)].maxn) t[u].maxp=t[lc(u)].maxp;
	else t[u].maxp=t[rc(u)].maxp;
}

void downn(int u)
{
	swap(lc(u),rc(u));
	t[u].rev^=1;
}

void down(int u)
{
	if(t[u].rev)
	{
		downn(lc(u)),downn(rc(u));
		t[u].rev=0;
	}
}

void rotate(int u)
{
	int fa=t[u].fa,gfa=t[fa].fa;
	bool d1=get(u),d2=get(fa);
	int sonu=t[u].ch[d1^1];
	if(notroot(fa)) t[gfa].ch[d2]=u;
	t[u].fa=gfa;
	t[u].ch[d1^1]=fa;
	t[fa].fa=u;
	t[fa].ch[d1]=sonu;
	t[sonu].fa=fa;
	up(fa);
}

void splay(int u)
{
	int now=u,top=0;
	st[++top]=now;
	while(notroot(now)) st[++top]=now=t[now].fa;
	while(top) down(st[top--]);
	while(notroot(u))
	{
		int fa=t[u].fa;
		if(notroot(fa))
		{
			if(get(u)==get(fa)) rotate(fa);
			else rotate(u);
		}
		rotate(u);
	}
	up(u);
}

void access(int u)
{
	for(int y=0;u;y=u,u=t[u].fa)
	{
		splay(u);
		rc(u)=y;
		up(u);
	}
}

void makeroot(int u)
{
	access(u);
	splay(u);
	downn(u);
}

int findroot(int u)
{
	access(u);
	splay(u);
	while(lc(u))
	{
		down(u);
		u=lc(u);
	}
	splay(u);
	return u;
}

void link(int x,int y)
{
	makeroot(x);
	t[x].fa=y;
}

void cut(int x,int y)
{
	makeroot(x);
	findroot(y);
	t[y].fa=rc(x)=0;
	up(x);
}

void work(Edge a)
{
	++node;
	t[node].maxn=t[node].val=a.x;
	t[node].maxp=t[node].p=data(a.u,node,a.v);
	makeroot(a.u);
	if(findroot(a.v)!=a.u)//判断u和v不连通
	{
		added++;
		s.insert(a.x);
		link(a.u,node),link(node,a.v);
		return;
	}
	if(t[a.u].maxn>a.x)
	{
		s.erase(s.find(t[a.u].maxn));
		s.insert(a.x);
		int l=t[a.u].maxp.l,u=t[a.u].maxp.u,r=t[a.u].maxp.r;
		cut(l,u),cut(u,r);
		link(a.u,node),link(node,a.v);
		return;
	}
}

int main()
{
	t[0].maxn=t[0].val=-INF;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		char s[N];
		scanf("%s",s+1);
		for(int j=1;j<=m;j++)
		{
			vis[i][j]=(s[j]=='1');
			if(vis[i][j])
			{
				id[i][j]=++node;
				t[node].maxn=t[node].val=-INF;
			}
		}
	}
	sumb=node;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			if(vis[i][j-1]) nxtl[i][j]=j-1;//nxtl[i][j]表示点(i,j)同一行左边最近的黑点在哪里
			else nxtl[i][j]=nxtl[i][j-1];
		}
		for(int j=m;j>=1;j--)
		{
			if(vis[i][j+1]) nxtr[i][j]=j+1;//nxtr的定义与nxtl同理
			else nxtr[i][j]=nxtr[i][j+1];
		}
	}
    //斜右向下的边
	for(int j=1;j<=m;j++)
	{
		top=0;//通过单调栈维护
		for(int i=n;i>=1;i--)
		{
			if(nxtr[i][j])//每次找到(i,j)右边最近的黑点,并把它加入到单调栈内
			{
				while(top&&sta[top].y>=nxtr[i][j]) top--;
				sta[++top]=Point(i,nxtr[i][j]);
			}
			if(vis[i][j])
			{
				while(top)
				{
					e[++tot]=Edge(id[i][j],id[sta[top].x][sta[top].y],abs(sta[top].x-i),abs(sta[top].y-j));//当前点同栈内的所有点连边,并把栈内的点删除
					top--;
				}
				sta[++top]=Point(i,j);//重新加入当前点
			}
		}
	}
    //斜左向下的边同理
	for(int j=m;j>=1;j--)
	{
		top=0;
		for(int i=n;i>=1;i--)
		{
			if(nxtl[i][j])
			{
				while(top&&sta[top].y<=nxtl[i][j]) top--;
				sta[++top]=Point(i,nxtl[i][j]);
			}
			if(vis[i][j])
			{
				while(top)
				{
					if(sta[top].x!=i&&sta[top].y!=j) e[++tot]=Edge(id[i][j],id[sta[top].x][sta[top].y],abs(sta[top].x-i),abs(sta[top].y-j));//这里加判断是因为横着的和竖着的边已经在第一类中讨论过了,所以不必再加一遍
					top--;
				}
				sta[++top]=Point(i,j);
			}
		}
	}
	if(!tot)//特判只有一个点(没有边)
	{
		puts("0");
		return 0;
	}
	sort(e+1,e+tot+1,cmp);
	int tmp=1;
	for(int L=0;L<=m;L++)//枚举L
	{
		while(tmp<=tot&&e[tmp].y<=L+1)//加入所有符合要求的边
		{
			work(e[tmp]);
			tmp++;
		}
		if(added==sumb-1)//如果能构成树(图联通)
			ans=min(ans,L+max(0,(*(--s.end()))-1));
	}
	printf("%d\n",ans);
	return 0;
}
/*
2 5
10001
01010
*/

常数挺大的(特别是拆点和加了 multiset以后)。

但还是因为很少人交这道题跑到了洛谷最优解。

posted @ 2022-10-28 20:39  ez_lcw  阅读(44)  评论(0)    收藏  举报