仙人掌

法一:类比基环树中的第二种处理方法

例题:仙人掌最大独立集

处理仙人掌图,可以先考虑在树上怎么做,然后在仙人掌里加一些对环的限制。

所有回边的影响,会在深搜树上的环顶被解决。此后就可以看作普通子树。

\(up[x]\) 记录 \(x\) 的环顶,\(up[x]=0\) 表示 \(x\) 不属于任何环或者 \(x\) 是环顶(\(x\) 不在任何环的中间和底部)。

如果在树上求最大独立集:\(dp[x][0/1]\) 表示以 \(x\) 为根的子树内,\(x\) 不选/选,的最大独立集。

如果是仙人掌,则需要额外考虑环顶与环底的影响。

定义:一个结点最多管辖环底。定义它管辖的环底为:在它的子树中且环顶不在它的子树中的环底。注意,如果一个结点本身就是环顶,它所在环的环底不被它管辖。

要额外增加对环顶环底的限制。

\(dp[x][0/1][0/1]\),第一维表示在 \(x\) 子树中,第二维表示 \(x\) 不选/选,第三维表示 \(x\) 管辖的环底不选/选(如果 \(x\) 不管辖环底则无所谓)。

初值:当进入一个结点时,令 \(dp[x][1][0/1]=1,dp[x][0][0/1]=0\),为什么 \(dp[x][1][1]\neq 2,dp[x][0][1]\neq 1\)?这里只有 \(x\) 才生效,因为 \(x\) 是我们确定的可能选的,而 \(x\) 管辖的环底却不一定存在,所以初值不计算环底。

递推:当前位于 \(x\)\(x\) 遍历所有相邻的非父节点的点。

  • 如果是已经访问过的点,e[x][i] 在当前搜索栈中,且 e[x][i] != pr,说明在这里形成了一个环。令 \(dp[x][0][1]=dp[x][1][0]=-\infty\),因为 \(x\)\(x\) 管辖的环底 是同一个点,要么都选,要么都不选。同时 up[x] = e[x][i],表示找到了 \(x\) 的环顶。

    注意这里要用一个栈记录当前的点!因为比如当前点是 \(x\),有一个 \(x\rightarrow y\rightarrow x\) 的环。从 \(x\) 出发搜到了 \(y\)\(y\) 回溯到 \(x\),此时 \(x\) 又用另一条路径搜到了 \(y\)。如果不用搜索栈判断,\(x\) 会把 \(y\) 当作自己的环顶。

  • 如果是树边,先搜过去。搜了之后第一件事就是用子节点的 \(up\) 尝试更新自己的 \(up\)

    如果 \(up[e[x][i]]=up[x]\),说明 \(x\) 是这个方向的环顶,则这个方向的子树不改变 \(x\) 对应的环顶是谁;如果 \(up[e[x][i]]=0\),说明这个方向的子树都没有环来改变 \(up[x]\);否则令 \(up[x]=up[e[x][i]]\)

    第二件事,更新 \(x\) 的 DP 值。更新又分两类:\(x\) 是环顶和 \(x\) 不是环顶。注意这里我们干的所有事,是在搜索完一个子节点之后做的。也就是每把一个子节点搜了,就要干一遍,而不是最后统一干

    • \(x\) 是这个方向的环顶,即 \(up[e[x][i]]=x\)\(x\) 此时和这个环的环底相邻了。(\(\leftarrow\) 表示累加)

      \[dp[x][0][0]\leftarrow \max\{dp[e[x][i]][0/1][0/1]\} \]

      因为 \(x\) 不选,表示对 \(x\) 的子节点和这个环的环底不会有影响;而 \(x\) 管辖的环底不选,根本不会对这颗子树的贡献有影响,因为 \(x\) 管辖的环底都不在这里。

      \[dp[x][0][1]\leftarrow \max\{dp[e[x][i]][0/1][0/1]\} \]

      这和上面的转移方程是一样的。原因还是 \(x\) 管辖的环底不在这里,所以第三维是 \(0/1\) 不会对贡献有影响,转移方程当然应该一样。

      \[dp[x][1][0],dp[x][1][1]\leftarrow dp[e[x][i]][0][0] \]

      (对于 \(x\) 和这颗子树,第三维是 \(0/1\) 都一样,所以就写在一起了)

      因为 \(x\) 选了,导致 \(e[x][i]\) 不能选,所以第二维是 \(0\)\(x\) 选了,导致这个环的环底不能选了,这个环的环底是 \(e[x][i]\) 管辖的,所以第三维也要是 \(0\)

    • \(x\) 不是这个方向的环顶,就把这个方向的子树当成正常的子树更新即可。这里本来还应该分成 “\(x\) 管辖的环底在这颗子树里” 和 “这颗子树没有到达 \(x\) 及以上的环” 两类,但是我们发现这两类的转移方程是一样的。因为状态定义里就说了,没有管辖的结点,第三维随便,本身两个的转移方程就应该一样。

      \[dp[x][0][0] \leftarrow \max(dp[e[x][i]][0][0], dp[e[x][i]][1][0]) \]

      \(x\) 不选,\(x\) 的儿子就可以随便选;同时 \(x\) 的儿子也要跟着保证 \(x\) 管辖的环底不被选,所以第三维是 \(0\)

      状态定义里就说了,没有管辖的结点,第三维随便。

      \[dp[x][0][1] \leftarrow max(dp[e[x][i]][0][1], dp[e[x][i]][1][1]) \]

      状态定义里就说了,没有管辖的结点,第三维随便。

      \[dp[x][1][0] \leftarrow dp[e[x][i]][0][0] \]

      状态定义里就说了,没有管辖的结点,第三维随便。

      \[dp[x][1][1] \leftarrow dp[e[x][i]][1][0] \]

      状态定义里就说了,没有管辖的结点,第三维随便。

