NOI 2024

hztmax0發表於2024-08-10

Day1 T1 集合(set)

容易發現兩個序列等價當且僅當,所有數字在序列中出現位置的集合構成集族相等。

考慮雜湊,對於一個集合 \(S\),令它的雜湊值為 \(f(S) = (\sum\limits_{x \in S} B^x) \mod P\),上述條件只需做兩遍雜湊即可滿足。

使用莫隊維護所有雜湊值,時間複雜度 \(O(q\sqrt n \log n)\)

注意到我們只需判斷 YesNo,並且答案具有單調性。設 \(ans_l\) 表示以 \(l\) 為區間左端點,最大的合法右端點。雙指標預處理 \(ans\) 即可。

時間複雜度 \(O(n \log n + q)\)

Code
#include <iostream>
#include <set>
#include <vector>
#include <algorithm>

using namespace std;
using LL = long long;

const int N = 2e5 + 5, M = 6e5 + 5; 
const int Base[2] = {int(1e9) + 7, int(1e9) + 9};
const LL Mod = 1e9 + 3; 

LL Mul (LL x, LL y) { return x * y % Mod; }
LL Pow (LL x, LL y) {
  LL res = 1; 
  while (y) {
    if (y & 1) {
      res = Mul(res, x);
    }
    x = Mul(x, x);
    y >>= 1; 
  }
  return res;
}

int n, m, q, ans[N]; 

struct Array {
  int a[N][3]; 
  LL all_val, hval[M];

  void Insert (LL &val, LL x, int o) { val = (val + Pow(Base[o], x)) % Mod; }
  void Erase (LL &val, LL x, int o) { val = (val - Pow(Base[o], x) + Mod) % Mod; }

  LL Add (int i) {
    for (int j = 0; j < 3; ++j) {
      Erase(all_val, hval[a[i][j]], 1);
      Insert(hval[a[i][j]], i, 0);
      Insert(all_val, hval[a[i][j]], 1);
    }
    return all_val;
  }

  void Remove (int i) {
    for (int j = 0; j < 3; ++j) {
      Erase(all_val, hval[a[i][j]], 1);
      Erase(hval[a[i][j]], i, 0);
      Insert(all_val, hval[a[i][j]], 1);
    }
  }

  void Init () {
    for (int i = 1; i <= n; ++i) {
      for (int j = 0; j < 3; ++j) {
        cin >> a[i][j];
      }
    }
  }  
} A, B; 

int main () {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> m >> q; 
  A.Init(), B.Init();
  for (int i = 1, j = 1; i <= n; ++i) {
    for (; j <= n && A.Add(j) == B.Add(j); ++j) {
    }
    if (j <= n) {
      A.Remove(j), B.Remove(j);
    }
    ans[i] = j;
    A.Remove(i), B.Remove(i);
  }
  for (int l, r; q--; ) {
    cin >> l >> r;
    cout << (ans[l] > r ? "Yes" : "No") << '\n';
  }
  return 0; 
}

Day1 T2 百萬富翁(richest)

\(\text{Case 1}\) 兩兩詢問即可。

\(\text{Case 2}\) 考慮 \(\text{dp}\) 決策點,設 \(f_{i, j}\) 表示我們要找 \(i\) 個人中最有錢的人,可以使用 \(j\) 次請求,最少需要的查詢數,轉移考慮列舉 \(k\) 表示將這些人平均分為 \(k\) 組(容易證明均分一定是最優的):

\(f_{i, j} = \min\limits_{k} \{ f(k, j - 1) + \frac{\lfloor \frac{i}{k} \rfloor(\lfloor \frac{i}{k} \rfloor - 1)}{2} \times k + (i \bmod k)\lfloor \frac{i}{k} \rfloor \}\)

直接 \(\text{dp}\) 時間複雜度 \(O(n^2t)\),但實際上不難發現 \(n = 10^6\) 時前 \(4\) 次最優決策都是 \(k = \frac{n}{2}\),於是我們可以直接從 \(n = 62500\) 開始跑,打表發現決策點分別為 \(\{ 500000, 250000, 125000, 62500, 20833, 3472, 183, 1 \}\),於是這道題就做完了。

實際上也可以將列舉組數改為列舉每組人數,這樣做的優勢是可以快速得到一組解(大概可以獲得 \(90\) 分左右),劣勢是不一定最優,需要手動調整才能得到滿分。

Code
#include "richest.h"
#include <algorithm>
#include <numeric>
#include <cassert>

using namespace std;

