首页 > 其他分享 >耳分解、双极定向和 P9394 Solution

耳分解、双极定向和 P9394 Solution

时间:2024-03-07 11:45:45浏览次数:39  
标签:连通 双极 int Solution back P9394 maxn low push

耳分解

设无向图 \(G'(V',E')\subset G(V,E)\),简单路径或简单环 \(P:x_1\to \dots \to x_k\) 被称为 \(G\) 关于 \(G'\) 的耳,当且仅当其满足 \(x_1,x_k\in V',x_2,x_3\dots x_{k-1}\not\in V'\)。如果 \(P\) 是简单路径,那么 \(P\) 称为开耳。

下面记树上 \(x,y\) 之间的路径为 \(P(x,y)\)。

一个无向连通图的一个连通子图序列 \((G_0,G_1,\dots G_k),G_i(V_i,E_i)\) 满足:

  • \(G_0\) 是简单环。
  • \(G_{i-1}\subset G_i\)。
  • \(E_i-E_{i-1}\) 是 \(G_{i-1}\) 关于 \(G_i\) 的一个耳(开耳)。

那么称 \((G_0,G_1,\dots G_k)\) 是一个 \(G\) 的耳分解(开耳分解)。

无向连通图 \(G\) 存在耳分解当且仅当其边双连通,\(G\) 存在开耳分解当且仅当其点双连通。

证明:

只证明前一个。开耳分解类似。

耳分解 \(\Rightarrow\) 边双连通。显然,增加一个耳不会改变边双连通性。

边双连通 $\Rightarrow $ 耳分解。这就是耳分解的构造算法。

  • 先求出 \(1\) 为根的 dfs 树(不存在横叉边!!)。找到一个非树边 \(1\to x\)。初始 \(G_0\) 设为 \(1\to x\) 和 \(P(1,x)\) 构成的简单环。
  • 设已经构造了 \(G_i\),找到一个点 $x $ 的父亲 \(y\) 在 \(G_i\) 中,自己不在。找到他子树的返祖边 \(v\to u\),那么当前的耳就是 \(P(y,v)\cup (v\to u)\)。
  • 重复上述过程直到点集为 \(V\),剩下的一个边就是一个耳。

双极定向

