1. 程式人生 > 其它 >省選日記 Day-34 - Day-30

省選日記 Day-34 - Day-30

省選日記 Day \(-34\) - Day \(-30\)

Day \(-34\) Feb 28, 2022, Monday

今天是 USACO 金組的結算日.

T1 Redistributing Gifts

這題的規則和背景均基於銀組 T1, 仍然是像銀組 T1 一樣建一個有向圖. 每次交換相當於沿著簡單環交換, 因為每一個方案都需要讓每一頭牛更加滿意, 所以我們可以認為任何最終狀態都可以通過每頭牛直接得到自己最後的禮物來得到, 不需要經過中間過程.

所以不考慮點集限制, 一個輸入的最終狀態數量等價於我們選擇若干邊把整個圖劃分成不同的環的方案數, 兩個方案不同當且僅當一個方案中存在選擇了的邊在另一個方案中沒有被選擇. 最終答案即為對於兩個點集求這個方案數然後相乘.

狀壓 DP, 提前寫的時候只處理了一個點集是否可以成環, 沒有考慮這個點集有多少成環方式, 所以交上就 WA 了.

記錄 \(Ed_{i, j}\) 表示 \(i\)\(j\) 是否有邊. 設 \(F(S)\) 為集合 \(S\) 的編號最大的元素, 用 \(h_{S, i}\) 表示以 \(F(S)\) 為起點, 以 \(i\) 為終點, \(S\) 點整合一條鏈的方案數.

\[h_{S, i} = \sum_{j \in (S - i), j < F(S)} h_{S - i, j}Ed_{j, i} \]

\(g_S\) 表示 \(S\) 點集連成一條環的方案數.

\[g_S = \sum_{j \in S, j < F(S)} h_{S, j}Ed_{j, F(S)} \]

\(f_S\)

表示把 \(S\) 連成若干環的方案數. 列舉子集 \(S'\) 作為一個環, 其餘部分連成若干環. 為了防止重複, 我們使得每種組合方案都有 \(F(S) \in S'\).

\[f_S = \sum_{S' \subseteq S, F(S) \in S'} g_Sf_{S - S'} \]

最後直接詢問兩個點集 \(A\), \(B\)\(f_Af_B\) 即可.

unsigned long long h[263005][18], g[263005], f[263005];
unsigned Ed[18], m, n, N;
char IO[20];
unsigned Cnt(0), Ans(0), Tmp(0);
signed main() {
  N = (1 << (n = RD()));
  for (unsigned i(0); i < n; ++i) {
    char Flg(1);
    for (unsigned j(0); j < n; ++j) {
      Tmp = RD() - 1;
      if (Flg) Ed[i] |= (1 << Tmp);
      if (Tmp == i) Flg = 0;
    }
  }
  for (unsigned i(0); i < n; ++i) h[1 << i][i] = 1; h[0][0] = 1;
  for (unsigned i(1); i < N; ++i) {
    unsigned Low(0);
    while ((1 << Low) <= (i >> 1)) ++Low;
    for (unsigned End(0); End <= Low; ++End) if (((i >> End) & 1)) {
      if ((Ed[End] >> Low) & 1) g[i] += h[i][End];
      for (unsigned To(0); To < Low; ++To) if ((!((i >> To) & 1)) && ((Ed[End] >> To) & 1)) {
        h[i | (1 << To)][To] += h[i][End];
      }
    }
  }
  f[0] = g[0] = 1;
  for (unsigned i(0); i < N; ++i) {
    unsigned Low(0);
    while ((1 << Low) <= (i >> 1)) ++Low;
    for (unsigned j(i); j >> Low; j = (j - 1) & i) {
      f[i] += f[i ^ j] * g[j];
    }
  }
  m = RD(), --N;
  for (unsigned i(1); i <= m; ++i) {
    scanf("%s", IO), Cnt = 0;
    for (unsigned j(0); j < n; ++j) if (IO[j] == 'G') Cnt |= (1 << j);
    printf("%llu\n", f[Cnt] * f[N ^ Cnt]);
  }
  return Wild_Donkey;
}

T2 Cow Camp

這是一道期望 DP, 設交 \(i\) 次期望得分為 \(f_i\), 顯然還剩 \(i\) 次機會時的策略應該是: 如果倒數第 \(i\) 次得到了比 \(f_i\) 大的結果, 那麼就見好就收, 停止提交, 如果沒有達到 \(f_i\), 則繼續交會期望得到更多的分. 因為除樣例外所有測試點的結果相互獨立, 成二項分佈, 則有方程:

\[f_i = \frac {\displaystyle{\sum_{j = 1}^{\lceil f_{i - 1} \rceil - 1} \binom {T - 1}{j - 1}}}{2^{T - 1}}f_{i - 1} + \frac{\displaystyle{\sum_{j = \lceil f_{i - 1} \rceil}^T j\binom {T - 1}{j - 1}}}{2^{T - 1}} \]

這樣我們得到了 \(O(K)\) 的演算法, 但是這樣顯然過不了, 所以繼續優化.

哪裡有取整, 哪裡就有整數分塊(bushi) ----Wild_Donkey

我們發現 \(\lceil f_i \rceil\) 的取值是 \(O(T)\) 的, 而且當 \(\lceil f_i \rceil\) 相同時, 轉移是十分有趣的. 假設對於 \(j \in [i, i + x)\) 的所有 \(\lceil f_{j - 1} \rceil\) 都是 \(Trs\), 設 \(Pre_j = \dfrac {\displaystyle{\sum_{i = 1}^{j} \binom {T - 1}{i - 1}}}{2^{T - 1}}\), \(Suf_j = \dfrac {\displaystyle{\sum_{i = j}^T j\binom {T - 1}{i - 1}}}{2^{T - 1}}\), 那麼就有方程:

\[\begin{aligned} f_{i + x} &= (...(f_iPre_{Trs - 1} + Suf_{Trs})Pre_{Trs - 1} + Suf_{Trs})...)\\ f_{i + x} &= f_i{Pre_{Trs - 1}}^x + Suf_{Trs}\sum_{i = 0}^{x - 1} {Pre_{Trs - 1}}^i \end{aligned} \]

可以用等比數列求和公式 \(Sum_n = a_1 \dfrac{1 - q^n}{1 - q}\), 在 \(O(\log x)\) 的時間內完成 \(x\) 次相同 \(\lceil f_i \rceil\) 的轉移.

我們每次確定一個 \(\lceil f_i \rceil\), 二分查詢下一個 \(\lceil f_i \rceil\) 值的位置. 這樣既可 \(O(T\log^2 K)\) 求出答案.

double C[1005][1005], Pre[1005], P[1005], Cur;
unsigned m, n;
unsigned A, B, t;
unsigned Cnt(0), Ans(0), Tmp(0);
inline unsigned Ceil(double x) { return (unsigned)(x + 0.99999999); }
inline double Pow(double x, unsigned y, double z) { while (y) { if (y & 1) z *= x; x *= x, y >>= 1; }return z; }
inline double Calc(double x, unsigned y, double pre, double p) {
  double pp(pre / (1 - p));
  return pp - Pow(p, y, pp - x);
}
signed main() {
  n = RD(), m = RD();
  C[0][0] = 1;
  for (unsigned i(1); i < n; ++i) {
    C[i][i] = C[i][0] = C[i - 1][0] / 2;
    for (unsigned j(1); j < i; ++j) {
      C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) / 2;
    }
  }
  for (unsigned i(n); i; --i) Pre[i] = Pre[i + 1] + C[n - 1][i - 1] * i;
  for (unsigned i(1); i <= n; ++i) P[i] = P[i - 1] + C[n - 1][i - 1];
  Cur = Pre[1];
  for (unsigned i(Ceil(Cur)), j(1); j <= m; ) {
    unsigned L(1), R(m - j + 1), Mid;
    while (L ^ R) {
      Mid = ((L + R) >> 1);
      if (Calc(Cur, Mid, Pre[i], P[i - 1]) > i) R = Mid;
      else L = Mid + 1;
    }
    if (L == m - j + 1) { printf("%.10lf\n", Calc(Cur, L - 1, Pre[i], P[i - 1])); return 0; }
    Cur = Calc(Cur, L, Pre[i], P[i - 1]), i = Ceil(Cur), j += L;
  }
  return Wild_Donkey;
}