vector<int> get_max (vector<vector<int>> vec) {
  int t = vec.size();
  vector<int> a, b;
  for (int k = 0; k < t; ++k) {
    int cnt = vec[k].size();
    for (int i = 0; i < cnt; ++i) {
      for (int j = i + 1; j < cnt; ++j) {
        a.push_back(vec[k][i]), b.push_back(vec[k][j]);
      }
    }
  }
  vector<int> c = ask(a, b); 
  vector<int> res;
  for (int k = t - 1; ~k; --k) {
    int cnt = vec[k].size();
    vector<int> win(cnt);
    for (int i = cnt - 1; ~i; --i) {
      for (int j = cnt - 1; j > i; --j) {
        if (c.back() == a[c.size() - 1]) ++win[i];
        if (c.back() == b[c.size() - 1]) ++win[j];
        c.pop_back();
      }
    }
    res.push_back(vec[k][find(win.begin(), win.end(), cnt - 1) - win.begin()]);
  }
  reverse(res.begin(), res.end());
  return res;
}

int richest (int N, int T, int S) {
  if (N == 1000 && T == 1 && S == 499500) {
    vector<int> v(N);
    iota(v.begin(), v.end(), 0);
    return get_max(vector<vector<int>>{v}).back();
  }
  else {
    assert(N == int(1e6) && T == 20 && S == int(2e6));
    const vector<int> point{500000, 250000, 125000, 62500, 20833, 3472, 183, 1};
    vector<int> now(N);
    iota(now.begin(), now.end(), 0);
    for (int i = 0; i < point.size(); ++i) {
      int cnt = now.size(), p = point[i], low = cnt / p, cur = 0; 
      vector<vector<int>> des(p);
      for (int j = 0; j < p; ++j) {
        for (int k = 0; k < low; ++k) {
          des[j].push_back(now[cur++]);
        }
      }
      for (int j = 0; cur < cnt; ++j) {
        des[j].push_back(now[cur++]);
      }
      now = get_max(des);
    }
    assert(now.size() == 1);
    return now.back();
  }
}

Day1 T3 樹的定向(tree)

對於某一時刻,假設已經確定了若干邊的方向。對於一個形如“\(a\) 不能到達 \(b\)”的限制 \((a, b)\),若路徑 \(a \rightarrow b\) 上存在一條邊已經定向且方向與路徑相反,則該限制已經滿足,刪除即可。若路徑 \(a \rightarrow b\) 上只存在一條邊未定向,則這條邊必須與路徑方向相反,我們將這條邊加入待處理的邊的佇列中,並刪除這個限制。

考慮如果這兩類限制都不存在,我們一定可以任意定向一條邊,由於題目要求字典序最小,所以我們會將第一條未定向的邊定成 \(0\)

證明:我們知道任意限制 \((a, b)\) 都滿足 \(\text{path}(u, v)\) 上未定向的邊不少於 \(2\) 條,所以對所有已定向的邊縮點,然後黑白染色,對於未定向的邊全部黑點連向白點/白點連向黑點,這給出了兩個對偶的合法解,因此任意定向一條邊後仍然存在解。

考慮如何動態維護每條限制 \((a, b)\) 上邊的情況。由於可重複限制,我們拆成 \(t = O(1)\) 個形如 \((x, k, o = 0/1)\) 的條件,它表示在 \(x\)\(2^k\) 級祖先鏈上,希望有至少一條邊是向上/向下定向。若這 \(t\) 個條件都不滿足,說明方案不合法。

對於一個條件 \((x, k, o = 0/1)\),若某一時刻 \(x\)\(2^k\) 級祖先鏈上存在一條邊和 \(o\) 方向相同,則它所屬限制已經得到滿足。若 \(x\)\(2^k\) 級祖先鏈上只有一條未定向邊,檢查所屬限制的其他條件,是否加起來只有一條未定向邊,如果是將這條邊加入佇列,檢查次數 \(O(1)\)

可以維護 \(ed_{i, k} = (true/false, id)\) 表示 \(i\)\(2^k\) 級祖先鏈上是否只有 \(\le 1\) 條邊,如果是且恰好有一條邊,記錄這條邊的編號。\(dir_{i, k, 0/1} = true/false\) 表示 \(i\)\(2^k\) 級祖先鏈上是否存在向上/向下的邊。對於靜態的情況,這兩個陣列容易倍增轉移。

容易說明對於一對 \((i, k)\)\(ed_{i, k}, dir_{i, k}\) 的修改次數都是 \(O(1)\) 的,所以我們考慮只在值發生變動的情況下更新下一層,對於每個狀態預處理下一層的所有狀態是簡單的。這樣訪問到的狀態總數是 \(O(n \log n)\) 的。

時間複雜度 \(O(n \log n)\)

Code
#include <iostream>
#include <vector>
#include <cassert>
#include <cmath>
#include <set>
#include <queue>

using namespace std;
using Pii = pair<int, int>;

const int N = 5e5 + 5, M = 19;

int cid, n, m;
int eu[N], ev[N], dep[N];
bool vis[N], ans[N];
int la[N], lb[N], icnt[N];
set<pair<int, bool>> sp[N];
bool done[N];
int fa[N][M];
pair<bool, int> ed[N][M];
bool dir[N][M][2]; 
vector<Pii> e[N];
vector<Pii> stv[N][M];
vector<int> st[N][M][2];
queue<pair<int, bool>> lq;