对于无向图 \(G(V,E)\) 和 \(s,t\in V,s\neq t\)。以下四个命题等价:

  • 添加 \(s\to t\) 后 \(G\) 点双连通。
  • 圆方树上的所有方点成链。并且 \(P(s,t)\) 是圆方树的直径。
  • 存在 DAG \(G'\) 以 \(G\) 为基图,且 \(s\) 是唯一入度为 \(0\) 的点,\(t\) 是唯一出度为 \(0\) 的点。
  • 存在 \(p\in S_n\),\(p_1=s,p_n=t\),任意前缀后缀的导出子图连通。

\(1,2\) 显然等价。

\(3\Rightarrow 4\):取定向后的拓扑排序即可。

\(4\Rightarrow 3\):当作拓扑排序定向即可。

\(1\Rightarrow 3\):在加开耳的过程中搞一下即可。

\(4\Rightarrow 1\):设存在(加边后)割点 \(u\),此时 \(s,t\) 在同一连通块。设另一任意连通块为 \(S\)。设 \(S\cup \{u\}\) 在 \(p\) 上最早晚的分别是 \(x,y\),则 \(x,y\) 至少有一个不是 \(u\)。此时根据 \(4\),\(S\) 应该和 \(s,t\) 连通块连通,矛盾。

如何构造 \(p\)(这样也构造了双极定向)?跑出 dfs 树(这些过程中不考虑加的 \(s\to t\) 边),记录每个点的 low。把每个点 \(u\) 按照先 dfs 儿子再操作自己的顺序,如果 \(u\not\in P(s,t)\),把 \(low_u\) 和 \(fa_u\) 结点的队列添加进 \(u\)。

接下来按顺序遍历 \(P(s,t)\),遍历到一个点就把他加进 \(p\),然后递归遍历其队列元素(如果没有被遍历过)。具体可以参见代码。这样做的正确性是不难发现的。

Solution

题意:可以把若干个点缩成一个点(不限次数),使得上述条件 \(4\) 被满足。最小化缩之后的点包含原来的点的个数的最大值。输出方案。

依靠前面的结论,就是希望圆方树的方点是链。这样不妨先考虑知道 \(s,t\) 怎么做。在这样的链上,圆点必须被缩成一个点,方点连接到的(不在链上的)圆点必须被缩成一个点。对这些取 \(\max\) 就是要求的答案。

有结论:固定一个点 \(u\),另一个点 \(v\) 在其子树内选取的话,最优的 \(v\) 是不断走向任意重儿子得到的结果(笔者写代码的时候没有注意到任意)。这个结论正确,因为不走向重儿子,在 \(u\) 上的影响都已经大于任何儿子内部的所有贡献了。

这个结论主要说明了:我们可以进行类似于贪心的过程,不会发生走向轻儿子(非当前最优)会导致总结果更优的情况。

那么可以在树上进行 dp。先求出每个点一直向重儿子走的贡献 \(f\),枚举选取的 \(s,t\) 的 LCA,取其重儿子和次重(非严格)儿子更新答案即可。

求出 \(s,t\) 之后,按上面的构造方法来即可。

下面的代码是可以被精简的。

// Problem: P9394 白鹭兰
// Platform: Luogu
// URL: https://www.luogu.com.cn/problem/P9394
// Memory Limit: 512 MB
// Time Limit: 1000 ms
// Author:British Union
// Long live UOB and koala
//
// Powered by CP Editor (https://cpeditor.org)

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=5e5+5,INF=1e9;
int dfn[maxn],st[maxn],tp=0,T=0,cnt=0,low[maxn],n,m;
vector<int> e[maxn],e2[maxn];
void tarjan(int u){
	dfn[u]=low[u]=++T;
	st[++tp]=u;
	for(auto v:e[u]){
		if(!dfn[v]){
			tarjan(v);
			low[u]=min(low[u],low[v]);
			if(dfn[u]==low[v]){
				++cnt;
				for(int x=0;x!=v;--tp){
					x=st[tp];
					e2[x].push_back(cnt);
					e2[cnt].push_back(x);
				}
				e2[u].push_back(cnt);
				e2[cnt].push_back(u);
			}
		}else low[u]=min(low[u],dfn[v]);
	}
}
int sze[maxn],f[maxn],g[maxn],msze[maxn],msze2[maxn],d[maxn];
void dfs1(int u,int fa){
	sze[u]=(u<=n);
	for(auto v:e2[u])if(v!=fa)dfs1(v,u),sze[u]+=sze[v];
	for(auto v:e2[u]){
		if(v==fa)continue;
		if(sze[v]>msze[u])msze2[u]=msze[u],msze[u]=sze[v];
		else if(sze[v]>msze2[u])msze2[u]=sze[v];
	}
}
struct qsy{
	int a,b,c;
};
bool operator <(qsy x,qsy y){
	return make_pair(x.a,-x.b)<make_pair(y.a,-y.b);
}
void calcf(int u,int fa){
	bool cld=0;
	qsy ans={0,INF,-1};
	for(auto v:e2[u]){
		if(v==fa)continue;
		calcf(v,u);
		cld=1;
		if(u<=n)ans=max(ans,(qsy){sze[v],max(f[v],sze[u]-sze[v]),d[v]});
		else ans=max(ans,(qsy){sze[v],max(f[v],sze[v]==msze[u]?msze2[u]:msze[u]),d[v]});
	}
	if(cld==0)f[u]=sze[u],d[u]=u;
	else f[u]=ans.b,d[u]=ans.c;
}
int mn=INF,minx,miny;
void upd(int ans,int x,int y){
	if(x>n||y>n)return ;
	if(ans<mn)mn=ans,minx=x,miny=y;
}
int bcnt[maxn];
void dp(int u,int fa){
	qsy m1={0,INF,-1},m2={0,INF,-1};
	for(auto v:e2[u]){
		if(v==fa)continue;
		dp(v,u);
		qsy cur={sze[v],f[v],d[v]};
		if(m1<cur)m2=m1,m1=cur;
		else if(m2<cur)m2=cur;
	}
	int ans=0;
	if(m2.b==INF){
		if(m1.b==INF){return ;}
		else ans=m1.b;
	}else ans=max(m1.b,m2.b);
	bcnt[m1.a]++,bcnt[m2.a]++;
	int res=0;
	for(auto v:e2[u]){
		if(v==fa)continue;
		if(bcnt[sze[v]]>0)bcnt[sze[v]]--;
		else{
			if(u<=n)res+=sze[v];
			else res=max(res,sze[v]);
		}
	}
	if(u<=n)res+=n-sze[u]+1;
	else res=max(res,n-sze[u]);
	ans=max(ans,res);
	g[u]=ans;int X=m1.c,Y=m2.c;
	if(Y==-1)Y=u;
	upd(g[u],X,Y);
}
int ban1,ban2,c[maxn],K,C;
void dfs2(int u,int fa){
	st[++tp]=u;
	if(u==miny){
		K=tp;
		for(int i=1;i<=K;i++)c[i]=st[i];
		return ;
	}
	for(auto v:e2[u]){
		if(v==fa)continue;
		dfs2(v,u);
	}
	--tp;
}
vector<int> pt[maxn];
int bel[maxn];
void dfs3(int u,int fa){
	if(u==ban1||u==ban2)return ;
	if(u<=n)pt[cnt].push_back(u);
	bel[u]=cnt;
	for(auto v:e2[u]){
		if(v==fa)continue;
		dfs3(v,u);
	}
}
vector<int> e3[maxn],L[maxn],e4[maxn];
bool vis[maxn],vis2[maxn];
int Ans[maxn],len=0,hzc[maxn],mk=0,Fa[maxn],rev[maxn];
void tarjan2(int u){
	dfn[u]=low[u]=++T;
	rev[dfn[u]]=u;
	st[++tp]=u;
	if(u==miny)for(int i=1;i<=tp;i++)vis2[st[i]]=1,hzc[++mk]=st[i];
	for(auto v:e3[u]){
		if(!dfn[v]){
			e4[u].push_back(v);
			e4[v].push_back(u);
			Fa[v]=u;
			tarjan2(v);
			low[u]=min(low[u],low[v]);
		}else low[u]=min(low[u],dfn[v]);
	}
	--tp;
}
void dfs4(int u){
	if(vis[u])return ;
	Ans[++len]=u;
	vis[u]=1;
	for(auto v:L[u])dfs4(v);
}
namespace checker{
	vector<int> cur;
	bool vis[maxn],vis2[maxn];
	void dfs(int u){
		if(vis[u])return;
		vis[u]=1;
		for(auto v:e[u]){
			if(!vis2[v])continue;
			dfs(v);
		}
	}
	bool check(){
		for(int i=1;i<=n;i++)vis[i]=vis2[i]=0;
		for(auto u:cur)vis2[u]=1;
		dfs(cur[0]);
		for(auto u:cur)if(!vis[u])return 0;
		return 1;
	}
	void checkans(){
		for(int i=1;i<=cnt;i++){
			for(auto u:pt[Ans[i]])cur.push_back(u);
			if(!check())exit(1);
		}
		cur.clear();
		for(int i=cnt;i>=1;i--){
			for(auto u:pt[Ans[i]])cur.push_back(u);
			if(!check())exit(1);
		}
	}
}
void dfs5(int u,int fa){
	for(auto v:e4[u]){
		if(v==fa)continue;
		dfs5(v,u);
	}
	if(!vis2[u])L[Fa[u]].push_back(u),L[rev[low[u]]].push_back(u);
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v;cin>>u>>v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	cnt=n;
	for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i);
	dfs1(1,0);calcf(1,0);dp(1,0);
	tp=0;dfs2(minx,0);cnt=0;
	for(int i=1;i<=K;i++){
		ban1=c[i-1],ban2=c[i+1];
		if(c[i]<=n){
			C=++cnt;
			dfs3(c[i],0);
		}else{
			for(auto v:e2[c[i]]){
				if(v==ban1||v==ban2)continue;
				C=++cnt;
				dfs3(v,c[i]);
			}
		}
	}
	cout<<mn<<" "<<cnt<<endl;
	for(int i=1;i<=n;i++)for(auto j:e[i])if(bel[i]!=bel[j])e3[bel[i]].push_back(bel[j]);
	minx=bel[minx],miny=bel[miny],T=0;tp=0;
	memset(dfn,0,sizeof(dfn));
	memset(low,0,sizeof(low));
	tarjan2(minx);
	dfs5(minx,0);
	for(int i=1;i<=mk;i++)dfs4(hzc[i]);
	for(int i=1;i<=cnt;i++){
		cout<<pt[Ans[i]].size()<<" ";
		for(auto u:pt[Ans[i]])cout<<u<<" ";
		cout<<endl;
	}
	// checker::checkans();
	return 0;
}