T3 Moo Network

把平面上兩個點的距離的平方作為這兩個點之間的邊權, 給出一些點求最小生成樹權值和.

由於點數過多, 不能直接連 \(O(n^2)\) 條邊. 所以考慮優化, 嘗試刪邊使得邊數在可接受的範圍內.

接下來引入一個最小生成樹常用 Trick:

如果存在 \(a, b, c\) 三個點, 邊權滿足 \(ab < ac\), \(bc < ac\). 當我們對這個圖直接跑 Kruskal 演算法時, 在考慮 \(ac\) 邊時, 一定已經考慮過 \(ab\), \(bc\) 兩條邊了, 考慮完 \(ab\) 邊之後, 無論是否加入, 一定有 \(a, b\) 連通, 同理考慮完 \(bc\) 邊, 一定有 \(b, c\) 連通. 所以在考慮 \(ac\) 邊之前已經有 \(a, b, c\) 連通了, 因此 \(ac\) 邊無意義.

如果我們選一個點做根節點, 用 Dijkstra 最小化根節點到其它點路徑上邊權最大值, 並且在過程中記錄鬆弛的邊的邊權和, 即可得到最小生成樹. 人們一般把這個過程稱為 Prim 演算法. (不過這樣做複雜度沒有優化, 無法完成此題)

對於本題來說, 幸運的是: 點是整點, 且縱座標只有 \([0, 10]\). 我們可以按行來考慮.

對於同一行內的點, 只有相鄰的點之間的邊有用, 這是顯然的, 對於一行內從左到右排列的三個點 \(a, b, c\), 一定有 \(ab < ac\), \(bc < ac\), 所以所有跨過節點的路徑都沒有意義.

對於不同行的點, 假設存在某個點 \(a\), 另一行存在 \(b, c\) 點, 且橫座標不在 \(a\) 的異側, 則 \(a, b, c\) 可以圍成一個鈍角 (直角) 三角形. 不失一般性, 設 \(ba < ca\), 則有 \(b\) 作為鈍角 (直角) 頂點, 也就是說 \(bc < ac\). 這時 \(ac\) 無意義. 因此對於 \(a\) 所在行外的某一行, \(a\) 只需要連線和它橫座標相同的點, 如果不存在這個點, 則連線左右最近的點. \(a\) 到其餘點的連邊都無效.

