基环树是一个由 n n n 个点及 n n n 条边组成的联通图,其比树多出一条边,所以称作基环树;
存在多颗基环树即基环树森林,不一定保证多颗基环树之间连通;
有向基环树又分为 内向基环树 和 外向基环树;
内向基环树,即每个点出度为 1 的基环树,外向基环树,即每个点入度为 1 的基环树;
对于有关基环树问题,一般有两种解决方式,
搜索每个节点,标记节点的 DFN 值,并记录每个结点的前驱;
若搜索到一条返祖边,则从当前节点沿前驱节点回溯至边的另一端点,则回溯路上的所有节点均为环上的节点;
vector g[MAXN];
int dfn[MAXN], cnt, fa[MAXN], loop[MAXN], len;
void dfs_loop(int i) {
dfn[i] = ++cnt;
for (int t = 0; t < g[i].size(); t++){
int v = g[i][t];
if (v == fa[i]) continue;
if (!dfn[v]) {
fa[v] = i;
dfs_loop(v);
} else {
if (dfn[v] < dfn[i]) continue;
loop[++len] = v;
for (; v != i; v = fa[v]) loop[++len] = fa[v];
}
}
}
记录每个节点的度数,进行拓扑排序;
则入度 ≥ 2 \geq 2 ≥2 的点即为环上的点;
则拓扑排序后度数不为 0 的节点即为环上的点;
void topo_loop() {
queue q;
for (int i = 1; i <= n; i++) {
if (de[i] == 1) q.push(i);
}
while (!q.empty()) {
int i = q.front();
q.pop();
for (int t = 0; t < g[i].size(); t++) {
int v = g[i][t];
if (de[v] > 1) {
de[v]--;
if (de[v] == 1) q.push(v);
}
}
}
return;
}
有 n n n 个人,每个人有一个最恨的人,若最恨的人加入任务,则自己一定不会加入任务,现给出每个人的战斗力值,求战斗力值总和最大值;
可以发现,若此题为一棵树,及只有 n − 1 n - 1 n−1 条边,则可使用树形 DP 解决;
定义 d p [ i ] [ 0 / 1 ] dp[i][0/1] dp[i][0/1] 表示以 i i i 为根的子树,且 i i i 号节点不选 / 选的时战斗力最大值;
则当 i i i 不选时,其子节点可选可不选;
当 i i i 选时,则其子节点一定不能选;
有状态转移方程,
d p [ i ] [ 0 ] = ∑ m a x ( d p [ v ] [ 0 ] , d p [ v ] [ 1 ] ) d p [ i ] [ 1 ] = ∑ d p [ v ] [ 0 ] dp[i][0] = \sum max(dp[v][0], dp[v][1]) \\ dp[i][1] = \sum dp[v][0] dp[i][0]=∑max(dp[v][0],dp[v][1])dp[i][1]=∑dp[v][0]
先考虑基环树上做法;
则考虑对于基环树上相比原树的多出的一条边,此边上的两点是肯定无法同时选的;
所以可将此边断开,并强制此边上的两点中一点不选,并以另一点为根进行树上 DP,最后取最大值即可;
#include
#include
#include
#include
using namespace std;
const int MAXN = 1000005;
const int INF = 2147483647;
int n;
long long a[MAXN], v[MAXN];
vector g[MAXN];
bool flag[MAXN];
int r;
void dfs_loop(int i){
flag[i] = true;
if (flag[v[i]]) r = i;
else dfs_loop(v[i]);
return;
}
long long dp[MAXN][2], tot = -1, ans = 0;
void dfs(int i) {
dp[i][0] = 0, dp[i][1] = a[i];
flag[i] = true;
for (int t = 0; t < g[i].size(); t++) {
int v = g[i][t];
if (v == r) {
dp[v][1] = -INF;
continue;
}
dfs(v);
dp[i][0] += max(dp[v][1], dp[v][0]);
dp[i][1] += dp[v][0];
}
return;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%lld %d", &a[i], &v[i]);
g[v[i]].push_back(i);
}
for (int i = 1; i <= n; i++) {
if (!flag[i]) {
dfs_loop(i); // 找环上边
dfs(r); // 断边 DP
tot = max(dp[r][0], dp[r][1]);
r = v[r];
dfs(r);
ans += max(tot, max(dp[r][0], dp[r][1]));
}
}
printf("%lld\n", ans);
return 0;
}
给定一个基环树森林,求每颗基环树直径之和;
若对于一棵树,可通过搜索得到每个节点的向下最长路和次长路,相加即为经过该节点的最长路,计算经过每个点最长路后比较最大值即可;
考虑有环情况,
可发现,直径有三种情况;
对于情况一,以环上每个节点为根,进行 DP 即可;
对于情况二,即
找到 d p [ i ] + d p [ j ] + d i s [ i ] [ j ] dp[i] + dp[j] + dis[i][j] dp[i]+dp[j]+dis[i][j] 的最大值;
直接枚举会超时,所以可以维护一个前缀和,问题转化为;
d p [ i ] + d p [ j ] + s u m [ i ] − s u m [ j ] dp[i] + dp[j] + sum[i] - sum[j] dp[i]+dp[j]+sum[i]−sum[j] 的最大值;
则可维护 s u m [ j ] − d p [ j ] sum[j] - dp[j] sum[j]−dp[j] 最小值,然后直接枚举 i i i 即可;
使用单调队列;
#include
#include
#include
#include
using namespace std;
const int MAXN = 2000005;
int n;
long long sum[MAXN], ans = 0, tot;
struct edge {
int to;
long long tot;
};
vector g[MAXN];
int dfn[MAXN], cnt, fa[MAXN], loop[MAXN], len;
void dfs_loop(int i) {
dfn[i] = ++cnt;
for (int t = 0; t < g[i].size(); t++){
int v = g[i][t].to;
if (v == fa[i]) continue;
if (!dfn[v]) {
fa[v] = i;
dfs_loop(v);
} else {
if (dfn[v] < dfn[i]) continue;
loop[++len] = v;
for (; v != i; v = fa[v]) loop[++len] = fa[v];
}
}
}
long long path, dp[MAXN];
bool flag[MAXN];
void dfs(int i) {
flag[i] = true;
for (int t = 0; t < g[i].size(); t++) {
int v = g[i][t].to;
long long tot = g[i][t].tot;
if (!flag[v]) {
dfs(v);
path = max(path, dp[i] + dp[v] + tot);
dp[i] = max(dp[i], dp[v] + tot);
}
}
return;
}
int q[MAXN], l, r;
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
int y;
long long z;
scanf("%d %lld", &y, &z);
g[i].push_back(edge({y, z}));
g[y].push_back(edge({i, z}));
}
for (int s = 1; s <= n; s++) {
if (!flag[s]) {
cnt = len = 0;
dfs_loop(s);
tot = 0;
for (int i = 1; i <= len; i++) flag[loop[i]] = true;
for (int i = 1; i <= len; i++) {
int v = loop[i];
path = 0ll;
dfs(v);
tot = max(tot, path); // 情况 1
}
loop[0] = loop[len];
for (int i = 1; i <= len; i++) {
int v = loop[i];
long long val = -1ll;
for (int j = 0; j < g[v].size(); j++) {
if (g[v][j].to == loop[i - 1]) val = max(val, g[v][j].tot);
}
sum[i] = sum[i - 1] + val; // 记录环上前缀和
}
for (int i = 1; i < len; i++) sum[i + len] = sum[len] + sum[i]; // 环,复制一份前缀和
l = r = 1;
q[1] = 0; // 单调队列维护 sum[i] - dp[i]
for (int i = 1; i < len * 2; i++) {
while (l <= r && q[l] <= i - len) l++;
tot = max(tot, sum[i] - sum[q[l]] + dp[loop[q[l] % len]] + dp[loop[i % len]]);
while (l <= r && sum[q[r]] - dp[loop[q[r] % len]] >= sum[i] - dp[loop[i % len]]) r--;
q[++r] = i;
}
ans += tot;
}
}
printf("%lld\n", ans);
return 0;
}
给定一颗基环树,现给定若干个 a , b a, b a,b 求满足下面条件的 x , y x, y x,y ,
若对于一棵树,此题则为求两点 LCA 到其距离;
则对于基环树,可分类讨论,
若两点不在同一颗基环树上,
输出 -1 ;
若两点在同一颗基环树上,且两点均在一颗以环上一点为根的子树里;
直接两点 LCA 到其距离即可;
若两点在同一颗基环树上,但两点不在一颗以环上一点为根的子树里;
则两点相遇点一定在两点所在子树的根节点之一,分别计算即可;
#include
#include
#include
#include
#include
#include
using namespace std;
const int MAXN = 500005;
int n, q;
vector g[MAXN];
int de[MAXN], root[MAXN], bel[MAXN], ro[MAXN], len[MAXN], tot = 0;
int dp[MAXN][32], log1[MAXN], dep[MAXN];
void topo_loop() { // 找环
queue q;
for (int i = 1; i <= n; i++) {
if (!de[i]) q.push(i);
}
while (!q.empty()) {
int u = q.front();
q.pop();
int v = dp[u][0];
de[v]--;
if (!de[v]) q.push(v);
}
return;
}
bool flag[MAXN];
void logset() {
log1[1] = 0;
for (int i = 2; i <= MAXN; i++) {
log1[i] = log1[i / 2] + 1;
}
return;
}
void dfs(int i, int r) {
bel[i] = r;
flag[i] = true;
for (int t = 0; t < g[i].size(); t++) {
int v = g[i][t];
if (!flag[v] && !de[v]) {
dep[v] = dep[i] + 1;
dp[v][0] = i;
for (int j = 1; j <= log1[dep[v]]; j++) {
dp[v][j] = dp[dp[v][j - 1]][j - 1];
}
dfs(v, r);
}
}
return;
}
int LCA(int u, int v) {
if (dep[u] < dep[v]) swap(u, v);
while (dep[u] != dep[v]) {
u = dp[u][log1[dep[u] - dep[v]]];
}
if (u == v) return u;
for (int i = log1[dep[u]]; i >= 0; i--) {
if (dp[u][i] != dp[v][i]) {
u = dp[u][i], v = dp[v][i];
}
}
return dp[u][0];
}
void dfs1(int i, int num, int dep) {
if (ro[i] != -1) return;
root[i] = num;
len[num]++;
ro[i] = dep;
dfs1(dp[i][0], num, dep + 1);
}
bool cmp(int a, int b, int x, int y) {
if (max(a, b) != max(x, y)) return max(a, b) < max(x, y);
if (min(a, b) != min(x, y)) return min(a, b) < min(x, y);
return a >= b;
}
int main() {
memset(ro, -1, sizeof(ro));
scanf("%d %d", &n, &q);
for (int i = 1; i <= n; i++) {
int x;
scanf("%d", &x);
dp[i][0] = x;
g[x].push_back(i);
de[x]++;
}
topo_loop();
logset();
for (int i = 1; i <= n; i++) {
if (de[i]) { // 环上节点
dfs(i, i); // 以节点为根,预处理 LCA
if (ro[i] == -1) dfs1(i, ++tot, 0); // 找此环信息
}
}
for (int i = 1; i <= q; i++) {
int x, y;
scanf("%d %d", &x, &y);
if (root[bel[x]] != root[bel[y]]) { // 情况 1
printf("-1 -1\n");
} else if (bel[x] == bel[y]) { // 情况 2
int lca = LCA(x, y);
printf("%d %d\n", dep[x] - dep[lca], dep[y] - dep[lca]);
} else { // 情况 3
int r1 = bel[x], r2 = bel[y];
int tot1 = dep[x] + (ro[r2] - ro[r1] + len[root[r1]]) % len[root[r1]];
int tot2 = dep[y] + (ro[r1] - ro[r2] + len[root[r2]]) % len[root[r2]];
if (cmp(tot1, dep[y], dep[x], tot2)) printf("%d %d\n", tot1, dep[y]);
else printf("%d %d\n", dep[x], tot2);
}
}
return 0;
}