「洛谷P7729」交通运输(Wormhole Transportaion) 题解

本文网址:https://www.cnblogs.com/zsc985246/p/17407035.html ,转载请注明出处。

前言

感谢 \(\color{red}{\text{AFewSuns}}\)\(\color{red}{\text{Y25t}}\)\(\color{red}{\text{ix35}}\) 三位巨佬提供的帮助!

很好的一道题目,有很大的难度,但是也很锻炼思维。

2023/5/18update:修正了 sub4 中的公式,更改了部分描述。

2023/5/19update:添加了代码。

2023/5/27update:日常修锅。

传送门

交通运输(Wormhole Transportaion)

题目大意

\(n\) 个点和 \(m\) 个点对,你需要构造一张 \(m-1\) 条边的无向图,使得 \(m\) 个点对间最短路之和最小。

最小值及取到最小值的方案数

\(2 \le n \le 2000,2 \le nm \le 2 \times 10^7,1 \le u_i,v_i \le n,u_i \neq v_i\)

保证所有无序对 \((u_i,v_i)\) 两两不同,且至少存在一种合法的方案

subtask \(n \le\) 特殊限制
\(1\) \(6\)
\(2\) \(2000\) \(m=n\)\(u_i=i,v_i=(i \bmod n) + 1\)
\(3\) \(2000\) \((u_i,v_i)\) 为无向边的图是仙人掌森林
\(4\) \(2000\) \((u_i,v_i)\) 为无向边的图是杏仁
\(5\) \(300\) \(m \le 1000\)
\(6\) \(2000\)

仙人掌森林:每条边在至多一个简单环中的无向图。

杏仁:恰好存在两个度数大于 \(2\) 的结点,其他结点度数都等于 \(2\),且所有环都经过两个度数大于 \(2\) 的结点的连通无向图。

思路