為了防止重複連邊, 我們列舉每個點, 然後只往左側的和橫座標相同的點連邊. 具體過程是先對每一行連線內部的邊. 然後對於每一行, 列舉除它所在行以外的其它 \(10\) 行, 使用雙指標連線兩行之間的邊.

每個點會向左或豎直連出 \(11\) 條邊. 所以邊數是 \(1e7\) 級別的, 所以 \(O(m \log m)\) 級別的 \(4s\) 可以過.

vector <pair<unsigned, unsigned> > a[11];
vector <pair<unsigned long long, pair<unsigned, unsigned> > > E;
unsigned Fa[1000005], Stack[1000005], STop(0), m, n;
unsigned long long Ans(0);
unsigned A, B, C, D, t;
unsigned Cnt(0), Tmp(0);
unsigned long long Sq(unsigned x) { return (unsigned long long)x * x; }
unsigned Find(unsigned x) {
  while (x ^ Fa[x]) Stack[++STop] = x, x = Fa[x];
  while (STop) Fa[Stack[STop--]] = x;
  return x;
}
signed main() {
  n = RD();
  for (unsigned i(1); i <= n; ++i) A = RD(), B = RD(), a[B].push_back({ A, ++Cnt });
  for (unsigned i(0); i <= 10; ++i) if (a[i].size()) {
    sort(a[i].begin(), a[i].end());
    for (unsigned j(a[i].size() - 1); j; --j)
      E.push_back({ Sq(a[i][j].first - a[i][j - 1].first) ,{a[i][j].second, a[i][j - 1].second} });
  }
  for (unsigned i(0); i < 10; ++i) if (a[i].size()) for (unsigned j(i + 1); j <= 10; ++j) if (a[j].size()) {
    unsigned Pls(Sq(j - i));
    for (unsigned fi(a[i].size() - 1), fj(a[j].size() - 1); (~fi) && (~fj); ) {
      if (a[i][fi].first >= a[j][fj].first)
        E.push_back({ Pls + Sq(a[i][fi].first - a[j][fj].first) ,{a[i][fi].second, a[j][fj].second} }), --fi;
      else
        E.push_back({ Pls + Sq(a[j][fj].first - a[i][fi].first) ,{a[j][fj].second, a[i][fi].second} }), --fj;
    }
  }
  sort(E.begin(), E.end()), Cnt = 0;
  for (unsigned i(1); i <= n; ++i) Fa[i] = i;
  for (auto i : E) {
    A = Find(i.second.first), B = Find(i.second.second);
    if (A ^ B) {
      Fa[A] = B;
      ++Cnt, Ans += i.first;
      if (Cnt == n - 1) break;
    }
  }
  printf("%llu\n", Ans);
  return Wild_Donkey;
}

Day \(-33\) Mar 01, 2022, Tuesday

我好不容易 AK 一次, 你卻讓我輸得, 這麼徹底. ----Wild_Donkey

