仙人掌
法一:类比基环树中的第二种处理方法
处理仙人掌图,可以先考虑在树上怎么做,然后在仙人掌里加一些对环的限制。
所有回边的影响,会在深搜树上的环顶被解决。此后就可以看作普通子树。
用
如果在树上求最大独立集:
如果是仙人掌,则需要额外考虑环顶与环底的影响。
定义:一个结点最多管辖环底。定义它管辖的环底为:在它的子树中且环顶不在它的子树中的环底。注意,如果一个结点本身就是环顶,它所在环的环底不被它管辖。
要额外增加对环顶环底的限制。
初值:当进入一个结点时,令
递推:当前位于
-
如果是已经访问过的点,且
e[x][i]
在当前搜索栈中,且e[x][i] != pr
,说明在这里形成了一个环。令 ,因为 和 管辖的环底 是同一个点,要么都选,要么都不选。同时up[x] = e[x][i]
,表示找到了 的环顶。注意这里要用一个栈记录当前的点!因为比如当前点是
,有一个 的环。从 出发搜到了 , 回溯到 ,此时 又用另一条路径搜到了 。如果不用搜索栈判断, 会把 当作自己的环顶。 -
如果是树边,先搜过去。搜了之后第一件事就是用子节点的
尝试更新自己的 :如果
,说明 是这个方向的环顶,则这个方向的子树不改变 对应的环顶是谁;如果 ,说明这个方向的子树都没有环来改变 ;否则令 。第二件事,更新
的 DP 值。更新又分两类: 是环顶和 不是环顶。注意这里我们干的所有事,是在搜索完一个子节点之后做的。也就是每把一个子节点搜了,就要干一遍,而不是最后统一干-
若
是这个方向的环顶,即 。 此时和这个环的环底相邻了。( 表示累加)因为
不选,表示对 的子节点和这个环的环底不会有影响;而 管辖的环底不选,根本不会对这颗子树的贡献有影响,因为 管辖的环底都不在这里。这和上面的转移方程是一样的。原因还是
管辖的环底不在这里,所以第三维是 不会对贡献有影响,转移方程当然应该一样。(对于
和这颗子树,第三维是 都一样,所以就写在一起了)因为
选了,导致 不能选,所以第二维是 ; 选了,导致这个环的环底不能选了,这个环的环底是 管辖的,所以第三维也要是 。 -
若
不是这个方向的环顶,就把这个方向的子树当成正常的子树更新即可。这里本来还应该分成 “ 管辖的环底在这颗子树里” 和 “这颗子树没有到达 及以上的环” 两类,但是我们发现这两类的转移方程是一样的。因为状态定义里就说了,没有管辖的结点,第三维随便,本身两个的转移方程就应该一样。 不选, 的儿子就可以随便选;同时 的儿子也要跟着保证 管辖的环底不被选,所以第三维是 。状态定义里就说了,没有管辖的结点,第三维随便。
状态定义里就说了,没有管辖的结点,第三维随便。
状态定义里就说了,没有管辖的结点,第三维随便。
状态定义里就说了,没有管辖的结点,第三维随便。
-
法二:圆方树
先复习一下圆方树:每个点双取出来作为方点,每个原图的点是圆点。点双(方点)向所有包含的结点(圆点)连边,方点的父节点是在 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;
}
找了圆方树有什么用?
把一个方点所包含的圆点中 在深搜树里面最浅的 称为它的代表点。
先用上面的方法建圆方树的边,但是边要带边权:
-
如果方点是一个简单环,所有包含其中的圆点到方点的边距离为走到代表点的最近距离,即
。显然环长可以在找到环的一瞬间求得。 -
如果方点是一个两点一线,则代表点到方点的边边权是
,另一个点到方点的边权是原图中这条边的边权。
建了圆方树有什么用呢?发现圆方树上的最短路径和仙人掌上的最短路径有联系。
查询两个结点
若
显然这也有两种选择:沿着深搜树的边,和 环长 - 沿着深搜树的边 两种情况。
特别注意!!!这里不要把圆方树此时的树边当成深搜树的树边,因为圆方树的树边权值是已经改过了的。要想得知此时的树边长度,必须在一开始深搜的时候额外记录一个
特别注意!!!
#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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!