仙人掌
法一:类比基环树中的第二种处理方法
处理仙人掌图,可以先考虑在树上怎么做,然后在仙人掌里加一些对环的限制。
所有回边的影响,会在深搜树上的环顶被解决。此后就可以看作普通子树。
用 \(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;
}