标签:连通,双极,int,Solution,back,P9394,maxn,low,push
From: https://www.cnblogs.com/british-union/p/18058546/ear-decomposition-zhicheng

相关文章

  • solution-p1927
    题解P1927【防护伞】原题直接暴力枚举每一个点最后求面积最小值就好了代码//此处应有头文件constdoublepi=3.1415926535;intn;doubleans=1<<30;//2^30structnode{ intx,y;}s[1005];doubledis(inti,intj)//勾股定理{ doublea=abs(s[i]......
  • solution-p1532
    题解P1532【卡布列克圆舞曲】原题一道较难搞的模拟因为蒟蒻不会奇奇怪怪的STL所以都是手打的思路一个数组b存储操作过程中的数每次扫一遍判断是否开始循环如果循环:记录循环开始的位置k从k开始到总操作次数len-1(第len个循环了)输出b[i]否则:len++,记录当前数用于......
  • solution-cf877d
    题解CF877D【OlyaandEnergyDrinks】原题一道几乎板子的广搜题。(然而我调了10几次才过我们只需要在广搜板子的基础上添加移动$1-k$步的部分即可就像这样:inth[]={-1,1,0,0};intl[]={0,0,-1,1};for(inti=0;i<4;i++){ for(intj=1;j<=k;j......
  • solution-cf236b
    题解CF236B【EasyNumberChallenge】原题此题一个暴力就可以过了。看着别的大佬不加记忆化吸口氧就过了,而我的却死活TLE可能因为我人丑常数大?注意到i*j*k的值会出现重复,所以考虑记忆化。时间复杂度\(O(n^3\sqrtn)\),跑得飞快代码constintN=1e6+10,M=1073741824......
  • solution-cf120e
    题解CF120E【PutKnight!】原题我一开始以为这题\(n\)为奇数就是先手赢,偶数就是后手赢没想到还真是这样那么要怎么证明呢?一般地,在一个空棋盘上下出一枚棋,会有8个格子被这颗棋限制:XXXXKXXXX容易看......
  • solution-at4703
    题解AT4703【RedorBlue】原题来介绍一下三元运算符:A?B:C如果表达式A为真,则执行B语句,否则执行C语句。其作用就相当于:if(A){ B;}else{ C;}例如1+1>2?puts("IAKIOI"):puts("qwq");将会输出qwq。那么此题代码就可以变得极简。代码#include<iostr......
  • solution-at945
    题解AT945【高橋君とお肉】原题来一篇正经的题解QwQ显然我们要把肉分成耗费时间尽量平均的两堆。于是考虑二分答案那么怎么检测一个答案的正确性呢?我们可以跑一个背包dp,让第一个烤肉架烤尽可能多的肉,最后检测第二个烤肉架能不能烤完剩下的肉即可。时间复杂度\(O(nlog^2n)......
  • p7915-solution
    P7915Solutionlink考虑枚举第一个操作选L还是R。这样原序列就被分为了两个栈,用四个指针\(p1,p2,p3,p4\)分别指向这两个栈的栈顶栈底。感性理解一下,某一个栈的栈顶\(x\)可以被pop当且仅当某一个栈的栈底等于\(x\)。于是直接dfs,每次优先选L,同时确定第\(2n-i+1\)......
  • p7914-solution
    P7914Solutionlink先考虑Subtask\(4\)。设\(dp_i\)表示长度为\(i\)的方案数,按题目定义转移:AB,ASB:\(\displaystyledp_n\getsdp_n+\sum_{i=1}^{n-1}\sum_{j=0}^kdp_i\timesdp_{n-i-j}\)(A):\(\displaystyledp_n\getsdp_n+dp_{n-2}\)(SA),(AS):\(\displa......
  • p7913-solution
    P7913Solutionlink先考虑有\(n\)个廊桥的分配。假设廊桥编号为\(1\simn\),我们用两个堆\(h1,h2\)分别存当前空闲的廊桥编号和正在使用廊桥的飞机的离开时间。对于国内和国外的飞机分别做一次以下操作:先按到达时间排序,从左到右扫,到第\(i\)架飞机的到达、离开时间为\(l_......