Live2D

Solution -「APIO 2018」「洛谷 P4630」铁人两项

Description

  Link.

  给定一个 n 个点 m 条边的无向图(不保证联通),求有序三元点对 (s,c,f) 的个数,满足 s,c,f 互不相同,且存在一条从 sc 再到 f 的简单路径。

  n105m2×105

Solution

  首先考虑这样一个问题,若 s,c,f 在同一点双中,是否一定满足条件。

  答案是肯定的,这里介绍一种 CF 某题解上提到的证明。

性质证明

  构造网络,将点双中的每个点拆点。对于点 u,连接 (ui,uo,1)。对于原图中的边 (u,v),连接 (uo,vi,1)。对于选定的 s,c,f,连接 (S,ci,2),(so,T,1),(fo,T,1),接下来只需要证明该网络的最大流为 2

  那么只需要考虑最小割 C。显然 C2,则只需证 C>1

  首先,割掉 (S,ci,2) 或同时割掉 (so,T,1),(fo,T,1) 都不能使 C1。接下来考虑其它类型的边。

  • 割掉 (ui,uo,1),相当于删除 u 点。因为这是一个点双,所以 cs,f 仍然连通,不满足。

  • 割掉 (uo,vi,1),相当于删除 (u,v) 边。显然其对于连通性的影响不大于删除 u 点或 v 点,由上种情况,亦不满足。

  到此,有 C>1。由因为 C2CN,所以 C=2。那么这样的路径一定存在,证毕。

  那么如果固定 s,f,合法的 c 就可以在圆方树 sf 路径上的所有圆点和所有方点所代表的圆点(除去 s,f)。这是因为 c 取在任意点双内部,由我们的结论,都一定可以从某点进入点双,经过 c,再从某点走出点双。

  但简单的计算会导致重复——一个圆点对多个方点有贡献。

  举个例子,对于 uwvw 是圆点,u,v 是方点,如果我们单纯地用点双大小作为方点的权值,w 就会在 uv 中分别计算一次。

  解决办法很巧妙:将圆点的权值设为 1。考虑 sf 的路径必然是”圆-方-圆-……-圆-方-圆“,两个端点的 1,去除了 sf 的贡献,中间的 1 去除了在左右的”方“中重复的贡献。那么,合法的 c 的数量就是 sf 的树上路径权值之和。

  于是,相当于求树上点对的路径权值和。反过来,固定 c,维护子树信息求出 (s,f) 的方案数,计算 c 的贡献即可。

  复杂度 O(n)

Code

#include <cstdio>

const int MAXN = 1e5, MAXM = 2e5;
int n, m, q, snode;
int dfc, top, dfn[MAXN + 5], low[MAXN + 5], stk[MAXN + 5];
int siz[MAXN * 2 + 5], val[MAXN * 2 + 5];
long long ans;

struct Graph {
	int ecnt, head[MAXN * 2 + 5], to[MAXM * 2 + 5], nxt[MAXM * 2 + 5];
	inline void link ( const int s, const int t ) {
		to[++ ecnt] = t, nxt[ecnt] = head[s];
		head[s] = ecnt;
	}
	inline void add ( const int u, const int v ) {
		link ( u, v ), link ( v, u );
	}
} src, tre;

inline bool chkmin ( int& a, const int b ) { return b < a ? a = b, true : false; }

inline void Tarjan ( const int u, const int f ) {
	dfn[u] = low[u] = ++ dfc, val[stk[++ top] = u] = -1;
	for ( int i = src.head[u], v; i; i = src.nxt[i] ) {
		if ( ( v = src.to[i] ) == f ) continue;
		if ( ! dfn[v] ) {
			Tarjan ( v, u ), chkmin ( low[u], low[v] );
			if ( low[v] >= dfn[u] ) {
				tre.add ( u, ++ snode ), val[snode] = 1;
				do tre.add ( snode, stk[top] ), ++ val[snode]; while ( stk[top --] ^ v );
			}
		} else chkmin ( low[u], dfn[v] );
	}
}

inline void calc ( const int u, const int f ) {
	siz[u] = u <= n;
	for ( int i = tre.head[u], v; i; i = tre.nxt[i] ) {
		if ( ( v = tre.to[i] ) ^ f ) {
			calc ( v, u );
			ans += 1ll * val[u] * siz[u] * siz[v];
			siz[u] += siz[v];
		}
	}
	ans += 1ll * val[u] * siz[u] * ( dfc - siz[u] );
}

int main () {
	scanf ( "%d %d", &n, &m ), snode = n;
	for ( int i = 1, u, v; i <= m; ++ i ) {
		scanf ( "%d %d", &u, &v );
		src.add ( u, v );
	}
	for ( int i = 1; i <= n; ++ i ) {
		if ( ! dfn[i] ) {
			dfc = top = 0;
			Tarjan ( i, 0 );
			calc ( i, 0 );
		}
	}
	printf ( "%lld\n", ans << 1 );
	return 0;
}
posted @   Rainybunny  阅读(128)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示