• 【科技】基环树小结


    最近比较系统地练了练基环树的题,最后在这里总结一波,留一点方法与套路。

    首先,基环树的模型应该是比较明显的。和树类比,除了题目中给出一棵树之类的这种很直接的方式,树的有关模型,较常见的有根据某个性质,我们可以得到除了根每个点都能找到唯一对应的父亲。

    而基环树除了给出$n$个点$n$条边,比较明显的有每个点对应了一个出点,这样就构成了一棵基环树森林。

    大概除了毒瘤题之外,基环树上做做dp就差不多了。

    dp的方法一般有两种,本质都是先在子树内dp好,然后扣环,下面只考虑环上的处理:

    1. 一种是边上带有限制的,一般体现在相邻两个点的某些限制。这时可以随便在环上选一条边,枚举限制生不生效,直接做树形dp就行了。
    2. 另一种是统计类的,比如求直径之类的。这时通常断环为链,有时需要再复制一遍,在链上dp。

    第一种类型的一个典型例子就是2018牛客多校的某题。

    题目概述:有$n$件物品,每件物品有一个价格和折扣,两个优惠,可以选择使用折扣或者选择不折扣而送一个其他物品,被送物品不能使用优惠,问凑齐所有物品的最小花费。

    每件物品对应了一个附赠的物品,很让人联想到基环树,而且树边上有限制。用$f_{i,0}$表示得到了$i$子树内的物品的最小花费,$f_{i,1}$表示得到了$i$子树内的物品且第$i$件物品不是送来的最小花费。传统的树形dp之后,给不给环上的第一个点限制就决定了环上最后一个点的状态,做两次dp就可以了。

    #include <cstdio>
    #include <queue>
    #include <iostream>
     
    using namespace std;
     
    typedef long long LL;
    const int N = 100005;
    const LL INF = 1e17 + 7;
     
    int n;
    int p[N], d[N], to[N], in[N];
    int flg[N], vis[N], st[N], tp;
    LL ans, f0[N], f1[N], g0[N], g1[N];
    vector<int> e[N];
    queue<int> Q;
     
    void Dfs(int x) {
      f0[x] = p[x] - d[x];
      f1[x] = p[x];
      for (int v : e[x]) {
        if (flg[v]) continue;
        Dfs(v);
        f0[x] += f0[v];
        f1[x] += f0[v];
      }
      for (int v : e[x]) {
        if (flg[v]) continue;
        f0[x] = min(f0[x], f1[x] - p[x] - f0[v] + f1[v]);
      }
    }
     
    LL Solve() {
      static LL re;
      g0[1] = f0[st[1]]; g1[1] = f1[st[1]];
      for (int i = 2; i <= tp; ++i) {
        g1[i] = g0[i - 1] + f1[st[i]];
        g0[i] = min(g1[i - 1] + f1[st[i]] - p[st[i]], g0[i - 1] + f0[st[i]]);
      }
      re = g0[tp];
      g0[1] = f1[st[1]] - p[st[1]]; g1[1] = INF;
      for (int i = 2; i <= tp; ++i) {
        g1[i] = g0[i - 1] + f1[st[i]];
        g0[i] = min(g1[i - 1] + f1[st[i]] - p[st[i]], g0[i - 1] + f0[st[i]]);
      }
      return min(re, g1[tp]);
    }
     
    int main() {
      scanf("%d", &n);
      for (int i = 1; i <= n; ++i) scanf("%d", &p[i]);
      for (int i = 1; i <= n; ++i) scanf("%d", &d[i]);
      for (int i = 1; i <= n; ++i) {
        scanf("%d", &to[i]);
        ++in[to[i]]; flg[i] = 1;
        e[to[i]].push_back(i);
      }
      for (int i = 1; i <= n; ++i) {
        if (!in[i]) Q.push(i);
      }
      for (; !Q.empty(); ) {
        int x = Q.front(); Q.pop();
        flg[x] = 0;
        if (--in[to[x]] == 0) Q.push(to[x]);
      }
       
      for (int i = 1; i <= n; ++i) {
        if (flg[i] && !vis[i]) {
          vis[i] = 1; st[tp = 1] = i;
          Dfs(i);
          for (int t = to[i]; t != i; t = to[t]) {
            vis[t] = 1; st[++tp] = t;
            Dfs(t);
          }
          ans += Solve();
        }
      }
      printf("%lld
    ", ans);
     
      return 0;
    }
    View Code

    第二种类型的一个典型例子就是IOI2018的Island,就是求基环树的直径,或只说最长简单路径。

    树上dp,环上单调队列维护$g_{i} - dis_{i}$,其中$g_{i}$表示$i$子树下以$i$为链头的最长链。

    #include <cstdio>
    #include <vector>
    #include <iostream>
    
    typedef long long LL;
    const int N = 1000005;
    
    int n, m;
    bool vis[N], vis_e[N], flg[N];
    int fa[N], fw[N], va[N << 1], id[N << 1], q[N];
    LL f[N], g[N], dis[N << 1], ans;
    int cva[N];
    std::vector<int> cir[N];
    
    int yun = 1, las[N], to[N << 1], wi[N << 1], pre[N << 1];
    inline void Add(int a, int b, int c) {
      to[++yun] = b; wi[yun] = c; pre[yun] = las[a]; las[a] = yun;
    }
    
    inline int Read(int &x) {
      x = 0; static char c;
      for (c = getchar(); c < '0' || c > '9'; c = getchar());
      for (; c >= '0' && c <= '9'; x = (x << 3) + (x << 1) + c - '0', c = getchar());
    }
    
    void Dfs_init(int x) {
      vis[x] = 1;
      for (int i = las[x]; i; i = pre[i]) {
        if (vis_e[i >> 1]) continue;
        vis_e[i >> 1] = 1;
        if (vis[to[i]]) {
          cir[++m].push_back(to[i]);
          cva[m] = wi[i];
          flg[to[i]] = 1;
          for (int t = x; t != to[i]; t = fa[t]) {
            cir[m].push_back(t);
            flg[t] = 1;
          }
        } else {
          fa[to[i]] = x;
          fw[to[i]] = wi[i];
          Dfs_init(to[i]);
        }
      }
    }
    void Dfs_cal(int x, int Fa) {
      for (int i = las[x]; i; i = pre[i]) {
        if (to[i] == Fa || flg[to[i]]) continue;
        Dfs_cal(to[i], x);
        f[x] = std::max(f[x], f[to[i]]);
        f[x] = std::max(f[x], g[x] + g[to[i]] + wi[i]);
        g[x] = std::max(g[x], g[to[i]] + wi[i]);
      }
    }
    LL Solve(int k, LL re = 0) {
      int nm = (int)cir[k].size();
      for (int i = 0; i < nm; ++i) {
        re = std::max(re, f[cir[k][i]]);
        id[i + 1] = id[i + 1 + nm] = cir[k][i];
        va[i + 1] = va[i + 1 + nm] = (i == 0)? cva[k] : fw[cir[k][i]];
      }
      for (int i = 1; i <= nm << 1; ++i) {
        dis[i] = dis[i - 1] + va[i - 1];
      }
      for (int i = 1, nl = 1, nr = 0; i <= nm << 1; ++i) {
        while (nl <= nr && i - q[nl] > nm - 1) ++nl;
        if (q[nl] != i && nl <= nr) {
          re = std::max(re, g[id[i]] + dis[i] + g[id[q[nl]]] - dis[q[nl]]);
        }
        while (nl <= nr && g[id[i]] - dis[i] >= g[id[q[nr]]] - dis[q[nr]]) --nr;
        q[++nr] = i;
      }
      return re;
    }
    
    int main() {
      scanf("%d", &n);
      for (int i = 1, x, y; i <= n; ++i) {
        Read(x); Read(y);
        Add(i, x, y); Add(x, i, y);
      }
      for (int i = 1; i <= n; ++i) {
        if (!vis[i]) {
          Dfs_init(i);
          for (int j = 0; j < (int)cir[m].size(); ++j) {
            Dfs_cal(cir[m][j], 0);
          }
          ans += Solve(m);
          cir[m].clear();
        }
      }
      printf("%lld
    ", ans);
    
      return 0;
    }
    View Code

    一个有趣的问题就是有关写法,因为有的问题中是无向的,可能会给出边表,而有的问题则是给出了每个点对应的点,可以理解为有向边。

    有向边的用拓扑排序一般会比较好写,不会爆栈,出的问题也很少,无向边直接给出边表处理起来比较麻烦,还是$Dfs$吧。

  • 相关阅读:
    [shell]Shell经常使用特殊符号
    谈谈大三找暑假实习
    使用zTree控件制作的表格形式的树形+数据菜单
    Bestcoder #47 B Senior&#39;s Gun
    使用awrextr.sql导出awr原始数据
    linux/shell 文本文件删除/删掉空行
    python 统计文本文件的行数
    check if a linux process is done using bash 检查进程是否在运行
    umount移动硬盘遇到device is busy问题
    Python读写文件
  • 原文地址:https://www.cnblogs.com/Dance-Of-Faith/p/9365714.html
Copyright © 2020-2023  润新知