void Build (int u) {
  dep[u] = dep[fa[u][0]] + 1;
  for (int k = 1; k < M; ++k) {
    fa[u][k] = fa[fa[u][k - 1]][k - 1];
  }
  for (auto i : e[u]) {
    int v = i.first, id = i.second;
    if (v != fa[u][0]) {
      ed[v][0] = {true, id};
      fa[v][0] = u, Build(v);
    }
  }
}

int Lca (int u, int v) {
  if (dep[u] < dep[v]) swap(u, v);
  for (int k = M - 1; ~k; --k) {
    if (dep[u] - dep[v] >= (1 << k)) { 
      u = fa[u][k];
    }
  }
  if (u == v) return u;
  for (int k = M - 1; ~k; --k) {
    if (fa[u][k] != fa[v][k]) {
      u = fa[u][k], v = fa[v][k];
    }
  }
  return fa[u][0];
}

int Kth_fa (int u, int x) {
  assert(x >= 0);  
  for (int k = M - 1; ~k; --k) {
    if ((x >> k) & 1) {
      u = fa[u][k];
    }
  }
  return u; 
}

void Check_limit (int i) {
  if (!icnt[i] && sp[i].size() == 1) {
    pair<int, bool> req = *sp[i].begin();
    if (!vis[req.first]) {
      vis[req.first] = 1;
      lq.push(req);
    }
    done[i] = true;
  }
}

void Update (int x, int k, int o = -1) {
  pair<bool, int> _ed;
  bool _dir[2];
  if (k == 0) {
    _ed = {true, 0};
    _dir[o] = 1, _dir[o ^ 1] = 0; 
  }
  else {
    if (ed[x][k - 1].first == true && ed[fa[x][k - 1]][k - 1].first == true && (!ed[x][k - 1].second || !ed[fa[x][k - 1]][k - 1].second)) {
      _ed = {true, ed[x][k - 1].second | ed[fa[x][k - 1]][k - 1].second};
    }
    else {
      _ed = {false, 0};
    }
    _dir[0] = dir[x][k - 1][0] || dir[fa[x][k - 1]][k - 1][0];
    _dir[1] = dir[x][k - 1][1] || dir[fa[x][k - 1]][k - 1][1];
  }
  if (_ed != ed[x][k] || _dir[0] != dir[x][k][0] || _dir[1] != dir[x][k][1]) {
    for (int o = 0; o < 2; ++o) {
      for (auto i : st[x][k][o]) {
        if (_dir[o]) done[i] = true;
        if (done[i] == true) continue;
        if (ed[x][k].first == false && _ed.first == true) {
          --icnt[i];
          sp[i].insert({_ed.second, o});
          Check_limit(i);
        }
        else if (ed[x][k].second && !_ed.second) {
          sp[i].erase({ed[x][k].second, o});
          Check_limit(i);
        }
      } 
    }
    ed[x][k] = _ed, dir[x][k][0] = _dir[0], dir[x][k][1] = _dir[1];
    for (auto s : stv[x][k]) {
      Update(s.first, s.second);
    }
  }
}

int main () {
  cin.tie(0)->sync_with_stdio(0);
  cin >> cid >> n >> m; 
  for (int i = 1, u, v; i < n; ++i) {
    cin >> eu[i] >> ev[i];
    e[eu[i]].push_back({ev[i], i});
    e[ev[i]].push_back({eu[i], i});
  }
  for (int i = 1; i <= m; ++i) {
    cin >> la[i] >> lb[i];
  }
  Build(1);
  for (int i = 1; i <= m; ++i) {
    int tp = Lca(la[i], lb[i]);
    auto Add_limit = [&](int x, int k, int o) -> void {
      st[x][k][o].push_back(i);
      if (ed[x][k].first == true) {
        sp[i].insert({ed[x][k].second, o});
      }
      else {
        ++icnt[i];
      }
    }; 
    auto Add = [&](int x, int y, int o) -> void {
      if (x == y) return;
      int k = int(log2(dep[x] - dep[y]));
      Add_limit(x, k, o);
      if (dep[x] - dep[y] == (1 << k)) return; 
      Add_limit(Kth_fa(x, dep[x] - dep[y] - (1 << k)), k, o);
    }; 
    Add(la[i], tp, 1);
    Add(lb[i], tp, 0);
    Check_limit(i);
  }
  for (int k = 1; k < M; ++k) {
    for (int i = 1; i <= n; ++i) {
      if (!fa[i][k]) continue;
      stv[i][k - 1].push_back({i, k});
      stv[fa[i][k - 1]][k - 1].push_back({i, k});
    }
  }
  int cur = 1;
  for (int i = 1; i < n; ++i, lq.pop()) {
    if (lq.empty()) {
      while (vis[cur]) ++cur;
      assert(cur < n);
      lq.push({cur, eu[cur] == fa[ev[cur]][0]});
      cur++;
    }
    pair<int, bool> tp = lq.front();
    int id = tp.first;
    bool o = tp.second;
    ans[id] = o ^ (eu[id] == fa[ev[id]][0]);
    int u = (ev[id] == fa[eu[id]][0] ? eu[id] : ev[id]);
    Update(u, 0, o);
  }
  for (int i = 1; i < n; ++i) {
    cout << ans[i]; 
  }
  cout << '\n';
  return 0; 
}

