做题记录 #1

A. P5721 分治 FFT

Date: 2025.10.04

Problem Link

Y5 下课程里分治结构有放 Antichain, Tree 两道 Poly 题,故进行了一个学习。

半在线卷积。虽然没学过这个东西,但是其思想是比较经典的。半在线要求每一个 \(f_i\)\(f_{1-i-1}\)\(g\) 卷积而得,发现不同的 \(f\) 对后面的贡献之间无关,可以拆开然后加起来。所以可以拆分成若干个区间分别对后面的东西贡献,考虑使用分治,每次做完左区间就卷积贡献到右区间,这样可以把一个前缀的贡献拆分成 \(\log\) 个区间去计算。分治每层复杂度 \(n\log n\),总复杂度 \(\mathcal{O}(n\log^2 n)\)

之前在学习决策单调性 dp 的高贵分治做法时也有看到类似的东西。它说单调队列做法的应用场景在 dp 转移与前缀具体 dp 值相关时,即有半在线需求时。它的做法也是分治用左区间贡献右区间,或许看到半在线问题就要试图思考拆分贡献分治转化,这样类似 CDQ 分治的东西。

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1 << 17, kP = 998244353, G = 3, iG = 332748118;
int Pow(int a, int b) {
  int ret = 1;
  for (; b > 0; b /= 2) {
    if (b % 2 == 1)
      ret = 1ll * ret * a % kP;
    a = 1ll * a * a % kP;
  }
  return ret;
}

int n, g[kN], f[kN];
int lim, ts[kN], tmp, a[kN], b[kN];

inline void NTT(int *f, int n, int o) {
  for (int i = 0; i < n; i++)
    ts[i] > i && (swap(f[i], f[ts[i]]), 0);
  for (int len = 2; len <= n; len *= 2) {
    int w = Pow(o == 1 ? G : iG, (kP - 1) / len);
    for (int l = 0; l < n; l += len)
      for (int i = l, cur = 1; i < l + len / 2; i++, cur = 1ll * cur * w % kP) {
        int a = f[i], b = 1ll * cur * f[i + len / 2] % kP;
        f[i] = (a + b) % kP, f[i + len / 2] = (a - b + kP) % kP;
      }
  }
}

void Divide(int l, int r) {
  if (l == r)
    return;
  int mid = (l + r) / 2, len = r - l + 1;
  Divide(l, mid);

  for (int i = 0; i < len; i++)
    ts[i] = (ts[i >> 1] >> 1) | ((i & 1) * len / 2);
  for (int i = l; i <= r; i++)
    a[i - l] = (i <= mid ? f[i] : 0), b[i - l] = g[i - l];
  NTT(a, len, 1), NTT(b, len, 1);
  for (int i = 0; i < len; i++)
    a[i] = 1ll * a[i] * b[i] % kP;
  NTT(a, len, -1);
  for (int i = mid + 1, inv = Pow(len, kP - 2); i <= r; i++)
    f[i] = (f[i] + 1ll * inv * a[i - l]) % kP;
  Divide(mid + 1, r);
}

int main() {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n, lim = 1 << __lg(n - 1) + 1;
  for (int i = 1; i < n; i++)
    cin >> g[i];
  f[0] = 1, Divide(0, lim - 1);
  for (int i = 0; i < n; i++)
    cout << f[i] << ' ';
  return 0;
}

B. ABC269Ex Antichain / C. CF1010F Tree (7)

Date: 2025.10.05

Problem Link B

Problem Link C

两个使用相同技巧的好题。有可能已经是套路了/xk

Tree 转化一下题意变成求出 \(n\) 个点的二叉树中包含 \(1\) 的连通块个数。然后这两题都可以直接列出一个卷积形式的 DP 式子。

Antichain: \(f_u[x]=[x=1]+(\prod\limits_{v\in son_x}f_v)[x]\),Tree: \(f_u[x]=[x=0]+(f_{l_u}*f_{r_u})[x-1]\),当然可以用生成函数表示。但是复杂度很高,因为 \(F_u\) 项数是 \(sz_u\) 的,复杂度保底一个 \(n^2\)

但是怎么优化这个东西呢?由于它的复杂度和子树大小强相关,所以考虑怎么优化我所计算的东西的子树大小和。考虑重链剖分,每次只暴力合并重链轻儿子的答案,重链放到一起做。这样我合并的信息量是轻儿子子树大小和的。类似 DSU on Tree,这是 \(\mathcal{O}(n\log n)\) 量级的。于是算子树的答案将重链和轻子树分开考虑。

对这两道题而言,接下来就是考虑重链上选哪个点/连通块伸入哪个点处,然后暴力把生成函数嵌套式拆开,形成前缀积的和之类的东西,然后分治合并轻儿子多项式 NTT 卷积起来即可。

虽然题目用到了超纲知识 Poly,但是对复杂度子树大小相关的重链拆分还是比较厉害。虽然真正需要使用到多项式的地方并不多,但是很多的 DP 转移和其有着一些联系,使用多项式的思考方式来做 DP 的特殊优化或许会比较方便。

Antichain
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using Poly = vector<int>;
using PII = pair<int, int>;
constexpr int kN = 1 << 18, kP = 998244353, G = 3, iG = 332748118;
inline int Pow(int a, int b) {
  int ret = 1;
  for (; b > 0; b /= 2) {
    b % 2 == 1 && (ret = 1ll * ret * a % kP);
    a = 1ll * a * a % kP;
  }
  return ret;
}

namespace POLY {
  int rev[kN], f[kN], g[kN];
  inline void NTT(int *f, int n, int o) {
    for (int i = 0; i < n; i++)
      i < rev[i] && (swap(f[i], f[rev[i]]), 0);
    for (int len = 2; len <= n; len *= 2) {
      int w = Pow(o == 1 ? G : iG, (kP - 1) / len);
      for (int l = 0; l < n; l += len)
        for (int i = l, cur = 1; i < l + len / 2; i++, cur = 1ll * cur * w % kP) {
          int a = f[i], b = 1ll * cur * f[i + len / 2] % kP;
          f[i] = (a + b) % kP, f[i + len / 2] = (a - b + kP) % kP;
        }
    }
  }
  inline Poly operator+(Poly a, Poly b) {
    a.size() < b.size() && (swap(a, b), 0);
    for (int i = 0; i < b.size(); i++)
      a[i] = (a[i] + b[i]) % kP;
    return a;
  }
  inline Poly operator*(const Poly &a, const Poly &b) {
    int sz = a.size() + b.size() - 1, len = 1 << __lg(sz) + 1;
    for (int i = 0; i < len; i++) {
      rev[i] = (rev[i >> 1] >> 1) | ((i & 1) * len / 2);
      f[i] = (i < a.size() ? a[i] : 0), g[i] = (i < b.size() ? b[i] : 0);
    }
    NTT(f, len, 1), NTT(g, len, 1);
    for (int i = 0; i < len; i++)
      f[i] = 1ll * f[i] * g[i] % kP;
    NTT(f, len, -1);
    Poly ret(sz);
    for (int i = 0, inv = Pow(len, kP - 2); i < sz; i++)
      ret[i] = 1ll * inv * f[i] % kP;
    return ret;
  }
}
using POLY::operator*;
using POLY::operator+;

int n;
vector<int> e[kN];
int sz[kN], hson[kN];
void Init(int x, int fa) {
  sz[x] = 1;
  for (auto v : e[x]) {
    if (v == fa)
      continue;
    Init(v, x), sz[x] += sz[v];
    (sz[v] > sz[hson[x]]) && (hson[x] = v);
  }
}

vector<Poly> vec, all;
Poly Mul(int l = 0, int r = vec.size() - 1) {
  if (l == r)
    return vec[l];
  int mid = (l + r) / 2;
  return Mul(l, mid) * Mul(mid + 1, r);
}
pair<Poly, Poly> Get(int l = 0, int r = all.size() - 1) {
  if (l == r)
    return {all[l], Poly{1}};
  int mid = (l + r) / 2;
  auto [lm, lp] = Get(l, mid);
  auto [rm, rp] = Get(mid + 1, r);
  return {lm * rm, lp + lm * rp};
}
Poly f[kN];
void DP(int x) {
  for (int i = x; i != 0; i = hson[i]) {
    for (auto v : e[i])
      v != hson[i] && (DP(v), 0);
  }
  for (int i = x; i != 0; i = hson[i]) {
    for (auto v : e[i])
      v != hson[i] && (vec.push_back(f[v]), 0);
    all.push_back(vec.empty() ? Poly{1} : Mul());
    vec.clear(), vec.shrink_to_fit();
  }
  auto [mul, pre] = Get();
  f[x] = mul + Poly{0, 1} * pre;
  all.clear(), all.shrink_to_fit();
}

