【科技】基環樹小結


最近比較系統地練了練基環樹的題,最后在這里總結一波,留一點方法與套路。

首先,基環樹的模型應該是比較明顯的。和樹類比,除了題目中給出一棵樹之類的這種很直接的方式,樹的有關模型,較常見的有根據某個性質,我們可以得到除了根每個點都能找到唯一對應的父親。

而基環樹除了給出$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\n", 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\n", ans);

  return 0;
}
View Code

 

一個有趣的問題就是有關寫法,因為有的問題中是無向的,可能會給出邊表,而有的問題則是給出了每個點對應的點,可以理解為有向邊。

有向邊的用拓撲排序一般會比較好寫,不會爆棧,出的問題也很少,無向邊直接給出邊表處理起來比較麻煩,還是$Dfs$吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM