首页 > 其他分享 >树上前缀和与差分

树上前缀和与差分

时间:2024-06-10 09:15:02浏览次数:17  
标签:pre 前缀 int 路径 差分 fa 树上 cur

树上前缀和

设 \(sum_i\) 表示根节点到节点 \(i\) 的权值总和。
则有:

  • 对于点权,\(x,y\) 路径上的和为 \(sum_x + sum_y - sum_{lca} - sum_{fa_{lca}}\)。
  • 对于边权,\(x,y\) 路径上的和为 \(sum_x + sum_y - 2 \times sum_{lca}\)。

习题:P4427 [BJOI2018] 求和

解题思路

预处理出 \(sum_{i,k}\) 表示根节点到节点 \(i\) 的深度的 \(k\) 次方和,这个过程的时间复杂度为 \(O(nk)\),后面即为树上点权前缀和问题。

参考代码
#include <cstdio>
#include <vector>
#include <algorithm>
using std::swap;
using std::vector;
const int N = 3e5 + 5;
const int K = 55;
const int LOG = 19;
const int MOD = 998244353;
vector<int> tree[N];
int fa[N][LOG], depth[N], sum[N][K];
void dfs(int u, int pre) {
    depth[u] = depth[pre] + 1;
    int d = 1;
    for (int i = 0; i < K; i++) {
        sum[u][i] = (sum[pre][i] + d) % MOD;
        d = 1ll * d * depth[u] % MOD;
    } 
    fa[u][0] = pre;
    for (int v : tree[u]) {
        if (v == pre) continue;
        dfs(v, u);
    }
}
int lca(int x, int y) {
    if (depth[x] < depth[y]) swap(x, y);
    int delta = depth[x] - depth[y];
    for (int i = LOG - 1; i >= 0; i--)
        if (delta & (1 << i)) x = fa[x][i];
    if (x == y) return x;
    for (int i = LOG - 1; i >= 0; i--) {
        if (fa[x][i] != fa[y][i]) {
            x = fa[x][i]; y = fa[y][i];
        }
    }
    return fa[x][0];
}
int main()
{
    int n; scanf("%d", &n);
    for (int i = 1; i < n; i++) {
        int x, y; scanf("%d%d", &x, &y);
        tree[x].push_back(y);
        tree[y].push_back(x);
    }
    depth[0] = -1;
    dfs(1, 0);
    for (int i = 1; i < LOG; i++) {
        for (int j = 1; j <= n; j++) fa[j][i] = fa[fa[j][i - 1]][i - 1];
    }
    int m; scanf("%d", &m);
    while (m--) {
        int i, j, k; scanf("%d%d%d", &i, &j, &k);
        int lca_ij = lca(i, j), f = fa[lca_ij][0];
        int ans1 = (sum[i][k] + MOD - sum[f][k]) % MOD;
        int ans2 = (sum[j][k] + MOD - sum[lca_ij][k]) % MOD;
        printf("%d\n", (ans1 + ans2) % MOD);
    }
    return 0;
}

树上差分

树上差分可以理解为对树上的某一段路径进行差分操作,这里的路径可以类比一维数组的区间进行理解。例如在对树上的一些路径进行频繁操作,并且询问某条边或者某个点在经过操作后的值的时候,就可以运用树上差分思想。

点差分

例题:P3128 [USACO15DEC] Max Flow P

问题描述:有 \(n\) 个节点,用 \(n-1\) 条边连接,所有节点都连通。给出 \(k\) 条路径,第 \(i\) 条路径为节点 \(s_i\) 到 \(t_i\)。每给出一条路径,路径上所有节点的权值加 \(1\)。输出最大权值点的权值。
数据范围:\(2 \le n \le 50000, 1 \le k \le 100000\)

树上两点 \(u,v\) 的路径指的是最短路径。可以把 \(u \rightarrow v\) 的路径分为两个部分:\(u \rightarrow LCA(u,v)\) 和 \(LCA(u,v) \rightarrow v\)。

