Solution -「基环树」做题记录
写的大多只是思路,比较简单的细节和证明过程就不放了,有需者自取。
基环树简介
简单说一说基环树吧。由名字扩展可得这是一类以环为基础的树(当然显然它不是树。
通常的表现形式是一棵树再加一条非树边,把图画出来是一种向外发散的有趣图案。
体现在【题目条件】上就是一个 \(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, '\n');
return 0;
}
「BZOJ1791」「IOI2008」Island 岛屿
求基环树森林中基环树们的直径和。
有题可知每一颗基环树都是一颗内向树。记一颗基环树环上节点的点集为 \(V\)。
对于每一颗根在环上的子树我们定义 \(g(u)\) 表示以 \(u\) 为根的子树的直径,\(f(u)\) 表示以 \(u\) 为起点在该子树中的最长链。
则可知整棵基环树的直径为 \(\max(g(u))\) 和 \(\max(f(a) + (a \to b) + f(b))\) 的较大值,其中 \(u, a, b \in V\)。
前一个东西简单,可以直接拓扑排序。后一个东西借鉴单调队列的思想,拆开算贡献就好了,最后扫一遍。
注意后面的那个东西需要区分 \((a \to b)\) 和 \((b \to 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, '\n');
return 0;
}
「BZOJ2791」「Poi2012」Rendezvous
码农屑题。就不说题目描述了,分类讨论。
若 \(a\) 和 \(b\) 不在一颗基环树上,肯定无解。
否则,若 \(a\) 和 \(b\) 在同一颗基环树的同一个子树下,由第二个限定条件可知一定是到 lca
最优。
记 \(dep(u)\) 为 \(u\) 节点在该子树下的深度,\(lca(u, v)\) 为节点 \(u\) 和 \(v\) 在该子树上的最近公共祖先,答案即为:
\]
若 \(a\) 和 \(b\) 不在同一颗基环树的同一个子树下。
则考虑拆分整个过程。不难发现可以等价于 \(a\) 先走到其子树在环上的根,\(b\) 同理,再考虑环上这两点的答案。
分别考虑 \(a \to b\) 的 \(x, y\) 和 \(b \to 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\n", 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\n", 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, '\n');
else if(key[a] == key[b]) {
// printf("Asoul %d %d %d\n", 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, '\n');
}
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\n", 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, '\n');
else if(Max(x_1, y_1) > Max(x_2, y_2))
print(x_2, ' '), print(y_2, '\n');
else {
if(Min(x_1, y_1) < Min(x_2, y_2))
print(x_1, ' '), print(y_1, '\n');
else if(Min(x_1, y_1) > Min(x_2, y_2))
print(x_2, ' '), print(y_2, '\n');
else {
if(x_1 >= y_1)
print(x_1, ' '), print(y_1, '\n');
else
print(x_2, ' '), print(y_2, '\n');
}
}
}
}
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, '\n');
return 0;
}
}
print(num[ans], '\n');
return 0;
}
「BZOJ3037」创世纪
基环森林上贪心题目。
根据题目要求,对于每一颗基环树,叶子一定不能取,且环上最多取一半。
那么就直接从叶子开始,如果可行就取,一直取到环上。
实现方式多种多样,可以采用拓扑的先跑基环树,单跑环(单环拓扑进不去)的短码写法。
证明在补了在补了 /fad。
Solution -「基环树」做题记录的更多相关文章
- 「BZOJ 1791」「IOI 2008」Island「基环树」
题意 求基环树森林所有基环树的直径之和 题解 考虑的一个基环树的直径,只会有两种情况,第一种是某个环上结点子树的直径,第二种是从两个环上结点子树内的最深路径,加上环上这两个结点之间的较长路径. 那就找 ...
- 「BZOJ 3242」「NOI 2013」快餐店「基环树」
题意 基环树上找到一个点(可以在边上)使得它到树上最远点的距离最小,输出最小距离 题解 如果是一棵树,答案就是树的直径\(/2\) 如果是基环树,那么很好证明删去环上的某一条边是不影响答案的.于是断环 ...
- Solution -「线段树」题目集合
T1 无聊的数列 来自:Link flag 帖先从水题入手. 首先分析题目,它是以等差数列为原型进行的修改.等差数列一大性质就是其差分数列的值除第一项以外均相等. 于是不难想到使用差分数列进行维护. ...
- Sam做题记录
Sam做题记录 Hihocoder 后缀自动机二·重复旋律5 求一个串中本质不同的子串数 显然,答案是 \(\sum len[i]-len[fa[i]]\) Hihocoder 后缀自动机三·重复旋律 ...
- BJOI做题记录
BJOI做题记录 终于想起还要做一下历年省选题了2333 然而咕了的还是比做了的多2333 LOJ #2178. 「BJOI2017」机动训练 咕了. LOJ #2179. 「BJOI2017」树的难 ...
- 退役IV次后做题记录
退役IV次后做题记录 我啥都不会了.... AGC023 D 如果所有的楼房都在\(S\)同一边可以直接得出答案. 否则考虑最左最右两边的票数,如果左边>=右边,那么最右边会投给左边,因为就算车 ...
- 退役III次后做题记录(扯淡)
退役III次后做题记录(扯淡) CF607E Cross Sum 计算几何屎题 直接二分一下,算出每条线的位置然后算 注意相对位置这个不能先搞出坐标,直接算角度就行了,不然会卡精度/px flag:计 ...
- 退役II次后做题记录
退役II次后做题记录 感觉没啥好更的,咕. atcoder1219 历史研究 回滚莫队. [六省联考2017]组合数问题 我是傻逼 按照组合意义等价于\(nk\)个物品,选的物品\(\mod k\) ...
- FJOI2017前做题记录
FJOI2017前做题记录 2017-04-15 [ZJOI2017] 树状数组 问题转化后,变成区间随机将一个数异或一,询问两个位置的值相等的概率.(注意特判询问有一个区间的左端点为1的情况,因为题 ...
随机推荐
- 服务器脚本搭建国基北盛openstack平台
@ 目录 基础环境搭建 控制节点网卡配置 计算节点网卡配置 主机映射 3,关闭防火墙和selinux以及NetworkManager 设置yum源 计算节点分区 配置openrc.sh环境变量 平台组 ...
- 1.12 Linux已经霸占了服务器领域!
如今的 IT 服务器领域是 Linux.UNIX.Windows 三分天下,Linux 系统可谓后起之秀,特别是"互联网热"以来,Linux 在服务器端的市场份额不断扩大,每年增长 ...
- 多级级联数据的展示-vue递归组件
如果采用普通的for循环方式,没办法确认数据到底有几层,要写几个for循环,所以想到了递归的方法. 那么在vue里然后实现呢? vue递归组件(组件中使用自己) 父组件中把数据以props形式传给子组 ...
- 【面试普通人VS高手系列】为什么要使用Spring 框架?
一个工作了4年的小伙伴,他说他从线下培训就开始接触Spring,到现在已经快5年时间了. 从来没有想过,为什么要使用Spring 框架. 结果在面试的时候,竟然遇到一个这样的问题. 大脑一时间短路了, ...
- 1903021121-刘明伟-java十一周作业-java面向对象编程
项目 内容 课程班级博客链接 19级信计班(本) 作业要求链接 第十一周作业 博客名称 1903021121-刘明伟-java十一周作业-java面向对象 要求 每道题要有题目,代码(使用插入代码,不 ...
- 好客租房41-react组件基础综合案例-渲染列表数据
1渲染列表 在state定义数据 进行数据渲染 //导入react import React from 'react' import ReactDOM from 'react-dom' //导入组件 ...
- linux篇-linux 主从配置
1准备两台服务器 一台是192.168.118.128 一台是192.168.118.129 2主服务器配置 192.168.118.128 修改my.cnf文件 server-id=1 log-bi ...
- map计算
map理解 参考1: https://github.com/rafaelpadilla/Object-Detection-Metrics 参考2:https://github.com/rafaelpa ...
- 04C++核心编程
Day01 笔记 1 C++概述 1.1 C++两大编程思想 1.1.1 面向对象 1.1.2 泛型编程 1.2 移植性和标准 1.2.1 ANSI 在1998制定出C++第一套标准 2 c++初识 ...
- .Net分表分库动态化处理
介绍 本期主角:ShardingCore 一款ef-core下高性能.轻量级针对分表分库读写分离的解决方案,具有零依赖.零学习成本.零业务代码入侵 背景 最近有个小伙伴来问我,分表下他有一批数据,这个 ...