Day2 T1 分數(fraction)

\(n \le m\),題意等價於我們有初始狀態 \((p, q) = (0, 1)\),我們可以進行如下操作若干次(其中 \(u\) 為任意正整數):

  • \((p, q) \gets (q, 2uq + p)\)

問能到達的狀態總數,在這過程中始終要求 \(p \le n, q \le m\)。考慮模擬這一過程,注意到無需判重,並且能夠到達的狀態數較少,可以得到一個 \(O(ans)\) 的做法,可以獲得 \(90 \sim 95\) 分。

我們可以將最終狀態 \((p, q)\) 所代表分數 \(\frac{p}{q}\) 寫成連分數形式 \([2u_1, 2u_2, \ldots, 2u_m]\),容易發現這個序列從後往前與搜尋時給出的引數 \(u\) 是相同的。

考慮序列中最大的一項 \(x = \max u_i\),如有多項取靠後的,我們將這一項待定,最終狀態可以表示為 \((ax+b, cx + d)\),我們在此時可以 \(O(1)\) 確定 \(x\) 的取值有多少個。這樣我們一次就搜出了多個解。

具體而言我們維護當前可能的最小 \(x\),並且記錄 \((p, q) / (a, b, c, d)\),如果當前位置填一個最小 \(x\) 都不滿足要求就剪掉。

這樣狀態數大大減少,在 \(n,m \le 3 \times 10^7\) 時狀態數約為 \(8 \times 10^7\)

Code
#include <iostream>

using namespace std;
using LL = long long;

const int Inf = 1e9;

int n, m;
LL ans;

int divi (int x, int y) { return !y ? Inf : x / y; }

bool Dfs2 (int a, int b, int c, int d, int low) {
  LL cnt = max(0, min(divi(n - b, a), divi(m - d, c)) - low + 1);
  cnt += max(0, min(divi(n - b, a), divi(n - d, c)) - low + 1);
  if (!cnt) return 0;
  ans += cnt;
  for (int u = 1; ; ++u) {
    bool t = Dfs2(c, d, 2 * u * c + a, 2 * u * d + b, max(low, u));
    if (!t) { 
      break;
    }
  }
  return 1;
}

bool Dfs (int p, int q, int low) {
  if (!Dfs2(0, q, 2 * q, p, low)) return 0; 
  for (int u = 1; ; ++u) {
    if (!Dfs(q, 2 * u * q + p, max(low, u + 1))) {
      break;
    }
  }
  return 1; 
}

int main () {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> m;
  if (n > m) swap(n, m);
  Dfs(0, 1, 1);
  cout << ans << '\n';
  return 0; 
}

Day2 T2 登山(mountain)

\([tl_i, tr_i]\) 為從 \(i\) 點開始衝刺,允許的終點深度範圍。\(lim_i\) 表示到達 \(i\) 點之後所有衝刺點深度 \(\le lim_i\)\(v_{i \rightarrow j}\) 表示從 \(i\) 滑落到點 \(j\) 時,經過點的最嚴格 \(lim\) 限制,即 \(v_{i \rightarrow j} = \min\limits_{u \in \text{path}(i, j)} lim_u\)

\(f_i\) 表示衝刺到 \(i\) 點的方案數,答案為 \(f_{2 \sim n}\)。考慮暴力從上往下轉移,假設當前 \(\text{dfs}\) 到點 \(u\),且我們已經算出 \(f_u\) 的值,在 \(u\) 子樹內列舉上一個衝刺到的點 \(i\),在 \(i\) 子樹內列舉從 \(i\) 滑落到的點 \(j\),若 \(dep_u \in [l_r, \min(v_{i \rightarrow j}, r_j)]\),則 \(f_i \gets f_i + f_u\),時間複雜度 \(O(n^3)\)

考慮優先列舉 \(j\),對於一個 \(i\),它能衝刺到一段深度區間為 \([l_j, \min(v_{i \rightarrow j}, r_j)]\) 的祖先鏈。做字首和,設 \(j\) 所在祖先鏈上深度為 \(i\) 的點到根的權值和為 \(s_i\),那麼貢獻就是 \(s_{\min(v_{i \rightarrow j}, r_j)} - s_{l_j - 1}\)(並且我們要求 \(l_j - 1 \le v_{i \rightarrow j}\)),即可做到 \(O(n^2)\)