先考虑简单的思路。首先对每条路径求 LCA,分别以 \(u\) 和 \(v\) 为起点到 LCA,把路径上每个节点的权值加 \(1\);然后对所有路径进行类似操作。把路径上每个节点加 \(1\) 的操作的复杂度为 \(O(n)\),共 \(k\) 次操作,会超时。

本题的关键是如何记录路径上每个节点的修改。显然,如果真的对每个节点都记录修改,肯定会超时。我们可以利用差分,因为差分的用途是“把区间问题转换为断电问题”,适用这种情况。

给定数组 \(a\),定义差分数组 \(D[k]=a[k]-a[k-1]\),即数组相邻元素的差。

从差分数组的定义可以推出:\(a[k]=D[1]+D[2]+ \cdots + D[k] = \sum\limits_{i=1}^{k} D[i]\)

这个式子描述了 \(a\) 和 \(D\) 的关系,即“差分是前缀和的逆运算” ,它把求 \(a[k]\) 转换为求 \(D\) 的前缀和。

对于区间 \([L,R]\) 的修改问题,比如把区间内每个元素都加上 \(d\),则可以对区间的两个端点 \(L\) 和 \(R+1\) 做以下操作:

  1. 把 \(D[L]\) 加上 \(d\);
  2. 把 \(D[R+1]\) 减去 \(d\)。

image

对 \(D\) 求前缀和,则可得到 \(a\) 数组,以上的更新相当于:

  1. \(1 \le x < L\),\(a[x]\) 不变;
  2. \(L \le x \le R\),\(a[x]\) 增加了 \(d\);
  3. \(R < x \le N\),\(a[x]\) 不变,因为被 \(D[R+1]\) 中减去的 \(d\) 抵消了。

利用差分能够把区间修改问题转换为只用端点做记录。如果不用差分数组,区间内每个元素都需要修改,时间复杂度为 \(O(n)\);转换为只修改两个端点后,时间复杂度降到 \(O(1)\),这就是差分的重要作用。

把差分思想用到树上,只需要把树上路径转换为区间即可。把一条路径 \(u \rightarrow v\) 分为两部分:\(u \rightarrow LCA(u,v)\) 和 \(LCA(u,v) \rightarrow v\),这样每条路径都可以当成一个区间处理。

记 \(LCA(u,v)=R\),并记 \(R\) 的父节点为 \(F=fa[R]\),要把路径上每个节点权值加 \(1\),有:

  1. 路径 \(u \rightarrow R\) 这个区间上,\(D[u]++\),\(D[F]--\);
  2. 路径 \(v \rightarrow R\) 这个区间上,\(D[v]++\),\(D[F]--\)。

经过以上操作,能通过 \(D\) 计算出 \(u \rightarrow v\) 上每个节点的权值。不过,由于两条路径在 \(R\) 和 \(F\) 这里重合了,这两个步骤把 \(D[R]\) 加了两次,把 \(D[F]\) 减了两次,需要调整为 \(D[R]--\) 和 \(D[F]--\)。

image

在本题中,对每条路径都用倍增法求一次 LCA,并做一次差分操作。当对于所有路径都操作完成后,再做一次 DFS,求出每个节点的权值,所有权值中的最大值即为答案。

\(k\) 次 LCA 的时间复杂度为 \(O(n \log n + k \log n)\);最后做一次 DFS,时间复杂度为 \(O(n)\);总的时间复杂度为 \(O((n+k) \log n)\)。

