借鉴博客
一些题目: (由易到难)
洛谷P3258松鼠的新家
洛谷P2680运输计划
洛谷P1084疫情控制
树上差分是一个很好的技巧. 主要分为下面两种:
首先我们除了一般的up,deep等数组以外,多开一个数组:sum。
sum用来记录点的出现次数(具体点说实际上记录的是点到其父亲的边的出现次数, 我们这样处理:
sum[u]++,sum[v]++,sum[LCA(u, v)]-=2。(记住:最后要从所有叶结点把权值向上累加)以一次操作为例,我们来看看效果(可以画一张图)。首先sum[u]++,一直推上去到根,这时候u到root的路径访问次数都+1,tmp[v]++后,v到lca路径加了1,u到lca路径加了1,而lca到根的路径加了2。
这时,我们只需要sum[LCA(u, v)]-=2,推到根,就能把那些多余的路径减掉,达到想要的目的。而这是一次操作,对于很多次操作的话,我们只需要维护sum,而不必每次更新到根,维护好sum最后dfs一遍即可。这时如果sum[i]==次数的话,说明 i 到其父亲的边是被所有路径覆盖的。如图
例题: 运输控制
首先如果我们知道了一个时间, 我们就可以找到那些点对之间不可能在这个时间完成或者可能, 所以我们考虑二分答案。 怎么check了?对于不满足条件的路径, 记一个数量cnt, 还要记录其中最大的路径值mx, 我们要做的就是找一条边它被覆盖了>=cnt次, 并且该条边权是>=mx-mid的, 如果存在那么这个mid就是答案的一种可能. 所以这里涉及了边的覆盖, 就要用上面所讲的知识
AC Code
const int maxn = 3e5 + 5;
int up[maxn][21], sum[maxn];
int deep[maxn], dis[maxn], dd[maxn];
int cnt, head[maxn];
int n, m;
struct node {
int to, next, w;
}e[maxn<<1];
void init() {
Fill(head,-1); cnt = 0; Fill(sum, 0);
Fill(up,0); Fill(deep,0); Fill(dis, 0);
}
void add(int u, int v, int w) {
e[cnt] = node{v, head[u], w};
head[u] = cnt++;
}
void dfs(int u,int fa,int d) {
deep[u] = d + 1;
for(int i = 1 ; i < 20 ; i ++) {
up[u][i] = up[up[u][i-1]][i-1];
}
for(int i = head[u] ; ~i ; i = e[i].next) {
int to = e[i].to;
if(to == fa) continue;
dis[to] = dis[u] + e[i].w;
dd[to] = e[i].w;
up[to][0] = u;
dfs(to, u, d+1);
}
}
int LCA_BZ(int u,int v) {
int mx = 0;
if(deep[u] < deep[v]) swap(u,v);
int k = deep[u] - deep[v];
for(int i = 0 ; i < 20 ; i ++) {
if((1<if(u != v) {
for(int i = 19 ; i >= 0 ; i --) {
if(up[u][i] != up[v][i]){
u = up[u][i];
v = up[v][i];
}
}
u = up[u][0];
}
return u;
}
struct Edge {
int u, v, f, w;
}q[maxn];
int cur, mx, flag;
int dfs2(int u, int fa) {
int num = sum[u];
for (int i = head[u] ; ~i ; i = e[i].next) {
int to = e[i].to;
if (to == fa) continue;
num += dfs2(to, u);
}
if (num >= cur && dd[u] >= mx) flag = 1;
return num;
}
bool ok(int x) {
Fill(sum, 0); // 注意是在不满足条件的这些路径中找, 不能把满足的圈进来.
cur = mx = flag = 0;
for (int i = 1 ; i <= m ; i ++) {
if (q[i].w > x) {
++ cur;
++sum[q[i].u]; ++sum[q[i].v];
sum[q[i].f] -= 2;
mx = max(mx, q[i].w - x);
}
}
if (!cur) return 1;
dfs2(1, -1);
return flag;
}
void solve() {
while(~scanf("%d%d", &n, &m)) {
init();
for (int i = 1 ; i < n ; i ++) {
int u, v, w;
scanf("%d%d%d",&u, &v, &w);
add(u, v, w); add(v, u, w);
}
up[1][0] = 1;
dfs(1,-1,0);
for (int i = 1 ; i <= m ; i ++) {
scanf("%d%d", &q[i].u, &q[i].v);
int tmp = LCA_BZ(q[i].u, q[i].v);
q[i].f = tmp;
q[i].w = dis[q[i].u] + dis[q[i].v] - 2*dis[tmp];
}
int l = 0, r = inf, mid, ans = -1;
while(l <= r) {
mid = (l + r) >> 1;
if (ok(mid)) {
r = mid - 1;
ans = mid;
}
else l = mid + 1;
}
printf("%d\n", ans);
}
}
此操作中我们这样维护:每次经过一条边,(如从u到v)我们让sum[u]++, sum[v]++, sum[LCA(u,v)]–, sum[up[LCA(u,v)][0]]–。(最后要把sum推上去)
以一次添加为例想象一下,首先u到根的路径上sum都+1,此时u到根间结点tmp都为1,之后v到根路径上sum+1,此时u到LCA前一个,v到LCA前一个点的tmp都+1,而LCA到根的所有点都+2,然后从sum[LCA]–,更新上去,此时u-v路上所有sum都+1,已经达到目的。
而多余的是什么部分呢,也就是LCA的上一个结点(up[LCA][0])到根的这一段都多加了1,所以sum[up[LCA(u, v)][0]]–, 更新上去,也就完成了。
实际操作时也不需要每次更新都推上去,只要把四个sum维护好,最后dfs走一边就更新完了。
例题: 松鼠的新家
很明显相邻的两个点之间的路径之间点都要加一, 最后统计的就是每个点被加了几次, 所以就用到上面的求点覆盖的知识. 注意最后要减去加重了的.
AC Code
const int maxn = 3e5 + 5;
int up[maxn][21], sum[maxn];
int deep[maxn], dis[maxn];
int cnt, head[maxn];
int n, m, q;
struct node {
int to, next;
}e[maxn<<1];
void init() {
Fill(head,-1); cnt = 0; Fill(sum, 0);
Fill(up,0); Fill(deep,0);
}
void add(int u, int v) {
e[cnt] = node{v, head[u]};
head[u] = cnt++;
}
void dfs(int u,int fa,int d) {
deep[u] = d + 1;
for(int i = 1 ; i < 20 ; i ++) {
up[u][i] = up[up[u][i-1]][i-1];
}
for(int i = head[u] ; ~i ; i = e[i].next) {
int to = e[i].to;
if(to == fa) continue;
up[to][0] = u;
dfs(to, u, d+1);
}
}
int LCA_BZ(int u,int v) {
int mx = 0;
if(deep[u] < deep[v]) swap(u,v);
int k = deep[u] - deep[v];
for(int i = 0 ; i < 20 ; i ++) {
if((1<if(u != v) {
for(int i = 19 ; i >= 0 ; i --) {
if(up[u][i] != up[v][i]){
u = up[u][i];
v = up[v][i];
}
}
u = up[u][0];
}
return u;
}
int ans[maxn];
int dfs2(int u, int fa) {
int num = sum[u];
for (int i = head[u] ; ~i ; i = e[i].next) {
int to = e[i].to;
if (to == fa) continue;
num += dfs2(to, u);
}
ans[u] = num;
return num;
}
int f[maxn];
void solve() {
while(~scanf("%d", &n)) {
init();
for(int i = 1 ; i <= n ; i ++) {
scanf("%d", f+i);
}
for (int i = 1 ; i < n ; i ++) {
int u, v;
scanf("%d%d",&u,&v);
add(u,v); add(v,u);
}
up[1][0] = 1;
dfs(1,-1,0);
for (int i = 1 ; i < n ; i ++) {
int u = f[i], v = f[i+1];
++sum[u]; ++sum[v];
int tmp = LCA_BZ(u, v);
--sum[tmp];
if (tmp != 1) --sum[up[tmp][0]];
}
dfs2(1, 0);
for (int i = 2 ; i <= n ; i ++) --ans[f[i]];
for (int i = 1 ; i <= n ; i ++) {
printf("%d\n", ans[i]);
}
}
}
疫情控制: 这道题还是挺难的, 主要就是二分答案, 然后分类点能否到达根节点, 标记根节点的儿子, 最后判断是否所有的根节点的儿子都被标记了即可, 注意标记要上传!!! 还有就是要对距离排序进行判断….. 多多思考, 就不贴代码了….