int main() {
#ifndef ONLINE_JUDGE
  freopen("input.in", "r", stdin);
  freopen("output.out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  cin >> n;
  for (int i = 2, fa; i <= n; i++)
    cin >> fa, e[fa].push_back(i);
  Init(1, 0), DP(1);
  for (int i = 1; i <= n; i++)
    cout << (i < f[1].size() ? f[1][i] : 0) << '\n';
  return 0;
}

\(\\\)

Tree
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using Poly = vector<int>;
using PII = pair<int, int>;
constexpr int kN = 1 << 17, kP = 998244353, G = 3, iG = 332748118;
inline int Pow(int a, int b) {
  int ret = 1;
  for (; b > 0; b /= 2) {
    b % 2 == 1 && (ret = 1ll * ret * a % kP);
    a = 1ll * a * a % kP;
  }
  return ret;
}

namespace POLY {
  int f[kN], g[kN], rev[kN];
  inline void NTT(int *f, int n, int o) {
    for (int i = 0; i < n; i++)
      rev[i] > i && (swap(f[i], f[rev[i]]), 0);
    for (int len = 2; len <= n; len *= 2) {
      int w = Pow(o == 1 ? G : iG, (kP - 1) / len);
      for (int l = 0; l < n; l += len)
        for (int i = l, cur = 1; i < l + len / 2; i++, cur = 1ll * cur * w % kP) {
          int a = f[i], b = 1ll * cur * f[i + len / 2] % kP;
          f[i] = (a + b) % kP, f[i + len / 2] = (a - b + kP) % kP;
        }
    }
  }
  inline Poly operator+(Poly a, Poly b) {
    a.size() < b.size() && (swap(a, b), 0);
    for (int i = 0; i < b.size(); i++)
      a[i] = (a[i] + b[i]) % kP;
    return a;
  }
  inline Poly operator*(const Poly &a, const Poly &b) {
    int sz = a.size() + b.size() - 1, len = 1 << __lg(sz - 1) + 1;
    for (int i = 0; i < len; i++) {
      rev[i] = (rev[i >> 1] >> 1 | ((i & 1) * len / 2));
      f[i] = (i < a.size() ? a[i] : 0), g[i] = (i < b.size() ? b[i] : 0);
    }
    NTT(f, len, 1), NTT(g, len, 1);
    for (int i = 0; i < len; i++)
      f[i] = 1ll * f[i] * g[i] % kP;
    NTT(f, len, -1);
    Poly ret(sz);
    for (int i = 0, inv = Pow(len, kP - 2); i < sz; i++)
      ret[i] = 1ll * inv * f[i] % kP;
    return ret;
  }
}
using POLY::operator*;
using POLY::operator+;

int n;
vector<int> e[kN];
int sz[kN], hson[kN];
void Init(int x, int fa) {
  sz[x] = 1;
  for (auto v : e[x]) {
    if (v == fa)
      continue;
    Init(v, x), sz[x] += sz[v];
    (sz[v] > sz[hson[x]]) && (hson[x] = v);
  }
}

Poly f[kN];
vector<Poly> vec;
pair<Poly, Poly> Solve(int l = 0, int r = vec.size() - 1) {
  if (l == r)
    return {vec[l], vec[l]};
  int mid = (l + r) / 2;
  auto [ml, pl] = Solve(l, mid);
  auto [mr, pr] = Solve(mid + 1, r);
  return {ml * mr, pl + ml * pr};
}
void DP(int x, int fa) {
  for (int i = x, lst = fa; i != 0; lst = i, i = hson[i]) {
    for (auto v : e[i])
      (v != hson[i] && v != lst) && (DP(v, i), 0);
  }
  for (int i = x; i != 0; fa = i, i = hson[i]) {
    int son = 0;
    for (auto v : e[i])
      (v != hson[i] && v != fa) && (son = v);
    vec.push_back(f[son] * Poly{0, 1});
  }
  f[x] = Solve().second + Poly{1};
  vec.clear(), vec.shrink_to_fit();
}

LL x;
int main() {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> x, x %= kP;
  for (int i = 1, u, v; i < n; i++) {
    cin >> u >> v;
    e[u].push_back(v), e[v].push_back(u);
  }
  Init(1, 0);
  f[0] = {1}, DP(1, 0);
  int ans = 0;
  for (int i = 1, fx = 1, fi = 1; i <= n; i++) {
    ans = (ans + 1ll * fx * Pow(fi, kP - 2) % kP * f[1][i]) % kP;
    fx = 1ll * fx * (x + i) % kP, fi = 1ll * fi * i % kP;
  }
  cout << ans << '\n';
  return 0;
}

D. QOJ7419 Jiry Matchings (7.5)

Date: 2025.10.07

Problem Link

的确,这题跟前两题很像。它依旧是列出卷积形式、子树大小复杂度的 DP,依旧重剖优化。只不过是凸函数 max+ 卷积做闵和而已。想起来的确很简单,但是写了我很久。因为这道题它统计的是匹配,这导致 DP 状态更加复杂:我无法像 Antichain 一样省去该点是否选的 0/1 一维。这也导致在重链上分治合并 DP 值的时候,由于链(区间)头尾都会有相互影响,还需要再加一维 0/1 记底端。

这让状态合并复杂了一些,并且在链区间只有一两个点的时候会给合并带来一些诡异 Corner。虽然我还没发现这些 Corner,但是有一个很聪明的方法可以规避。就是不去做中点分治,类似二进制拆分地分治合并答案,这样散的全在重链最底下,一个点的链这种问题不复存在因为现在当且仅当是叶子才能一个点一个区间,不会存在对下面合并的干扰。比较深刻。

UPD: 是我纯唐。只需要对 [11] 的单点初始状态跟单点 1 取 max 即可

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <array>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
#define int LL
using Convex = vector<LL>;
using PII = pair<int, int>;
constexpr int kN = 2e5 + 5;

inline Convex operator*(const Convex &a, const Convex &b) {
  assert(!a.empty() && !b.empty());
  Convex ret = {a[0] + b[0]};
  int i = 0, j = 0;
  for (; i + 1 < a.size() && j + 1 < b.size();) {
    if (a[i + 1] - a[i] > b[j + 1] - b[j])
      ret.push_back(a[++i] + b[j]);
    else
      ret.push_back(a[i] + b[++j]);
  }
  for (; i + 1 < a.size(); ret.push_back(a[++i] + b[j]));
  for (; j + 1 < b.size(); ret.push_back(a[i] + b[++j]));
  ret.shrink_to_fit();
  return ret;
}
inline Convex operator+(Convex a, Convex b) {
  a.size() < b.size() && (swap(a, b), 0);
  for (int i = 0; i < b.size(); i++)
    a[i] = max(a[i], b[i]);
  return a;
}

int n;
vector<PII> e[kN];
int sz[kN], hson[kN];
void Init(int x, int fa) {
  sz[x] = 1;
  for (auto [v, _] : e[x]) {
    if (v == fa)
      continue;
    Init(v, x), sz[x] += sz[v];
    (sz[v] > sz[hson[x]]) && (hson[x] = v);
  }
}
array<Convex, 4> f[kN];
vector<array<Convex, 2>> vec;
vector<array<Convex, 4>> hvy;
array<Convex, 2> Mul(int l = 0, int r = vec.size() - 1) {
  if (l == r)
    return vec[l];
  int mid = (l + r) / 2;
  auto [l0, l1] = Mul(l, mid);
  auto [r0, r1] = Mul(mid + 1, r);
  return {l0 * r0, l0 * r1 + l1 * r0};
}
array<Convex, 4> Get(int l = 0, int r = hvy.size() - 1) {
  if (l == r)
    return hvy[l];
  int mid = (l + r) / 2;
  auto [l00, l01, l10, l11] = Get(l, mid);
  auto [r00, r01, r10, r11] = Get(mid + 1, r);
  return {l01 * r00 + l00 * r10, l01 * r01 + l00 * r11, l10 * r10 + l11 * r00, l11 * r01 + l10 * r11};
}

void DP(int x, int fa, int fw) {
  for (int i = x, lst = fa; i != 0; lst = i, i = hson[i]) {
    for (auto [v, w] : e[i])
      v != lst && v != hson[i] && (DP(v, i, w), 0);
  }
  for (int i = x; i != 0; fa = i, i = hson[i]) {
    for (auto [v, w] : e[i])
      v != fa && v != hson[i] && (vec.push_back({f[v][1], f[v][3]}), 0);
    auto [f0, f1] = (vec.size() ? Mul() : (array<Convex, 2>{Convex{0}, Convex{0}}));
    hvy.push_back({f0, f0 + f1, f0, f0 * Convex{LL(-1e18), fw}});
    vec.clear(), vec.shrink_to_fit();
    for (auto [v, w] : e[i])
      v == hson[i] && (fw = w);
  }
  f[x] = Get();
  hvy.clear(), hvy.shrink_to_fit();
}

signed main() {
#ifndef ONLINE_JUDGE
  freopen("input.in", "r", stdin);
  freopen("output.out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  cin >> n;
  for (int i = 1, u, v, w; i < n; i++) {
    cin >> u >> v >> w;
    e[u].emplace_back(v, w), e[v].emplace_back(u, w);
  }
  Init(1, 0), DP(1, 0, 0);
  for (int i = 1; i < f[1][1].size(); i++)
    cout << f[1][1][i] << ' ';
  for (int i = f[1][1].size(); i < n; i++)
    cout << "? ";
  return 0;
}

Day15A. 区间 (3)

Date: 2025.10.07

敢考我就敢不会。

不妨只考虑从左往右走。考虑如果不能直接走到终点的时候一定会走到能走到的,右端点最靠右的区间。所以可以只连这样的边,得到一个有拓扑序的、出度为 \(1\) 的有向图,鉴定为一棵树。所以可以记一下 nxt,再从右往左枚举,贡献大概就是一些后缀和。从右往左就 l,r 取反即可。

其实赛时想到了正解的全部,但是在想合答案的时候发现我左端点右端点排序做两次的时候两个序列并不完全相同,觉得会有一些重复/漏贡献。但是这个东西它其实不同的时候一定是包含关系,考虑个但呢,完全没必要管它。有点唐。

感觉是太菜了,习惯了模拟赛做不出题,才会因为一点点小问题就全盘否定自己的做法吧。

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 4e5 + 1, kM = 2 * kN;

unsigned seed;
int n, q, l[kN], r[kN], w[kN];
inline unsigned get(unsigned &x) {
  x ^= x << 13;
  x ^= x >> 17;
  x ^= x << 5;
  return x % 1000;
}

int nxt[kM], cnt[kM], pos[kM];
LL sum[kM], f[kM], ans[kM];
inline void Solve() {
  for (int i = 1; i <= n; i++)
    nxt[l[i]] = r[i], sum[l[i]] += w[i];
  for (int i = 1; i <= 2 * n; i++)
    nxt[i] = max(nxt[i - 1], nxt[i]);
  for (int i = 2 * n; i >= 1; i--) {
    sum[i] += sum[i + 1];
    if (nxt[i] == i) {
      f[i] = -sum[i + 1], cnt[i] = 1, pos[i] = i;
      continue;
    }
    pos[i] = pos[nxt[i]], cnt[i] = cnt[nxt[i]] + 1;
    f[i] = f[nxt[i]] + sum[i + 1] - sum[pos[i] + 1];
  }
  for (int i = 1; i <= n; i++)
    ans[i] += f[r[i]];
  fill_n(sum + 1, 2 * n, 0);
  fill_n(nxt + 1, 2 * n, 0);
}

int main() {
#ifndef ONLINE_JUDGE
  freopen("input.in", "r", stdin);
  freopen("output.out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  cin >> n;
  for (int i = 1; i <= n; i++)
    cin >> l[i] >> r[i];
  for (cin >> q >> seed; q--;) {
    LL sum = 0;
    for (int i = 1; i <= n; i++)
      sum += (w[i] = get(seed));
    for (int i = 1; i <= n; i++)
      ans[i] = sum - w[i];
    Solve();
    for (int i = 1; i <= n; i++)
      l[i] = 2 * n + 1 - l[i], r[i] = 2 * n + 1 - r[i], swap(l[i], r[i]);
    Solve();
    LL Ans = 0;
    for (int i = 1; i <= n; i++)
      Ans ^= ans[i];
    cout << Ans << '\n';
  }
  return 0;
}

Day15B. 最长上升子序列 (2.5)

Date: 2025.10.07

敢考我就敢不会。

维护当前的异或和 cur,往最后加数 x,只能加 [cur >> __lg(x) & 1] == 0 的 x。发现我只能该改变 \(x\) 的最高位,于是可以将没作过最高位的位删除掉。有一个关键结论是,我选择去修改第 i 位的时候,第 0~i-1 位一定已经全 1 了。因为我修改第 i 位时选择的 \(x\),一定不会影响更高位,所以可以简单归纳证明。依此结论,可以简单 DP:\(f_i\) 表示 [0~i-1] 已填满后将 [0~i] 填满的最大序列长度。转移考虑枚举最高位为 \(i\) 的数然后每一个 1 重新加上 和取 max 即可。

赛时想到了关键结论,但是潜意识觉得它是错的,于是在尝试证伪的时候使用了本质也是归纳证明的东西,但是最小情况这个结论他是对的,我忘记去思考这个东西,于是倒闭了。所以在思考、判定结论的时候不仅要尝试去 证明/证伪,更要思考自己的 证明/证伪 有无道理啊!

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e5 + 1;
constexpr LL kA = (1ll << 60) - 1;

int T, n;
LL a[kN], cur;
vector<LL> vec[60], dgt;
LL f[60];
int main() {
#ifndef ONLINE_JUDGE
  freopen("input.in", "r", stdin);
  freopen("output.out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  for (cin >> T; T--;) {
    cin >> n;
    for (int i = 1; i <= n; i++)
      cin >> a[i], dgt.push_back(__lg(a[i]));
    sort(dgt.begin(), dgt.end());
    dgt.erase(unique(dgt.begin(), dgt.end()), dgt.end());
    for (int i = 1; i <= n; i++) {
      LL w = 0;
      for (int d = 0; d < dgt.size(); d++)
        w |= (a[i] >> dgt[d] & 1) << d;
      vec[__lg(a[i] = w)].push_back(w);
    }
    int cnt = 1;
    LL ans = f[0] = 1;
    vec[0].clear();
    for (int i = 1; i < dgt.size(); i++) {
      f[i] = 0;
      for (auto w : vec[i]) {
        LL cur = 1;
        for (w -= 1ll << __lg(w); w > 0; w -= 1ll << __lg(w))
          cur += f[__lg(w)];
        f[i] = max(f[i], cur);
      }
      ans += f[i], vec[i].clear();
    }
    dgt.clear();
    cout << ans << '\n';
  }
  return 0;
}

Day15C. 两棵树 (4)

Date: 2025.10.07

考场上没怎么想。

其实这题的思维难度甚至比前两题更低。考虑对于一个点 \(x\),它的子树大小只取决于根在他的哪个“无根树子树”中。相当于固定 \(x\) 之后,以 \(i\) 为根的 \(x\) 的子树大小这个以 dfn 为下标的序列 \(a\) 它只有 \(deg_x + 1\) 个连续段。

考虑直接枚举 \(x\)\(T_2\) 中的所有“无根树子树”,相当于对 \(T_1\) 加了个子树大小的限制。但是面对这种限制,连续段还不是很好考虑,但是你发现你可以通过改变遍历顺序来让这些连续段变得更加有序,比如将点 \(x\) 的连边按子树大小排序。这样 \(n-a_i>k\) 的点在 \(a\) 中将至多只有两个区间。所以现在相当于对两个 \(dfn\) 集合的交集的所有点 +1 贡献,这个可以直接扫描线二维数点,最坏 2*2 个矩形,写起来有点恶心/xk

其实全程都很好想,没开有点亏其实。

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e5 + 1;

int n, sz[2][kN];
vector<int> e[2][kN];
int dfn[2][kN], dfc, R[2][kN];
void Size(int x, int fa, int o) {
  sz[o][x] = 1;
  for (auto v : e[o][x])
    (v != fa) && (Size(v, x, o), sz[o][x] += sz[o][v]);
}
void Dfs(int x, int fa, int o) {
  dfn[o][x] = ++dfc;
  sort(e[o][x].begin(), e[o][x].end(), [&](int i, int j) { return sz[o][i] > sz[o][j]; });
  for (auto v : e[o][x])
    (v != fa) && (Dfs(v, x, o), 0);
  R[o][x] = dfc;
}

vector<PII> scan[kN];
inline void Add(int y, int l, int r, int w) {
  if (r < l)
    return;
  scan[y].emplace_back(l, w);
  r < n && (scan[y].emplace_back(r + 1, -w), 0);
}
int t[kN];
inline void Add(int x, int w) {
  for (; x <= n; x += x & -x)
    t[x] += w;
}
inline int Ask(int x) {
  int ret = 0;
  for (; x > 0; x -= x & -x)
    ret += t[x];
  return ret;
}

int id[kN], ans[kN];
int main() {
#ifndef ONLINE_JUDGE
  freopen("input.in", "r", stdin);
  freopen("output.out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  cin >> n;
  for (int i = 1, u, v; i < n; i++) {
    cin >> u >> v;
    e[0][u].push_back(v), e[0][v].push_back(u);
  }
  for (int i = 1, u, v; i < n; i++) {
    cin >> u >> v;
    e[1][u].push_back(v), e[1][v].push_back(u);
  }
  Size(1, 0, 0), Size(1, 0, 1);
  Dfs(1, 0, 0), dfc = 0, Dfs(1, 0, 1);
  
  for (int x = 1; x <= n; x++) {
    int i = (x != 1), siz, l, r;
    for (int j = (x != 1), v; j < e[1][x].size(); j++) {
      v = e[1][x][j], siz = n - sz[1][v];
      for (; i < e[0][x].size() && n - sz[0][e[0][x][i]] <= siz; i++);
      l = (i < e[0][x].size() ? dfn[0][e[0][x][i]] : R[0][x] + 1), r = R[0][x];
      if (sz[0][x] > siz) {
        Add(dfn[1][v], 1, dfn[0][x] - 1, 1);
        if (R[1][v] < n)
          Add(R[1][v] + 1, 1, dfn[0][x] - 1, -1);
        r = n;
      }
      Add(dfn[1][v], l, r, 1);
      if (R[1][v] < n)
        Add(R[1][v] + 1, l, r, -1);
    }
    if (x == 1)
      continue;
    siz = sz[1][x], i = (x != 1);
    for (; i < e[0][x].size() && n - sz[0][e[0][x][i]] <= siz; i++);
    l = (i < e[0][x].size() ? dfn[0][e[0][x][i]] : R[0][x] + 1), r = R[0][x];
    if (sz[0][x] > siz) {
      Add(1, 1, dfn[0][x] - 1, 1);
      Add(dfn[1][x], 1, dfn[0][x] - 1, -1);
      if (R[1][x] < n)
        Add(R[1][x] + 1, 1, dfn[0][x] - 1, 1);
      r = n;
    }
    Add(1, l, r, 1);
    Add(dfn[1][x], l, r, -1);
    if (R[1][x] < n)
      Add(R[1][x] + 1, l, r, 1);
  }

  for (int i = 1; i <= n; i++)
    id[dfn[1][i]] = i;
  for (int i = 1; i <= n; i++) {
    for (auto [x, w] : scan[i])
      Add(x, w);
    ans[id[i]] = Ask(dfn[0][id[i]]);
  }
  for (int i = 1; i <= n; i++)
    cout << ans[i] << ' ';
  return 0;
}

Day15D. 维修道路 (4.5)

Date: 2025.10.07

考场上几乎没看。

赛后自己想的时候没有看到询问独立蒙逼挺久,看题解看到离线部分分线段树分治想怎么做的时候才发现。询问独立的话好做不少:他割掉 \(k\) 条边后,原来的无向连通图最多被分成 \(\mathcal{O}(k)\) 个无向连通块,用一些有向边进行连接的一个图。如果我们可以快速获得这个图,那么直接跑 SCC 再缩一下点就能直接得出答案,这一部分是 \(\mathcal{O}(k+m)\) 的,\(m\)\(k^2\)。这个复杂度可以接受因为有 \(\sum k \leq 5*10^5\)

最大难点就在于怎么搞到这个图。无向连通块是好搞的,直接拿 dfn 序列割就好了,多一倍常数的小事情,新去掉的边可以用 \(\mathcal{O}(k^2)\) 空间存一下。那原图的连边呢?比较有意思,现在相当于考虑两个 dfn 区间之间的连边,你发现你的 \(n\) 居然只有 \(3000\),这个边数相当于一个邻接矩阵矩形和。这个东西是静态的可以二维前缀和 \(\mathcal{O}(1)\) 做,于是就做完了。

代码写起来很爽。为什么连 D 都这么简单我却没有在模拟赛中获得任何一分呢?哦原来是两个 (3) 不到的题爆了我啊。敢出我真敢不会呗。搞笑来的

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 3e3 + 1, kM = 5e5 + 1, kK = 200 + 3;

namespace SCC {
  int n;
  vector<int> e[kK];
  int dfn[kK], low[kK], dfc, bel[kK], scc;
  int siz[kK], sz[kK], oud[kN];
  bool ins[kK];
  vector<int> stk;
  inline void Add(int u, int v) { e[u].push_back(v); }
  void Init(vector<int> &p) {
    for (int i = 1; i <= n; i++)
      e[i].clear(), dfn[i] = low[i] = bel[i] = sz[i] = siz[i] = oud[i] = 0;
    n = p.size() - 1;
    for (int i = 1; i <= n; i++)
      sz[i] = p[i] - p[i - 1];
    dfc = scc = 0;
  }
  void Tarjan(int x) {
    dfn[x] = low[x] = ++dfc;
    ins[x] = 1, stk.push_back(x);
    for (auto v : e[x]) {
      if (!dfn[v])
        Tarjan(v), low[x] = min(low[x], low[v]);
      else if (ins[v])
        low[x] = min(low[x], dfn[v]);
    }
    if (dfn[x] == low[x]) {
      scc++;
      for (int i = 0; i != x;) {
        i = stk.back(), stk.pop_back();
        bel[i] = scc, ins[i] = 0, siz[scc] += sz[i];
      }
    }
  }
  inline int Calc() {
    for (int i = 1; i <= n; i++)
      !dfn[i] && (Tarjan(i), 0);
    for (int i = 1; i <= n; i++) {
      for (auto v : e[i])
        oud[bel[i]] += (bel[v] != bel[i]);
    }
    int ans = 0;
    for (int i = 1; i <= scc; i++)
      if (oud[i] == 0) {
        if (ans != 0)
          return 0;
        ans = siz[i];
      }
    return ans;
  }
}

int n, m, q;
PII ed[kM];
int sum[kN][kN];
vector<int> e[kN];
int dfn[kN], R[kN], dfc, fa[kN];
void Dfs(int x, int fa) {
  dfn[x] = ++dfc, ::fa[x] = fa;
  for (int v : e[x])
    !dfn[v] && (Dfs(v, x), 0);
  R[x] = dfc;
}

int cnt[kK][kK];
int main() {
#ifndef ONLINE_JUDGE
  freopen("input.in", "r", stdin);
  freopen("output.out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> m >> q;
  for (int i = 1, u, v; i <= m; i++) {
    cin >> u >> v, ed[i] = {u, v};
    e[u].push_back(v), e[v].push_back(u);
  }
  Dfs(1, 0);
  for (int i = 1; i <= m; i++) {
    auto [u, v] = ed[i];
    u = dfn[u], v = dfn[v];
    u > v && (swap(u, v), 0), sum[u][v]++;
  }
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= n; j++)
      sum[i][j] += sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1];
  }

  vector<int> sp;
  vector<PII> vec;
  for (int k, pre = 0; q--;) {
    cin >> k, sp = {0, n}, vec.clear();
    for (int i = 1, x, ban0, ban1; i <= k; i++) {
      cin >> x, x = (x + pre) % m + 1;
      cin >> ban0 >> ban1;
      auto [u, v] = ed[x];
      ban0 && (vec.emplace_back(u, v), 0);
      ban1 && (vec.emplace_back(v, u), 0);
      u == fa[v] && (swap(u, v), 0);
      if (v == fa[u])
        sp.push_back(dfn[u] - 1), sp.push_back(R[u]);
    }
    sort(sp.begin(), sp.end());
    sp.erase(unique(sp.begin(), sp.end()), sp.end());
    for (auto [u, v] : vec) {
      u = lower_bound(sp.begin(), sp.end(), dfn[u]) - sp.begin();
      v = lower_bound(sp.begin(), sp.end(), dfn[v]) - sp.begin();
      cnt[u][v]++;
    }
    SCC::Init(sp), k = SCC::n;
    for (int i = 1; i < k; i++)
      for (int j = i + 1; j <= k; j++) {
        int li = sp[i - 1] + 1, ri = sp[i], lj = sp[j - 1] + 1, rj = sp[j];
        int w = sum[ri][rj] - sum[li - 1][rj] - sum[ri][lj - 1] + sum[li - 1][lj - 1];
        (w > cnt[i][j]) && (SCC::Add(i, j), 0);
        (w > cnt[j][i]) && (SCC::Add(j, i), 0);
      }
    
    int ans = SCC::Calc();
    cout << ans << '\n', pre = (pre + ans) % m;

    for (int i = 1; i <= k; i++)
      fill_n(cnt[i] + 1, k, 0);
  }
  return 0;
}

E. ABC311Ex Many Illumination Plans (9)

Date: 2025.10.08 (做这题时间代价极大,其实很久都在思考这个时间复杂度的事情,但是并没有那么难证,是我自己唐了)

极为猎奇的东西。

放在分治题单完全想不到跟分治什么关系,因为这次连怎么转化为 size 相关 DP 都不会。

首先,考虑弱化,只做整棵树的答案。这个问题类似一个背包,但是具有着相邻颜色不同的限制,设计 DP 也可以参考合并背包:\(f_{x, m, 0/1}\) 表示当前在 \(x\) 的子树内,用了 \(m\) 的容量,父亲被删除等待连到祖先上的点颜色都是 0/1,的最大美丽值。转移就是背包合并 max+ 卷积,然后把 \(x\) 本身加进去。但是劣完了,背包完全不是凸的,没有闵和说法,依旧 size 无关,复杂度 \(\mathcal{O}(nV^2)\) 一坨。

反思一下,发现如果没有这个 0/1 的限制,我直接暴力把整个子树背包加进来就是 \(\mathcal{O}(\sum sz_x V)\) 的,全是前途,略显无敌。但是有限制能怎么办呢?我实在想要让复杂度 size 相关,所以你发现可以把我的 DP 状态分 0/1 传下去,然后到边界空状态就 0/1 都设为这个状态,反上来就更新原点的 0/1 状态,做完所有儿子之后就插入这个点。像这样:

array<vector<LL>, 2> DP(int x, vector<LL> &fa) {
  array<vector<LL>, 2> fx = ...;  // 设置初值 用来合并父亲当前的背包
  for (auto v : son[x]) {
    for (int o : {0, 1})
	  fx[o] = DP(v, fx[o])[o];
  }
  //  向背包 fx[col[x]] 插入 x (由 fx[col[x] ^ 1] 贡献而来) 
  return fx;
}

复杂度是 \(\mathcal{O}(2^nV)\) 的,劣的很,但是 size 相关,并且 \(2^n\) 是因为我会对一个 size 很大的儿子也做两遍,所以直接继承重儿子的答案,复杂度看上去挺优秀的。分析一下,现在的复杂度相当于 $$F(sz_x)=F(sz_{Heavyson_x})+2\sum\limits_{v\in Lightson_x} F(sz_v) + \mathcal{O}(V)$$
由于 \(F(x)\) 是一个关于 \(x\) 的多项式,所以最坏情况所有的轻儿子一定是顶到最大值的,即轻子树大小一定和重子树一样。那么设有 \(x\) 个子树,则有

\[F(n)=(2x-1)F(\frac nx) + \mathcal{O}(V) \]

由主定理得 \(F(n)=\mathcal{O}(n^{\log_x 2x-1}V)\),在 \(x=2\) 时取最大值 \(\mathcal{O}(n^{\log_23}V)\)

考虑扩展到做所有子树。那么依旧重链剖分,跟上面一模一样做

\[G(sz_x)=G(sz_{Heavyson_x})+\sum\limits_{v\in Lightson_x}(F(sz_v) + G(sz_v)) \]

\(F+G\) 也是关于 \(x\) 的多项式,在 \(x\) 子树大小全相同时最坏,有

\[G(n)=xG(\frac nx)+(x-1)F(\frac nx) \]

\(n^{\log_x x}=\mathcal{O}(n)\) 右侧在 \(x=3\) 取最大,由主定理得 \(G(n)=2F(\frac n3)=\mathcal{O}(n^{\log_2 3}V)\)

因此这个复杂度是正确的。诡异至极。

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <array>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 200 + 1, kV = 5e4 + 1;
inline void Max(LL &x, LL w) { x = max(x, w); }

int n, V, col[kN], w[kN];
vector<int> e[kN];
LL b[kN], ans[kN];

int hson[kN], sz[kN];
void Init(int x) {
  sz[x] = 1;
  for (auto v : e[x]) {
    Init(v), sz[x] += sz[v];
    sz[v] > sz[hson[x]] && (hson[x] = v);
  }
}
array<vector<LL>, 2> DP(int x, vector<LL> &fa, int sv) {
  array<vector<LL>, 2> fx;
  if (hson[x]) {
    fx = DP(hson[x], fa, sv);
    for (auto v : e[x]) {
      if (v == hson[x])
        continue;
      for (int o : {0, 1})
        fx[o] = DP(v, fx[o], 0)[o];
    }
  } else
    fx = {fa, fa};
  for (int i = 0; i <= V - w[x]; i++) {
    LL val = fx[col[x] ^ 1][i] + b[x];
    Max(fx[col[x]][i + w[x]], val), sv && (Max(ans[x], val), 0);
  }
  if (sv) {
    for (auto v : e[x])
      v != hson[x] && (DP(v, fa, 1), 0);
  }
  return fx;
}

int main() {
#ifndef ONLINE_JUDGE
  freopen("input.in", "r", stdin);
  freopen("output.out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> V;
  for (int i = 2, fa; i <= n; i++)
    cin >> fa, e[fa].push_back(i);
  for (int i = 1; i <= n; i++)
    cin >> b[i] >> w[i] >> col[i];
  Init(1);
  vector<LL> I(V + 1, -1e18);
  I[0] = 0, DP(1, I, 1);
  for (int i = 1; i <= n; i++)
    cout << ans[i] << '\n';
  return 0;
}

F. QOJ5020 举办乘凉州喵,举办乘凉州谢谢喵 (8.5)

Date: 2025.10.09

脑子题。我的想法大致如此:你考虑这样的一条链 u->v,它的贡献很诡异,如果看成一个个点来看,它的领域有较多重叠,某些经典邻域染色题的思考让我们想到应该尝试不考虑 (x, d-1) 这样的短贡献,即对于一个点,如果他被走到了,那么我们希望它被距离它刚好 d 的点计算到贡献。但是直接做做不到,因为这样的话还需要向上考虑,极麻烦且划分很没意义。所以考虑把 LCA(u,v) 的邻域先给处理掉,这样剩下的只有路径上其他点子树内距离恰为 d 的点,正好不重不漏。这个信息它也可以做树上前缀后缀和啥的然后作差。

做树上前缀和的话,就是 令 \(f_{i,x}\)\(i\) 子树内 \(dis_i=x\) 的点数

\[g_{x,d}=\sum\limits_{i\in 1\rightarrow x}f_{i, d} \]

然后我觉得这个不好优化,但是看到这个作差觉得 \(g\) 都补上一个 \(1\) 的邻域,作差不会改变答案,就变成了 \(g_{i, d}\) 表示距离 1->x 为 \(d\) 的点数。然后一样不会优化。似乎是官解的一个前缀,但是官解只做到了俩 log,有点劣,而且看不懂他那个二维数点,所以放弃了。

所以看了下其他人的题解。非常巧妙啊!你还是去考虑剥掉 LCA 邻域后一条祖先链子树内的答案,但是这里为了让链的问题更加独立所以补上 LCA 的两个儿子的 d-1 邻域。于是换一下分配方案,可以对链上每一个点的链外子树去考虑了,这个东西令我们想到 重链剖分!这条祖先链上最多只有 \(\log n\) 个轻边,即除了这 \(\log n\) 个轻边的祖先点以外,每个点的贡献都是轻儿子子树内距离 \(\leq d\) 的点数。由于经典结论轻儿子子树大小和为 \(n\log n\) 的,所以可以直接对询问离线,然后暴力枚举统计每个点的轻儿子子树内的,距离恰为 \(d\) 的点,维护后缀和作差查询。复杂度 \(O(n\log n)\)

对于轻边,考虑贡献差异:会少去掉一个重儿子多出轻边儿子的子树邻域。但是这个都是可以直接定位到是那棵子树的,所以再计算对于每个点,子树内距离 \(\leq d\) 的点数。欸这个其实和前面补 LCA 邻域的东西一样,可以长剖维护距离恰好为 \(d\) 的点数然后同样后缀和作差查询,因为有 \(q\log n\) 次查询所以复杂度一个 \(log\)

LCA 邻域点数是点分治宝宝题,一样是恰好距离为 \(d\) 然后前缀和。总共复杂度一个 \(\log\)

这个拆分的确没有想到,有概率是被 Black Radius 诅咒了,但是没做出来是我纯菜/hsh

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 2e5 + 2;

int n, q;
vector<int> e[kN];
int fa[kN], dep[kN], sz[kN], hson[kN];
int dfn[kN], dfc, R[kN], id[kN], top[kN];
void Init(int x, int fa) {
  sz[x] = 1;
  ::fa[x] = fa, dep[x] = dep[fa] + 1;
  for (auto v : e[x]) {
    if (v == fa)
      continue;
    Init(v, x), sz[x] += sz[v];
    sz[v] > sz[hson[x]] && (hson[x] = v);
  }
}
void Dissect(int x, int top) {
  id[dfn[x] = ++dfc] = x;
  ::top[x] = top;
  if (hson[x])
    Dissect(hson[x], top);
  for (auto v : e[x])
    (v != hson[x] && v != fa[x]) && (Dissect(v, v), 0);
  R[x] = dfc;
}
inline int LCA(int x, int y) {
  for (; top[x] != top[y]; x = fa[top[x]])
    dep[top[x]] < dep[top[y]] && (swap(x, y), 0);
  return dep[x] < dep[y] ? x : y;
}

int ans[kN];
vector<PII> vlca[kN], vch[kN];

namespace TaskLCA {
  bool del[kN];
  int sz[kN], dep[kN], cnt[kN], mx;
  int Size(int x, int fa) {
    sz[x] = 1;
    for (auto v : e[x])
      v != fa && !del[v] && (sz[x] += Size(v, x));
    return sz[x];
  }
  int Root(int x, int fa, int w) {
    for (auto v : e[x]) {
      if (v != fa && !del[v] && sz[v] > w / 2)
        return Root(v, x, w);
    }
    return x;
  }
  int dfn[kN], R[kN], dfc, id[kN];
  void Init(int x, int fa) {
    id[dfn[x] = ++dfc] = x;
    for (auto v : e[x]) {
      if (v != fa && !del[v])
        dep[v] = dep[x] + 1, Init(v, x);
    }
    R[x] = dfc;
  }
  inline void Calc(int L, int R, int o) {
    for (int p = L; p <= R; p++)
      cnt[dep[id[p]]]++, mx = max(mx, dep[id[p]]);
    for (int i = 1; i <= mx; i++)
      cnt[i] += cnt[i - 1];
    for (int p = L; p <= R; p++) {
      for (auto [d, i] : vlca[id[p]])
        d >= dep[id[p]] && (ans[i] += o * cnt[min(d - dep[id[p]], mx)]);
    }
    fill_n(cnt, mx + 1, 0), mx = 0;
  }
  void Divide(int x) {
    del[x] = 1;
    dfc = dep[x] = 0, Init(x, 0);
    Calc(1, dfc, 1);
    for (auto v : e[x])
      (!del[v]) && (Calc(dfn[v], R[v], -1), 0);
    for (auto v : e[x]) {
      if (!del[v])
        Divide(Root(v, x, (sz[v] > sz[x] ? Size(v, x) : sz[v])));
    }
  }
  inline void Solve() { Divide(Root(1, 0, Size(1, 0))); }
}

namespace LightSon {
  int cnt[kN], sum[kN];
  void Dfs(int x) {
    for (int i = x; i != 0; i = hson[i]) {
      for (auto v : e[i])
        v != fa[i] && v != hson[i] && (Dfs(v), 0);
    }
    int i = x;
    for (; hson[i] != 0; i = hson[i]);
    for (; i != fa[x]; i = fa[i]) {
      int mx = 0;
      cnt[0]++;
      for (auto v : e[i])
        if (v != fa[i] && v != hson[i]) {
          for (int p = dfn[v], w; p <= R[v]; p++)
            w = dep[id[p]] - dep[i], cnt[w]++, mx = max(mx, w);
        }
      for (int i = mx; i >= 0; i--)
        sum[i] = sum[i + 1] + cnt[i];
      for (auto [d, p] : vch[i])
        ans[abs(p)] += p / abs(p) * (sum[0] - sum[d + 1]);
    }
    for (int i = 0; sum[i] > 0; i++)
      sum[i] = cnt[i] = 0;
  }
  inline void Solve() { Dfs(1); }
}

namespace Subtree {
  int h[kN], hson[kN];
  void Init(int x, int fa) {
    for (auto v : e[x]) {
      if (v == fa)
        continue;
      Init(v, x);
      h[v] > h[hson[x]] && (hson[x] = v);
    }
    h[x] = h[hson[x]] + 1;
  }
  int *f[kN], *g[kN], pool[2 * kN], *F = pool, *G = pool + kN;
  void DP(int x) {
    if (hson[x])
      f[hson[x]] = f[x] + 1, g[hson[x]] = g[x] + 1, DP(hson[x]);
    f[x][0] = g[x][0] = 1;
    h[x] > 1 && (g[x][0] += g[x][1]);
    for (auto v : e[x]) {
      if (v == fa[x] || v == hson[x])
        continue;
      f[v] = F, g[v] = G, F += h[v], G += h[v], DP(v);
      for (int i = 0; i < h[v]; i++)
        f[x][i + 1] += f[v][i];
      for (int i = h[v]; i >= 0; i--)
        g[x][i] = f[x][i] + (i + 1 < h[x] ? g[x][i + 1] : 0);
    }
    for (auto [d, p] : vch[x])
      ans[abs(p)] -= p / abs(p) * (g[x][0] - (d < h[x] ? g[x][d] : 0));
  }
  void Solve() {
    Init(1, 0);
    f[1] = F, g[1] = G, F += h[1], G += h[1], DP(1);
  }
}

int main() {
#ifndef ONLINE_JUDGE
  freopen("input.in", "r", stdin);
  freopen("output.out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  cin >> n;
  for (int i = 1, u, v; i < n; i++) {
    cin >> u >> v;
    e[u].push_back(v), e[v].push_back(u);
  }
  Init(1, 0), Dissect(1, 0);
  cin >> q;
  for (int i = 1, x, y, d, lca; i <= q; i++) {
    cin >> x >> y >> d, lca = LCA(x, y);
    vlca[lca].emplace_back(d, i);
    cerr << lca << ' ';
    for (auto v : {x, y}) {
      for (; top[v] != top[lca]; v = fa[top[v]])
        vch[top[v]].emplace_back(d, i), vch[hson[v]].emplace_back(d, -i);
      if (v != lca)
        vch[hson[lca]].emplace_back(d, i), vch[hson[v]].emplace_back(d, -i);
    }
  }
  TaskLCA::Solve();
  LightSon::Solve();
  Subtree::Solve();
  for (int i = 1; i <= q; i++)
    cout << ans[i] << '\n';
  return 0;
}

Day16A. 双重心 (4)

Date: 2025.10.09

双重心肯定是搞 大小为 \(\frac n2\) 的子树出来,很快想到了需要做一个以 \(i\) 为根时 大小为 \(k\) 的子树个数。我轻松想到了怎么处理 1->i 路径导致的 n-sz 型子树,但想了大概三个小时拼尽全力只能想到带 \(\log\) 的启发式合并做常规子树,看数据范围 5e6 就觉得没有任何能过的可能,就懒得写了。然后有较多的人通过 \(\mathcal{O}(n\log n)\) 过了,有点小丑。

下考跟 fzx 交流了一下,发现这个东西居然可以直接拉 dfn 下来对每个 size 存对应子树 dfn,然后二分做找点。这个形式看上去极为优美。所以我思考了一下,发现这个东西也可以优化到线性,发现对于每个 size 我询问的 dfn 左端点右端点分别是单调的。只需要维护两个指针,下去的时候询问左端点上来的时候询问右端点即可。非常简单。

但是这个贡献确实做了我一会,然后卡空间又卡时间,跑的跟 fzx 带 \(\log\) 没差多少,怎么回事呢,被最优解拉了三四倍。观察了一下代码,这个人不会被卡空间因为它是链式前向星也没有扩容烦恼,对原树上就是双重心的情况似乎没有再 DFS,感觉很厉害,但是看不太懂/hsh

感觉不是很知道自己什么原因想不到这个东西,只能说纯菜了。

Code
#pragma GCC optimize("Ofast,unroll-loops")

// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <ctime>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e6 + 1, kP = 998244353;

int n, type, A, B;
inline int grnd(int &s) {
  s ^= s << 13;
  s ^= s >> 17;
  s ^= s << 5;
  if (s < 0) s = -s;
  return s;
}

int sz[kN], dfn[kN], id[kN], dfc, cnt[kN];
vector<int> e[kN];
int Pool[2 * kN], *buc[kN], bsiz[kN], *Cur = Pool;
void Init(int x, int fa) {
  sz[x] = 1, id[dfn[x] = ++dfc] = x;
  for (auto v : e[x]) {
    if (v == fa)
      continue;
    Init(v, x), sz[x] += sz[v];
  }
  bsiz[sz[x]]++;
}

int c1, c2;
int ans[kN], l[kN], r[kN];
void Dfs(int x, int fa) {
  (sz[x] == n / 2) && (c1 = x, c2 = fa);
  int tgt = abs(n / 2 - sz[x]), tl, tr;
  if (sz[x] != n / 2) {
    for (int &i = l[tgt]; i <= bsiz[tgt] && buc[tgt][i] < dfn[x]; i++);
    tl = l[tgt];
  }
  cnt[sz[x]]--;
  for (auto v : e[x])
    if (v != fa) {
      cnt[n - sz[v]]++, Dfs(v, x);
      cnt[n - sz[v]]--;
    }
  cnt[sz[x]]++;
  
  if (sz[x] != n / 2) {
    for (int &i = r[tgt]; i <= bsiz[tgt] && buc[tgt][i] < dfn[x] + sz[x]; i++);
    tr = r[tgt];
    int w = (sz[x] > n / 2 ? (tr - tl) : (tl + bsiz[tgt] - tr + cnt[tgt]));
    w = 1ll * w * tgt % kP * (sz[x] > n / 2 ? n - sz[x] : sz[x]) % kP;
    ans[x] = (ans[x] + w) % kP, ans[fa] = (ans[fa] + w) % kP;
  }
}

int Get(int x, int fa) {
  sz[x] = 1;
  int ret = 0;
  for (auto v : e[x]) {
    if (v != fa)
      ret = (ret + Get(v, x)) % kP, sz[x] += sz[v];
  }
  return (ret + 1ll * sz[x] * (n / 2 - sz[x])) % kP;
}
int main() {
#ifndef ONLINE_JUDGE
  freopen("input.in", "r", stdin);
  freopen("output.out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> type;
  if (n % 2 == 1)
    return cout << "0\n", 0;
  if (type == 2)
    cin >> A >> B;
  for (int i = 1, u, v; i < n; i++) {
    if (type == 2)
      u = max(1, i - grnd(B) % A), v = i + 1;
    else
      cin >> u >> v;
    e[u].push_back(v), e[v].push_back(u);
  }
  for (int i = 1; i <= n; i++)
    e[i].shrink_to_fit();
  Init(1, 0);
  for (int i = 1; i <= n; i++)
    buc[i] = Cur, Cur += bsiz[i] + 1, buc[i][bsiz[i] = 0] = 0;
  for (int i = 1, siz; i <= n; i++)
    siz = sz[id[i]], buc[siz][++bsiz[siz]] = i;
  
  Dfs(1, 0);
  if (c1 != 0) {
    for (int i = 1; i <= n; i++)
      ans[i] = (ans[i] + n / 2) % kP;
    int w = (Get(c1, c2) + Get(c2, c1)) % kP;
    ans[c1] = (ans[c1] + w) % kP, ans[c2] = (ans[c2] + w) % kP;
  }
  int Ans = 0;
  for (int i = 1, cur = 1; i <= n; i++)
    Ans = (Ans + 1ll * cur * ans[i]) % kP, cur = 2333ll * cur % kP;
  cout << Ans << '\n';
  return 0;
}

Day16D. 生成树 (4)

Date: 2025.10.09

简单题,赛时想到了全部做法但是写不完。

因为权值是个排列,所以相当于安排一个加边顺序。首先我们要使 1~n-1 的边是这个图的最小生成树,也就是说 n~m 作为非树边 (u, v),必须在加入之前树上 u->v 的路径已全部加过。直接做没法知道这个非树边具体什么时候被允许加入,所以可以先考虑暴力,\(\mathcal{O}((n-1)!)\) 枚举树边的相对顺序之后,就可以确定非树边的允许加入时间,就可以 DP 了。但其实直接 DP 的性质不够独立,毕竟是到时间就 “允许加入”,太难受。因此可以倒着加边,改成非树边到时间 “强制考虑加入”,这样会好考虑很多。

可以列一下 DP 转移,因为非树边两两不同,并做好了区分,所以我们只在乎其数量;连续的加入非树边 转移系数是一个连乘的形式,可以预处理阶乘和阶乘逆元做。单次是 \(\mathcal{O}(n)\),总共是 \(\mathcal{O}(n!)\) 的。

那么现在的 DP 性质足够好,考虑到阶乘的复杂度太大了,考虑状压 DP 转移:现在我有 \(s\) 这一个已加入的树边边集,加一个树边进去,首先将所有必须现在考虑的非树边转移掉,然后把新加入的树边放在操作顺序序列最开头。转移是简单的,记一下已考虑边数、方案数、树边位置和即可。总复杂度 \(\mathcal{O}(2^nn)\)

赛时其实写了不少小 bug,而且并不是很会高维前缀和,毕竟以前做的都是 min/max 这样的可重复贡献信息,这次要统计必考虑非树边数是加法就写炸了,其实赛时再多给十几二十分钟也不一定能调出来,纯实力问题了这下。

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 24, kM = kN * kN, kS = 1 << 22, kP = 998244353;

int n, m;
vector<PII> e[kN];
int cnt[kS], msk;
bool Dfs(int x, int fa, int ed) {
  if (x == ed)
    return 1;
  for (auto [v, id] : e[x]) {
    if (v != fa && Dfs(v, x, ed))
      return msk |= 1 << id, 1;
  }
  return 0;
}

struct Status { int a, b, cnt, sum; } f[kS];

int fac[kM] = {1, 1}, inv[kM] = {0, 1}, ifc[kM] = {1, 1};
int main() {
#ifndef ONLINE_JUDGE
  freopen("input.in", "r", stdin);
  freopen("output.out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> m;
  for (int i = 2; i <= m; i++) {
    fac[i] = 1ll * fac[i - 1] * i % kP;
    inv[i] = 1ll * (kP - kP / i) * inv[kP % i] % kP;
    ifc[i] = 1ll * ifc[i - 1] * inv[i] % kP;
  }
  for (int i = 0, u, v; i < n - 1; i++) {
    cin >> u >> v;
    e[u].emplace_back(v, i), e[v].emplace_back(u, i);
  }
  for (int i = n, u, v; i <= m; i++) {
    cin >> u >> v, Dfs(u, 0, v);
    cnt[msk]++, msk = 0;
  }
  for (int i = 0; i < n - 1; i++) {
    for (int s = 1; s < 1 << n - 1; s++)
      (s >> i & 1) && (cnt[s] += cnt[s ^ (1 << i)]);
  }
  for (int s = 0; s < 1 << n - 2; s++)
    swap(cnt[s], cnt[((1 << n - 1) - 1) ^ s]);
  f[0] = {0, 0, 1, 0};
  for (int s = 1; s < 1 << n - 1; s++) {
    int popcnt = __builtin_popcount(s);
    f[s] = {m - n + 1 - cnt[s], popcnt};
    for (int i = 0; i < n - 1; i++) {
      if (!(s >> i & 1))
        continue;
      int t = s ^ (1 << i), lst = cnt[t] - cnt[s], pre = f[t].a + f[t].b;
      f[s].cnt = (f[s].cnt + 1ll * f[t].cnt * fac[pre + lst] % kP * ifc[pre]) % kP;
      f[s].sum = (f[s].sum + 1ll * f[t].sum * fac[pre + lst + 1] % kP * ifc[pre + 1]) % kP;
    }
    f[s].sum = (f[s].sum + 1ll * f[s].b * f[s].cnt) % kP;
  }
  cout << f[(1 << n - 1) - 1].sum << '\n';
  return 0;
}

G. CF553E Kyoya and Train (7)

Date: 2025.10.10

看到这玩意儿在 CDQ分治 题单里才意识到半在线卷积是 CDQ分治,有点绷。

首先暴力做这个东西可以直接 DP,设 \(f_{i, j}\) 表示当前走到 \(i\),时间为 \(j\) 的最小代价,转移需要时刻判一下时间是不是越过 \(t\) 了,如果越过要罚款。这个特判比较麻烦,不好处理,考虑倒着做,从 \(n\) 开始往前推,这样可以在初始状态处理罚款了。首先对于 \(j>t\) 的状态可以直接预先处理代价,转移形如

\[f_{i,j}=\min\limits_{v\in to_i}(\sum\limits_{k=1}^j f_{v,j+k}p_{i,k}+c_i) \]

是一个差卷积的形式。

这个卷积转移是半在线的,考虑分治 FFT,但是似乎并不好做,因为我们实际上的 f 是需要在多种转移取 min 的,但是我的贡献累计又是累加的了。因此再开一个状态存储转移:\(g_{i,j}\) 表示转移走第 \(i\) 条边,到达时间为 \(j\) 的代价和。这样可以用 f 往前更新 g,细分到 l=r 时用 g 取 min 更新 f。总时间复杂度 \(\mathcal{O}(mt\log^2t)\)

时间复杂度非常好,但是跑不过暴力卷积最短路转移 \(\mathcal{O}(nmt\log t)\),我写的东西慢了足足一倍,妈的。

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <cmath>
#include <iostream>
#include <iomanip>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using Poly = vector<double>;
using PII = pair<int, int>;
constexpr int kN = 50 + 1, kM = 100 + 1, kT = 4e4 + 1;

namespace POLY {
  const double Pi = acos(-1);
  constexpr int kS = 1 << 17;
  struct Complex {
    double real, imag;
    inline Complex operator+(const Complex &x) const { return {real + x.real, imag + x.imag}; }
    inline Complex operator-(const Complex &x) const { return {real - x.real, imag - x.imag}; }
    inline Complex operator*(const Complex &x) const { return {real * x.real - imag * x.imag, real * x.imag + x.real * imag}; }
    inline Complex operator*=(const Complex &x) { return *this = *this * x; }
    inline Complex operator/=(const int &n) { return *this = {real / n, imag / n}; }
  } f[kS], g[kS];
  
  int n, rev[kS];
  inline void FFT(Complex *f, int o) {
    for (int i = 0; i < n; i++)
      rev[i] = rev[i >> 1] >> 1 | ((i & 1) * n / 2);
    for (int i = 0; i < n; i++)
      rev[i] > i && (swap(f[i], f[rev[i]]), 0);
    for (int len = 2; len <= n; len *= 2) {
      Complex w = {cos(2 * Pi / len), o * sin(2 * Pi / len)}, cur = {1, 0};
      for (int l = 0; l < n; l += len, cur = {1, 0})
        for (int i = l; i < l + len / 2; i++, cur *= w) {
          Complex a = f[i], b = cur * f[i + len / 2];
          f[i] = a + b, f[i + len / 2] = a - b;
        }
    }
  }
  inline void Mul() {
    FFT(f, 1), FFT(g, 1);
    for (int i = 0; i < n; i++)
      f[i] *= g[i];
    FFT(f, -1);
    for (int i = 0; i < n; i++)
      f[i] /= n;
  }
}

int n, m, t, x;
int a[kM], b[kM], c[kM], dis[kN][kN];
double p[kM][kT];
double f[kN][kT], g[kM][kT];

void Divide(int l, int r) { 
  if (l >= t)
    return;
  else if (l == r) {
    for (int i = 1; i < n; i++)
      f[i][l] = 1e18;
    for (int i = 1; i <= m; i++)
      a[i] != n && (f[a[i]][l] = min(f[a[i]][l], g[i][l] + c[i]));
    return;
  }
  int mid = (l + r) / 2;
  Divide(mid + 1, r);
  int len = POLY::n = 1 << __lg(r - l + r - mid - 1) + 1;
  for (int i = 1; i <= m; i++) {
    if (a[i] == n)
      continue;
    for (int j = 0; j < len; j++)
      POLY::f[j] = {j < r - l ? p[i][j + 1] : 0, 0};
    for (int j = 0; j < len; j++)
      POLY::g[j] = {j < r - mid ? f[b[i]][r - j] : 0, 0};
    POLY::Mul();
    for (int j = l; j <= mid; j++)
      g[i][j] += POLY::f[r - j - 1].real;
  }
  Divide(l, mid);
}

int main() {
#ifndef ONLINE_JUDGE
  freopen("input.in", "r", stdin);
  freopen("output.out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> m >> t >> x;
  fill(dis[0], dis[n + 1], 1e9);
  for (int i = 1; i <= n; i++)
    dis[i][i] = 0;
  for (int i = 1; i <= m; i++) {
    cin >> a[i] >> b[i] >> c[i];
    dis[a[i]][b[i]] = c[i];
    for (int j = 1, x; j <= t; j++)
      cin >> x, p[i][j] = x / 1e5;
  }
  for (int k = 1; k <= n; k++) {
    for (int i = 1; i <= n; i++)
      for (int j = 1; j <= n; j++)
        dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
  }
  for (int i = 0; i < 2 * t; i++)
    f[n][i] = (i > t) * x;
  for (int i = 1; i < n; i++) {
    for (int j = t; j < 2 * t; j++)
      f[i][j] = dis[i][n] + x;
  }
  Divide(0, 2 * t - 1);
  cout << fixed << setprecision(7) << f[1][0] << '\n';
  return 0;
}

Day16B. 线段树 (5.5)

Date: 2025.10.10

厉害题,但是题面太长了赛时没有看,其实是比较有意思的。

首先,线段树叶子节点区间拼起来一定是整个序列。然后你发现由他给的询问区间的左右端点划分,当作叶子节点的对应区间断点,因为我一定会细分到这种程度才可以做到能够划分所有的询问区间。然后问题转化为了,我现在有一些叶子区间和询问区间,问怎么构造一个二叉树做到高度最小且深度最大点访问次数最少。很明显由于叶子个数确定,这个最小高度是可以预测的。

我们可以做的操作是,将两个相邻区间合并成一个大区间,即用一个节点将两个相邻子树连成一个大子树。考虑我的答案贡献,首先叶子区间的访问次数就是被区间覆盖的次数;而大小为 2 子树需要询问区间卡在节点区间的中间断点,才能产生 \(2\) 的贡献,即其中间断点的访问次数的两倍。叶子过于特殊,所以特判掉区间长度为 \(2\) 的可能,这样叶子的贡献要么被长度为 \(2\) 区间包含,要么因为深度过小导致没有贡献。然后直接做区间 DP,每次就尝试合并两个子树,当且仅当两个子树高度都严格小于我当前区间的预测高度,然后将答案和取 min 即可。这样就是 \(\mathcal{O}(n^3)\) 的。

这样算出的答案怎么对应到我实际询问的 每次枚举一个实际区间,将所有询问区间与其求交的答案。由于答案实际上只由我的询问区间断点贡献,所以只需要找到一个刚好包含实际区间的区间 DP 答案即可。通过双指针可以单次查询 \(\mathcal{O}(1)\)。不会卡复杂度瓶颈。

所以现在只有这个 \(\mathcal{O}(n^3)\) 的区间 DP 要优化。因为一些有意思的事情,这个区间 DP 是具有决策单调性的。即 \(p_{i, j - 1}\leq p_{i,j}\leq p_{i + 1, j}\)。感性理解是简单的,哥哥的解释证明是看不明白的,但是并没有什么关系,如果想到了决策单调性优化写代码和打表验证都是简单的,没想到会证明也没有用。这样复杂度就轻松降到了 \(\mathcal{O}(n^2)\)。(其实今天以前都不是很会证决策单调性这个复杂度平方,这下深刻学习了)

听说这个题的标准弱化被哥哥放进了长训 DP 题单,但是我并没有做长训那边的题,效率太低了。比较吃亏

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e3 + 1, kP = 998244353;

int n, m;
string msk[kN];
int L[kN], R[kN], cov[kN], cnt[kN];
int dep[kN][kN], ans[kN][kN], pos[kN][kN];
int main() {
#ifndef ONLINE_JUDGE
  freopen("input.in", "r", stdin);
  freopen("output.out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  cin >> n;
  for (int i = 1; i <= n; i++) {
    cin >> msk[i], msk[i] = '#' + msk[i];
    for (int j = 1; j <= n; j++)
      (msk[i][j] == '1') && (cnt[i - 1]++, cnt[j]++, cov[i]++, cov[j + 1]--);
  }
  cnt[0]++, cnt[n]++;
  for (int i = 1, lst; i <= n; i++) {
    cnt[i - 1] > 0 && (lst = i);
    cov[i] += cov[i - 1];
    if (cnt[i] == 0)
      continue;
    else if (lst != 0) {
      L[++m] = lst, R[m] = i;
      dep[m][m] = 0, ans[m][m] = cov[i];
    }
  }
  for (int i = 1; i < m; i++)
    dep[i][i + 1] = 1, ans[i][i + 1] = 2 * cnt[R[i]], pos[i][i + 1] = i;
  for (int len = 3; len <= m; len++)
    for (int l = 1, r = len, d = __lg(len - 1) + 1; r <= m; l++, r++) {
      dep[l][r] = d, ans[l][r] = 1e9;
      for (int mid = pos[l][r - 1]; mid <= pos[l + 1][r]; mid++)
        if (dep[l][mid] < d && dep[mid + 1][r] < d) {
          int cur = (dep[l][mid] == d - 1) * ans[l][mid] + (dep[mid + 1][r] == d - 1) * ans[mid + 1][r];
          cur < ans[l][r] && (ans[l][r] = cur, pos[l][r] = mid);
        }
    }
  
  int Ans = 0;
  for (int l = 1, tl = 1; l <= n; l++) {
    for (; tl < m && L[tl + 1] <= l; tl++);
    for (int r = l, cur = 1, tr = 1; r <= n; r++) {
      for (; tr < m && R[tr] < r; tr++);
      Ans = (Ans + 1ll * cur * dep[tl][tr]) % kP, cur = 2333ll * cur % kP;
      Ans = (Ans + 1ll * cur * ans[tl][tr]) % kP, cur = 2333ll * cur % kP;
    }
  }
  cout << Ans << '\n';
  return 0;
}

Day16C. 连通块 (8.5)

Date: 2025.10.10

赛时被“删去前 \(n-k\) 个点”的表述吓到了,没细想,事实说明这句话纯纸老虎,但是这个题是真难。

首先删除前 \(n-k\) 个点可以看作是保留前 \(k\) 个点。由经典结论得 \(n\) 个点有标号无根树数量是 \(n^{n-2}\) 个,因此可以计算所有合法树的连通块数量和。

考虑枚举这个答案,即预设只保留前 \(k\) 个点后图由 \(m\) 个连通块组成,其大小分别为 \(sz_1\cdots sz_m\)。那么考虑对这样的合法的原树计数。首先由于这些连通块之间最后并不联通,因此其之间两两不能有边;然后用剩下的 \(n-k\) 个单点将这些连通块连接起来形成一整棵树。考虑将这个连通块缩成一个点,并使用 Matrix-Tree 定理,构造一个完全图,去掉这 \(m\) 个连通块的完全图这个子图里的所有边,第 \(i\) 个连通块和其他单点的连边数量为 \(sz_i\),的生成树数量即为所求。列出矩阵:

\[\begin{bmatrix} sz_1(n-k)&0&\cdots&0&-sz_1&-sz_1&\cdots&-sz_1\\ 0&sz_2(n-k)&\cdots&0&-sz_2&-sz_2&\cdots&-sz_2\\ \vdots&\vdots&\ddots&\vdots&\vdots&\vdots&\ddots&\vdots\\ 0&0&\cdots&sz_m(n-k)&-sz_m&-sz_m&\cdots&-sz_m\\ -sz_1&-sz_2&\cdots&-sz_m&n-1&-1&\cdots&-1\\ -sz_1&-sz_2&\cdots&-sz_m&-1&n-1&\cdots&-1\\ \vdots&\vdots&\ddots&\vdots&\vdots&\ddots&\vdots\\ -sz_1&-sz_2&\cdots&-sz_m&-1&-1&\cdots&n-1\\ \end{bmatrix} \]

现在求这个矩阵行列式,那么我们先去掉最后一行最后一列,做一做高斯消元,把左下角这坨全部消掉,距离上三角只剩下右下角这坨。先看下目前情况:

\[\begin{bmatrix} n-1-\frac{k}{n-k}&-1-\frac{k}{n-k}&\cdots&-1-\frac{k}{n-k}\\ -1-\frac{k}{n-k}&n-1-\frac{k}{n-k}&\cdots&-1-\frac{k}{n-k}\\ \vdots&\vdots&\ddots&\vdots\\ -1-\frac{k}{n-k}&-1-\frac{k}{n-k}&\cdots&n-1-\frac{k}{n-k} \end{bmatrix} \]

矩阵大小为 \((n-k-1)\times(n-k-1)\)。注意到这有大量的重复的 \(-1-\frac{k}{n-k}\) 项,考虑相邻两项作差,然后加一下,让前 \(n-k-2\) 行对角变成 \(n\),最后一列变成全 \(-n\),除了最后一行没变。那么此时再做类似高斯消元,让最后一行只剩下最后一个数:\(n-(1-\frac{k}{n-k})(n-k-1)=\frac{n}{n-k}\)。而现在大矩阵变成了上三角,可以轻松求出其行列式,即合法生成树个数为

\[(n-k)^mn^{n-k-2}\frac{n}{n-k}\prod sz_i \]

把所有跟 \(m\)\(sz_i\) 无关的全部撇出去,关键的就是 \((n-k)^m\prod sz_i\)

那么对于这个东西,再设计 DP:\(f_{i,j}\) 表示现在已经有了 \(i\) 个连通块,用了 \(j\) 个点的方案数。考虑转移,首先有一个新加子树贡献 \(n-k\),枚举新联通块大小 \(sz\),其可能生成树数量为 \(sz^{sz-2}\),再乘一个 \(sz\) 做子树大小贡献。而选点方案数,假设转移后的总点数是 \(j\),则此时选取方案数为 \(j\choose sz\)。但是我的子树和子树之间并没有做区分,所以直接钦定选点方案的编号最小值的一个顺序,钦定每次新加一棵子树就作为当前编号最小值,即方案数变为 \(j-1\choose sz-1\),此时转移为:

\[f_{i,j}=\sum\limits_{sz=1}^{\min(d, j)} (n-k)sz^{sz-1} {j-1\choose sz-1}f_{i-1,j-sz} \]

于是可以直接做到 \(\mathcal{O}(k^3)\)。考虑优化,这个转移其实类似一个背包,每个新加入子树 \(sz\) 有一个价值之类的东西,如果不要求对每个子树个数都求一个 \(f\) 则可以直接平方背包。于是考虑最后统计答案时,\(f_i\) 会多乘上一个 \(i\) 的个数贡献,所以再设 \(g_j=\sum if_{i, j}\)。此时背包转移 \(f_j\) 会把每个 \(f_{i,j}\) 全都转移一遍,那么新增的 \(g\) 则是一个 \(f+g\) 的转移。于是做到了 \(\mathcal{O}(k^2)\)

很厉害的题,之前完全没做过生成树计数向的题,这次做到好的了,感恩 dmy。

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e3 + 1, kP = 1e9 + 7;
inline int Pow(int a, int b) {
  int ret = 1;
  for (; b > 0; b /= 2) {
    if (b % 2 == 1)
      ret = 1ll * ret * a % kP;
    a = 1ll * a * a % kP;
  }
  return ret;
}

int T, n, k, d;
int f[kN], g[kN], c[kN];
int C[kN][kN];
int main() {
#ifndef ONLINE_JUDGE
  freopen("input.in", "r", stdin);
  freopen("output.out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  for (int i = 0; i < kN; i++) {
    C[i][0] = C[i][i] = 1;
    for (int j = 1; j < i; j++)
      C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % kP;
  }
  for (cin >> T; T--;) {
    cin >> n >> k >> d;
    if (n == k) {
      cout << (d == n) << '\n';
      continue;
    }
    for (int i = 1; i <= min(k, d); i++)
      c[i] = 1ll * Pow(i, i - 1) * (n - k) % kP;
    f[0] = 1;
    for (int i = 1; i <= k; i++)
      for (int sz = 1; sz <= i && sz <= d; sz++) {
        int w = 1ll * C[i - 1][sz - 1] * c[sz] % kP;
        f[i] = (f[i] + 1ll * w * f[i - sz]) % kP;
        g[i] = (g[i] + 1ll * w * (g[i - sz] + f[i - sz])) % kP;
      }
    int ans = 1ll * Pow(n, n - k - 1) * g[k] % kP * Pow(n - k, kP - 2) % kP;
    cout << 1ll * ans * Pow(Pow(n, n - 2), kP - 2) % kP << '\n';

    for (int i = 0; i <= k; i++)
      c[i] = f[i] = g[i] = 0;
  }
  return 0;
}

Day17A 优势种类 (2.5)

Date: 2025.10.11

不难发现只要有两个相同数间隔不超过一个即可无限扩展。要求字典序最小,那么肯定是考虑从小到大考虑扩展,每次考虑相同数中相邻两个,若无间隔或间隔内数比当前数大,或前面存在比当前数大的数且能扩展到,则考虑直接扩展,向后扩展要求后面的一个数比现在这个数大。这个事情我用了 set 维护比当前数大的位置,其实并不需要,只需要暴力枚举拓展即可。但是没有消耗太多太多的时间,因此还算可以接受。

写的比较丑,调了一小会,好在样例给的比较小并且比较强,dmy 良心时刻。

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
#include <set>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 2e5 + 2;

int T, n, a[kN];
vector<int> buc[kN], vec;
multiset<int> s;
vector<PII> chk[kN];
int main() {
#ifndef ONLINE_JUDGE
  freopen("input.in", "r", stdin);
  freopen("output.out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  for (cin >> T; T--;) {
    cin >> n;
    for (int i = 1; i <= n; i++)
      cin >> a[i], buc[a[i]].push_back(i), s.insert(i);
    for (int w = 1; w <= n; w++) {
      if (buc[w].empty())
        continue;
      bool flag = 0;
      int l = 1e9, r = 0;
      vec.clear();
      for (int j = 0; j < buc[w].size(); j++) {
        if (s.find(buc[w][j]) != s.end())
          vec.push_back(buc[w][j]);
      }
      for (auto p : vec)
        chk[p].emplace_back(w, 1), chk[p + 1].emplace_back(w, -1);
      for (int i = 1; i < vec.size(); i++) {
        i > 1 && (s.erase(vec[i - 2]), 0);
        if (vec[i] - vec[i - 1] > 2)
          continue;
        int l = vec[i - 1], r = vec[i];
        // cerr << l << ' ' << r << " -> ";
        auto it = s.lower_bound(l);
        for (; it != s.begin();)
          l = *prev(it), s.erase(prev(it));
        // cerr << l << ' ' << r << ' ' << w << '\n';
        if (l != vec[i - 1] || it != s.end() && (++it) != s.end() && *it == l + 1) {
          auto ir = s.upper_bound(r);
          for (; it != ir;)
            it++, s.erase(prev(it));
          for (auto it = s.upper_bound(r); it != s.end() && *it == r + 1;)
            s.erase(it), it = s.upper_bound(r), r++;
          chk[l].emplace_back(w, 1), chk[r + 1].emplace_back(w, -1);
        }
      }
      for (auto p : vec)
        s.find(p) != s.end() && (s.erase(p), 0);
    }
    assert(s.empty());
    for (int i = 1; i <= n; i++) {
      for (auto [w, o] : chk[i])
        o > 0 ? (s.insert(w), 0) : (s.erase(s.find(w)), 0);
      cout << *s.rbegin() << ' ';
    }
    cout << '\n';

    s.clear();
    for (int i = 1; i <= n + 1; i++)
      chk[i].clear(), buc[i].clear();
  }
  return 0;
}

/*
10
8 8 4 8 2 6 10 8 8 1

8 8 4 8 2 6 8 8 8 1
my: 8 8 8 8 2 6 8 8 8 1 
*/

Day17B 信号塔 (3)

Date: 2025.10.11

看到这个题又让我想到了 Black Radius,可惜那题只能染一个关键点的/xk

考虑转移白点。一开始我想着要预处理出所有可能出现的极长黑色连续段,再去做转移。其实并不是很好预处理,很容易做成立方,感觉平方也不是很能实现。实际上可以不做预处理,每次考虑一个区间,看它能否成为 极长的黑色连续段。这个事情可以考虑找到区间中点左右两边的点,然后尝试能否扩展成这个区间,做一些特判即可。

算比较有 corner 的,写的时候要想清楚。

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e3 + 1, kP = 998244353;

int T, n;
string s;
vector<int> tw;
int f[kN];
int main() {
#ifndef ONLINE_JUDGE
  freopen("input.in", "r", stdin);
  freopen("output.out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  for (cin >> T; T--;) {
    cin >> n >> s, s = '#' + s;
    for (int i = 1; i <= n; i++)
      s[i] == '1' && (tw.push_back(i), 0);
    if (tw.size() == 0) {
      cout << "1\n";
      continue;
    }
    f[0] = 1;
    for (int i = 1; i <= n + 1; i++) {
      if (i <= n && s[i] == '1')
        continue;
      f[i] = f[i - 1];
      for (int l = 1, r = i - 1, p = 0; l < i; l++) {
        if (s[l - 1] == '1')
          continue;
        for (; p + 1 < tw.size() && tw[p + 1] <= (l + r) / 2; p++);
        bool ac = 0;
        if (tw[p] >= l)
          ac |= min(2 * tw[p] - l, n) == r || max(2 * tw[p] - r, 1) == l;
        if (!ac && (p == tw.size() - 1 || tw[p + 1] > r))
          continue;
        ac |= min(2 * tw[p + 1] - l, n) == r || max(2 * tw[p + 1] - r, 1) == l;
        int tl = 2 * tw[p] - l, tr = 2 * tw[p + 1] - r;
        ac |= tl + 1 >= tr && tl <= r && tr >= l;
        f[i] = (f[i] + ac * f[l - 1]) % kP;
      }
    }
    cout << f[n + 1] << '\n';

    fill_n(f, n + 2, 0), tw.clear();
  }
  return 0;
}

Day17C 黑暗密钥 (4)

Date: 2025.10.12

赛时没看这道题,搞彩排去了,时间不太够打这场的积极性也不是很高导致的。

异或全局操作第 \(k\) 大最小值,题面似乎只差把 01-Trie 写里面了。于是题目可以看作为,在 01-Trie 上走,尝试走答案更小的一边,如果子树大小不够 \(k\) 只能提供当前深度的 \(2\) 的幂的答案贡献走另一边。如果只有一个待选异或值,或者任选异或值,都可以轻松单 \(\log\) 解决。

但是现在查询会有一个待选区间,我只能在这个区间走。那么现在会出现大量的两边跑状况,让复杂度飙升。但是考虑到,我的 01-Trie 是一个维护值域的结构,其实 01-Trie 是一个基本与权值线段树等价的结构!这意味着我们可以类似的,对于每棵子树,计算其不对异或值作限制的答案。那么类似线段树的做查询,我们会在两边跑的途中遇到大量的整块,即整个子树的无限制答案。这样我们可以考虑预处理这些答案,这样我的复杂度再次降到 \(\mathcal{O}(\log V)\)

考虑这个预处理,其实可以直接暴力把所有的第 \(k\) 大最小值记下来,然后跑树形 DP。由于每个数只会贡献 \(\log V\) 个点,所以总复杂度可以做到 \(\mathcal{O}(n\log V)\)

这个比较深刻地利用了 01-Trie 和值域线段树的联系去思考,还是比较有意思的。当然有一些 corner/xk

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e5 + 1, kA = (1 << 30) - 1;

int n, q, a[kN], tot = 1;
struct Node {
  int son[2], sz;
  vector<int> f;
} t[30 * kN];
inline void Insert(int w) {
  int x = 1;
  for (int d = 29; d >= 0; d--) {
    int o = w >> d & 1;
    !t[x].son[o] && (t[x].son[o] = ++tot);
    x = t[x].son[o], t[x].sz++;
  }
}
void Build(int x, int dep) {
  // cerr << x << ' ' << dep << '\n';
  if (!x)
    return;
  t[x].f.resize(t[x].sz + 1, 0);
  // if (dep == -1)
  //   cerr << "666: " << t[x].son[0] << ' ' << t[x].son[1] << '\n';
  if (!t[x].son[0] && !t[x].son[1])
    return;
    // cerr << "?";
  int s[2] = {t[x].son[0], t[x].son[1]};
  Build(s[0], dep - 1), Build(s[1], dep - 1);
  // cerr << x << ' ' << dep << ' ' << t[x].sz << '\n';
  // cerr << s[0] << ' ' << t[s[0]].sz << ' ' << s[1] << ' ' << t[s[1]].sz << '\n';
  for (int i = 1; i <= t[x].sz; i++) {
    t[x].f[i] = kA;
    for (auto o : {0, 1}) {
      // cerr << i << ' ' << t[s[o]].sz << ' ' << t[s[o]].f.size() << '\n';
      if (i > t[s[o]].sz)
        t[x].f[i] = min(t[x].f[i], (1 << dep) + t[s[!o]].f[i - t[s[o]].sz]);
      else
        t[x].f[i] = min(t[x].f[i], t[s[o]].f[i]);
      // cerr << "out\n";
    }
  }
}

int Query(int l, int r, int k, int x = 1, int dep = 29) {
  if (dep < 0)
    return 0;
  else if (!x)
    return kA;
  else if (l == 0 && r == kA)
    return t[x].f[k];
  if ((l >> dep & 1) == (r >> dep & 1)) {
    int o = l >> dep & 1, s0 = t[x].son[o], s1 = t[x].son[!o];
    if (k <= t[s0].sz)
      return Query(l, r, k, s0, dep - 1);
    return Query(l, r, k - t[s0].sz, s1, dep - 1) + (1 << dep);
  }
  int ans = kA, s0 = t[x].son[0], s1 = t[x].son[1];
  if (k > t[s0].sz)
    ans = min(ans, Query(l, kA, k - t[s0].sz, s1, dep - 1) + (1 << dep));
  else
    ans = min(ans, Query(l, kA, k, s0, dep - 1));
  if (k > t[s1].sz)
    ans = min(ans, Query(0, r, k - t[s1].sz, s0, dep - 1) + (1 << dep));
  else
    ans = min(ans, Query(0, r, k, s1, dep - 1));
  return ans;
}

int main() {
#ifndef ONLINE_JUDGE
  freopen("input.in", "r", stdin);
  freopen("output.out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> q, t[1].sz = n;
  t[0] = {0, 0, 0, vector<int>(1, 0)};
  for (int i = 1; i <= n; i++)
    cin >> a[i], Insert(a[i]);
  Build(1, 29);
  for (int l, r, k; q--;) {
    cin >> l >> r >> k;
    cout << Query(l, r, k) << '\n';
  }
  return 0;
}

Day17D 城镇地图 (3.5)

Date: 2025.10.12

赛时没看这道题。

两种 \(k\) 等价于两个题目:合法方案数和等价方案集合大小平方和。前者可以直接做轮廓线 DP,是比较简单的;后者考虑集合大小平方和等于等价状态对个数。于是暴力设计状态为轮廓线分别为 \(s_1,s_2\) 的两个等价网络阵列方案数,暴力转移即可。

其实比较简单,但是我没咋写过这个轮廓线 DP/hsh 不过这题的确好写到一定程度了。

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 66 + 2, kM = 6 + 1, kB = 1 << kM, kP = 1e4 + 7;
inline void Add(int &x, int w) { x = (x + w) % kP; }

int Tc, n, m, T;
int mp[kN][7], f[kN][kB];

namespace K1 {
  int f[kN][kB];
  inline void Solve() {
    f[0][0] = 1;
    for (int i = 1; i <= n; i++) {
      for (int j = 1; j <= m; j++)
        for (int s = 0; s < 1 << m + 1; s++) {
          for (int r : {0, 1})
            for (int d : {0, 1}) {
              if (d && i == n || r && j == m)
                continue;
              if (mp[i][j] == -1 || r + d + (s >> j & 1) + (s & 1) == mp[i][j]) {
                int t = (s ^ (s & 1 << j) ^ (s & 1)) | (d << j) | r;
                Add(f[j][t], f[j - 1][s]);
              }
            }
        }
      fill(f[0], f[m], 0);
      for (int s = 0; s < 1 << m + 1; s++)
        Add(f[0][s ^ (s & 1)], f[m][s]), f[m][s] = 0;
    }
    int ans = 0;
    for (int s = 0; s < 1 << m + 1; s++)
      Add(ans, f[0][s]), f[0][s] = 0;
    cout << ans << '\n';
  }
}

namespace K2 {
  int f[kN][kB][kB];
  inline void Solve() {
    f[0][0][0] = 1;
    for (int i = 1; i <= n; i++) {
      for (int j = 1; j <= m; j++)
        for (int s1 = 0; s1 < 1 << m + 1; s1++)
          for (int r1 : {0, 1})
            for (int d1 : {0, 1}) {
              if (d1 && i == n || r1 && j == m)
                continue;
              int cnt1 = r1 + d1 + (s1 >> j & 1) + (s1 & 1);
              int t1 = (s1 ^ (s1 & 1 << j) ^ (s1 & 1)) | (d1 << j) | r1;
              if (mp[i][j] != -1 && cnt1 != mp[i][j])
                continue;
              for (int s2 = 0; s2 < 1 << m + 1; s2++)
                for (int r2 : {0, 1})
                  for (int d2 : {0, 1}) {
                    if (d2 && i == n || r2 && j == m)
                      continue;
                    else if (r2 + d2 + (s2 >> j & 1) + (s2 & 1) != cnt1)
                      continue;
                    int t2 = (s2 ^ (s2 & 1 << j) ^ (s2 & 1)) | (d2 << j) | r2;
                    Add(f[j][t1][t2], f[j - 1][s1][s2]);
                  }
            }
      fill(f[0][0], f[m - 1][1 << m + 1], 0);
      for (int s1 = 0; s1 < 1 << m + 1; s1++) {
        for (int s2 = 0; s2 < 1 << m + 1; s2++)
          Add(f[0][s1 ^ (s1 & 1)][s2 ^ (s2 & 1)], f[m][s1][s2]), f[m][s1][s2] = 0;
      }
    }
    int ans = 0;
    for (int s1 = 0; s1 < 1 << m + 1; s1++) {
      for (int s2 = 0; s2 < 1 << m + 1; s2++)
        Add(ans, f[0][s1][s2]), f[m][s1][s2] = 0;
    }
    cout << ans << '\n';
  }
}

int main() {
#ifndef ONLINE_JUDGE
  freopen("input.in", "r", stdin);
  freopen("output.out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  for (cin >> Tc; Tc--;) {
    cin >> n >> m >> T;
    for (int i = 1; i <= n; i++) {
      for (int j = 1; j <= m; j++)
        cin >> mp[i][j];
    }
    T == 1 ? K1::Solve() : K2::Solve();
  }
  return 0;
}
posted @ 2025-10-06 10:33  Lightwhite  阅读(17)  评论(1)    收藏  举报