参考代码
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 50005;
const int LOG = 16;
vector<int> tree[N];
int d[N], fa[N][LOG], a[N], ans;
void dfs(int cur, int pre) {
    d[cur] = d[pre] + 1;
    fa[cur][0] = pre;
    for (int i = 1; i < LOG; i++) fa[cur][i] = fa[fa[cur][i - 1]][i - 1];
    for (int nxt : tree[cur])
        if (nxt != pre) dfs(nxt, cur);
}
int lca(int x, int y) {
    if (d[x] < d[y]) swap(x, y);
    int len = d[x] - d[y];
    for (int i = LOG - 1; i >= 0; i--) 
        if (1 << i <= len) {
            x = fa[x][i]; len -= 1 << i;
        }
    if (x == y) return x;
    for (int i = LOG - 1; i >= 0; i--) 
        if (fa[x][i] != fa[y][i]) {
            x = fa[x][i]; y = fa[y][i];
        }
    return fa[x][0];
}
void calc(int cur, int pre) {
    for (int nxt : tree[cur])
        if (nxt != pre) {
            calc(nxt, cur); 
            a[cur] += a[nxt];
        } 
    ans = max(ans, a[cur]);
}
int main()
{
    int n, k;
    scanf("%d%d", &n, &k);
    for (int i = 1; i < n; i++) {
        int x, y;
        scanf("%d%d", &x, &y);
        tree[x].push_back(y); tree[y].push_back(x);
    }
    dfs(1, 0); // 计算每个节点的深度并预处理fa数组
    while (k--) {
        int s, t;
        scanf("%d%d", &s, &t);
        int r = lca(s, t);
        a[s]++; a[t]++; a[r]--; a[fa[r][0]]--; // 树上差分
    }  
    calc(1, 0); // 用差分数组求每个节点的权值
    printf("%d\n", ans);
    return 0;
}

边差分

例题:P6869 [COCI2019-2020#5] Putovanje

显然针对每一条边只会考虑购买单程票和多程票的一种,这取决于该条边被经过的次数 \(k\),这样一来这条边上的最少花费是 \(\min (k c_1, c_2)\)。

这里需要根据若干条路径计算出每条边经过的次数,可以借助差分思想,注意它和点差分不同。对于边相关的问题,一般我们会将每个点与它父亲节点相连的边与该点绑定,从而将边上信息的维护转化为对点的信息的维护

image

参考代码
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 200005;
const int LOG = 19;
vector<int> tree[N];
int d[N], fa[N][LOG], cnt[N], a[N], b[N], c1[N], c2[N];
void dfs(int cur, int pre) {
    d[cur] = d[pre] + 1;
    fa[cur][0] = pre;
    for (int i = 1; i < LOG; i++) fa[cur][i] = fa[fa[cur][i - 1]][i - 1];
    for (int nxt : tree[cur]) 
        if (nxt != pre) dfs(nxt, cur);
}
int lca(int x, int y) {
    if (d[x] < d[y]) swap(x, y);
    int len = d[x] - d[y];
    for (int i = LOG - 1; i >= 0; i--) 
        if ((1 << i) <= len) {
            x = fa[x][i]; len -= 1 << i;
        }
    if (x == y) return x;
    for (int i = LOG - 1; i >= 0; i--)
        if (fa[x][i] != fa[y][i]) {
            x = fa[x][i]; y = fa[y][i];
        }
    return fa[x][0];
}
void calc(int cur, int pre) {
    for (int nxt : tree[cur]) 
        if (nxt != pre) {
            calc(nxt, cur);
            cnt[cur] += cnt[nxt];
        }
}
int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 1; i < n; i++) {
        scanf("%d%d%d%d", &a[i], &b[i], &c1[i], &c2[i]);
        tree[a[i]].push_back(b[i]);
        tree[b[i]].push_back(a[i]);
    }
    dfs(1, 0);
    for (int i = 1; i < n; i++) {
        int r = lca(i, i + 1);
        cnt[i]++; cnt[i + 1]++; cnt[r] -= 2;
    }
    calc(1, 0);
    LL ans = 0;
    for (int i = 1; i < n; i++) {
        if (d[a[i]] > d[b[i]]) ans += min(1ll * c1[i] * cnt[a[i]], 1ll * c2[i]);
        else ans += min(1ll * c1[i] * cnt[b[i]], 1ll * c2[i]);
    }
    printf("%lld\n", ans);
    return 0;
}

标签:pre,前缀,int,路径,差分,fa,树上,cur
From: https://www.cnblogs.com/ronchen/p/18048232