觀察到 \(i\) 不斷往上跳時,按照 \(\min(v_{i \rightarrow j}, r_j)\) 不同劃分成若干段。一方面 \(\min\)\(r_j\) 的段數顯然是 \(O(n)\) 的,另一方面我們讓 \(v_{i \rightarrow j}\) 對應一個深度最大的 \(x\) 使得 \(lim_x = v_{i \rightarrow j}\),從 \(x\) 繼續往上跳段數 \(O(n)\),此時 \(j \rightarrow x\) 的路徑長什麼樣無關,只需對於 \(x\) 預處理即可。這啟發我們拆貢獻並擴散轉移。

具體而言,我們還是列舉 \(u\),並且我們已經知道了 \(s_{dep_u}\),這對 \(u\) 子樹內所有點都有效。分別考慮 \(l_j - 1 = dep_u\)\(\min(v_{i \rightarrow j}, r_j) = dep_u\) 的貢獻怎麼算。

  • 對於 \(l_j - 1 = dep_u\) 的貢獻,在 \(u\) 子樹內列舉所有 \(l_j - 1 = dep_u\)\(j\),列舉總量線性(下同)。在 \(j\) 的祖先鏈上倍增求出一段 \(i\)(要求 \(v_{i \rightarrow j} \ge l_j - 1\)),樹狀陣列維護鏈加即可。

  • 對於 \(\min(v_{i \rightarrow j}, r_j) = dep_u\) 的貢獻,我們繼續分類討論:

    • \(r_j < v_{i \rightarrow j}\),貢獻就是 \(s_{r_j}\),在 \(u\) 子樹內列舉所有 \(r_j = dep_u\)\(j\),要求 \(v_{i \rightarrow j} \ge r_j + 1\),其他和上面差不多。

    • \(v_{i \rightarrow j} \le r_j\),根據前面所述,我們知道存在一個深度最大的 \(x\) 使得 \(lim_x = v_{i \rightarrow j}\),列舉 \(x\),此時 \(i\) 的範圍已經與 \(j\) 無關了,我們只需對於每個 \(x\) 預處理 \(j\) 的數量 \(w_x\),對於 \(i\) 我們只要求 \(v_{i \rightarrow x} \ge lim_x\),和前面差不多,兩部分乘起來即可。

考慮如何預處理 \(w_x\),特判 \(j = x\) 的情況,我們要求 \(\forall u(u \neq x) \in \text{path}(j, x), lim_u > lim_x\),且 \(lim_x \in [l_j - 1, r_j]\),注意到對於一個 \(j\),合法的 \(x\) 一定是 \(j\) 所在祖先鏈上的字尾最小值,因此對於每個點向第一個 \(lim\) 小於自己的祖先連邊,構成一棵新樹,在新樹上的祖先鏈與滿足第一個限制的點一一對應。

列舉 \(j\),只需考慮第二個限制,由於在新樹上 \(lim\) 具有單調性,所以合法的 \(x\) 構成新樹上的一條鏈,差分即可。

時間複雜度 \(O(n \log n)\)

Code
#include <iostream>
#include <vector>
#include <cassert>

using namespace std;

const int N = 1e5 + 5, M = 17;
const int Mod = 998244353;

int n;
int fa[N][M], tl[N], tr[N], lim[N][M], dep[N];
int f[N], s[N], w[N], virt_fa[N];
int dfn[N], now, siz[N], c[N];
vector<int> e[N], vec[N][3], vt[N];

int Kth_fa (int x, int y) {
  assert(y >= 0);
  for (int k = M - 1; ~k; --k) {
    if ((y >> k) & 1) {
      x = fa[x][k];
    } 
  }
  return x;
}

int Jump (int x, int y, int limit = -1) {
  if (lim[x][0] < y || dep[x] <= limit) return -1;
  for (int k = M - 1; ~k; --k) {
    if (lim[fa[x][0]][k] >= y && dep[x] > limit - (1 << k)) {
      x = fa[x][k];
    }
  }
  return x;
}

void Init (int u) {
  dfn[u] = ++now, siz[u] = 1;
  for (auto v : e[u]) {
    Init(v);
    siz[u] += siz[v];
  }
}

void Bit_add (int x, int y) {
  if (!x) return; 
  for (; x <= n; x += (x & -x)) {
    c[x] = (c[x] + y) % Mod;
  }
}

int Bit_query (int l, int r) {
  int res = 0; 
  for (--l; l; l -= (l & -l)) {
    res = (res - c[l] + Mod) % Mod;
  }
  for (; r; r -= (r & -r)) {
    res = (res + c[r]) % Mod;
  }
  return res;
} 

