Note -「基环树」做题记录

写的大多只是思路,比较简单的细节和证明过程就不放了,有需者自取。

基环树简介

简单说一说基环树吧。由名字扩展可得这是一类以环为基础的树(当然显然它不是树。

通常的表现形式是一棵树再加一条非树边,把图画出来是一种向外发散的有趣图案。

体现在【题目条件】上就是一个 (n) 个点 (n) 条边的连通图或保证每一个点的入度 / 出度为 (1) (有向图:前者称为外向树,后者称为内向树)。

常常会把一些在树上做的 dp 放在基环树上以提高题目难度。

惯用思路是先把以环上的点为根的子树内的信息跑完,再整合环的信息。或者直接拆掉环上的边跑树 dp,当然也有其他的各种神魔做法。

好像后续还有仙人掌、仙人球之类的复杂结构。


「ZJOI2008」骑士

题目大意和树形 dp 板子没有上司的舞会类似,只不过树改为了基环树。

考虑环上任意两点不可能都取到,所以可以断掉环上任意一边,以该边两端点 (a, b) 分开跑舞会的 dp 即可。

(dp(u, 0)) 表示 (u) 节点不取能得到的最大值,则该基环树的答案为 (max(dp(a, 0), dp(b, 0)))

基环树题目多半都是基环树森林,并且常常有子环。((

#include <cstdio>
#include <vector>
using namespace std;

typedef long long LL;
LL Max(LL x, LL y) { return x > y ? x : y; }
LL Min(LL x, LL y) { return x < y ? x : y; }
LL Abs(LL x) { return x < 0 ? -x : x; }

int read() {
    int k = 1, x = 0;
    char s = getchar();
    while (s < '0' || s > '9') {
        if (s == '-')
            k = -1;
        s = getchar();
    }
    while (s >= '0' && s <= '9') {
        x = (x << 3) + (x << 1) + s - '0';
        s = getchar();
    }
    return x * k;
}

void write(LL x) {
    if (x < 0) {
        putchar('-');
        x = -x;
    }
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}

void print(LL x, char s) {
    write(x);
    putchar(s);
}

const int MAXN = 1e6 + 5;

LL dp[MAXN][2];
bool flag[MAXN], vis[MAXN];
int val[MAXN], to[MAXN], s, t, p;

struct edge {
	int v, pos;
	edge() {}
	edge(int V, int Pos) {
		v = V;
		pos = Pos;
	}
}; 

vector<edge> mp[MAXN];

void Add_Edge(int u, int v, int p) {
	mp[u].push_back(edge(v, p));
	mp[v].push_back(edge(u, p));
} 

void Circle(int u, int fa) {
	vis[u] = true;
	for(size_t i = 0; i < mp[u].size(); i++) {
		int v = mp[u][i].v;
		if(v == fa)
			continue;
		if(vis[v]) {
			s = u;
			t = v;
			p = mp[u][i].pos;
			continue;
		}
		Circle(v, u);
	}
}

void Tree_Dp(int u, int fa) {
	dp[u][0] = 0;
	dp[u][1] = val[u];
	for(size_t i = 0; i < mp[u].size(); i++) {
		int v = mp[u][i].v;
		if(v == fa || flag[mp[u][i].pos])
			continue;
		Tree_Dp(v, u);
		dp[u][1] += dp[v][0];
		dp[u][0] += Max(dp[v][0], dp[v][1]);
	}
}

int main() {
	int n = read();
	for(int i = 1; i <= n; i++) {
		val[i] = read(), to[i] = read();
		Add_Edge(i, to[i], i);
	} 
	LL ans = 0;
	for(int i = 1; i <= n; i++) {
		if(vis[i])
			continue;
		Circle(i, -1);
		LL tmp = -1;
		flag[p] = true;
		Tree_Dp(s, -1);
		tmp = dp[s][0];
		Tree_Dp(t, -1);
		tmp = Max(tmp, dp[t][0]);
		flag[p] = false;
		ans += tmp; 
	}
	print(ans, '
');
	return 0;
}

「BZOJ1791」「IOI2008」Island 岛屿

求基环树森林中基环树们的直径和。

有题可知每一颗基环树都是一颗内向树。记一颗基环树环上节点的点集为 (V)

对于每一颗根在环上的子树我们定义 (g(u)) 表示以 (u) 为根的子树的直径,(f(u)) 表示以 (u) 为起点在该子树中的最长链。

则可知整棵基环树的直径为 (max(g(u)))(max(f(a) + (a o b) + f(b))) 的较大值,其中 (u, a, b in V)

前一个东西简单,可以直接拓扑排序。后一个东西借鉴单调队列的思想,拆开算贡献就好了,最后扫一遍。

注意后面的那个东西需要区分 ((a o b))((b o a))

#include <cstdio>

typedef long long LL;
LL Max(LL x, LL y) { return x > y ? x : y; }
LL Min(LL x, LL y) { return x < y ? x : y; }
LL Abs(LL x) { return x < 0 ? -x : x; }

int read() {
    int k = 1, x = 0;
    char s = getchar();
    while (s < '0' || s > '9') {
        if (s == '-')
            k = -1;
        s = getchar();
    }
    while (s >= '0' && s <= '9') {
        x = (x << 3) + (x << 1) + s - '0';
        s = getchar();
    }
    return x * k;
}

void write(LL x) {
    if (x < 0) {
        putchar('-');
        x = -x;
    }
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}

void print(LL x, char s) {
    write(x);
    putchar(s);
}

const LL INF = 1e18;
const int MAXN = 1e6 + 5;

struct node {
	int v, w;
	node() {}
	node(int V, int W) {
		v = V;
		w = W;
	}
} q[MAXN];

LL f[MAXN], g[MAXN];
int que[MAXN], in[MAXN], n;

int main() {
	n = read();
	for(int i = 1; i <= n; i++) {
		q[i].v = read(), q[i].w = read();		
		in[q[i].v]++;
	}
	int head = 1, tail = 0;
	for(int i = 1; i <= n; i++)
		if(!in[i])
			que[++tail] = i;
	while(head <= tail) {
		int u = que[head++], v = q[u].v;
		LL val = f[u] + q[u].w;
		g[v] = Max(g[v], f[v] + val);
		g[v] = Max(g[v], g[u]);
		f[v] = Max(f[v], val);
		in[v]--;
		if(!in[v])
			que[++tail] = v;
	}		
	LL ans = 0;
	for(int i = 1, u, End; i <= n; i++)
		if(in[i]) {
			u = i, End = u;
			LL m1 = f[u], m2 = f[u], pre = q[u].w, res1 = g[u], res2 = -INF;
			u = q[u].v;
			while(u != End) {
				in[u] = 0;
				res1 = Max(res1, g[u]);
				res1 = Max(res1, f[u] + m1 + pre);
				res2 = Max(res2, f[u] + m2 - pre);
				m1 = Max(m1, f[u] - pre);
				m2 = Max(m2, f[u] + pre);
				pre += q[u].w;
				u = q[u].v;
			}
			ans += Max(res1, res2 + pre);			
		}
	print(ans, '
');
	return 0;
}

「BZOJ2791」「Poi2012」Rendezvous

码农屑题。就不说题目描述了,分类讨论。

(a)(b) 不在一颗基环树上,肯定无解。

否则,若 (a)(b) 在同一颗基环树的同一个子树下,由第二个限定条件可知一定是到 lca 最优。

(dep(u))(u) 节点在该子树下的深度,(lca(u, v)) 为节点 (u)(v) 在该子树上的最近公共祖先,答案即为:

[x = dep(a) - dep(lca(a, b)), y = dep(b) - dep(lca(a, b)) ]

(a)(b) 不在同一颗基环树的同一个子树下。

则考虑拆分整个过程。不难发现可以等价于 (a) 先走到其子树在环上的根,(b) 同理,再考虑环上这两点的答案。

分别考虑 (a o b)(x, y)(b o a)(x, y) ,再按限定条件输出。

更改了基环树找环的方法,统一使用拓扑霍霍。

#include <queue>
#include <cstdio>
#include <vector>
using namespace std;

int Max(int x, int y) { return x > y ? x : y; }
int Min(int x, int y) { return x < y ? x : y; }
int Abs(int x) { return x < 0 ? -x : x; }

int read() {
    int k = 1, x = 0;
    char s = getchar();
    while (s < '0' || s > '9') {
        if (s == '-')
            k = -1;
        s = getchar();
    }
    while (s >= '0' && s <= '9') {
        x = (x << 3) + (x << 1) + s - '0';
        s = getchar();
    }
    return x * k;
}

void write(int x) {
    if (x < 0) {
        putchar('-');
        x = -x;
    }
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}

void print(int x, char s) {
    write(x);
    putchar(s);
}

const int MAXN = 5e5 + 5;

queue<int> q;
bool vis[MAXN]; 
// vis 工具 
vector<int> in[MAXN], out[MAXN];
int key[MAXN], flag[MAXN], f[MAXN][25], dep[MAXN], Group[MAXN], pos[MAXN], size[MAXN], tot = 0, len = 0;
// flag 是否在环上
// key 该子树在环上的根
// Group 属于那一个连通块 

void dfs(int u, int fa) {
	key[u] = tot;
	for(size_t i = 0; i < out[u].size(); i++) {
		int v = out[u][i];
		if(v == fa || flag[v])
			continue;
		dep[v] = dep[u] + 1;
		f[v][0] = u;
		for(int i = 1; i < 25; i++)
			f[v][i] = f[f[v][i - 1]][i - 1];		
		dfs(v, u);
	}
}

int lca(int u, int v) {
	if(dep[u] < dep[v])
		u ^= v ^= u ^= v;
	for(int i = 24; i >= 0; i--)
		if(dep[f[u][i]] >= dep[v])
			u = f[u][i];
	if(u == v)
		return u;
	for(int i = 24; i >= 0; i--)
		if(f[u][i] != f[v][i]) {
			u = f[u][i];
			v = f[v][i];
		}
	return f[u][0];
}

void Color(int u, int fa) {
	Group[u] = tot;
	for(size_t i = 0; i < out[u].size(); i++) {
		int v = out[u][i];
		if(Group[v])
			continue;
		Color(v, u);
	}
}

void Get_Circle(int u, int fa) {
	if(vis[u])
		return ;
	pos[u] = ++tot;
//	printf("pos[%d] = %d
", u, pos[u]);
	vis[u] = true;
	for(size_t i = 0; i < in[u].size(); i++) {
		int v = in[u][i];
		if(v == fa)
			continue;
		if(flag[v])
			Get_Circle(v, u);
	}
}

int main() {
//	freopen("13.in", "r", stdin);
	int n = read(), m = read();
	for(int i = 1, v; i <= n; i++) {
		v = read();
		in[i].push_back(v);
		flag[v]++;
		out[v].push_back(i);
	}
	// 是否在环上 
	for(int i = 1; i <= n; i++)
		if(!flag[i])
			q.push(i);
	while(!q.empty()) {
		int u = q.front();
		q.pop();
		for(size_t i = 0; i < in[u].size(); i++) {
			int v = in[u][i];
			flag[v]--;
			if(!flag[v])
				q.push(v);
		}
	}
	// LCA 初始化
	for(int i = 1; i <= n; i++)
		if(flag[i]) {
			dep[i] = 1;
			tot = i;
			dfs(i, -1);
		}
	tot = 0;
	// 在哪一个连通块中 
	for(int i = 1; i <= n; i++) 
		if(flag[i] && !Group[i]) {
			tot++;
			Color(i, -1);
		} 
	// 判断环间两点距离 
	for(int i = 1; i <= n; i++)
		if(flag[i] && !vis[i]) {
			tot = 0;
			Get_Circle(i, -1);
			size[Group[i]] = tot;
		}	
//	for(int i = 1; i <= n; i++)
//		printf("flag[%d] = %d, pos[%d] = %d
", i, flag[i], i, pos[i]);
	for(int i = 1, a, b; i <= m; i++) {
		a = read(), b = read();
		if(Group[a] != Group[b]) 
			print(-1, ' '), print(-1, '
');
		else if(key[a] == key[b]) {
//			printf("Asoul %d %d %d
", key[a], key[b], lca(a, b));
			int Lca = lca(a, b), x = dep[a] - dep[Lca], y = dep[b] - dep[Lca];
			print(x, ' '), print(y, '
');
		}
		else {
//			puts("Asoul");
            int root_a = key[a], root_b = key[b], dist;
            dist = (pos[root_b] - pos[root_a] + size[Group[a]]) % size[Group[a]];
            int x_1 = dep[a] - 1 + dist, y_1 = dep[b] - 1;
            dist = (pos[root_a] - pos[root_b] + size[Group[a]]) % size[Group[a]];
            int x_2 = dep[a] - 1, y_2 = dep[b] - 1 + dist;
			
//			printf("%d %d %d %d
", x_1, y_1, x_2, y_2);
			
			if(Max(x_1, y_1) < Max(x_2, y_2))
				print(x_1, ' '), print(y_1, '
');
			else if(Max(x_1, y_1) > Max(x_2, y_2))
				print(x_2, ' '), print(y_2, '
');
			else {
				if(Min(x_1, y_1) < Min(x_2, y_2))
					print(x_1, ' '), print(y_1, '
');
				else if(Min(x_1, y_1) > Min(x_2, y_2))
					print(x_2, ' '), print(y_2, '
');		
				else {
					if(x_1 >= y_1)
						print(x_1, ' '), print(y_1, '
');
					else
						print(x_2, ' '), print(y_2, '
');
				}		
			}
		}
	}
	return 0;
}

「CF1027F」 Session in BSU

码量小清新!

(n) 门考试,对于第 (i) 门可以在 (a_i)(b_i) 两个时间去考,一个时间只能考一门,求考完所有考试的情况下,整个考试周期最早到多久结束(即使得最后一门考试的时间最早)。如果不能考完所有考试,输出 (-1)

考虑 (a_i)(b_i) 连边,每一条边即表示一个我们需要处理的分配时间需求。这样就能形成一个懒得定性的图。

考虑每一个连通块。

若该连通块点数小于边数,则会出现需求量大于时间点数的情况,无解,结束整个程序。

若该连通块点数等于边数,则该连通块内的所有时间点都会分配考试,故答案即为该连通块内的时间最大值。

若该连通块点数大于边数,其实只能是边数比点数少一的情况,所以这是一颗树。即需求量比时间点少一,那就说明该连通块有一个点可以不取,此时答案即为该连通块内的时间最小值。

最后答案即为所以连通块答案的最大值。

统计边数啦点数啦之类的都应该是 dfs 的基本功了嘛。

#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;

int Max(int x, int y) { return x > y ? x : y; }
int Min(int x, int y) { return x < y ? x : y; }
int Abs(int x) { return x < 0 ? -x : x; }

int read() {
    int k = 1, x = 0;
    char s = getchar();
    while (s < '0' || s > '9') {
        if (s == '-')
            k = -1;
        s = getchar();
    }
    while (s >= '0' && s <= '9') {
        x = (x << 3) + (x << 1) + s - '0';
        s = getchar();
    }
    return x * k;
}

void write(int x) {
    if (x < 0) {
        putchar('-');
        x = -x;
    }
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}

void print(int x, char s) {
    write(x);
    putchar(s);
}

const int MAXN = 1e6 + 5;

struct edge {
	int u, v;
	edge() {}
	edge(int U, int V) {
		u = U;
		v = V;
	}
} q[MAXN];

bool vis[MAXN << 1];
vector<int> mp[MAXN << 1];
int ma1, ma2, cnt, tot, num[MAXN << 1];

void Add_Edge(int u, int v) {
	mp[u].push_back(v);
	mp[v].push_back(u); 
}

void dfs(int u, int fa) {
	vis[u] = true;
	tot++;
	if(u > ma1) {
		ma2 = ma1;
		ma1 = u;
	}
	else if(u > ma2)
		ma2 = u;
	for(size_t i = 0; i < mp[u].size(); i++) {
		int v = mp[u][i];
		cnt++;
		if(v == fa || vis[v])
			continue;
		dfs(v, u);
	}
}

int main() {
	int n = read();
	for(int i = 1; i <= n; i++) {
		q[i].u = read(), q[i].v = read();
		num[i] = q[i].u, num[i + n] = q[i].v;
	}
	sort(num + 1, num + (n << 1) + 1);
	int len = unique(num + 1, num + (n << 1) + 1) - num - 1;
	for(int i = 1; i <= n; i++) {
		q[i].u = lower_bound(num + 1, num + len + 1, q[i].u) - num, q[i].v = lower_bound(num + 1, num + len + 1, q[i].v) - num;
		Add_Edge(q[i].u, q[i].v);
	}
	int ans = 0;
	for(int i = 1; i <= len; i++) 
		if(!vis[i]) {
			ma1 = 0, ma2 = 0, cnt = 0, tot = 0;
			dfs(i, -1);
			cnt /= 2;
			if(cnt < tot)
				ans = Max(ans, ma2);
			else if(cnt == tot) 
				ans = Max(ans, ma1);
			else {
				print(-1, '
');
				return 0;
			}
		}
	print(num[ans], '
');
	return 0;
}

「BZOJ3037」创世纪

基环森林上贪心题目。

根据题目要求,对于每一颗基环树,叶子一定不能取,且环上最多取一半。

那么就直接从叶子开始,如果可行就取,一直取到环上。

实现方式多种多样,可以采用拓扑的先跑基环树,单跑环(单环拓扑进不去)的短码写法。

证明在补了在补了 /fad。

原文地址:https://www.cnblogs.com/Chain-Forward-Star/p/15248889.html