首页 > 其他分享 >【EA的练习赛2】【洛谷P7274】草地(单调栈,LCT维护最小生成树)

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

时间:2022-10-28 20:46:14浏览次数:95  
标签:LCT 练习赛 洛谷 nm 关键字 int top 连通 黑点

学到了很多。

我们分步走。

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

  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以后)。

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

标签:LCT,练习赛,洛谷,nm,关键字,int,top,连通,黑点
From: https://www.cnblogs.com/ez-lcw/p/16837420.html

相关文章

  • 洛谷P3391 【模板】文艺平衡树
    题目描述您需要写一种数据结构(可参考题目标题),来维护一个有序数列。其中需要提供以下操作:翻转一个区间,例如原有序序列是5 4 3 2 1,翻转区间是[2,4] 的话,结果是5 2 ......
  • 洛谷P8805题解
    原题P8805[蓝桥杯2022国B]机房思路概述题意分析给定一个\(n\)个点的无根树,每个点的权值等于其出边数量。对于给定的\(m\)组询问,第\(i(1≤i≤n)\)组询问包......
  • 洛谷 P6294
    首先有一个显而易见的性质:每次取都是取最大的一个数。然后就变成了加数,取最大值,加数,取最大值。。。直接单走一个优先队列(但是很明显这个数据不打算把优先队列放过去。......
  • 洛谷 P1077 [NOIP2012 普及组] 摆花 (DP)
    https://www.luogu.com.cn/problem/P1077题目描述摆上m盆花。一共有n种花,从1到n标号。为了在门口展出更多种花,规定第i种花不能超过ai盆,摆花时同一种花放在一起,且不同......
  • 洛谷 P6963
    不难发现,包含关系只可能是短的路径被长的路径包含。那么我们考虑按照路径长度从小到大,一条一条路径边加入边判断。考虑先将树上的所有边断开,每加入一条路径的时候就将这......
  • 洛谷 P7023
    首先可以发现一些有用的性质:每个数至多操作一次如果一个数,在原数列中有它的倍数,那么改变成那个数一定是最优的。否则可以改变成所有数的最小公倍数。贪心的,按出现次数......
  • 洛谷P3225 [HNOI2012]矿场搭建
    SLOJH7136.「HNOI2012」矿场搭建题目描述煤矿工地可以看成是由隧道连接挖煤点组成的无向图。为安全起见,希望在工地发生事故时所有挖煤点的工人都能有一条出路逃到救援......
  • 洛谷 P8595 「KDOI-02」一个网的路 蓝 题解
    定义树形\(DP\),这个挺明显的,我认为\(DP\)让读者理解的最重要的一步是:定义。所以我先详细说明\(f\)数组的定义,至于为什么这么定义后面再讲。\(f_{u,type}\),其中\(......
  • 洛谷 P3592
    首先不难发现最终答案中只会出现\(c_i\)中的数,所以可以将\(c_i\)离散化。设\(f_{i,j,k}\)表示区间\([l,r]\),最小值不小于\(k\)的最大收益,\(cnt_{i,j}\)表示区间......
  • 洛谷P1021题解
    原题P1021[NOIP1999提高组]邮票面值设计思路概述题意分析给定两个整数\(N,K(N+K≤15)\),设计\(K\)种邮票面值\(\{a_1,a_2\dotsa_K\}\),并用共\(N\)张上述邮票......