void Dfs (int u) {
  if (u != 1) {
    f[u] = Bit_query(dfn[u], dfn[u] + siz[u] - 1);
  }
  s[u] = (s[fa[u][0]] + f[u]) % Mod;
  auto Add_list = [&](int u, int v, int val) -> void {
    Bit_add(dfn[fa[u][0]], (Mod - val) % Mod);
    Bit_add(dfn[v], val);
  };
  for (auto j : vec[u][0]) {
    int topi = Jump(j, tl[j] - 1, dep[u]);
    if (topi != -1) {
      Add_list(topi, j, 1ll * (Mod - 1) * s[u] % Mod);
    } 
  }
  for (auto j : vec[u][1]) {
    int topi = Jump(j, tr[j] + 1, dep[u]);
    if (topi != -1) {
      Add_list(topi, j, s[u]);
    }
  }
  
  for (auto x : vec[u][2]) {
    int val = 1ll * w[x] * s[u] % Mod;
    int topi = Jump(x, lim[x][0], dep[u]);
    if (topi != -1) {
      Add_list(topi, x, val);
    }
  }
  for (auto v : e[u]) {
    Dfs(v);
  }
}

void Calc (int u) {
  for (auto v : vt[u]) {
    Calc(v);
    w[u] = (w[u] + w[v]) % Mod;
  }
}

int main () {
  cin.tie(0)->sync_with_stdio(0);
  int tid, T;
  cin >> tid >> T;
  while (T--) {
    cin >> n;
    fill(e + 1, e + n + 1, vector<int>());
    for (int i = 2, l, r, h; i <= n; ++i) {
      cin >> fa[i][0] >> l >> r >> h;
      e[fa[i][0]].push_back(i);
      dep[i] = dep[fa[i][0]] + 1; 
      tl[i] = dep[i] - r, tr[i] = dep[i] - l;
      lim[i][0] = dep[i] - h - 1; 
    }
    lim[1][0] = -1;
    for (int k = 1; k < M; ++k) {
      for (int i = 1; i <= n; ++i) {
        fa[i][k] = fa[fa[i][k - 1]][k - 1];
        lim[i][k] = min(lim[i][k - 1], lim[fa[i][k - 1]][k - 1]);
      }
    }
    fill(vec[1], vec[n] + 3, vector<int>());
    for (int i = 2; i <= n; ++i) {
      int ned[3] = {tl[i] - 1, tr[i], lim[i][0]};
      for (int o = 0; o < 3; ++o) {
        int kthf = Kth_fa(i, dep[i] - ned[o]);
        if (kthf) {
          vec[kthf][o].push_back(i);
        }
      }
    }
    now = 0, Init(1);
    fill(c + 1, c + n + 1, 0);
    fill(f + 1, f + n + 1, 0);
    fill(s + 1, s + n + 1, 0);
    fill(vt + 1, vt + n + 1, vector<int>());
    fill(w + 1, w + n + 1, 0);
    for (int i = 2; i <= n; ++i) {
      virt_fa[i] = fa[Jump(i, lim[i][0])][0];
      vt[virt_fa[i]].push_back(i);
    }
    for (int i = 2; i <= n; ++i) {
      auto Add_limit = [&](int limit, int v) -> void {
        int top = (lim[virt_fa[i]][0] <= limit ? virt_fa[i] : fa[Jump(virt_fa[i], limit + 1)][0]);
        w[top] = (w[top] + v) % Mod;
      };
      Add_limit(min(tr[i], lim[i][0] + 1), 1);
      Add_limit(tl[i] - 2, Mod - 1);
    }
    Calc(1);
    for (int i = 2; i <= n; ++i) {
      w[i] += (tl[i] - 1 <= lim[i][0] && lim[i][0] <= tr[i]);
    }
    f[1] = 1, Dfs(1);
    for (int i = 2; i <= n; ++i) {
      cout << f[i] << ' '; 
    }
    cout << '\n';
  }
  return 0; 
}

Day2 T3 樹形圖(graphee)

首先用 Tarjan 找到能夠到達所有點的 SCC,其他 SCC 中的點一定是三類點。沒有這樣的 SCC 則全是三類點,特判即可。

容易發現一個點是一類點,當且僅當以它為根的 \(\text{dfs}\) 樹,只有樹邊和反祖邊。列舉每一個點線性 \(\text{check}\) 可做到 \(O(n^2)\) 找到所有一類點。

注意到特殊性質 C 給出了一個一類點,這啟示我們用一個一類點去找其他一類點。對於給出的一類點建 \(\text{dfs}\) 樹,我們猜測:

  • 一個點是一類點當且僅當它子樹內恰好有一條出子樹邊(一定是反祖邊),且該反祖邊指向一個一類點。

必要性:若一個點子樹內無出子樹邊,或有多於一條,顯然不合法。若指向的不是一類點,則手玩容易發現使指向的點不合法的邊仍是前向/橫叉邊。充分性:換根相當於替換了一條樹邊和反祖邊,對其他邊沒有影響。

