基环树总结

基环树

一、定义

基环树是一个由 n n n 个点及 n n n 条边组成的联通图,其比树多出一条边,所以称作基环树;

存在多颗基环树即基环树森林,不一定保证多颗基环树之间连通;

有向基环树又分为 内向基环树 和 外向基环树;

内向基环树,即每个点出度为 1 的基环树,外向基环树,即每个点入度为 1 的基环树;

对于有关基环树问题,一般有两种解决方式,

  1. 将环上的每个节点作为根,先计算其子树贡献,再将其贡献放入环中;
  2. 将环断开一条边,则作普通树处理;

二、找环

1. tarjan 算法

搜索每个节点,标记节点的 DFN 值,并记录每个结点的前驱;

若搜索到一条返祖边,则从当前节点沿前驱节点回溯至边的另一端点,则回溯路上的所有节点均为环上的节点;

vector <edge> 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. 拓扑排序

记录每个节点的度数,进行拓扑排序;

则入度 ≥ 2 \geq 2 2 的点即为环上的点;

则拓扑排序后度数不为 0 的节点即为环上的点;

void topo_loop() {
	queue <int> 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;
}

三、题目

骑士

1. 题目

n n n 个人,每个人有一个最恨的人,若最恨的人加入任务,则自己一定不会加入任务,现给出每个人的战斗力值,求战斗力值总和最大值;

2. 分析

可以发现,若此题为一棵树,及只有 n − 1 n - 1 n1 条边,则可使用树形 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,最后取最大值即可;

3. 代码
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm> 
using namespace std;
const int MAXN = 1000005;
const int INF = 2147483647;
int n;
long long a[MAXN], v[MAXN];

vector <int> 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;
}

Island

1. 题目

给定一个基环树森林,求每颗基环树直径之和;

2. 分析

若对于一棵树,可通过搜索得到每个节点的向下最长路和次长路,相加即为经过该节点的最长路,计算经过每个点最长路后比较最大值即可;

考虑有环情况,

可发现,直径有三种情况;

  1. 直径为经过环上一点的最长路,而不经过环上的边;
  2. 直径为经过环上的边,即为 环上一点的向下最长路 + 环上另一点的向下最长路 + 两点间的距离;

对于情况一,以环上每个节点为根,进行 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 即可;

使用单调队列;

3. 代码
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 2000005;
int n;
long long sum[MAXN], ans = 0, tot;

struct edge {
	int to;
	long long tot;
};
vector <edge> 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;
}

RAN-Rendezvous

1. 题目

给定一颗基环树,现给定若干个 a , b a, b a,b 求满足下面条件的 x , y x, y x,y

  1. 从顶点 a 沿着出边走 x 步和从顶点 b 沿着出边走 y 步后到达的顶点相同;
  2. 在满足条件 1 的情况下 max(x,y) 最小;
  3. 在满足条件 1 和 2 的情况下 min(x,y) 最小;
  4. 在满足条件 1 、2 和 3 的情况下 x>=y ;
2. 分析

若对于一棵树,此题则为求两点 LCA 到其距离;

则对于基环树,可分类讨论,

  1. 若两点不在同一颗基环树上,
    输出 -1 ;

  2. 若两点在同一颗基环树上,且两点均在一颗以环上一点为根的子树里;

    直接两点 LCA 到其距离即可;

  3. 若两点在同一颗基环树上,但两点不在一颗以环上一点为根的子树里;

    则两点相遇点一定在两点所在子树的根节点之一,分别计算即可;

3. 代码
#include <cstdio>
#include <cmath>
#include <vector>
#include <queue>
#include <cstring>
#include <algorithm> 
using namespace std;
const int MAXN = 500005;
int n, q;
vector <int> 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 <int> 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;
}
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
基环树c,可以使用深度优先搜索(DFS)算法来实现。以下是使用vector存储图的一个示例代码: ```cpp #include <iostream> #include <vector> using namespace std; vector<bool> visited; // 记录节点是否已经访问过 vector<int> parent; // 记录每个节点的父节点 vector<bool> inCycle; // 记录节点是否在中 vector<int> cycle; // 存储到的 // 深度优先搜索 bool dfs(vector<vector<int>>& graph, int node, int parentNode) { visited[node] = true; parent[node] = parentNode; for (int neighbor : graph[node]) { if (!visited[neighbor]) { if (dfs(graph, neighbor, node)) return true; } else if (neighbor != parentNode) { // 到了 cycle.push_back(neighbor); inCycle[neighbor] = true; int cur = node; while (cur != neighbor) { cycle.push_back(cur); inCycle[cur] = true; cur = parent[cur]; } cycle.push_back(neighbor); // 将最后一个节点加入中 return true; } } return false; } // 基环树c vector<int> findCycle(vector<vector<int>>& graph) { int n = graph.size(); visited.resize(n, false); parent.resize(n, -1); inCycle.resize(n, false); for (int i = 0; i < n; i++) { if (!visited[i] && dfs(graph, i, -1)) break; } return cycle; } int main() { int n = 4; // 图的节点数 vector<vector<int>> graph(n); // 添加图的边 graph[0].push_back(1); graph[1].push_back(0); graph[1].push_back(2); graph[2].push_back(1); graph[2].push_back(3); graph[3].push_back(2); vector<int> cycle = findCycle(graph); // 输出c中的节点 cout << "c中的节点: "; for (int node : cycle) { cout << node << " "; } cout << endl; return 0; } ``` 以上代码通过深度优先搜索遍历图,当遇到一个已经访问过的节点时,如果该节点不是当前节点的父节点,则到了一个。然后通过记录每个节点的父节点,可以回溯到构成该的所有节点。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值