相关文章

  • 程序分享--常见算法/编程面试题:最长公共前缀
    关注我,持续分享逻辑思维&管理思维&面试题;可提供大厂面试辅导、及定制化求职/在职/管理/架构辅导;推荐专栏《10天学会使用asp.net编程AI大模型》,目前已完成所有内容,持续上传中。一顿烧烤不到的费用,让人能紧跟时代的浪潮。从普通网站,到公众号、小程序,再到AI大模型网站。干货满满......
  • [题解]P6374 「StOI-1」树上询问
    题意简述给定一个\(N\)个节点的树,接下来有\(q\)次询问。每次询问给定\(a,b,c\),请问存在多少个节点\(i\),使得这棵树在以\(i\)为根的情况下,\(a\)和\(b\)的LCA是\(c\)。解题思路首先通过分析样例,我们发现:\(a,b\)的LCA一定在它们之间的简单路径上,所以如果\(c\)不在\(a,b\)之间的简......
  • 【一百零九】【算法分析与设计】树状数组求解前缀最大值,673. 最长递增子序列的个数,
    树状数组求解前缀最大值树状数组可以求解和前缀区间有关的问题,例如前缀和,前缀区间最值.可以利用logn......
  • 三变量的SVAR模型#step1:ADF平稳检验、差分、时序图
            无论是VAR,SVAR,ARMA还是ARIMA,大多数时间序列模型的第一步都是数据的平稳性检验,通常我们使用ADF检验1.1ADF检验原假设1.2ADF检验假设原假设(H0​):时间序列存在单位根(非平稳)。备择假设(H1​):时间序列不存在单位根(平稳)。1.3ADF标准检验代码%生成一个示例时间......
  • 机器学习策略篇:详解进行误差分析(Carrying out error analysis)
    从一个例子开始讲吧。假设正在调试猫分类器,然后取得了90%准确率,相当于10%错误,,开发集上做到这样,这离希望的目标还有很远。也许的队员看了一下算法分类出错的例子,注意到算法将一些狗分类为猫,看看这两只狗,它们看起来是有点像猫,至少乍一看是。所以也许的队友给一个建议,如何针对狗的......
  • 2559. 统计范围内的元音字符串数(前缀和) o(n)时间复杂度
    给你一个下标从 0 开始的字符串数组 words 以及一个二维整数数组 queries 。每个查询 queries[i]=[li,ri] 会要求我们统计在 words 中下标在 li 到 ri 范围内(包含 这两个值)并且以元音开头和结尾的字符串的数目。返回一个整数数组,其中数组的第 i 个元素对......
  • 组合数前缀和计算
    记录一下,下文的除法非特殊注明都是向下取整。求\(F(n,k)=\sum_{i=0}^{k}\binom{n}{i}\pmodp\)。首先使用卢卡斯定理。\[\begin{aligned}&\sum_{i=0}^{k}\binom{n}{i}\\=&\sum_{i=0}^{k}\binom{\frac{n}{p}}{\frac{i}{p}}\binom{n\bmodp}{i\bmodp}\\=&\s......
  • 求前缀表达式的值
    1.题目7-7求前缀表达式的值分数25全屏浏览切换布局作者 DS课程组单位 浙江大学算术表达式有前缀表示法、中缀表示法和后缀表示法等形式。前缀表达式指二元运算符位于两个运算数之前,例如2+3*(7-4)+8/4的前缀表达式是:++2*3-74/84。请设计程序计算前缀表达式......
  • 图论-SPFA与差分约束
    闻道有先后,术业有专攻当用来判断负环的时候,SPFA还挺好用的intpre[N];voidprint_path(ints,intt){if(s==t){cout<<s;return;}print_path(s,pre[t]);cout<<""<<t;}inthead[N],cnt;structEdge{intfrom,to,nxt,c;}e[......
  • 前缀和解决字符串变化问题
    题目小苯有一个长度为\(n\)的字符串\(s\),每次操作他可以选择一个位置的字母将其的大小写反转,也就是说如果字符是小写,则操作后会变成大写,如果字符是大写则反之。他现在希望将\(s\)变为:“前面若干字符是大写,后面的字符全是小写”的样子,例如:"AABBccdd"。(注意:全大写和全小写均不合法......