法二:圆方树

先复习一下圆方树:每个点双取出来作为方点,每个原图的点是圆点。点双(方点)向所有包含的结点(圆点)连边,方点的父节点是在 DFS 树上深度最浅的圆点。

观察仙人掌的结构,发现它的圆方树的每个方点,要么代表一个简单环,要么代表两个点一条边。

于是我们可以按照正常深搜的方式(不记录 dfn,low)如果找到一个环,把这个环变成一个方点,把这个环上所有点连到这个方点上。

把所有环的方点处理完之后,再把所有不是环的方点找出来。

cnt = n;

void dfs(int x, int pr, int dth) {
	fa[x] = pr;
	vis[x] = stk[x] = true;
	d[x] = dth;
	for (int i = 0; i < e[x].size(); i++) {
		if (!vis[e[x][i].to]) {
			dfs(e[x][i].to, x, dth + e[x][i].val);
			if (up[e[x][i].to] != 0 && up[e[x][i].to] != x)
				up[x] = up[e[x][i].to];
		}
		else if (e[x][i].to != pr && stk[e[x][i].to]) {
			up[x] = e[x][i].to;
			cnt++; //方点计数
			//这里开始环上的点连方点
			for (int j = x; true; j = fa[j]) {
				E[n + cnt].push_back(Edge(j, ));
				E[j].push_back(Edge(n + cnt, ));
				if (j == e[x][i].to)
					break;
			}
		}
	}
	//只有两个点的方点
	for (int i = 0; i < e[x].size(); i++)
		if (e[x][i].to != pr && up[e[x][i].to] == 0 && !stk[e[x][i].to]) {
			cnt++;
			E[x].push_back(Edge(n + cnt, ));
			E[n + cnt].push_back(Edge(x, ));
			E[e[x][i].to].push_back(Edge(n + cnt, ));
			E[n + cnt].push_back(Edge(e[x][i].to, ));
		}
	stk[x] = false;
}

找了圆方树有什么用?

仙人掌最短路径

把一个方点所包含的圆点中 在深搜树里面最浅的 称为它的代表点。

先用上面的方法建圆方树的边,但是边要带边权:

  • 如果方点是一个简单环,所有包含其中的圆点到方点的边距离为走到代表点的最近距离,即 \(\min(\text{走树边到代表点的距离},\text{环长 - 走树边到代表点的距离})\)。显然环长可以在找到环的一瞬间求得。

  • 如果方点是一个两点一线,则代表点到方点的边边权是 \(0\),另一个点到方点的边权是原图中这条边的边权。

建了圆方树有什么用呢?发现圆方树上的最短路径和仙人掌上的最短路径有联系。

查询两个结点 \(u,v\) 在仙人掌中的最短路,可以先看 \(lca(u,v)\):若 \(lca(u,v)\) 是圆点,答案就是 \(u,v\) 在圆方树中的路径长度。

\(lca(u,v)\) 是方点,记 \(u\) 在到达 \(lca(u,v)\) 前到的最后一个结点是 \(uu\)\(lca(u,v)\)\(u\) 的方向走一步到 \(uu\)),\(vv\) 类似定义。

\(u\rightarrow uu,v\rightarrow vv\),在仙人掌中可以看作是 \(u,v\) 经过若干条边,此时已经来到了同一个简单环。现在的问题就是怎么让 \(uu,vv\) 到一起。

显然这也有两种选择:沿着深搜树的边,和 环长 - 沿着深搜树的边 两种情况。

特别注意!!!这里不要把圆方树此时的树边当成深搜树的树边,因为圆方树的树边权值是已经改过了的。要想得知此时的树边长度,必须在一开始深搜的时候额外记录一个 \(dist\),表示从深搜树的根走到这里走了多远。\(|dist[uu]-dist[vv]|\) 才是此时 \(uu,vv\) 之间的路径。

特别注意!!!\(|dist[uu]-dist[vv]|\) 也不是最终答案!记 \(uu,vv\) 所处的环长度为 \(len\)\(\min(len-|dist[uu]-dist[vv]|,|dist[uu]-dist[vv]|)\) 才是此时 \(uu,vv\) 在环上汇合的最短路径。

#include <bits/stdc++.h>

using namespace std;
const int N = 2e4 + 5;

int n, m, q;
struct Edge {
	int to, val;
	Edge(int t = 0, int v = 0) {
		to = t, val = v;
	}
};
vector<Edge> e[N];