用樹狀陣列動態維護出子樹邊狀態,時間複雜度 \(O((n + m) \log n)\)

接著考慮找二類點,特判掉沒有一類點的情況,此時所有在能夠到達所有點的 SCC 中的全是二類點,否則我們在刪邊時需要使一類點仍然是一類點,相當於欽定若干反祖邊和全部樹邊無法刪除,我們稱為必要邊,判斷一條邊是否是必要邊容易樹形 \(\text{dp}\),我們猜測:

  • 若一個點子樹內恰好有一條必要出子樹邊(該邊一定連向一個一類點),則該點一定是二類點(刪除所有非必要邊即可,換根也相當於替換了一條樹邊和反祖邊,對其他邊沒有影響)。
  • 若一個點子樹內恰好無必要出子樹邊,則該點是二類點當且僅當,存在一條非必要出子樹邊連向一個一/二類點(考慮如果是一類點和前面類似,如果是二類點,將這必要條出子樹邊保留,其一定為樹邊,並且會增加一條反祖邊,不影響合法性,繼續考慮上面的二類點,歸納即可)。
  • 其他情況一定是三類點(有超過一條必要出子樹邊顯然不合法,沒有出子樹邊顯然不合法,所有出子樹邊連向三類點,相當於在本來不合法的基礎上又加了一條邊,當然也不合法)。

這樣我們就完成了找剩下一類點和二類點的操作,現在我們的瓶頸在於需要 \(O(n^2)\) 找到第一個一類點。

考慮如果一個點是一類點,建出 \(\text{dfs}\) 樹後只有樹邊和反祖邊,這告訴我們可以透過不斷縮葉子的操作使這棵樹只有一個點。我們模擬這一過程,若某一時刻沒有葉子,且還剩下至少 \(2\) 個點說明沒有一類點,否則最後剩下的一定是一個一類點。

具體而言,我們不斷處理入度為 \(1\) 的點 \(u\),這個點對應某一以一類點為根的 \(\text{dfs}\) 樹上的葉子,假設有邊 \(u \rightarrow v\),我們將 \(u\)\(v\) 用並查集合並在一起,並檢查 \(v\) 的入邊,因為此時可能成為新葉子的點只有 \(v\)。若 \(v\) 有一條入邊 \(w \rightarrow v\),並且 \(w\)\(v\) 已經合併,則我們可以刪除這一條邊。

我們發現我們無需一次檢查 \(v\) 的所有入邊,只需知道是否還至少存在兩條即可。我們用 \(\text{std::deque}\) 維護,每次只需檢查第一條和最後一條,注意到一共只會檢查 \(O(n)\) 個點,每一條邊只會刪除一次,時間複雜度 \(O((n + m) \alpha(n))\)

總時間複雜度 \(O((n + m) \log n)\)

Code
#include <iostream>
#include <vector>
#include <stack>
#include <queue>
#include <deque>
#include <numeric>

using namespace std;

const int N = 1e5 + 5; 

int n, m, tot;
int tans[N];
vector<int> e[N];

namespace Graph {
  int dfn[N], now, low[N], deg[N], srt, block[N];
  vector<int> vec[N];
  bool ins[N];
  stack<int> st;

  void Tarjan (int u) {
    dfn[u] = low[u] = ++now, ins[u] = 1;
    st.push(u);
    for (auto v : e[u]) {
      if (!dfn[v]) {
        Tarjan(v);
        low[u] = min(low[u], low[v]);
      }
      else if (ins[v]) {
        low[u] = min(low[u], dfn[v]);
      }
    }
    if (low[u] == dfn[u]) {
      vec[++tot].clear();
      for (int v = 0; v != u; ins[v] = 0, st.pop()) {
        v = st.top();
        vec[tot].push_back(v);
        block[v] = tot;
      }
    }
  }

  int Init () {
    fill(dfn + 1, dfn + n + 1, 0), now = tot = 0; 
    for (int i = 1; i <= n; ++i) {
      if (!dfn[i]) Tarjan(i);
    }
    fill(deg + 1, deg + tot + 1, 0);
    for (int i = 1; i <= n; ++i) {
      for (auto j : e[i]) {
        if (block[i] != block[j]) {
          ++deg[block[j]];
        }
      }
    }
    srt = 0; 
    for (int i = 1; i <= tot; ++i) {
      if (deg[i] == 0) {
        if (!srt) srt = i; 
        else {
          for (int i = 1; i <= n; ++i) {
            cout << 3; 
          }
          cout << '\n';
          return 1;
        }
      }
      else {
        for (auto j : vec[i]) {
          tans[j] = 3;
        }
      }
    }
    return 0; 
  }
}

namespace Find_one { 
  deque<int> r[N];
  int fa[N];