今天模擬賽特別簡單, 但是有一個地方掛了 \(10'\), 痛失 AK 機會.

讀入字串之前, 一定要特判它的長度是否是 \(0\), 否則某些弱智快讀收不到有意義的字元可能會一直等待讀入字元而超時.

YNOI2019

區間眾數, 就是 Day -35 提到的空間 \(O(n)\), 時間 \(O(n \sqrt n)\) 的區間眾數.

思考好久還是不會, 即使有唐爺爺的點撥也不會, 所以今天毅然看題解.

好了現在看完題解再次對自己的智商產生了懷疑. 首先離散化. 對每個權值 \(i\) 建一個 vector \(List_i\) 儲存它每次出現的位置. 對於原序列, 我們記錄每個元素 \(a_i\) 在它所屬 vector 中的下標 \(Pos_i\). 這些我這些天都想到了, 但是查詢的手段真是過於巧妙.

對於詢問區間 \([L, R]\). 查詢整塊得到一個答案 \(Ans\), 用 \(B\) 表示塊長. 我們知道散點最多使答案變成 \(Ans + 2B\). 而眾數只會出現在散點和整塊眾數這 \(O(2B)\) 個值中, 而整塊的眾數不需要單獨列舉. 以左邊散點為例, 列舉每個散點 \(a_i\), 如果它可以作為眾數, 那麼必須滿足 \(List_{a_i, Pos_i + Ans - 1} \leq R\). 我們從 \(Pos_i + Ans - 1\), 開始往後遍歷 \(List_{a_i}\), 只要在 \([L, R]\), 就增加 \(Ans\), 並且移動遍歷 \(List\) 所用的指標. 右側的散點對稱操作即可. 因為 \(Ans\) 最多增加 \(2B\), 其它的操作都是 \(O(1)\), 因此一次查詢是 \(O(B)\) 的.

vector<unsigned> List[500005];
unsigned a[500005], Pos[500005], Cond[1005000], m, n;
unsigned A, B, C, L, R, BL, BR;
unsigned Cnt(0), Ans(0);
inline unsigned* Pnt(unsigned x, unsigned y) { return Cond + x * (A + 1) + y; }
signed main() {
  n = RD(), m = RD(), B = max((n / 1000) + 1, (unsigned)(n / (sqrt(m) + 1)) + 1), A = n / B;
  for (unsigned i(1); i <= n; ++i) Pos[i] = a[i] = RD();
  sort(Pos + 1, Pos + n + 1), C = unique(Pos + 1, Pos + n + 1) - Pos - 1;
  for (unsigned i(1); i <= n; ++i) a[i] = lower_bound(Pos + 1, Pos + C + 1, a[i]) - Pos;
  for (unsigned i(0); i < A; ++i) {
    memset(Pos, 0, (C + 1) << 2);
    unsigned* To(Pnt(i, i)), * Cur(a + i * B + 1), Tmp(0);
    for (unsigned j(i); j < A; ++j, ++To) {
      for (unsigned k(1); k <= B; ++k, ++Cur) Tmp = max(++Pos[*Cur], Tmp);
      (*To) = Tmp;
    }
  }
  for (unsigned i(1); i <= n; ++i) List[a[i]].push_back(i), Pos[i] = List[a[i]].size() - 1;
  for (unsigned i(1); i <= m; ++i) {
    L = (RD() ^ Ans), R = (RD() ^ Ans);
    if (L > R) swap(L, R);
    if (R > n) return 0;
    BL = (L + B - 2) / B, BR = (R / B);
    if (BL >= BR) {
      Ans = 0;
      for (unsigned k(L); k <= R; ++k) {
        unsigned Cur(Pos[k] + Ans), Now(a[k]);
        while ((Cur < List[Now].size()) && (List[Now][Cur] <= R)) ++Cur, ++Ans;
      }
      printf("%u\n", Ans);
      continue;
    }
    Ans = *Pnt(BL, BR - 1), BL = (B - ((L - 1) % B)) % B, BR = R % B;
    for (unsigned j(0), k(L); j < BL; ++j, ++k) {
      unsigned Cur(Pos[k] + Ans), Now(a[k]);
      while ((Cur < List[Now].size()) && (List[Now][Cur] <= R)) ++Cur, ++Ans;
    }
    for (unsigned j(0), k(R); j < BR; ++j, --k) {
      unsigned Cur(Pos[k] - Ans), Now(a[k]);
      while ((Cur < 0x3f3f3f3f) && (List[Now][Cur] >= L)) --Cur, ++Ans;
    }
    printf("%u\n", Ans);
  }
  return Wild_Donkey;
}

知道這個做法後我十分激動, 高速敲鍵盤, 半個小時就寫完了. 但是一來忘記了強制線上要異或, 二來忘記了最靠右的整塊編號可能算出負數, unsigned 炸成正無窮導致 RE.

Day \(-32\) Mar 02, 2022, Wednesday

今天巨難, 比 Ynoi 都難.

T1 博弈論

這道題還是比較簡單的.

先考慮只有 \(1\) 的情況, 根據 Sg 函式的定義, 可以認為 \(\{1,1,...,1\}\) 這種局面的 Sg 值是 \(a_1 \And 1\).

如果只有 \(2\), 那麼它有兩種操作, 我們可以選擇取一個 \(2\) 分裂成兩個 \(1\), 或者按規則刪除對應數量的 \(2\).

目標局面中可能既有 \(1\) 也有 \(2\), 每次可以選擇對 \(1\) 操作, 也可以選擇對 \(2\) 操作. 顯然這是一個 Nim 的形式, 所以我們可以單獨計算 \(1\) 的 Sg 值和 \(2\) 的 Sg 值, 異或起來就是當前局面的 Sg 值.

由於形如 \(\{2,2,...,2\}\) 的局面一步只能生成 \(2\)\(1\), 而 \(\{1,1\}\) 的 Sg 值為 \(0\), 所以這等價於直接將這個 \(2\) 刪除. 所以我們可執行的操作變成了刪除 \(1\)\(2\) 或刪除 \(2\)\(2\). 這樣便可以得到僅包含 \(2\) 的局面的 Sg 值, \(a_2 \% 3\).

這裡需要證明兩個結論: 對於任何正整數 \(x\), \(Sg\{x\} = 1\); 對於任何奇數 \(x\), \(Sg\{x, x\} = 0\).

用歸納法, 假設對 \(i < x\) 都滿足上面的結論. 對於 \(\{x\}\) 有兩種操作, 一個是把它分成 \(j + k\), 另一個是刪除它自己.

對於第一個操作得到的局面 \(\{j, k\}\). 如果 \(j = k\), 則 \(j\) 為奇數且 \(j < x\), 根據假設 \(Sg\{j, k\} = Sg\{j, j\} = 0\). 如果 \(j \neq k\), 則 \(j < x\), \(k < x\), 有 \(Sg\{j, k\} = Sg\{j\} \oplus Sg\{k\} = 1 \oplus 1 = 0\).

對於第二個操作得到的局面 \(\varnothing\), 有 \(Sg\varnothing = 0\).

因此 \(Sg\{x\} = 1\).

如果 \(x\) 是奇數, \(\{x, x\}\) 可以變成 \(\{x\}\) 或者是 \(\{x, j, k\}\). 已經知道 \(Sg\{x\} = 1\), 根據前面也知道 \(Sg\{j, k\} = 0\), 因此 \(Sg\{x, j, k\} = Sg\{x\} \oplus Sg\{j, k\} = 1 \oplus 0 = 1\), 所以 \(Sg\{x, x\} = 0\).

邊界條件為 \(\{1\}\)\(\{1, 1\}\) 都滿足這兩個結論, 兩個結論得證.

繼續討論更多的僅包含一個數字的局面 \(\{x, x,...,x\}\), 發現如果選擇一個 \(x\), 將其分裂為 \(j\), \(k\). 當 \(j = k\) 時, 必須有 \(j\) 是奇數, 有 \(Sg\{j, k\} = Sg\{j, j\} = 0\); 當 \(j \neq k\) 時, 有 \(Sg\{j, k\} = Sg\{j\} \oplus Sg\{k\} = 1 \oplus 1 = 0\). 因此對於任意 \(x\), 分裂一個 \(x\) 到達的局面的 Sg 值和刪除一個 \(x\) 到達的局面的 Sg 值是相等的. 規則可以改為每次選擇數字 \(x\), 可以選擇刪除 \(x\)\(x\)\(1\)\(x\), 得到等價問題.

如果用 \(f_{i, j}\) 表示由 \(j\)\(i\) 組成的局面的 Sg 值, 那麼有 \(f_{i, j}\) 為最小的沒有在 \(f_{i, j - i} (j \geq i)\)\(f_{i, j - 1}\) 中出現的自然數. 邊界條件為 \(f_{i, 0} = 0, f_{i, 1} = 1\).

對於奇數 \(i\), 有 \(f_{i, j} = j \And 1\).

同樣使用歸納法證明, 先考慮 \(0 < j < i\) 的情況. 如果對於 \(k < j\) 都有 \(f_{i, k} = k \And 1\). 當 \(j \And 1 = 0\) 時, 有 \((j - 1) \And 1 = 1\). 則 \(f_{i, j} = 0\). 當 \(j \And 1 = 1\) 時, 有 \((j - 1) \And 1 = 0\), 則 \(f_{i, j} = 1\). 邊界 \(f_{i, 0} = 0\) 滿足該結論, \(0 < j < i\) 的情況得證.

對於 \(j \geq i\) 的情況. 當 \(j \And 1 = 0\) 時, 有 \((j - 1) \And 1 = (j - i) \And 1 = 1\). 則 \(f_{i, j} = 0\). 當 \(j \And 1 = 1\) 時, 有 \((j - 1) \And 1 = (j - i) \And 1 = 0\), 則 \(f_{i, j} = 1\). 已證明 \(0 \leq j < i\) 時結論成立, 因此結論得證.

對於偶數 \(i\), 有 \(f_{i, j} = j \% (i + 1) \And 1 + 2[j \% (i + 1) = i]\).

仍然是先考慮 \(0 < j < i\) 的情況, 這種情況下 \(j \% (i + 1) = j \neq i\), 所以我們可以簡化結論為 \(f_{i, j} = j \And 1\). 因為這些情況的轉移和 \(i\) 無關, 所以證明和前面 \(i\) 為奇數時相同.

\(j = i\) 時, \(j \% (i + 1) = j = i\), 又因為 \(j \And 1 = i \And 1 = 0\), 所以結論可以寫成 \(f_{i, j} = 2\). 因為 \(f_{i, j - 1} = (j - 1) \And 1 = (i - 1) \And 1 = 1\), \(f_{i, j - i} = f_{i, 0} = 0\), 所以 \(f_{i, j} = 2\), \(j = i\) 時結論得證.

接下來討論 \(j > i\) 的情況.

\(j \% (i + 1) < i\) 時, 如果 \((j \% (i + 1)) \And 1 = 1\), 則 \(((j - i) \% (i + 1)) \And 1 = 0\), \(((j - 1) \% (i + 1)) \And 1 = 0\), 所以 \(f_{i, j - i}\), \(f_{i, j - 1}\) 沒有出現 \(1\), 因為每 \(i + 1\)\(j\) 才會出現一個 \(2\), 所以 \(f_{i, j - i}\), \(f_{i, j - 1}\) 至多有一個 \(2\), 至少有一個 \(0\). 因此 \(f_{i, j} = 1\).

如果 \((j \% (i + 1)) \And 1 = 0\), 則 \(((j - 1) \% (i + 1)) \And 1 = 1\), \(((j - i) \% (i + 1)) \And 1 = 1\), 則 \(f_{i, j - 1} = f_{i, j - i} = 1\), 因此 \(f_{i, j} = 0\).

\(j \% (i + 1) = i\) 時, 我們知道 \((j - i) \% (i + 1) = 0\), \(((j - 1) \% (i + 1) = i - 1) \And 1 = 1\), 因此 \(f_{i, j} = 2\).

結論得證. 因此有:

\[f_{i, j} = \begin{cases} j \And 1 & \text{if } i \And 1 = 1\\ j \% (i + 1) \And 1 + 2[j \% (i + 1) = i] & \text{if } i \And 1 = 0 \end{cases} \]

這樣我們便可以 \(O(1)\) 得到任意 \(f_{i, j}\). 先手必勝當且僅當 \(f_{1, a_1} \oplus f_{2, a_2} \oplus ... \oplus f_{n, a_n} \neq 0\).

unsigned m, n;
unsigned A, B, C, D, t;
unsigned Cnt(0), Ans(0), Tmp(0);
signed main() {
  n = RD();
  for (unsigned i(1); i <= n; ++i) {
    A = RD() % (i + 1);
    if ((A == i) && (!(i & 1))) Ans ^= 2;
    else Ans ^= (A & 1);
  }
  printf(Ans ? "tomato fish+1s\n" : "rainy day+1s\n");
  return Wild_Donkey;
}

T2 LCT

發現自己已經忘記只寫過一遍的 LCT 了, 但是現在不是寫 LCT 的時候, 放掉.

T3 數論

這是一道十分困難的反演題, 是 SDOI2018 舊試題加強版. 目前實力不允許我進行如此大膽的嘗試.

風暴之眼

LSQ 在 NOIP 之前給我推薦的題, 我那時還是在床上想出來的. 如今重新看, 發現實力降低了, 不會做了, 被藍題虐了. 臨近省選終於搞出這道題, 這道題是個樹上計數 DP.

因為兩種型別的特殊性, 它們都有一個穩定顏色, 也就是說一個狀態的點一旦變成某個顏色就再也不能變化了, 因此每個點從開始到結束最多變化一次.

因為輸入的是穩態, 所以我們可以確定一些點的型別: 如果一個點的鄰居存在和它顏色不一樣的點, 那麼可以證明如果這個點是某種型別則這個狀態下一刻還會發生變化. 對於剩下的點, 它周圍的點顏色都和它相同, 它無論是什麼型別都有可能使穩態建立.

接下來設計狀態開始 DP. 樹形 DP 的核心就是把每棵子樹作為子問題, 並且通過子樹的結果推得自己的結果.

考慮如果是鏈, 如何由長度 \(i\) 的鏈的方案數計算在鏈頂增加一個點的方案數.

如果我們加入的點的兒子和它不同色, 那麼這個點的型別確定了, 只要它和兒子的初態有一個是它的終態即可, 或者仰仗父親給我們提供終態顏色.

如果這個點和兒子同色, 那麼先考慮它的狀態要求它和鄰居全是一種顏色的情況, 這時我們需要讓它和兒子沒有任何時刻是另一種顏色, 也就是它和兒子初態終態需要相同, 對兒子的型別不作要求, 但是需要警告父親不能有任意時刻出現別的顏色; 再考慮它的狀態要求它和鄰居至少有一個是這種顏色的情況, 這時兒子的型別也不重要, 只要兒子和自己初始顏色有一個是自己終態顏色即可, 或是仰仗父親提供.

討論完了鏈, 發現轉移需要記錄兩個狀態: 初態和對父親的要求. 初態容易表示, 但是對父親的要求有兩種: 父親需要至少有一個時刻為自己的終態, 父親不能有任意時刻不為自己的終態. 因為每個點最多變一次色, 所以我們可以通過要求父親的初態來表示. 這樣既可設計出狀態: \(f_{i, 0/1, 0/1}\) 表示點 \(i\) 的子樹在強制限定 \(i\) 的初態和父親的初態時的方案數.

接下來設計轉移, 先考慮邊界, 對於葉子 \(i\) 來說:

\[\begin{aligned} f_{i, a_i, a_{Fa}} &= 1 + [a_{Fa} = a_i]\\ f_{i, a_i, a_{Fa} \oplus 1} &= 1\\ f_{i, a_i \oplus 1, a_{Fa}} &= [a_{Fa} = a_i]\\ f_{i, a_i \oplus 1, a_{Fa} \oplus 1} &= 1 \end{aligned} \]

非葉子節點有億點點麻煩:

\[\begin{aligned} f_{i, a_i, a_{Fa}} &= f_{i, a_i, a_{Fa} \oplus 1} + [a_{Fa} = a_i] \prod_{j \in Son_i} f_{j, a_i, a_i}[a_j = a_i]\\ f_{i, a_i, a_{Fa} \oplus 1} &= \prod_{j \in Son_i} (f_{j, 0, a_i} + f_{j, 1, a_i})\\ f_{i, a_i \oplus 1, a_{Fa}} &= f_{i, a_i \oplus 1, a_{Fa} \oplus 1} - [a_{Fa} \neq a_i]\prod_{j \in Son_i}f_{j, a_i \oplus 1, a_i \oplus 1}[a_j \neq a_i]\\ f_{i, a_i \oplus 1, a_{Fa} \oplus 1} &= \prod_{j \in Son_i} (f_{j, 0, a_i \oplus 1} + f_{j, 1, a_i \oplus 1})\\ \end{aligned} \]

我們發現這個演算法沒有考慮傳遞的方向, 導致同色連通塊陷入囚徒困境, 即所有點都在等待他人更新, 所以它 CW 了. (Completely Wrong)

接下來討論一個點的情況, 和父親顏色相同的點的狀態有四種: 需要父親的顏色傳遞到自己, 無需父親即可變成自己的終態, 自始至終都可以獨自保持自己的終態, 禁止父親出現終態以外的顏色. 這四個狀態互不重合且覆蓋了所有情況, 分別用 \(0, 1, 2, 3\) 表示它們, 其中 \(0, 1\) 狀態初態和終態相反, 型別也已確定. \(2, 3\) 的初態和終態相同, 其中 \(3\) 是要求鄰居始終為自己終態的型別, \(2\) 是另一種型別.

設計狀態 \(f_{i, j}\) 表示第 \(i\) 個點為狀態 \(j\), 它子樹合法的方案數. 前三個狀態的轉移是:

\[\begin{aligned} f_{i, 0} &= \prod_{j \in Son_i} (f_{j, 0}[a_j = a_i] + f_{j, 2}[a_j \neq a_i])\\ f_{i, 1} &= \prod_{j \in Son_i} (f_{j, 0} + f_{j, 1} + f_{j, 2}) - f_{i, 0}\\ f_{i, 2} &= \prod_{j \in Son_i} (f_{j, 0}[a_j = a_i] + f_{j, 1} + f_{j, 2} + f_{j, 3})\\ f_{i, 3} &= [a_{Fa} = a_i]\prod_{j \in Son_i} (f_{j, 2} + f_{j, 3})[a_i = a_j] \end{aligned} \]

我們設一個新的根 \(R\) 作為原來根 \(r\) 的父親, 使它們終態相同. 對於 \(r\) 初態不變的狀態, 答案即為 \(f_{r, 3} + f_{r, 2}\), 對於 \(r\) 初態改變的狀態, 取 \(f_{r, 1}\) 即可.

const unsigned long long Mod(998244353);
inline void Mn(unsigned& x) { x -= (x >= Mod) ? Mod : 0; }
unsigned long long Ans(1);
unsigned m, n, A, B;
struct Node {
  vector<Node*> E;
  unsigned f[4], Size;
  char Final;
}N[200005];
inline void DFS(Node* x, Node* Fa) {
  unsigned long long C1(1), C2(1), C3(1), C4(Fa->Final ^ x->Final ^ 1);
  for (auto i : x->E) if (i != Fa) {
    DFS(i, x);
    C1 = C1 * i->f[(x->Final ^ i->Final) << 1] % Mod;
    C2 = C2 * (i->f[0] + i->f[1] + i->f[2]) % Mod;
    C3 = C3 * (((i->Final == x->Final) ? i->f[0] : 0) + i->f[1] + i->f[2] + i->f[3]) % Mod;
    if (C4) C4 = C4 * ((i->Final == x->Final) ? (i->f[2] + i->f[3]) : 0) % Mod;
  }
  x->f[0] = C1;
  x->f[1] = Mod + C2 - C1, Mn(x->f[1]);
  x->f[2] = C3;
  x->f[3] = C4;
}
signed main() {
  n = RD();
  for (unsigned i(1); i <= n; ++i) N[i].Final = RD();
  for (unsigned i(1); i < n; ++i)
    A = RD(), B = RD(), N[A].E.push_back(N + B), N[B].E.push_back(N + A);
  N[0].Final = N[1].Final, DFS(N + 1, N), Ans = N[1].f[1] + N[1].f[2] + N[1].f[3];
  printf("%llu\n", Ans % Mod);
  return Wild_Donkey;
}

這絕對是我做過最難的藍題.

Day \(-31\) Mar 03, 2022, Thursday

光是風暴之眼就給我做吐了.

今天的內容是仙人掌.

仙人掌圖是任意一條邊至多隻出現在一個簡單環的無向連通圖. 這是當時寫圓方樹的時候學弟推薦我學的.

結合之前點雙建立的圓方樹, 發現仙人掌圖是圓方樹的弱化版本, 兩環之間有割點連線, 每個點雙連通分量都是一個簡單環. 所以我們稱仙人掌建出來的圓方樹為狹義圓方樹.

然後這個任務一直延續了三天.

Day \(-30\) Mar 04, 2022, Friday

今天模擬賽十分的不爽. 早上又雙叒叕沒報上名.

T1

\((0, 0)\) 跳馬最少步數到 \((x, y)\), 有 \(1000\) 個詢問, \(x, y \leq 10^9\).

不要在 \(n\) 組詢問的時候從 \(0\) 迴圈到 \(n\), 不要讓等待, 成為遺憾.

演算法十分簡單, 整個圖密集分佈著可以 \(O(1)\) 查詢的點, 我們把它們稱為傳送門. 密集到任意座標搜 \(4\) 步就可以到達至少一個傳送門. 而從傳送門到 \((0, 0)\) 的路徑是最優的, 因此我們用終點搜出它 \(4\) 步之內能到達的所有點, \(O(1)\) 地判斷是否是傳送門, 如果是, 就把搜尋步數加傳送門到 \((0, 0)\) 的步數更新答案.

這個演算法是雙向搜尋的變體, 被我稱為二段跳演算法. 單次詢問需要 \(O(8^4)\), 如果我想, 還可以記搜, 變成 \(O(8^2)\).

仙人掌的完成

我們需要在仙人掌上多次詢問兩點最短路.

解決方案是建立圓方樹, 然後作為樹上問題進行查詢.

從任意點開始 DFS, 每次來到一個點就把這個點加入棧中, 每次遇到已經來過的點, 說明找到了環, 那麼就不斷把棧頂彈出直到這個已經來過的點變成棧頂, 彈出的這些點加上彈出後的棧頂就變成了一個環, 也就是點雙連通分量.

對於鏈上的點來說, 它不會被上面的過程彈出, 所以在回溯的時候, 如果一個點仍未被彈出, 那麼回溯時彈出這個點, 將其本身作為新的點雙.

對於環上兩點的最短距離可以處理環上每個點沿同一方向到第一個點的距離, 相當於字首和, 這樣就可以查詢兩點沿某一個方向的最短路, 然後用環長減去這個最短路.

這個演算法是基於 Tarjan 演算法的, 但是略有不同, 無需記錄 DFS 序和 Low 值. 姑且叫它 Jantar 演算法 (誤).

一些細節會在程式碼中體現, 建圓方樹時不需要給單點也分配一個方點, 這是和廣義圓方樹不同的地方. 每個點出棧的時候, 記錄.

inline void Jantar(Node* x, Node* Co) {
  x->Istk = 1, * (++STop) = x;
  for (auto i : x->E) if (i.second != Co) {
    Node* Cur(i.second);
    if (!(x->Istk)) x->Istk = 1, * (++STop) = x;
    if (Cur->Istk) {
      Tr* Mid(++CntT);
      Mid->Fa = Get(Cur), Mid->Cir = i.first, Mid->Pre = 0;
      while (*STop != Cur) {
        (*STop)->Istk = 0;
        Tr* Sn(Get(*STop));
        Sn->Pre = Mid->Cir;
        Mid->Cir += (*(STop--))->From;
        Sn->Fa = Mid;
      }
    }
    else if (!(Get(i.second)->Fa)) Cur->From = i.first, Jantar(Cur, x);
  }
  if (!(Get(x)->Fa)) Get(x)->Fa = Get(Co), Get(x)->Pre = x->From;
  if (x->Istk) --STop, x->Istk = 0;
}

我們建出來的圓方樹中, 每個環有一個點作為父親, 它有一個方點作為兒子, 這個方點的其它兒子是這個環上其它的點.

對於每個方點到兒子的邊權, 定義為兒子在原圖上到方點的父親的點的最短距離, 通過每個點記錄字首和和方點記錄環長來計算.

我們用樹鏈剖分完成查詢最短路的任務. 注意最後同一個環上任意兩點的距離需要對字首和做差, 然後結合環長比較兩個方向的距離得到.

unsigned long long Ans(0);
unsigned m, n;
unsigned A, B, C, D, t;
unsigned Cnt(0), Tmp(0);
struct Tr {
  vector<Tr*> Son;
  Tr* Fa, * Heavy, * Top;
  unsigned long long ToFa, Pre, Cir, Dep;
  unsigned Size, DFSr, Deep;
}T[20005], * CntT, * List[20005];
struct Node {
  vector<pair<unsigned, Node*> > E;
  unsigned From;
  char Istk;
}N[10005], * Stack[10005], ** STop(Stack);
inline Tr* Get(Node* x) { return T + (x - N); }
inline void PreDFS(Tr* x) {
  x->Size = 1;
  unsigned Mx(0);
  for (auto i : x->Son) {
    i->Dep = x->Dep + i->ToFa, i->Deep = x->Deep + 1, PreDFS(i);
    x->Size += i->Size;
    if (Mx < i->Size) Mx = i->Size, x->Heavy = i;
  }
}
inline void DFS(Tr* x) {
  List[x->DFSr = ++Cnt] = x;
  if (x->Heavy) x->Heavy->Top = x->Top, DFS(x->Heavy);
  for (auto i : x->Son) if (i != x->Heavy) i->Top = i, DFS(i);
}
inline void Qry(Tr* x, Tr* y) {
  Tr* Lx(NULL), * Ly(NULL);
  while (x->Top != y->Top) {
    if (x->Top->Deep > y->Top->Deep) swap(x, y), swap(Lx, Ly);
    Ly = y->Top, Ans += y->Dep - y->Top->Fa->Dep, y = y->Top->Fa;
  }
  if (x->Deep > y->Deep) swap(x, y), swap(Lx, Ly);
  if (y->Deep > x->Deep) Ans += y->Dep - x->Dep, Ly = List[(y = x)->DFSr + 1];
  if ((Lx && Ly) && (x > T + n)) {
    Ans -= Lx->Dep - x->Dep;
    Ans -= Ly->Dep - y->Dep;
    unsigned long long Del((Lx->Pre > Ly->Pre) ? (Lx->Pre - Ly->Pre) : (Ly->Pre - Lx->Pre));
    Ans += min(Del, x->Cir - Del);
  }
  return;
}
signed main() {
  n = RD(), m = RD(), t = RD(), CntT = T + n;
  for (unsigned i(1); i <= n; ++i) T[i].Cir = 0x3f3f3f3f3f3f3f3f;
  for (unsigned i(1); i <= m; ++i) {
    A = RD(), B = RD(), C = RD();
    N[A].E.push_back({ C, N + B });
    N[B].E.push_back({ C, N + A });
  }
  Jantar(N + 1, NULL);
  for (Tr* i(T + 2); i <= CntT; ++i) i->Fa->Son.push_back(i), i->ToFa = min(i->Pre, i->Fa->Cir - i->Pre);
  T[1].Top = T + 1, PreDFS(T + 1), DFS(T + 1);
  for (unsigned i(1); i <= t; ++i) {
    A = RD(), B = RD(), Ans = 0;
    Qry(T + A, T + B);
    printf("%llu\n", Ans);
  }
  return Wild_Donkey;
}