vector<Edge> E[N];

int d[N], fa[N], up[N]; //深搜树上的深度,深搜树上的父节点,深搜树上的环顶 
bool vis[N], stk[N]; 
int cnt = 0;
int len[N]; //环长 

void dfs(int x, int pr, int dth) {
	fa[x] = pr;
	vis[x] = stk[x] = true;
	d[x] = dth;
	for (int i = 0; i < e[x].size(); i++) {
		if (!vis[e[x][i].to]) {
			dfs(e[x][i].to, x, dth + e[x][i].val);
			if (up[e[x][i].to] != 0 && up[e[x][i].to] != x) //更新环顶 
				up[x] = up[e[x][i].to];
		}
		else if (e[x][i].to != pr && stk[e[x][i].to]) {
			up[x] = e[x][i].to;
			cnt++;
			len[n + cnt] = d[x] - d[e[x][i].to] + e[x][i].val;
			//这里开始环上的点连方点
			for (int j = x; true; j = fa[j]) {
				E[n + cnt].push_back(Edge(j, min(d[j] - d[e[x][i].to], len[n + cnt] - (d[j] - d[e[x][i].to]))));
				E[j].push_back(Edge(n + cnt, min(d[j] - d[e[x][i].to], len[n + cnt] - (d[j] - d[e[x][i].to]))));
				if (j == e[x][i].to)
					break;
			}
		}
	}
	//只有两个点的方点
	for (int i = 0; i < e[x].size(); i++)
		if (e[x][i].to != pr && up[e[x][i].to] == 0 && !stk[e[x][i].to]) {
			cnt++;
			E[x].push_back(Edge(n + cnt, 0));
			E[n + cnt].push_back(Edge(x, 0));
			E[e[x][i].to].push_back(Edge(n + cnt, d[e[x][i].to] - d[x]));
			E[n + cnt].push_back(Edge(e[x][i].to, d[e[x][i].to] - d[x]));
		}
	stk[x] = false;
}

int pw[30];
int st[30][N], dist[30][N], dep[N]; 
//st:倍增LCA ,dist:圆方树上走到倍增LCA的 最短距离,dep:圆方树上不带权的深度(用来求LCA) 
void srh(int x, int pr, int dth2) { //在圆方树上搜一遍 
	st[0][x] = pr;
	dep[x] = dth2;
	for (int i = 0; i < E[x].size(); i++)
		if (E[x][i].to != pr) {
			dist[0][E[x][i].to] = E[x][i].val;
			srh(E[x][i].to, x, dth2 + 1);
		}
}
void init() {
	pw[0] = 1;
	for (int i = 1; i <= 20; i++)
		pw[i] = pw[i - 1] * 2;
	for (int i = 1; i <= 20; i++)
		for (int j = 1; j <= n + cnt; j++) {
			st[i][j] = st[i - 1][st[i - 1][j]];
			dist[i][j] = dist[i - 1][j] + dist[i - 1][st[i - 1][j]];
		}
}
int anc(int u, int k) {
	int x = u;
	for (int i = 15; i >= 0; i--)
		if (k >= pw[i])
			x = st[i][x], k -= pw[i];
	return x;
}
int dis(int u, int k) {
	int x = u, sum = 0;
	for (int i = 15; i >= 0; i--)
		if (k >= pw[i])
			sum += dist[i][x], x = st[i][x], k -= pw[i];
	return sum;
}
int lca(int u, int v) {
	if (dep[u] > dep[v])
		return lca(v, u);
	v = anc(v, dep[v] - dep[u]);
	if (u == v)
		return u;
	for (int i = 15; i >= 0; i--)
		if (st[i][u] != st[i][v])
			u = st[i][u], v = st[i][v];
	return st[0][u];
}
int slv(int u, int v) {
	int lc = lca(u, v);
	if (lc <= n) { //圆点 
		return dis(u, dep[u] - dep[lc]) + dis(v, dep[v] - dep[lc]);
		//直接加起来 
	}
	else {
		int tmp = dis(u, dep[u] - dep[lc] - 1) + dis(v, dep[v] - dep[lc] - 1);
		int uu = anc(u, dep[u] - dep[lc] - 1), vv = anc(v, dep[v] - dep[lc] - 1);
		return tmp + min(abs(d[uu] - d[lc] - (d[vv] - d[lc])), len[st[0][uu]] - abs(d[uu] - d[lc] - (d[vv] - d[lc])));
	}
}

int main() {
	cin >> n >> m >> q;
	for (int i = 1, u, v, w; i <= m; i++) {
		cin >> u >> v >> w;
		e[u].push_back(Edge(v, w));
		e[v].push_back(Edge(u, w));
	}
	dfs(1, 0, 0);
	srh(1, 0, 0);
	init();
	for (int i = 1, u, v; i <= q; i++) {
		cin >> u >> v;
		cout << slv(u, v) << endl;
	}
	return 0;
}
posted @ 2024-02-29 08:58  FLY_lai  阅读(6)  评论(0编辑  收藏  举报