  int Find (int x) {
    if (fa[x] == x) return x;
    return fa[x] = Find(fa[x]);
  }

  int Solve () {
    fill(r + 1, r + n + 1, deque<int>());
    for (int i = 1; i <= n; ++i) {
      for (auto j : ::e[i]) { 
        r[j].push_back(i);
      }
    }
    queue<int> q;
    for (int i = 1; i <= n; ++i) {
      if (r[i].size() == 1) {
        q.push(i);
      }
    }
    iota(fa + 1, fa + n + 1, 1);
    for (; !q.empty(); q.pop()) {
      int u = q.front(), v = Find(r[u].back());
      fa[u] = v; 
      for (; !r[v].empty() && Find(r[v].front()) == v; r[v].pop_front());
      for (; !r[v].empty() && Find(r[v].back()) == v; r[v].pop_back()); 
      if (r[v].size() == 1) q.push(v);
    }
    int t = 0; 
    for (int i = 1; i <= n; ++i) {
      if (fa[i] == i) {
        if (!t) t = i;
        else return 0; 
      }
    }
    return t;
  }
}

namespace Dfs_tree {
  struct Bit { 
    int c[N];

    void Init () { fill(c + 1, c + n + 1, 0); }

    void Add (int x, int y) {
      for (; x <= n; x += (x & -x)) {
        c[x] += y;
      }
    }

    int Query (int l, int r) {
      int res = 0;
      for (--l; l; l -= (l & -l)) {
        res -= c[l];
      }
      for (; r; r -= (r & -r)) {
        res += c[r];
      }
      return res;
    }
  } dyn, unec, nec;

  int dfn[N], now, siz[N], c[N], root, shal[N], dep[N];
  vector<int> vec[N];

  void Build (int u) {
    dfn[u] = ++now;
    siz[u] = 1; 
    for (auto v : e[u]) {
      if (!dfn[v]) {
        dep[v] = dep[u] + 1; 
        Build(v);
        siz[u] += siz[v];
      }
      else {
        vec[v].push_back(u);
      }
    }
  }

  void Init (int rt) {
    root = rt;
    fill(dfn + 1, dfn + n + 1, 0);
    fill(vec + 1, vec + n + 1, vector<int>());
    tans[rt] = 1, now = 0, dep[rt] = 0, Build(rt);
  }

  namespace Find_other_one {
    void Dfs (int u) {
      if (dfn[u] != 1) {
        if (dyn.Query(dfn[u], dfn[u] + siz[u] - 1) == 1) {
          tans[u] = 1;
        }
      }
      if (tans[u] == 1) shal[u] = dep[u];
      for (auto v : vec[u]) {
        dyn.Add(dfn[v], (tans[u] != 1) + 1);
      } 
      for (auto v : e[u]) {
        if (dfn[v] > dfn[u]) {
          shal[v] = max(shal[v], shal[u]);
          Dfs(v);
        }
      }
    }

    void Solve () { fill(shal + 1, shal + n + 1, -1), dyn.Init(), Dfs(root); }
  }

  namespace Find_two {
    void Dfs (int u) {
      if (!tans[u]) {
        int nec_cnt = nec.Query(dfn[u], dfn[u] + siz[u] - 1);
        if (nec_cnt == 1) {
          tans[u] = 2; 
        }
        else if (!nec_cnt && unec.Query(dfn[u], dfn[u] + siz[u] - 1)) {
          tans[u] = 2;
        }
      }
      for (auto v : vec[u]) {
        if (tans[u] == 1 && shal[v] > dep[u]) {
          nec.Add(dfn[v], 1);
        }
        else if (tans[u] == 1 || tans[u] == 2) {
          unec.Add(dfn[v], 1);
        }
      }
      for (auto v : e[u]) {
        if (dfn[v] > dfn[u]) {
          Dfs(v);
        }
      }
    }

    void Solve () {
      nec.Init(), unec.Init();
      Dfs(root);
    }
  }
}

int main () {
  cin.tie(0)->sync_with_stdio(0);
  int tid, T;
  cin >> tid >> T;
  while (T--) {
    cin >> n >> m;
    fill(e + 1, e + n + 1, vector<int>());
    for (int i = 1, u, v; i <= m; ++i) {
      cin >> u >> v;
      e[u].push_back(v);
    }   
    fill(tans + 1, tans + n + 1, 0);
    if (Graph::Init()) continue;
    int rt = Find_one::Solve();
    if (!rt) {
      for (int i = 1; i <= n; ++i) {
        cout << (!tans[i] ? 2 : 3);
      }
      cout << '\n';
      continue;
    }
    Dfs_tree::Init(rt);
    Dfs_tree::Find_other_one::Solve();
    Dfs_tree::Find_two::Solve();
    for (int i = 1; i <= n; ++i) {
      cout << (!tans[i] ? 3 : tans[i]);
    }
    cout << '\n';
  }
  return 0; 
}