树
URAL1039 Anniversary Party
\(O(n)\)
一棵树, 相邻点不能同时选, 问最大点权和
int ind[MAXN], val[MAXN]; int f[MAXN][2]; void DFS(int u, int fa) { f[u][1] += val[u]; for (int i = head[u]; i; i = adj[i].nex) { int v = adj[i].to; if (v == fa) continue; DFS(v, u); f[u][0] += max(f[v][0], f[v][1]); f[u][1] += f[v][0]; } } int main() { n = in(); for (int i = 1; i <= n; ++ i) val[i] = in(); for (int i = 1; i < n; ++ i) { int l = in(), k = in(); ++ ind[l]; addedge(k, l); } for (int i = 1; i <= n; ++ i) if (!ind[i]) { DFS(i, 0); printf("%d\n", max(f[i][0], f[i][1])); } return 0; }
CF1038A.The Fair Nut and the Best Path
好题
\(O(n)\)
一棵树, 有点权和边权, 你可以选一条路径, 满足在点上加油(加点权), 走过一条边耗油(减边权), 任意时刻油量不得为负
不能只选一个点, 求最大点权和\(-\)边权和
例如下图:
\(2\rightarrow 1\rightarrow 3\)
\(2\rightarrow 4\)
Sol:
在一条路径上走, 如果某时刻油量为负了, 那就不合法了
同时, 如果把"+点-边"看做一组 从下一个非负的组开始肯定可以更加优
所以只需要考虑最优, 而不需要考虑合不合法了
LL ans; LL f[MAXN], fi[MAXN], se[MAXN]; void DFS(int u, int fa) { for (int i = head[u]; i; i = adj[i].nex) { int v = adj[i].to; LL w = adj[i].w; if (v == fa) continue; DFS(v, u); se[u] = max(se[u], fi[v] + val[v] - w); if (se[u] > fi[u]) swap(se[u], fi[u]); } ans = max(ans, fi[u] + se[u] + val[u]); }
HDU2196: Computer
\(O(n)\)
维护树上每个点最远能到达的距离, 有边权
- 可以维护每个点向下的最大次大路径, 和往哪个儿子走, 然后第二遍\(DFS\)再算一下往上走的最长路径
- 还可以第一遍只维护最大路径, 第二遍时先处理出除了最大儿子的其他儿子往上的最长, 同时算次大路, 最后更新给大儿子, 这样保证到达这个点时祖先向上走的最长路都已经算好了
第二种的代码(第一种大概要记录两条路的方向, 好烦)
//注意初始化啊, WA了好几发 LL up[MAXN], dw[MAXN], g1[MAXN]; void DFS(int u, int fa) { for (int i = head[u]; i; i = adj[i].nex) { int v = adj[i].to; LL w = adj[i].w; if (v == fa) continue; DFS(v, u); if (dw[v] + w > dw[u]) g1[u] = v, dw[u] = dw[v] + w; } } void DFS2(int u, int fa) { LL se = 0; for (int i = head[u]; i; i = adj[i].nex) { int v = adj[i].to; LL w = adj[i].w; if (v == fa) continue; if (v != g1[u]) up[v] = w + max(dw[u], up[u]), se = max(se, dw[v] + w); else up[v] = w; } up[g1[u]] += max(se, up[u]); for (int i = head[u]; i; i = adj[i].nex) { int v = adj[i].to; if (v == fa) continue; DFS2(v, u); } } int main() { while (~scanf("%d", &n)) { nume = 0; memset(head, -1, sizeof head); memset(up, 0, sizeof up); memset(dw, 0, sizeof dw); memset(g1, 0, sizeof g1); for (int i = 1; i < n; ++ i) { int u; LL w; scanf("%d%lld", &u, &w); addedge(u, i + 1, w); addedge(i + 1, u, w); } DFS(1, 0); DFS2(1, 0); for (int i = 1; i <= n; ++ i) printf("%lld\n", max(up[i], dw[i])); } return 0; }
BZOJ1040 骑士
\(O(n)\)
有\(n(\le 10^6)\)个骑士, 每个人有且仅有一个仇恨对象, 并有一个点权
问选出一些骑士, 满足其中任意两人没有仇恨关系, 最大点权和
Sol:
发现是基环树森林, 那么用栈记录当前的DFS路径, 就可以找环
然后换上的每个点为根做子树的树型DP, 然后再拿环上的点做一个DP, 钦点第一个点选不选
然后WA了, 原来互相仇恨会产生重边, 发现一个联通里最多一组重边, 并且形成了二元环, 那就用异或判反向边的方式回避这个问题, 让他自然形成二元环
也可以单独分类讨论,当树来做
错误原因: 栈中的前一段可能不是环!!!
bool vis[MAXN], flag, onc[MAXN]; int stk[MAXN], top, rec[MAXN], cnt; void DFS(int u, int fa) // findcircle { if (flag) return; if (vis[u]) { flag = true; while (stk[top] != u) rec[++ cnt] = stk[top --]; rec[++ cnt] = stk[top --]; return ; } vis[u] = true; stk[++ top] = u; for (int i = head[u]; i != -1; i = adj[i].nex) { if ((i ^ 1) == fa) continue; int v = adj[i].to; DFS(v, i); } -- top; } LL f[MAXN][2], g[MAXN][2]; void DFS2(int u, int fa) { vis[u] = true; f[u][0] = 0; f[u][1] += val[u]; for (int i = head[u]; i != -1; i = adj[i].nex) { int v = adj[i].to; if ((i ^ 1) == fa || onc[v]) continue; DFS2(v, i); f[u][0] += max(f[v][0], f[v][1]); f[u][1] += f[v][0]; } } LL circle(int opt) { memset(g, 0, sizeof g); g[1][0] = (1LL * (opt == 0) * f[rec[1]][0]); g[1][1] = (1LL * (opt == 1) * f[rec[1]][1]); for (int i = 2; i <= cnt; ++ i) { g[i][0] = max(g[i - 1][0], g[i - 1][1]) + f[rec[i]][0]; g[i][1] = g[i - 1][0] + f[rec[i]][1]; } if (opt == 1) return g[cnt][0]; else return max(g[cnt][0], g[cnt][1]); } void solve() { for (int i = 1; i <= n; ++ i) { if (vis[i]) continue; cnt = top = 0; flag = false; memset(stk, 0, sizeof stk); memset(rec, 0, sizeof rec); DFS(i, -1); for (int j = 1; j <= cnt; ++ j) onc[rec[j]] = true; for (int j = 1; j <= cnt; ++ j) DFS2(rec[j], -1); ans += max(circle(0), circle(1)); } } int main() { memset(head, -1, sizeof head); n = in(); for (int i = 1; i <= n; ++ i) { val[i] = in(); int x = in(); addedge(i, x); addedge(x, i); } solve(); printf("%lld\n", ans); return 0; } /* 0853~1151 */
NOI2002贪吃的九头龙
好题
\(O(n)\)
题意概述:
给一个\(n(\le 300)\)的以\(1\)为根的树, 以及树边的边权
贪吃龙有\(m\)个头
要求:
- 树上的每个点属于一个头(点不要求连续), 每个头至少一个点
- 假如相邻两点属于一个头, 则需要花费边权
- 树根属于大头, 且大头恰有\(k\)个节点
Sol:
仔细观察, 发现除了大头以外, 其他的头都没有区别
而且为了尽量少花费边权, 我们会尽量使得相邻点属于的头不同
也就是说, 只要满足总个数的要求, 就不会无解
然后, 假如这个节点不是大头, 那么假如至少还有另一种不是大头, 那么可以使得儿子节点的头与他不同
如果没有大头了, 那不是大头的儿子就都要花费了
简单设\(f[u][j][0/1]\)为\(u\)为根的子树, 选了\(j\)个大头, \(u\)这个点是不是大头的最有答案
那么可以想到依次扫儿子, 然后枚举这个儿子选了\(k\)个大头来转移
然后考虑让\(j\)倒着扫, 实现滚动
于是发现可以\(j==k\),所以倒着扫也不能解决, 那只能开一个暂时的数组, 存储上一个儿子\(DP\)完时的数据
如果儿子是\(0\), 那么只有\(m=2\)且这个点是\(0\)才需要代价
f[u][0][0] = f[u][1][1] = 0; for (int i = head[u]; i; i = adj[i].nex) { int v = adj[i].to, w = adj[i].w; if (v == fa) continue; DFS(v, u); memcpy(pre, f[u], sizeof pre); memset(f[u], 0x3f3f3f, sizeof f[u]); for (int j = K; j >= 0; -- j) { for (int k = 0; k <= j; ++ k) { f[u][j][1] = min(f[u][j][1], pre[k][1] + min(f[v][j - k][1] + w, f[v][j - k][0])); f[u][j][0] = min(f[u][j][0], pre[k][0] + min(f[v][j - k][0] + (m == 2) * w, f[v][j - k][1])); } } }
BZOJ1304: CQOI2009叶子的染色
给定\(m\)个节点的树, 只知道\(1\)到\(n\)是叶子, 不知道根是哪个
节点可以是透明, 白色, 和黑色. 给出根到每个叶子\(i\)的路径中最后一个有色的节点颜色为\(c[i]\), 即从\(i\)往上跳(包括自己)遇到的第一个颜色
随意钦点一个根, 使得满足以上条件且需要染最少的点, 求最少染色数
Sol:
- 显然染在越浅的地方越优
- 所以可以从下往上染色时, 此时处理的子树的根一定要染色
- 若儿子原先染色跟自己相同, 可以消去它的颜色, 少染一次
- 若不同, 只能保留他的颜色
- 设\(f[u][0/1]\)为以\(u\)为根的儿子方向子树代价, \(g[u][0/1]\)为父亲方向子树代价
- 计算\(g\)的时候算兄弟的贡献时, 可以用父亲的贡献减去自己参与计算时的贡献
int f[MAXN][2], g[MAXN][2]; void DFS(int u, int fa) { int sum = 0, cnt0 = 0, cnt1 = 0; for (int i = head[u]; i; i = adj[i].nex) { int v = adj[i].to; if (v == fa) continue; DFS(v, u); f[u][0] += min(f[v][0] - 1, f[v][1]); f[u][1] += min(f[v][1] - 1, f[v][0]); } } void DFS2(int u, int fa) { ans = min(ans, min(g[u][0] + f[u][0], g[u][1] + f[u][1]) - 1); for (int i = head[u]; i; i = adj[i].nex) { int v = adj[i].to; if (v == fa) continue; g[v][0] = min(g[u][0] + f[u][0] - min(f[v][0] - 1, f[v][1]) - 1, g[u][1] + f[u][1] - min(f[v][1] - 1, f[v][0])) + 1; g[v][1] = min(g[u][1] + f[u][1] - min(f[v][1] - 1, f[v][0]) - 1, g[u][0] + f[u][0] - min(f[v][0] - 1, f[v][1])) + 1; DFS2(v, u); } }
XY题: 好朋友
题意:
\(n\)个点\(n\)条无向边的连通图, 保证没有重边自环, 有边权, 每个点必须选择仅一个相邻的点连接, 问最小的连边的最大值为多少, 不满足连边需求输出”no answer”
输入格式:
第一行一个正整数n,下面n行每行三个数u,v,w,表示u到v有一条边权为w的双向边。
样例输入:
4 1 2 3 2 3 10 3 4 3 1 4 1样例输出:
3数据范围:
50%的数据满足n<=20;
80%的数据满足n<=1000;
100%的数据满足n<=100000,-10^9<=w<=10^9
时间限制: 1S
空间限制: 64M
\(n\)个点\(n\)条边一定要想到环+树, 这题保证联通, 也就是只一个基环树
- 偶数环有两种取边的方法
- 不在环上的点取边确定
- 如果确定不在环上的点向环上的点连边, 则环上的点也取边确定, 相当于其他点
依据这些性质, 可以考虑从一些叶子开始跑, 并将当前度数为\(1\)的点压进队列, 保证队列里的点取边确定
假如这样仍然有没访问到的点, 那么他们都有\(>1\)的度, 即形成一个环, 对于环只要DFS一下即可
注意无解的情况:
- \(n\)是奇数
- 队列里某点的出边连向已经取过的点
- 环上的点确定后, 发现某时刻一个点度数为\(0\) (从两边都给他减了度数)
- DFS环发现是奇数环
int ind[MAXN], ans = INF; queue <int> q; int mat[MAXN]; bool vis[MAXN], vis2[MAXN], now; void DFS(int u, int cnt, int m1, int m2, int fa) { if (now) return; if (vis2[u]) { if ((cnt - 1) % 2 != 0) { printf("no answer\n"); exit(0); } ans = min(ans, max(m1, m2)); now = true; return ; } vis2[u] = true; for (int i = head[u]; i != -1; i = adj[i].nex) { if (now) return; int v = adj[i].to; if ((i ^ 1) == fa || vis[v]) continue; if (cnt % 2 == 1) DFS(v, cnt + 1, min(m1, adj[i].w), m2, i); else DFS(v, cnt + 1, m1, min(m2, adj[i].w), i); } } int main() { memset(head, -1, sizeof head); n = in(); if (n % 2 == 1) return printf("no answer\n"), 0; for (int i = 1; i <= n; ++ i) { int u = in(), v = in(), w = in(); addedge(u, v, w); addedge(v, u, w); ind[u] ++, ind[v] ++; } for (int i = 1; i <= n; ++ i) if (ind[i] == 1) q.push(i); while (!q.empty()) { int u = q.front(); q.pop(); vis[u] = true; int link = 0; for (int i = head[u]; i != -1; i = adj[i].nex) { int v = adj[i].to; if (vis[v]) continue; if (mat[v]) return printf("no answer\n"), 0; ind[v] --; vis[v] = true; mat[u] = mat[v] = 1; ans = min(ans, adj[i].w); link = v; } for (int i = head[link]; i != -1; i = adj[i].nex) { int v = adj[i].to; if (vis[v]) continue; ind[v] --; if (ind[v] == 0) return printf("no answer\n"), 0; if (ind[v] == 1) q.push(v); } } int sta = 0; for (int i = 1; i <= n; ++ i) if (!vis[i]) { sta = i; break; } if (!sta) return printf("%d\n", ans), 0; DFS(sta, 1, INF, INF, -1); printf("%d\n", ans); return 0; }
URAL1018 Binary Apple Tree
\(n\)个点的树, 有树边, 根为\(1\), 要求保留\(m\)条边, 仍然是一颗以\(1\)为根的树, 求他的最大边权和
int siz[MAXN], f[MAXN][MAXN]; void DFS(int u, int fa) { siz[u] = 0; for (int i = head[u]; i; i = adj[i].nex) { int v = adj[i].to; if (v == fa) continue; DFS(v, u); siz[u] += siz[v] + 1; for (int j = min(m, siz[u]); j >= 0; -- j) for (int k = min(j - 1, siz[v]); k >= 0; -- k) f[u][j] = max(f[u][j], f[u][j - 1 - k] + f[v][k] + adj[i].w); } } int main() { n = in(), m = in(); for (int i = 1; i < n; ++ i) { int u = in(), v = in(), w = in(); addedge(u, v, w); addedge(v, u, w); } DFS(1, 0); printf("%d\n", f[1][m]); return 0; }
POJ3107 Godfather
\(n\)个结点的树,要求删除一个节点之后,让剩下的块中节点数量最多的最少. 按编号顺序输出所有方案
Sol:
求树的所有重心
最大的儿子\(\le n/2\)就是重心, 假如大儿子\(>n/2\)就一定可以往那边移, 某时刻\(\le n/2\)
int siz[MAXN], hsiz[MAXN]; void DFS(int u, int fa) { siz[u] = 1; for (int i = head[u]; i; i = adj[i].nex) { int v = adj[i].to; if (v == fa) continue; DFS(v, u); siz[u] += siz[v]; hsiz[u] = max(hsiz[u], siz[v]); } hsiz[u] = max(hsiz[u], n - siz[u]); } int main() { n = in(); for (int i = 1; i < n; ++ i) { int u = in(), v = in(); addedge(u, v); addedge(v, u); } DFS(1, 0); for (int i = 1; i <= n; ++ i) if (hsiz[i] <= n / 2) printf("%d ", i); return 0; }
CF337D Book of Evil
\(n(\le 10^5)\)个点的树, 有\(m(\le n)\)个点有标记, 问有几个点和离他最远的标记点的距离小于\(d(\le n)\)
Sol:
记录每个点子树内的答案数, 在算一遍子树外的答案数
int mark[2][MAXN]; bool vis[2][MAXN]; void DFS1(int u, int fa) { for (int i = head[u]; i; i = adj[i].nex) { int v = adj[i].to; if (v == fa) continue; DFS1(v, u); if (vis[0][v]) { vis[0][u] = true; mark[0][u] = max(mark[0][u], mark[0][v] + 1); } } // printf("0 %d : %d %d\n", u, mark[0][u], vis[0][u]); } void DFS2(int u, int fa) { // printf("1 %d : %d %d\n", u, mark[1][u], vis[1][u]); int fi = -1, se = -1; for (int i = head[u]; i; i = adj[i].nex) { int v = adj[i].to; if (v == fa) continue; if (vis[0][v]) se = max(se, mark[0][v] + 1); if (se > fi) swap(fi, se); } for (int i = head[u]; i; i = adj[i].nex) { int v = adj[i].to; if (v == fa) continue; if (vis[0][u]) { if (!vis[0][v] || mark[0][v] + 1 != fi) { vis[1][v] = true; mark[1][v] = fi + 1; } else if (mark[0][v] + 1 == fi && se != -1) { vis[1][v] = true; mark[1][v] = se + 1; } } if (vis[1][u]) { vis[1][v] = true; mark[1][v] = max(mark[1][v], mark[1][u] + 1); } DFS2(v, u); } } int main() { n = in(); m = in(); d = in(); for (int i = 1; i <= m; ++ i) { p[i] = in(), mark[0][p[i]] = mark[1][p[i]] = 0; vis[0][p[i]] = vis[1][p[i]] = true; } for (int i = 1; i < n; ++ i) { int u = in(), v = in(); addedge(u, v); addedge(v, u); } DFS1(1, 0); DFS2(1, 0); int ans = 0; for (int i = 1; i <= n; ++ i) if ((!vis[0][i] || (vis[0][i] && mark[0][i] <= d)) && (!vis[1][i] || (vis[1][i] && mark[1][i] <= d))) ++ ans; printf("%d\n", ans); return 0; }
CF739B Alyona and a tree
\(n(\le 200000)\)个点的树, 有点权边权, 根为\(1\), 一个节点\(u\)能控制子树(不含自己)中和他的距离小于那个点点权的点
求每个点控制的点数
Sol:
记录当前根到这个点路径上各个深度是哪个点, 然后由于\(dis[u]-dis[x]\le a[u]\)满足单调性, 可以二分找
然后差分做标记
int n, m; LL a[MAXN]; int lg[MAXN], rec[MAXN], tag[MAXN]; LL dis[MAXN], dep[MAXN]; void solve(int u) { int l = 1, r = dep[u], ret; while (l <= r) { int mid = (l + r) >> 1; if (dis[u] - a[u] <= dis[rec[mid]]) ret = mid, r = mid - 1; else l = mid + 1; } ++ tag[u]; -- tag[rec[ret - 1]]; } void DFS(int u, int fa) // dis[u] - a[u] <= dis[x] { dep[u] = dep[fa] + 1; rec[dep[u]] = u; solve(u); for (int i = head[u]; i != -1; i = adj[i].nex) { int v = adj[i].to; if (v == fa) continue; dis[v] = dis[u] + adj[i].w; DFS(v, u); } } void DFS2(int u, int fa) { for (int i = head[u]; i != -1; i = adj[i].nex) { int v = adj[i].to; if (v == fa) continue; DFS2(v, u); tag[u] += tag[v]; } } int main() { memset(head, -1, sizeof head); n = in(); for (int i = 1; i <= n; ++ i) a[i] = in(); for (int i = 2; i <= n; ++ i) { int to = in(), w = in(); addedge(to, i, w); addedge(i, to, w); } DFS(1, -1); DFS2(1, -1); for (int i = 1; i <= n; ++ i) printf("%d ", tag[i] - 1); return 0; }
ZOJ3649 Social Net
题意: 给一个图, 有点权边权, 让你求最大生成树的值, 之后询问这棵树上的两个点\(x,y\)的路径信息, 即从\(x\)走向\(y\)的路径上, 后面的点减去前面的点的最大差值(有方向)
Sol:
可以想到用一些倍增数组维护信息, 需要:
- 向上跳...步后的祖先
- 向上跳...步经过的最大/最小值
- 向上跳...步这个路径 从上到下/从下到上 的最大差值(两个方向)
注意: 预处理维护倍增最大差值时要考虑前半段后半段, 以及跨越两段的差值, 而查询时也一样, 不能只把二进制拆分后的每段最大差值求最大, 也要像之前一样考虑跨越两端的差值
错因: 之前只做过倍增拆分后段与段之间无关的题(如最大最小, 求和), 没想到查询时也要这样维护, 考虑不周全
//错误片段 int LCA(int x, int y) { int ret = 0, mnl = a[x], mxr = a[y]; while (dep[x] < dep[y]) { ret = max(ret, f[1][lg[dep[y] - dep[x]]][y]); mxr = max(mxr, mx[lg[dep[y] - dep[x]]][y]); y = up[lg[dep[y] - dep[x]]][y]; } while (dep[x] > dep[y]) { ret = max(ret, f[0][lg[dep[x] - dep[y]]][x]); mnl = min(mnl, mn[lg[dep[x] - dep[y]]][x]); x = up[lg[dep[x] - dep[y]]][x]; } ret = max(ret, mxr - mnl); if (x == y) return ret; for (int i = lg[dep[x]]; i >= 0; -- i) if (up[i][x] != up[i][y]) { ret = max(ret, max(f[0][i][x], f[1][i][y])); mnl = min(mnl, mn[i][x]); mxr = max(mxr, mx[i][y]); x = up[i][x], y = up[i][y]; } mnl = min(mnl, mn[0][x]), mxr = max(mxr, mx[0][y]); ret = max(max(ret, mxr - mnl), max(f[0][0][x], f[1][0][y])); return ret; }
做法已经改对了, 可是还是WA, 对拍也没有问题...
次日, 数据生成的点权可以再大一点? 题目给了\(10^5\), 我rand
\(5*10^4\)
一拍真错了, 错因: INF
设得太小
int n, m, q; LL a[MAXN]; struct Edge { int u, v; LL w; } edge[MAXN]; bool cmp(Edge a, Edge b) { return a.w > b.w; } int fa[MAXN]; int getf(int x) { return fa[x] == x ? x : (fa[x] = getf(fa[x])); } bool unite(int x, int y) { int fx = getf(x), fy = getf(y); if (fx == fy) return true; else { fa[fx] = fy; return false; } } LL Kruskal() { LL ret = 0; for (int i = 1; i <= n; ++ i) fa[i] = i; sort(edge + 1, edge + m + 1, cmp); for (int i = 1, cnt = 0; i <= m; ++ i) { int u = edge[i].u, v = edge[i].v; if (unite(u, v)) continue; addedge(u, v); addedge(v, u); ret += 1LL * edge[i].w; if (++ cnt == n - 1) break; } return ret; } int dep[MAXN], lg[MAXN]; LL up[18][MAXN], mx[18][MAXN], mn[18][MAXN], f[2][18][MAXN]; //f[0] 向上走的路径, f[1]向下走的路径 void DFS(int u, int fa) { dep[u] = dep[fa] + 1; up[0][u] = fa; mx[0][u] = max(a[fa], a[u]); mn[0][u] = u == 1 ? a[u] : min(a[fa], a[u]); f[0][0][u] = ((u == 1) ? 0LL : max(0LL, a[fa] - a[u])); f[1][0][u] = ((u == 1) ? 0LL : max(0LL, a[u] - a[fa])); for (int i = 1; (1 << i) <= dep[u]; ++ i) { up[i][u] = up[i - 1][up[i - 1][u]]; mx[i][u] = max(mx[i - 1][u], mx[i - 1][up[i - 1][u]]); mn[i][u] = min(mn[i - 1][u], mn[i - 1][up[i - 1][u]]); f[0][i][u] = max(max(f[0][i - 1][u], f[0][i - 1][up[i - 1][u]]), mx[i - 1][up[i - 1][u]] - mn[i - 1][u]); f[1][i][u] = max(max(f[1][i - 1][u], f[1][i - 1][up[i - 1][u]]), mx[i - 1][u] - mn[i - 1][up[i - 1][u]]); } for (int i = head[u]; i != -1; i = adj[i].nex) { int v = adj[i].to; if (v == fa) continue; DFS(v ,u); } } void LCA(LL x, LL y, LL & lca, LL & mnl, LL & mxr) { mnl = a[x], mxr = a[y]; while (dep[x] < dep[y]) { mxr = max(mxr, mx[lg[dep[y] - dep[x]]][y]); y = up[lg[dep[y] - dep[x]]][y]; } while (dep[x] > dep[y]) { mnl = min(mnl, mn[lg[dep[x] - dep[y]]][x]); x = up[lg[dep[x] - dep[y]]][x]; } if (x == y) return (void)(lca = x); for (int i = lg[dep[x]]; i >= 0; -- i) if (up[i][x] != up[i][y]) { mnl = min(mnl, mn[i][x]); mxr = max(mxr, mx[i][y]); x = up[i][x], y = up[i][y]; } mnl = min(mnl, mn[0][x]), mxr = max(mxr, mx[0][y]); lca = up[0][x]; } LL getup(LL x, LL lca) { LL nowmn = 100001, ret = 0, i ; while (x != lca) { i = lg[dep[x] - dep[lca]]; ret = max(ret, f[0][i][x]); ret = max(ret, mx[i][x] - nowmn); nowmn = min(nowmn, mn[i][x]); x = up[i][x]; } return ret; } LL getdw(int lca, int x) { LL nowmx = 0, ret = 0, i; while (x != lca) { i = lg[dep[x] - dep[lca]]; ret = max(ret, f[1][i][x]); ret = max(ret, nowmx - mn[i][x]); nowmx = max(nowmx, mx[i][x]); x = up[i][x]; } return ret; } int main() { for (int i = 1; i <= 50000; ++ i) lg[i] = lg[i - 1] + ((2 << lg[i - 1]) == i); while (scanf("%d", &n) != EOF) { nume = 0; memset(head, -1, sizeof head); memset(mn, 0x3f3f3f, sizeof mn); memset(mx, 0, sizeof mx); memset(f, 0, sizeof f); memset(up, 0, sizeof up); memset(dep, 0, sizeof dep); for (int i = 1; i <= n; ++ i) scanf("%lld", &a[i]); scanf("%d", &m); for (int i = 1; i <= m; ++ i) { LL u, v, w; scanf("%lld%lld%lld", &u, &v, &w); edge[i] = (Edge) { u, v, w }; } printf("%lld\n", Kruskal()); DFS(1, 0); scanf("%d", &q); while (q --) { LL x, y; scanf("%lld%lld", &x, &y); LL lca, mnl, mxr; LCA(x, y, lca, mnl, mxr); printf("%lld\n", max(mxr - mnl, max(getup(x, lca), getdw(lca, y)))); } } return 0; } /* */
Codeforces 294E. Shaass the Great
题意:
一棵\(N(\le 5000)\)个点的树, 有边权, 你要选择一条边删去, 再新连一条同样权值的边, 要求保证仍然是一棵树, 且所有点对距离和最小, 求这个值
Sol:
假设删去一条边\((u,v)\), \(v\)方向的大小为\(l\), \(u\)方向大小为\(n-l\)
则对于\(u\)方向的某一个边\((p,q)\), \(q\)靠近\(u\):
称\(q\rightarrow p\)方向为\(p\)的子树,大小为\(x\)
- 若新边连到子树外, 则这条边的贡献仍然是\(w*x*(n-x)\)为变
- 若连到子树内, 则贡献变为\(w*(x+l)*(n-x-l)\), 增加了\(w*l*(n-2x)\)(可能为负)
对于\(v\)方向也同理
那么枚举删边之后, 显然\(l\)是确定的, 而对于某条边\((p,q)\), \(x\)也是确定的, 所以可以对于两个方向设\(f[u][0/1]\)表示\(u\)节点子树内/外有连接点时, 子树内所有边的最小增加贡献(可能为负), 然后DP一下
- \(f[u][0]\), 内, 连接处在某一个儿子
- 否则儿子也不存在连接点
注意自己设定的DP的定义: 连接点可以放在字数的根, 不计算根到根的父亲的贡献
\(O(n^2)\)
int n, m; LL tot, ans; int head[MAXN], nume; struct Adj { int nex, to, w; } adj[MAXN << 1]; void addedge(int from, int to, int w) { adj[++ nume] = (Adj) { head[from], to, w }; head[from] = nume; } void link(int u, int v, int w) { addedge(u, v, w); addedge(v, u, w); } int siz[MAXN][2]; LL f[MAXN][2]; void DFS2(int u, int fa, int l) { f[u][0] = f[u][1] = 0; siz[u][1] = 1; for (int i = head[u]; i; i = adj[i].nex) { int v = adj[i].to; if (v == fa) continue; // printf("%d --> %d (%d)\n", u, v, l); DFS2(v, u, l); siz[u][1] += siz[v][1]; f[u][1] += f[v][1]; } f[u][0] = f[u][1]; for (int i = head[u]; i; i = adj[i].nex) { int v = adj[i].to, w = adj[i].w; if (v == fa) continue; f[u][0] = min(f[u][0], f[u][1] - f[v][1] + f[v][0] + 1LL * w * l * (n - 2 * siz[v][1] - l)); } // printf("f[%d][0] = %lld f[%d][1] = %lld\n", u, f[u][0], u, f[u][1]); } void DFS(int u, int fa) // 枚举每一条边 { siz[u][0] = 1; for (int i = head[u]; i; i = adj[i].nex) { int v = adj[i].to, w = adj[i].w; if (v == fa) continue; DFS(v, u); // printf("%d <==> %d (%d)\n", u, v, w); tot += 1LL * w * siz[v][0] * (n - siz[v][0]); siz[u][0] += siz[v][0]; DFS2(u, v, siz[v][0]); DFS2(v, u, n - siz[v][0]); ans = min(ans, f[u][0] + f[v][0]); } } int main() { n = in(); for (int i = 1; i < n; ++ i) { int u = in(), v = in(), w = in(); link(u, v, w); } DFS(1, 0); printf("%lld\n", ans + tot); return 0; }
什么? 删掉的边直接连两个树的重心就是最优的?
怎么证啊
换一个思路, 不是考虑每条边的贡献, 而是每个点的贡献
删边后得到两棵树, 此时总距离和=子树内距离和+子树间距离和
而两个子树都只有一个出口, 即所有点要到这个点, 那么显然是选择子树中到所有点距离和最小的那个点重心了
BZOJ2427: [HAOI2010]软件安装 <简单环缩点 + 树型DP>
简单题
竟然忘记只有树根才能取值??!!
统计子树内外的颜色数量
子树内: 所有点标记+1, 同种颜色DFS序相邻的LCA 标记-1
子树外: 所有同种颜色的LCA就是第一个独占他的
子树外:
考虑找出每个子树独占的颜色数, 然后子树外的颜色数就是 总颜色数 - 独占颜色
处理出每种颜色的最左边和最右边的DFS序, 然后最小的/最深的 独占这个颜色的子树就是 最小的包含这个区间的子树区间, 在这个子树的根打标记, 然后贡献他的祖先
可以将颜色区间和子树区间排序, 用个指针插入set来二分找到
struct Node { int x, y, id; bool operator < (const Node b) const { return (y != b.y) ? (y < b.y) : (x > b.x); } } a[MAXN], col[MAXN]; /// a[u] = (Node) { dfn[u], idx, u } ; if (!col[c[u]].id) { col[c[u]] = (Node) { dfn[u], dfn[u], c[u] }; } else { col[c[u]].x = min(col[c[u]].x, dfn[u]); col[c[u]].y = max(col[c[u]].y, dfn[u]); } /// sort(a + 1, a + n + 1, cmp); sort(col + 1, col + m + 1, cmp); for (int i = 1, j = 0; i <= m && j <= n; ++ i) { if (!col[i].id) continue; while (a[j + 1].x <= col[i].x && j + 1 <= n) ++ j, S.insert(a[j]); set <Node> :: iterator it = S.lower_bound(col[i]); Node fd = *it; tag[fd.id] ++; }
其实这个子树的根就是所有这种颜色的LCA
所以直接不断求相邻LCA即可
子树内
树上差分
首先给每个点打上一个标记, 表示这个点的颜色可以贡献给所有祖先
然后考虑怎么减去重复计算的颜色
对于每种颜色, 按照DFS序把相邻(挑出这种颜色后相邻, 即原DFS序中最近的)的两个点的标记-1
为什么只需要减去相邻的LCA而不是所有两点的LCA呢?
考虑树上一个节点有多个儿子, 如果这棵子树有这种颜色, 那么应当只贡献一次, 所以加总过来应当是\(\sum tag_{son} - num\)
然后还有儿子有这种颜色, 父亲也有的情况, 也应当-1
即应当对所有 相邻的 有这种颜色的 两个儿子的 父亲的标记-1
然后就会发现DFS序上相邻的LCA-1, 就会枚举到所有层面:
- 两个兄弟有同种颜色: 左儿子的最右DFS序 和 右儿子最左DFS序 相邻
- 第一个祖先和自己相邻
这样就是完整的了
线性做法(查询\(\ge\)k):
要算一个子树内外的颜色数量:
子树内的DFS序是连续的, 子树外是除了这一段的前缀 + 后缀拼成的
所以可以复制一整段DFS序接到后面
然后放一个指针在后面表示子树内的右端点
另一个指针在前面找出最靠右的符合的左端点(右指针向右移动左指针肯定也向右, 保证了复杂度)
查询的时候看最右左端点是否在子树左边界以内即可
NOIP2012d2t3 疫情控制
有一棵有边权的树, 从1到n标号, 根是1号
有m个军队分布在点上(一个点可能有多个)
要求 同时 移动一些军队到一些点(但是不能移到 根 ), 使得根到每个叶子的路径上都有军队, 并且移动时间最小(即军队最大移动时间最小)
Sol:
看错几次题意;
可以想到二分答案然后检验, 暴力把所有军队尽量跳到最靠近根的点(倍增), 然后有些点跳到1了, 还有些留在子树里跳不上来, 如果后者可以满足某棵子树的要求, 那么之后就不用管这个子树了
接下来: 有些子树一个军队也没有, 其他子树军队全部跳到了1
这些到1的要调到一些1的儿子
如果直接贪心匹配最远的和最近的, 那么自己的军队跳上去又跳回来是会多算的
假如所有点跳上去又跳回原来的树, 时间仍然允许, 也就是就算多算一点也没事, 那么就可以上述贪心匹配了, 大不了多算了跳回自己
接下来是关键:
假如某个子树有些点跳上去回不来(显然是深度最深的那几个), 那么根据贪心的策略, 选那些深度较浅的点(所有)跳回来会产生浪费(可以自己留着, 让这些点去满足其他的), 而深度更深(其他子树)的点肯定又跳不过来.
所以得到结论: 这棵子树深度最深的的点假如跳不回来, 就让他留着
这样之后, 剩下的点都是就算多算距离也能回到自己, 就可以贪心匹配了
BZOJ3626: [LNOI2014]LCA <树 + 转化 + 树剖 + 线段树 + 差分>
给出一个 \(n\) 个节点的有根树(编号为 \(0\) 到 \(n-1\),根节点为 \(0\))。一个点的深度定义为这个节点到根的距离 \(+1\)。
有q次询问,每次询问给出l r z
求 \(\sum_{l<=i<=r}dep[LCA(i,z)]\)
Sol:
假设只有一个询问, l r z
显然可以对与 \([l, r]\) 和 \(z\) 暴力求所有LCA的深度, 复杂度\(O(n\lg n)\)
有没有更快的做法呢?
随便以 \(1\) 为根, 然后查询每个点能作为几个LCA(i, z), \(O(n)\)
具体做法就是统计每个点的子树中有\([l, r]\)里的几个点, LCA都在1~z的链上, 设这条链为\(u_0, u_1, \dots, u_k\), 容易得到答案为\(\sum dep[u_i] \cdot (cnt[u_i] - cnt[u_{i + 1}])\)
然后考虑本体:
区间\([l, r]\)没有什么具体意义, 应该不能对单个询问\(O(n)\)暴力\([l, r]\)变成\(O(\lg n)\), 能优化的话应该是优化所有查询
不难想到有些询问的\([l, r]\)是有交集的, 对于这部分可以尝试同意对一些询问z统计答案
上面那个式子化简得到\(\sum_{i=0} (i + 1) \cdot (cnt[u_i] - cnt[u_{i + 1}]) = \sum_{i=0} cnt[u_i]\)
这样就比较方便了, 如果能快速得到cnt[]
, 用树剖求和1~z的链就行了
然后想到差分\([l, r]\rightarrow [1, l - 1], [1, r]\), 所以先离线询问, 排序, 依次对链上的cnt[]
加1, 这时r=i的询问\([1, r]\)已经搞定, 求一次答案, 然后减1来一遍减去\([1, l - 1]\)的答案即可
\(O(n\lg ^2 n)\)
struct Ask { int l, r, z, id; } ask[MAXN]; bool cmpl(Ask a, Ask b) { return a.l < b.l; } bool cmpr(Ask a, Ask b) { return a.r < b.r; } const LL MOD = 201314; LL ans[MAXN]; int siz[MAXN], dep[MAXN]; int idx, dfn[MAXN], fa[MAXN], top[MAXN], hson[MAXN]; void DFS(int u, int ma) { siz[u] = 1; fa[u] = ma; dep[u] = dep[ma] + 1; for (int i = head[u]; i; i = adj[i].nex) { int v = adj[i].to; if (v == ma) continue; DFS(v, u); siz[u] += siz[v]; if (!hson[u] || siz[v] > siz[hson[u]]) hson[u] = v; } } void DFS2(int u, int t) { dfn[u] = ++ idx; top[u] = t; if (hson[u]) DFS2(hson[u], t); for (int i = head[u]; i; i = adj[i].nex) { int v = adj[i].to; if (v == hson[u] || v == fa[u]) continue; DFS2(v, v); } } namespace Segt { #define ls (now << 1) #define rs (now << 1 | 1) #define mid ((l + r) >> 1) const int MAXT = MAXN << 2; LL tag[MAXT], sum[MAXT]; void pushup(int now) { sum[now] = (sum[ls] + sum[rs]) % MOD; } void addtag(int now, int l, int r, int v) { (sum[now] += (1LL * v * (r - l + 1) % MOD)) %= MOD; (tag[now] += v) %= MOD; } void pushdown(int now, int l, int r) { if (!tag[now]) return ; addtag(ls, l, mid, tag[now]); addtag(rs, mid + 1, r, tag[now]); tag[now] = 0; } LL query(int now, int l, int r, int x, int y) { if (x > r || y < l) return 0; if (x <= l && r <= y) return sum[now]; pushdown(now, l, r); return (query(ls, l, mid, x, y) + query(rs, mid + 1, r, x, y)) % MOD; } void modify(int now, int l, int r, int x, int y, int v) { if (x > r || y < l) return; if (x <= l && r <= y) { addtag(now, l, r, v); return; } pushdown(now, l, r); modify(ls, l, mid, x, y, v); modify(rs, mid + 1, r, x, y, v); pushup(now); } void clear(int now, int l, int r) { sum[now] = tag[now] = 0; if (l == r) return; clear(ls, l, mid); clear(rs, mid + 1, r); } #undef ls #undef rs #undef mid } void modify(int x, int y, int v) { while (top[x] != top[y]) { if (dep[top[x]] > dep[top[y]]) swap(x, y); Segt::modify(1, 1, n, dfn[top[y]], dfn[y], v); y = fa[top[y]]; } if (dep[x] > dep[y]) swap(x, y); Segt::modify(1, 1, n, dfn[x], dfn[y], v); } LL query(int x, int y) { LL ret = 0; while (top[x] != top[y]) { if (dep[top[x]] > dep[top[y]]) swap(x, y); (ret += Segt::query(1, 1, n, dfn[top[y]], dfn[y])) %= MOD; y = fa[top[y]]; } if (dep[x] > dep[y]) swap(x, y); (ret += Segt::query(1, 1, n, dfn[x], dfn[y])) %= MOD; return ret; } int main() { n = in(); m = in(); for (int i = 2; i <= n; ++ i) { int fa = in() + 1; addedge(fa, i); } DFS(1, 0); DFS2(1, 1); for (int i = 1; i <= m; ++ i) ask[i] = (Ask) { (int)in() + 1, (int)in() + 1, (int)in() + 1, i } ; sort(ask + 1, ask + m + 1, cmpr); for (int i = 1, j = 1; i <= n; ++ i) { modify(1, i, 1); while (ask[j].r < i && j <= m) ++ j; while (ask[j].r == i && j <= m) { (ans[ask[j].id] += query(1, ask[j].z)) %= MOD; ++ j; } } Segt::clear(1, 1, n); sort(ask + 1, ask + m + 1, cmpl); for (int i = 1, j = 1; i <= n; ++ i) { while (ask[j].l < i && j <= m) ++ j; while (ask[j].l == i && j <= m) { (ans[ask[j].id] += query(1, ask[j].z)) %= MOD; ++ j; } modify(i, 1, -1); } for (int i = 1; i <= m; ++ i) printf("%lld\n", (ans[i] + MOD) % MOD); return 0; }