称以 \((u_i,v_i)\) 为无向边的图为 \(G\),我们构造的图为 \(G'\)

定义 “删去” 表示这条边或路径在 \(G\) 中,但不在 \(G'\) 中。“保留” 表示这条边或路径既在 \(G\) 中也在 \(G'\)

\(d(u,v)\) 表示在 \(G'\)\(u\)\(v\) 的最短路


求解最短路最小值

题目中保证至少存在一种合法的方案,那么我们可以知道,\(G\) 中一定有环。

\(G\) 中无环,那么 \(G\) 就是普通森林,对于任意森林里的一棵树 \(T\),令其有 \(k\) 条边,\(k+1\) 个节点。

由于 \(T\)\(k+1\) 个节点,所以我们要使其连通至少需要 \(k\) 条边。那么我们所需的总边数为 \(m\),大于题目要求的 \(m-1\),所以不存在合法方案,矛盾。

那么我们就可以猜测,最短路最小值与环有关。

设环内边数量为 \(x\)


先考虑环外的边。

1.1

我们观察不在 \(G\) 的环上的\((5,6)\)

假设我们选择了一个中转点进行连边。

可以发现,如果我们选择一个不在 \(G\) 中的中转点 \(7\),那么我们会连接 \((5,7),(6,7)\) 两条边,\(d(5,6)=2\),使用两条边。但实际上我们可以连接 \((5,6)\),此时 \(d(5,6)=1\) 且只使用了一条边。

所以,我们不会选择不在 \(G\) 中的节点作中转。

同样的,如果我们选择一个\(G\) 中的中转点 \(2\),那么我们会连接 \((2,5),(2,6)\) 两条边,\(d(5,6)+d(2,6)=3\),使用两条边。但如果我们连接 \((5,6),(2,6)\)\(d(5,6)+d(2,6)=2\),同样使用两条边。

所以,我们不会选择在 \(G\) 中的节点作中转。

综上,我们不会作中转。

也就是说,如果 \((u,v) \in G\),且 \((u,v)\) 不是环边,那么 \((u,v) \in G'\)

根据这条性质,我们可以知道,环外的边的答案就是 \(m-x\)


接下来考虑环内的边。

1.2

我们假设这个环长度为 \(k\)

显然,如图所示将环断成一条链最优。答案为 \(2 \times (k - 1)\)

那如果有多个环?显然我们只需断一个环。而又因为我们要让答案最小,即选择一个环 \(i\) 使 \(2 \times (k_i - 1)\) 最小,所以 \(i\) 一定是最小环。

我们设最小环的长度为 \(cnt\),那么最后的答案就是 \(x-1+cnt-1=x+cnt-2\)

这样我们就算出了总答案为 \((m-x)+(x+cnt-2)=m-cnt-2\)

关于最小环环长,我们只需要求出 \(i,j\) 的最短路 \(dis(i,j)\)\(i,j\) 的第一条路与最短路不同的最短路 \(dis2(i,j)\),然后枚举每对距离为 \(1\) 的点对 \(i,j\),取 \(dis2(i,j)+1\) 的最小值即为环长。


sub2

\(G\) 是一个大小为 \(n\),编号顺次为 \(1,2,3,\dots,n\) 的环。

2.1

此时 \(G'\) 是一棵树,我们令 \(G'\) 的根节点为 \(1\)

我们只需要计算使 \(d(1,2)+d(2,3)+\cdots+d(n-1,n)+d(n,1)=2n-2\) 成立的 \(G'\) 的个数。

可以发现,\(d\) 的和实际上就是 \(1 \to 2 \to \dots \to n \to 1\) 的路径长,而 \(2n-2\) 代表每条边正好经过两次。如上图红色路径所示。

那么这样的路径实际上就是我们 dfs 的遍历过程,也就是说我们进入了一棵子树之后会遍历完内部的所有点后再出来。

所以任意一个子树内所有节点的编号必定连续。

我们推广到环上编号不连续的情况,即对于任意一个子树内所有节点的编号组成的集合 \(S\),一定存在环上一段路径,使得这段路径上节点的编号组成的集合与 \(S\) 相同。下方称此结论为条件 \(1\)


然后考虑如何计数。

我们设在 \(G'\) 中,\(1\) 的儿子中编号最大的是 \(y\),子树 \(y\) 中点的编号是 \([x,n]\)

不难发现,除去子树 \(y\) 后的树编号是 \([1,x-1]\),也满足条件 \(1\),是一个子问题。

但因为 \(y\) 不是 \([x,n]\) 中编号最大的点,所以它不是子问题。但是如果我们将它拆分成 \([x,y]\)\([y,n]\) 两个区间,我们发现,这两个区间都是满足条件 \(1\) 的,都是子问题。

那么我们就把答案拆分成了三个子问题:\([1,x-1],[x,y],[y,n]\)

\([1,x-1]\) 而不是 \([1,x]\) 是因为 \(y\)\(1\) 的儿子,\((1,y)\) 这条边一定存在。

发现区间的左右端点对答案没有影响,所以设 \(f_i\) 表示长度为 \(i\) 的连续区间的方案数。

那么答案 \(f_n=\sum_{i+j+k=n+1} f_i f_j f_k\)

因为我们钦定了根节点为 \(1\),所以不会算重。

至于具体计算,用 \(h_{n}=\sum_{i+j=n+1} f_i f_j\) 辅助就可以达到 \(O(n^2)\)


sub3

\(G\) 是仙人掌森林。

那么我们只需要找出最小环的个数 \(tot\),然后将最小环的答案乘上 \(tot\) 就好了。

注意不是乘方,时刻牢记我们只断一个环。


sub4

\(G\) 是杏仁。

题目中说,杏仁是恰好存在两个度数大于 \(2\) 的结点,其他结点度数都等于 \(2\),且所有环都经过两个度数大于 \(2\) 的结点的连通无向图。

我们翻译一下:\(G\) 中有两个点 \(S,T\),它们之间有 \(k\) 条不交路径,如下图。我们设这些路径长度从小到大依次为 \(L_1,L_2,\dots,L_k\)

显然最小环的长度 \(cnt=L_1+L_2\),数量 \(tot\) 也好算,但是我们不能用 \(f_{cnt}\) 乘以数量来计算答案,因为会算重。

思考什么时候会算重。

3.1

如图,我们令绿色的环为 \(x\),红色的环为 \(y\),路径 \(S \to 3 \to T\)\(L_1\)\(S \to 1 \to 2 \to T\)\(L_2\)\(S \to 4 \to 5 \to T\)\(L_3\)

发现保留路径 \(L_2\),改变 \(L_1\) 与保留路径 \(L_3\),改变 \(L_1\) 的方案有一部分是重复的。

据此,我们定义 \(g_{a,b}\) 表示长度为 \(a\)\(b\) 的路径构成的最小环中,保留路径 \(b\)\(a,b\) 两条路径之间没有连边的方案数。那么对于上图情况,最后答案就是 \(2f_5-g_{2,3}\)

拓展到整个图,令长为 \(L_1\) 的路径个数为 \(t_1\),长为 \(L_2\) 的路径个数为 \(t_2\)

  • \(t_1 \neq 1\) 时,答案为 \(tot \times f_{cnt} - t_1 \times (t_1-2) \times g_{L_1,L_1}\)
  • \(t_1 = 1\) 时,答案为 \(tot \times f_{cnt} - (t_2-1) \times g_{L_1,L_2}\)

现在只需要考虑怎么计算 \(g_{a,b}\) 了。这里同样钦定一个点是根节点。我们不需要知道根节点具体是谁。

可以发现与条件 \(1\) 差别不大,可以推出 \(g_{a,b}=\sum_{i+j=a+1} f_i f_j\)

发现其实与 \(b\) 无关,所以直接定义 \(g_i\) 为只更改最小环上长度为 \(i\) 的路径的方案数。

其实仔细观察一下就能发现,这个递推式跟 sub2 中给出的解决 \(f\) 预处理问题的 \(h\) 一模一样。所以我们直接计算 \(f\)\(g\) 就可以了。


sub6

没有特殊限制。

我们类比 sub4 的解法,定义一个 \(G'\) 被图 \(G\) 中的环 \(C\) 影响,当且仅当所有不在 \(C\) 上的 \((u,v) \in G'\),且存在 \((u,v) \in C,(u,v) \not\in G'\)

如果 \(G'\)\(C_1,C_2,\dots,C_t\) 影响,那么 \(G'\) 只能在 \(G\)\(C_1,C_2,\dots,C_t\) 相交的路径 \(P\) 上作修改得到。

那么对于求解方案,我们可以找到 \(P\) 上的一段区间 \(P'\),长度为 \(P'_l\),然后只对 \(P'\) 进行改动。

发现这与我们 \(g\) 数组的定义很像。但是要注意的是,这里 \(P'\) 的端点所连的边也都会改动,所以我们要做一个容斥,答案是 \(g_{P'_l}-2 \times g_{P'_l-1}+g_{P'_l-2}\)

但是其实可以发现,对于 \(G'\) 只被一个环 \(C\) 影响时,我们无法完全计算到它的贡献。因为多个环的相交部分一定不大于 \(\frac{cnt}{2}\)

\(L\) 为组成最小环的路径,其中 \(L_1,L_2\)\(L_1,L_3\) 均构成最小环。

如果相交部分 \(L_1 > \frac{cnt}{2}\),那么 \(L_1>\frac{L_1+L_2}{2}\),即 \(L_1>L_2\),同理 \(L_1>L_3\)

此时发现 \(L_2+L_3<L_1+L_2\),即 \(L_2+L_3<cnt\),与 \(cnt\) 为最小环长的定义矛盾。

那么我们就需要统计 \(G'\) 只被一个环 \(C\) 影响的部分答案。

那么我们只需要进行一个容斥即可。我们用总方案 \(g_{cnt}\) 减去只包含长度小于等于 \(\frac{cnt}{2}\) 的路径的方案 \(cnt \times \sum_{i=1}^{\frac{cnt}{2}}g_{i}-2 \times g_{i-1}+g_{i-2}\)


我们令在 \(G\)\(dis(i,j)\) 表示 \(i\)\(j\) 的最短路。

那么首先我们需要找出最小环长度 \(cnt\) 和数量 \(tot\),然后将 \(f\) 数组和 \(g\) 数组计算出来。

接下来我们枚举每个路径 \(P'\),即枚举路径的端点 \(s,t\),注意需要保证 \(dis(s,t) \le \frac{cnt}{2}\)。对答案的贡献为 \(g_{dis(s,t)}-2 \times g_{dis(s,t)-1}+g_{dis(s,t)-2}\)

最后用容斥统计 \(G'\) 只被一个环 \(C\) 影响的部分答案。

然后将单个最小环的答案乘上 \(tot\),按 sub4 的方法去重就好了。


分析时间复杂度。

处理两点之间最短路、两点之间第一条路与最短路不同的最短路(代码注释中称为次短路),用 bfs 可以做到 \(O(nm)\)

处理最小环信息,直接做就好,复杂度 \(O(n^2)\)

\(f\) 数组与 \(g\) 数组。复杂度 \(O(n^2)\)

枚举路径计算答案是 \(O(n^2)\)

总时间复杂度 \(O(nm+n^2)\)

那么终于,这道题结束了。

代码实现

本代码需要吸氧。也可以把 long long 改成 int 卡常。

#include<bits/stdc++.h>
#define ll long long
#define For(i,a,b) for(ll i=(a);i<=(b);++i)
#define Rep(i,a,b) for(ll i=(a);i>=(b);--i)
#define pb push_back
const ll N=2e3+10;
const ll p=1e9+7;
using namespace std;

ll ksm(ll a,ll b){ll res=1;while(b){if(b&1)res=res*a%p;a=a*a%p;b>>=1;}return res;}

ll n,m,sid;
vector<ll>e[N];//图G

//下方称次短路为i到j的第一条边与最短路不同的最短路
ll dis[N][N];//dis[i][j]:i到j的最短路
ll dis2[N][N];//dis2[i][j]:i到j的次短路
ll num[N][N];//num[i][j]:i到j的次短路的条数
ll tmp[N][N];//tmp[i][j]:从i到终点s第一条边是(i,j)的最短路条数
ll cnt=1e9,tot;//cnt:最小环长度 tot:最小环数量
void bfs(ll s){//处理dis和dis2
	//s是最短路终点
	//从终点向外拓展,以便寻找最短路的第一条边
	For(x,1,n)for(ll y:e[x])tmp[x][y]=0;//清空
	queue<pair<ll,ll>>q;//第二个信息表示上一个点,0表示不是第一次加入队列,需取次短路计算
	q.push({s,n+1});
	dis[s][s]=0;
	while(q.size()){
		ll x=q.front().first,pre=q.front().second;
		q.pop();
		ll d=pre?dis[x][s]+1:dis2[x][s]+1;//距离
		for(ll y:e[x]){
			if(y==pre)continue;
			ll res=1;
			if(!pre)res=(num[x][s]-tmp[x][y]+p)%p;//从x到s不经过边(x,y)的最短路条数
			if(d<dis[y][s]){//bfs距离单调不减,所以此时一定是第一次更新到起点y
				dis[y][s]=d;
				q.push({y,x});//y第一次加入队列
			}else if(d<dis2[y][s]){//不是第一次更新点y,所以处理次短路
				dis2[y][s]=d;
				num[y][s]=res;
				tmp[y][x]=res;
				q.push({y,0});
			}else if(d==dis2[y][s]){//次短路长度相同,更新数量
				num[y][s]=(num[y][s]+res)%p;
				tmp[y][x]=res;
			}
		}
	}
}
ll f[N],g[N];//定义同思路
ll h[N];//提前记录容斥的式子
void init(){//预处理,dis、dis2、cnt、tot、f和g
	For(x,1,n)For(y,1,n)dis[x][y]=dis2[x][y]=1e9;
	For(x,1,n)bfs(x);//处理dis、dis2和num
	//枚举两个距离为1的点,计算cnt和tot
	For(x,1,n){
		for(ll y:e[x]){
			if(x<y){
				if(dis2[x][y]+1<cnt)cnt=dis2[x][y]+1,tot=num[x][y];
				else if(dis2[x][y]+1==cnt)tot=(tot+num[x][y])%p;
			}
		}
	}
	tot=tot*ksm(cnt,p-2)%p;//去重,因为每个最小环上都有cnt个距离为1的点对
	//处理f和g
	f[1]=g[1]=1;
	For(i,2,n+1){
		For(j,1,i-1)f[i]=(f[i]+g[j]*f[i-j])%p;
		For(j,1,i)g[i]=(g[i]+f[j]*f[i+1-j])%p;
	}
	//这里直接把g[i]-2*g[i-1]+g[i-2]处理出来了
	For(i,1,n){
		h[i]=(g[i]-2*g[i-1]+2*p)%p;
		if(i>1)h[i]=(h[i]+g[i-2])%p;
	}
}

void mian(){
	
	scanf("%lld%lld%lld",&n,&m,&sid);
	For(i,1,m){
		ll x,y;
		scanf("%lld%lld",&x,&y);
		e[x].pb(y),e[y].pb(x);
	}
	init();//预处理
	ll ans=0;
	For(x,1,n){
		For(y,x+1,n){
			if(dis[x][y]+dis2[x][y]==cnt){//能构成最小环
				ans=(ans+h[dis[x][y]])%p;
				if(dis[x][y]==dis2[x][y])ans=(ans+h[dis[x][y]]*num[x][y])%p;
			}
		}
	}
	//大于cnt/2的情况
	ll res=f[cnt];
	For(i,1,cnt/2)res=(res-cnt*h[i]%p+p)%p;
	ans=(ans+tot*res)%p;
	printf("%lld\n%lld",m+cnt-2,ans);
	
}

int main(){
	int T=1;
//	scanf("%d",&T);
	while(T--)mian();
	return 0;
}

尾声

如果你发现了问题,你可以直接回复这篇题解

如果你有更好的想法,也可以直接回复!

posted @ 2023-05-16 22:12  zsc985246  阅读(222)  评论(0编辑  收藏  举报