做题记录 #2

A. ABC427G Takahashi's Expectation 2 (7)

Date: 2025.10.12

非常厉害的题目。

首先肯定考虑维护一个答案函数,初始为 \(y=x\),那么每次操作相当于用一条横线砍它,上面 -b 下面 +a。

考场上想了一个正确性其实不是很显然,感觉也不一定对的东西:首先肯定时时刻刻我的斜线条数是 加入次数+1 的。因为我对断点上做一次操作,会让中间一个断点合到一起,并在两边多加一个断点。那么考虑找一下这个断点的性质,首先断点初的差分一定是 a+b,然后我认为值域有交的斜线一定定义域连续。考虑一个这样的连续段,每次切过值域重合处,会相当于把我的断点向左平移一段,然后最后加一个断点。平移量是可以简单得出的。然后维护端点位置,相当于区间减和单点插入...或许可以平衡树 /xk

正解是比较有道理的。

考虑我暴力维护这个东西。首先可以简化一下题意,默认每次一定会减掉一个 \(B\),如果我真能加就加上 \(A+B\),最后求答案的时候把答案减去 \(nB\) 即可。那么对应到它给的礼物,第 \(i\) 个礼物的价值要提高 \((i-1)B\),这样才能正确比较。这样我的过程还拥有了当前心情值不减的优美性质。不妨设 \(d=A+B\)

那么有了这个性质,我们会很想要一个递减的礼物序列。如果礼物序列递减,那么上一个礼物不能买,下一个礼物将也不能买,具有着状态单调性。这样可以二分,找到一个最后一个能买的位置,然后前面全都能买,即得出答案。但是对于相邻的两个礼物 \(x<y\) 怎么办?

讨论一下这样的对产生的贡献:一开始 \(\leq \min(x,y-d)\) 的数会被加两次 \(d\)\(\min(x,y-d)<w\leq y\) 的数会被加一次,其他的根本不加。因此这个操作 \((x,y)\) 等价于 \((\min(x,y-d),y)\)。此时的两次操作是互不干扰的!因此可以调换顺序,变为 \((y, \min(x+d,y))\)。此时右边是比左边小的,做到了改变大小关系。

那么我现在暴力去做这个操作,去维护一个递减的、与原序列等价的序列 \(c\),每次插入 \(x\) 相当于找到第一个 \(<x\)\(c_i\),将其左边插进 \(x\),再找到最后一个 \(\geq x-d\)\(c_j\),然后 Assign([i, j], x), Add([j + 1, end], a + b)。此时可以大力平衡树直接 \(\mathcal{O}(n\log n)\) 爆掉,但是我不太会平衡树所以没写,当然我们不需要这么优秀的复杂度,写个平衡树还是太困难了(除非你没想到其他东西)。

但是这个我们其实可以做到快速合并两个有序序列!有左右相邻的两个已经变得有序的区间,可以归并地做这个操作:考虑把右侧序列一个一个插入到左侧,而且右侧序列它也是有序的,我做操作是多出来一段 \(r_i\),所以到 \(r_{i+1}\) 的时候它的 \(i\) 也不会到这个里面,所以可以双指针维护,简单合并即可。合并复杂度是 \(\mathcal{O}(|L|+|R|)\) 的。

于是可以二进制分组,查询暴力枚举块然后二分即可,查询为瓶颈,复杂度 \(\mathcal{O}(n\log^2 n)\)

Code

#pragma GCC optimize("Ofast,unroll-loops")
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>

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

int n, q;
LL A, B, d;

inline void operator+=(Block &a, Block &b) {
  int i = 0, j = 0, cnt = 0;
  Block ret;
  for (; j < b.size(); j++) {
    for (; i < a.size() && a[i] + cnt * d >= b[j]; i++)
      ret.push_back(a[i] + cnt * d);
    cnt++;
    for (; i < a.size() && a[i] + cnt * d >= b[j]; i++)
      ret.push_back(b[j]);
    ret.push_back(b[j]);
  }
  for (; i < a.size(); ret.push_back(a[i++] + cnt * d));
  a.swap(ret), ret.clear();
}
vector<Block> b;
inline void Insert(LL w) {
  b.emplace_back(1, w);
  for (int sz = b.size() - 1; sz > 0 && b[sz].size() == b[sz - 1].size();) {
    b[sz - 1] += b[sz];
    b[sz].clear(), b[sz].shrink_to_fit();
    b.pop_back(), sz--;
  }
}
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 >> A >> B, d = A + B;
  for (int i = 1, p; i <= n; i++)
    cin >> p, Insert(p + (i - 1) * B);

  cin >> q;
  for (LL op, x; q--;) {
    cin >> op >> x;
    if (op == 2) {
      for (auto &vb : b) {
        int l = 0, r = vb.size() - 1, ans = 0;
        for (int mid; l <= r;) {
          mid = (l + r) / 2;
          if (vb[mid] >= x + mid * d)
            l = mid + 1, ans = mid + 1;
          else
            r = mid - 1;
        }
        x += ans * d;
      }
      cout << x - 1ll * n * B << '\n';
    } else
      Insert(x + n * B), n++;
  }
  return 0;
}

B. QOJ7899 Say Hello to the Future (7.5)

Date: 2025.10.13

很暴力的分治题。

首先考虑不带修怎么做。可以设 \(f_i\) 表示目前考虑到前 \(i\) 位的合法分割方案数,那么转移就是找一个区间 [l, r],使得 \(\forall l\leq i\leq r, a_i\leq r-l+1\)。如果想要优化这个转移就需要更加快速地处理这个区间。这个 DP 比较依赖前面的值,而且这个区间限制不太适合维护前缀和之类的事情,于是考虑 CDQ 分治。每次考虑 [l, r] 中跨过 mid 的区间转移,相当于每次拿一个 [i, mid] 和 [mid + 1, j] 拼一起,而两侧都给对面的区间端点设置了一个限制。这个限制类似二维数点,可以扫描线。

然后考虑带修。首先这个修改相当于把一个 \(a_i\) 限制删除,那么原先可行的方案现在一定也可行。那么考虑这样的修改给我带来的方案数增量。这个增量相当于找到一个之前没有的转移区间,给答案加上一个前缀方案数加后缀方案数。所以再算一个 \(g_i\) 表示考虑到 [i, n] 的合法分割方案数。考虑什么样的区间 [l, r] 使得修改单点后能“新增”:需要最大值超过区间长,并且修改选到了这个最大值,且次大值不超过区间长。

那么类似的,还是考虑分治,但是此时分治就不需要 CDQ 了。每次还是考虑统计越过 mid 的区间。考虑删除的点在左半区间,则对于一个前半区间的后缀 [i, mid],算出它的最大值及位置、和次大值,然后此时相当于对于所有限制 \(\geq i\) 的、位处左侧最大次大限制之间的右半前缀,可以加上这个贡献。这同样是一个类似二维数点的东西,还是可以扫描线。在右半同理。

于是就做完了。感觉这个题其实前面 CDQ 优化是有点难想的,首先一定要记住某一侧的 \(a_i\) 的意义只有对另一侧的边界作限制,因此区间内次大值在另一侧并不需要特殊考虑;后面的扫描线有一些 Corner 需要考虑一下。实现非常需要手法。另外记录树状数组修改,后续撤销操作的时候一定要特判,不然在外层修改里层还在 push_back 就炸了。调了我好久,唐完了

ylx 宝宝说可以单 \(\log\),但是他写了 5k 写了半天没写出来,比较令人难过。

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 + 2, kP = 998244353;

int n, a[kN];
int f[kN], g[kN], lim[kN], p[kN];

int t[kN];
vector<PII> his;
inline void Add(int x, int w, int o = 1) {
  o && (his.emplace_back(x, w), 0);
  for (; x <= n; x += x & -x)
    t[x] = (t[x] + w) % kP;
}
inline int Ask(int x) {
  int ret = 0;
  for (; x > 0; x -= x & -x)
    ret = (ret + t[x]) % kP;
  return ret;
}
inline void Clear() {
  for (auto [x, w] : his)
    Add(x, kP - w, 0);
  his.clear();
}

void Init(int *f, int l, int r) {
  if (l == r) {
    a[l] == 1 && (f[l] = (f[l] + f[l - 1]) % kP);
    return;
  }
  int mid = (l + r) / 2;
  Init(f, l, mid);
  for (int i = mid, mx = 0; i >= l; i--)
    mx = max(mx, a[i]), lim[i] = i + mx - 1;
  iota(p + l, p + mid + 1, l);
  sort(p + l, p + mid + 1, [&](int i, int j) { return lim[i] < lim[j]; });
  for (int i = mid + 1, mx = 0, j = l; i <= r; i++) {
    mx = max(mx, a[i]);
    for (; j <= mid && lim[p[j]] <= i; j++)
      Add(p[j], f[p[j] - 1]);
    f[i] = (f[i] + Ask(min(mid, i - mx + 1))) % kP;
  }
  Clear();
  Init(f, mid + 1, r);
}

int ans[kN];
struct Info { int i, p, val; };
vector<Info> scan[kN];
void Divide(int l, int r) {
  if (l == r) {
    if (a[l] > 1)
      ans[l] = (ans[l] + 1ll * f[l - 1] * g[l + 1]) % kP;
    return;
  }
  int mid = (l + r) / 2;
  Divide(l, mid), Divide(mid + 1, r);

  for (int i = mid + 1, mx = 0; i <= r; i++)
    mx = max(mx, a[i]), lim[i] = i - mx + 1;
  int mx = 0, sec = 0, p = -1;
  for (int i = mid; i >= l; i--) {
    if (a[i] > mx)
      sec = mx, mx = a[i], p = i;
    else if (a[i] > sec)
      sec = a[i];
    if (mx == sec)
      continue;
    if (i + mx - 1 <= r)
      scan[max(i + mx - 1, mid + 1)].push_back({i, p, kP - f[i - 1]});
    if (i + sec - 1 <= r)
      scan[max(i + sec - 1, mid + 1)].push_back({i, p, f[i - 1]});
  }
  for (int i = r; i > mid; i--) {
    if (lim[i] >= l)
      Add(min(mid, lim[i]), g[i + 1]);
    for (auto [i, p, val] : scan[i])
      ans[p] = (ans[p] + 1ll * (Ask(mid) - Ask(i - 1) + kP) * val) % kP;
    scan[i].clear();
  }
  Clear();

  for (int i = mid, mx = 0; i >= l; i--)
    mx = max(mx, a[i]), lim[i] = i + mx - 1;
  mx = sec = 0, p = -1;
  for (int i = mid + 1; i <= r; i++) {
    if (a[i] > mx)
      sec = mx, mx = a[i], p = i;
    else if (a[i] > sec)
      sec = a[i];
    if (mx == sec)
      continue;
    if (i - mx + 1 >= l)
      scan[min(i - mx + 1, mid)].push_back({i, p, kP - g[i + 1]});
    if (i - sec + 1 >= l)
      scan[min(i - sec + 1, mid)].push_back({i, p, g[i + 1]});
  }
  for (int i = l; i <= mid; i++) {
    if (lim[i] <= r)
      Add(max(mid + 1, lim[i]), f[i - 1]);
    for (auto [i, p, val] : scan[i])
      ans[p] = (ans[p] + 1ll * Ask(i) * val) % kP;
    scan[i].clear();
  }
  Clear();
}

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 >> a[i];
  f[0] = g[0] = 1;
  for (int o : {0, 1}) {
    Init(!o ? f : g, 1, n);
    reverse(a + 1, a + n + 1);
  }
  reverse(g, g + n + 2);
  fill_n(ans + 1, n, f[n]);

  Divide(1, n);
  for (int i = 1; i <= n; i++)
    cout << ans[i] << ' ';
  return 0;
}

C. QOJ9604 Cyberangel (8)

Date: 2025.10.13

诡异题。

首先不难将题意转化为,从小到大加入某个值对应的数,然后求出每次加完的所有区间 max 和 的和。维护这个区间信息还是可以用分治,每次算出当前区间内跨过 mid 的子区间的答案总和。对于所有这样的问题,可以自顾自地作按值域从小到大插入的计算贡献。由于区间跨过 mid 维护 max,于是可以考虑维护 左半后缀 max 以及 右半前缀 max。

此时你考虑我要维护什么东西,怎么用这个前缀后缀 max。比如我现在考虑某个 左半后缀 max 点 \(p\) 的贡献,它是 \(a_p(p-pre_p)(r_p-mid-1)\)。其中 \(pre_p\) 表示 \(p\) 的上一个前缀 max 点,\(r_p\) 表示右半第一个大于 \(a_p\) 的点。首先 \(pre_p\) 和前缀 max 这种东西可以使用单调栈,此时难点在怎么维护 \(r_p\) 的答案贡献。考虑到我是从小到大加入,维护 \(r_p\) 时,如果出现了加点时“换边”这种事情,可以把所有没有 \(r_p\) 的点全部拿出来指定;如果在同侧并且单调栈把别的点给弹掉了,新点就要继承指定 \(r\) 为被弹点的所有点。这个事情可以并查集维护。然后就直接记一下被指定点对应的 \(\sum a_p(p-pre_p)\) 的和即可。

总复杂度 \(\mathcal{O}(n\alpha(n)\log n)\)。说起来简单,但又有点细节。首先两边要记两个单调栈两个并查集,然后尤其要注意继承别的点的 \(\sum\) 的同时要把相对它贡献的差别扣掉。答案很大需要 __int128。

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

using namespace std;
using LL = long long;
using i128 = __int128;
using PII = pair<int, int>;
constexpr int kN = 1e6 + 1;
constexpr LL Base = 1e18;

i128 ans;
int n, id[kN];
LL m, a[kN];
inline bool Comp(int i, int j) { return a[i] != a[j] ? a[i] < a[j] : i < j; }

struct DSU {
  int fa[kN];
  i128 w[kN];
  inline void Init(int l, int r) {
    for (int i = l; i <= r; i++)
      fa[i] = i, w[i] = 0;
  }
  int Find(int x) { return x == fa[x] ? x : fa[x] = Find(fa[x]); }
  inline void Merge(int x, int y) { fa[x] = y, w[y] += w[x]; }
} L, R;

void Divide(int l, int r) {
  if (l == r)
    return ans += a[l] * (m - a[l] + 1), void();
  int mid = (l + r) / 2;
  Divide(l, mid);
  Divide(mid + 1, r);
  
  inplace_merge(id + l, id + mid + 1, id + r + 1, Comp);
  L.Init(l, r), R.Init(l, r);
  vector<int> sl, sr, vl, vr;
  i128 cur = 0;
  for (int o = l; o <= r; o++) {
    int i = id[o];
    if (i <= mid) {
      int pre = l - 1;
      for (; !sl.empty() && sl.back() < i; sl.pop_back()) {
        int j = sl.back(), tr = R.Find(j);
        cur -= i128(j - pre) * a[j] * (tr == j ? r - mid : tr - mid - 1);
        cur -= L.w[j] * (i - j);
        R.w[tr] -= i128(j - pre) * a[j];
        pre = j, L.Merge(j, i);
      }
      if (!sl.empty()) {
        int j = sl.back(), tr = R.Find(j);
        cur -= i128(i - pre) * a[j] * (tr == j ? r - mid : tr - mid - 1);
        R.w[tr] -= i128(i - pre) * a[j];
      }
      for (auto p : vr) {
        cur -= L.w[p] * (i - l + 1);
        L.Merge(p, i);
      }
      vr.clear();
      sl.push_back(i), vl.push_back(i);
      R.w[i] += a[i] * (i - l + 1);
      cur += i128(a[i]) * (i - l + 1) * (r - mid);
    } else {
      int pre = r + 1;
      for (; !sr.empty() && sr.back() > i; sr.pop_back()) {
        int j = sr.back(), tl = L.Find(j);
        cur -= i128(pre - j) * a[j] * (tl == j ? mid - l + 1 : mid - tl);
        cur -= R.w[j] * (j - i);
        L.w[tl] -= i128(pre - j) * a[j];
        pre = j, R.Merge(j, i);
      }
      if (!sr.empty()) {
        int j = sr.back(), tl = L.Find(j);
        cur -= i128(pre - i) * a[j] * (tl == j ? mid - l + 1 : mid - tl);
        L.w[tl] -= i128(pre - i) * a[j];
      }
      for (auto p : vl) {
        cur -= R.w[p] * (r - i + 1);
        R.Merge(p, i);
      }
      vl.clear();
      sr.push_back(i), vr.push_back(i);
      L.w[i] += a[i] * (r - i + 1);
      cur += i128(a[i]) * (r - i + 1) * (mid - l + 1);
    }
    ans += (o == r ? m - a[i] + 1 : a[id[o + 1]] - a[i]) * cur;
  }
}

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 = 1; i <= n; i++)
    cin >> a[i];
  iota(id, id + n + 1, 0);
  Divide(1, n);

  string Ans = "";
  for (; ans > 0; ans /= 10)
    Ans += ans % 10 + '0';
  reverse(Ans.begin(), Ans.end());
  cout << Ans << '\n';
  return 0;
}

Day18A. Easy Counting Problem (3.5)

Date: 2025.10.14

好题。考场上一直在想一个 BCBCB 这样的奇数 BC 段怎么去处理,想着可能需要分两边然后考虑两边分多少个 BC,DP 转移做之类的,但是并没有动手写。实际上这种段并不需要特殊考虑,因为无论怎么分,我能给出去的 BC 个数一定是长度一半;且留下一个 B 或者 C 并不会影响到其他地方过来的 BC 到另一边去。剩下的部分应该和正解等价。

不过官解的说法更加 形式化,比较美丽:观察这两个翻转操作,发现 A和C,B和D 之间必须要有其他的字母才能做操作,如果贴在一起,则一定可以拆成两边两个子问题;显然相邻两个字母相同也可以拆成两个子问题。于是现在变成了 AD 交替出现,BC 插在其中的状态。此时只要有 BC 就可以任意挪到另外的地方,那么方案数就是把能动的 BC 插到 AD 中间的方案数。这个是极其简单的。

今天运动会有项目所以心思有点飘,如果认真做也有可能做掉(?不过以我当时那个想法肯定写的很丑就是了。

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 = 1e6 + 1, kP = 1e9 + 7;

int T;
string s;
int fac[kN], inv[kN], ifc[kN];
inline int C(int n, int x) {
  if (n < x || x < 0)
    return 0;
  return 1ll * fac[n] * ifc[x] % kP * ifc[n - x] % kP;
}
inline int Get(vector<char> &v) {
  int bc = 0, cnt = 0, cur = 0;
  for (int i = 0; i < v.size(); i++) {
    if (v[i] == 'A' || v[i] == 'D')
      cnt++, bc += cur / 2, cur = 0;
    else
      cur++;
  }
  bc += cur / 2;
  return C(bc + cnt, cnt);
}
int main() {
#ifndef ONLINE_JUDGE
  freopen("input.in", "r", stdin);
  freopen("output.out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  fac[0] = fac[1] = ifc[0] = ifc[1] = inv[1] = 1;
  for (int i = 2; i < kN; 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 (cin >> T; T--;) {
    cin >> s, s = '#' + s;
    int ans = 1;
    vector<char> cur;
    for (int i = 1; i <= s.size(); i++) {
      if (i == s.size() || abs(s[i] - s[i - 1]) == 2 || s[i] == s[i - 1])
        ans = 1ll * ans * Get(cur) % kP, cur.clear();
      if (i != s.size())
        cur.push_back(s[i]);
    }
    cout << ans << '\n';
  }
  return 0;
}

Day18B. 构造合并 (2)

Date: 2025.10.14

做出这道题难点在发现他要求方案里的 \(x\leq y\)。不过证明还是有点意思的。

做法就是可以简单把全 1 合成 \(n\) 的二进制位拆分,然后猜如果不是只剩下一个数那么一定会剩下两个数。做两个数是简单的,考虑到最大数比比它小的二的幂加起来都大,所以从小到大送上来即可。

证法大概是倒推。如果能合成一个数的话,那么这个数一定是 \(n\),考虑倒推,因为正着做的时候怎么做操作都会有两倍,所以倒推就是除以二。那么如果不是二的幂,只靠除以二是到不了 1 的,所以不可能。

代码极其简单。

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>;

int n;
vector<PII> ans;
inline int Back() {
  int ret = (n & -n);
  return n -= ret, ret;
}
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 d = 1; 2 * d <= n; d *= 2) {
    for (int i = 1; i <= n / (2 * d); i++)
      ans.emplace_back(d, d);
  }
  int x = 1 << __lg(n);
  n -= x;
  int cur = Back();
  for (; n > 0;) {
    int tgt = Back();
    for (; cur < tgt; cur *= 2)
      ans.emplace_back(cur, x), x -= cur;
    assert(tgt == cur);
    ans.emplace_back(tgt, cur), cur *= 2;
  }
  cout << ans.size() << '\n';
  for (auto [u, v] : ans) 
    cout << u << ' ' << v << '\n';
  return 0;
}

Day18D. 构造立方 (6)

Date: 2025.10.14

聪明题,但是考场上没有看懂题面。因为说是有形式化题意,但是只看到了下面的形式化补充版,完全看不懂。没发现上面有个一句话加十几个字的简单表述。

考虑到我虽然切片内部进行了颜色去重,但是我外面是个可重集。也就是说,我一定可以给维度之间的切片指定一个一一对应。不过维度和维度之间是比较独立的,完全可以令这个一一对应一定是第 \(i\) 个切片和第 \(i\) 个切片之间对应,否则直接置换掉即可。

于是考虑我的限制本质,以三维为例,就是如果有一个 \(c_{i,j,k}=col\),则需要存在 \(c_{j,?,?}=c_{k,?,?}=c_{?,i,?}=c_{?,k,?}=c_{?,?,i}=c_{?,?,j}=col\)。对于一般情况,可以考虑直接循环移位,因为这样会把一个相同数加入到三个维度的三个不同切片中,循环位移只需要 3 的位置数量代价即可解决,一定是最优的。但是当存在两维相同时,再循环移位将不优,如 \(c_{i,i,j}=col\),限制就只有 \(c_{j,?,?}=c_{?,i,?}=c_{?,?,i}=col\),那么可以直接使用 \(c_{j,j,i}=col\) 即可 2 代价解决,两个不同切片的限制,所以是最优的。

所以总体算法就是,把所有值拿出来做循环位移的“置换”。非常聪明的本质转化,深刻学习了。

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

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e6 + 1, kV = 1e3 + 1;

int k, n, ans;
inline int Get(array<int, 4> &a) {
  int id = 0;
  for (int i = 0; i < k; i++)
    id = n * id + a[i];
  return id;
}
int col[kN], p[kV];
int main() {
#ifndef ONLINE_JUDGE
  freopen("input.in", "r", stdin);
  freopen("output.out", "w", stdout);
#endif
  cin.tie(0)->sync_with_stdio(0);
  cin >> k >> n;
  int m = 1;
  for (int _ = 1; _ <= k; _++, m *= n);
  array<int, 4> a, b;
  vector<int> v;
  for (int ID = 0; ID < m; ID++) {
    for (int x = ID, i = k - 1; i >= 0; i--)
      a[i] = x % n, x /= n;
    if (col[Get(a)])
      continue;
    for (int i = 0; i < k; i++)
      v.push_back(a[i]);
    sort(v.begin(), v.end());
    for (int i = 0; i < k; i++)
      a[i] = lower_bound(v.begin(), v.end(), a[i]) - v.begin();
    ans++;
    for (int i = 0; i < v.size(); i++) {
      for (int j = 0; j < k; j++)
        b[j] = v[(a[j] + i) % v.size()];
      col[Get(b)] = ans;
    }
    v.clear();
  }
  cout << ans << '\n';
  for (int i = 0; i < m; i++)
    cout << col[i] << ' ';
  return 0;
}

D. P3206 [HNOI2010] 城市建设 (5)

Date: 2025.10.15

简单题。

这个题,看上去可以做什么 线段树分治和 LCT 之类的事情,但是比较劣。受 Day13D 蝴蝶图 的影响,不难糊出一个中点分治,每次把询问区间内边权不变的边拿出来,先默认变了的边全部连上,做最小生成树得出全部必连的边,提前合成连通块;再考虑不加变了的边,做最小生成森林,删去所有没用的边。于是每次递归到两边,必然可以让点数边数都大致 \(\mathcal{O}(q)\)

顿时理解为什么存在人类会做蝴蝶图,看来是有源来的。

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 = 2e4 + 1, kM = 5e4 + 1;

int n, m, q;
int fa[kN], sz[kN];
int Find(int x) { return x == fa[x] ? x : Find(fa[x]); }

struct Backup { int u, v, su, sv; };
vector<Backup> bak;
inline void Undo() {
  auto [u, v, su, sv] = bak.back();
  bak.pop_back();
  fa[u] = u, fa[v] = v, sz[u] = su, sz[v] = sv;
}

struct Edge {
  int u, v, w;
  inline bool Merge() {
    int fu = Find(u), fv = Find(v);
    if (fu == fv)
      return 0;
    sz[fu] < sz[fv] && (swap(fu, fv), 0);
    bak.push_back({fu, fv, sz[fu], sz[fv]});
    fa[fv] = fu, sz[fu] += sz[fv];
    return 1;
  }
} ed[kM];
inline bool Comp(int i, int j) { return ed[i].w < ed[j].w; }

struct Query { int id, w; } que[kM];
LL ans[kM];
bool vis[kM];
vector<int> tmp, must;
void Solve(int l, int r, auto e0, auto e1, LL cur) {
  int beg = bak.size();
  for (int i = l; i <= r; i++)
    vis[que[i].id] = 1;
  for (auto i : e1)
    (vis[i] ? tmp : e0).push_back(i);
  e1.swap(tmp), tmp.clear();
  for (int i = l; i <= r; i++)
    vis[que[i].id] = 0;

  int rec = bak.size();
  for (auto i : e1)
    ed[i].Merge();
  sort(e0.begin(), e0.end(), Comp);
  for (auto i : e0)
    (ed[i].Merge() ? must : tmp).push_back(i);
  for (; bak.size() > rec; Undo());
  e0.swap(tmp), tmp.clear();
  for (auto i : must)
    cur += ed[i].w, ed[i].Merge();
  must.clear();
  
  rec = bak.size();
  for (auto i : e0)
    ed[i].Merge() && (tmp.push_back(i), 0);
  for (; bak.size() > rec; Undo());
  e0.swap(tmp), tmp.clear();

  if (l == r) {
    auto [id, w] = que[l];
    ed[id].w = w, e0.push_back(id);
    sort(e0.begin(), e0.end(), Comp);
    for (auto i : e0)
      cur += ed[i].Merge() * ed[i].w;
    for (; bak.size() > beg; Undo());
    return ans[l] = cur, void();
  }
  int mid = (l + r) / 2;
  Solve(l, mid, e0, e1, cur);
  Solve(mid + 1, r, e0, e1, cur);
  for (; bak.size() > beg; Undo());
}

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;
  vector<int> ve;
  iota(fa, fa + n + 1, 0), fill_n(sz + 1, n, 1);
  for (int i = 1, u, v, w; i <= m; i++) {
    cin >> u >> v >> w;
    ed[i] = {u, v, w}, ve.push_back(i);
  }
  for (int i = 1; i <= q; i++)
    cin >> que[i].id >> que[i].w;
  Solve(1, q, vector<int>(), ve, 0);
  for (int i = 1; i <= q; i++)
    cout << ans[i] << '\n';
  return 0;
}

E. QOJ3998 The Profiteer (8)

Date: 2025.10.16

离人类较远的极为厉害的题目。

又是一个对于所有区间求解答案的题目,但是它不太可能再去做什么后缀前缀拼起来的事情了。切换一下思路,首先很明显对于一个左端点,合法右端点一定是个后缀。所以考虑计算得出 \(f_l\),即对于 \(l\) 第一个能合法的右端点位置。

考虑对于一个 \(l\) 怎么求解这个 \(f\)。合法性具有单调性,考虑二分,check 一下背包内答案和之类的即可。此处有 \(n\) 个问题,考虑整体二分。此时就是要把当前左端点集合划分为两个集合,即左端点在这个集合时,右端点为 rmid 的区间全部 合法/非法。但这个事情感觉很难,因为直接写实际上还是分开的二分 check。但是发现 \(f\) 也具有单调性!所以只需要找到一个 左端点的一个划分点 \(mid\),使得 \([mid,rmid]\) 这个区间是合法的,那么 \([l,mid]\) 一定全合法,\((mid,r]\) 一定全非法,左端点集合一定是个区间。这个 \(mid\) 也可以二分。于是就基本做完了。

实现上有一定手法。因为 \(nk\) 卡的很满,整体二分自己带一个 \(\log\),二分划分点的时候区间长上是不能带 log 的。但是每次贰分到某一半,另一半就能确定了,可以直接填上,这样每次 check 的加入背包次数是当前二分的 \(r-l+1\) 的,整个二分的复杂度是 \(\mathcal{O}(nk)\) 的。所以总体复杂度 \(\mathcal{O}(nk\log n)\)

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 + 1;

int n, k, E;
struct Item { int v, a, b; } p[kN];
int f[kN], tmp[kN];
inline void Add(int l, int r, int o) {
  r = min(r, n);
  for (int i = l; i <= r; i++) {
    int w = !o ? p[i].a : p[i].b;
    for (int j = k; j >= w; j--)
      f[j] = max(f[j], f[j - w] + p[i].v);
  }
}
LL ans = 0;
void Divide(int ll, int lr, int rl, int rr) {
  if (lr < ll)
    return;
  else if (rl == rr)
    return ans += (lr - ll + 1ll) * (n - rl + 1), void();

  vector<int> bak(k + 1);
  copy_n(f, k + 1, bak.begin());
  int rmid = (rl + rr) / 2;
  Add(max(rl, lr + 1), rmid, 1), Add(rmid + 1, rr, 0);
  int l = ll, r = min(rmid, lr), lmid = ll - 1;
  for (int mid; l <= r;) {
    mid = (l + r) / 2;
    copy_n(f, k + 1, tmp);
    Add(l, mid - 1, 0), Add(mid, r, 1);
    LL sum = accumulate(f + 1, f + k + 1, 0ll);
    copy_n(tmp, k + 1, f);
    if (sum <= 1ll * k * E)
      Add(l, mid, 0), lmid = mid, l = mid + 1;
    else
      Add(mid, r, 1), r = mid - 1;
  }
  
  assert(lmid <= rmid);

  copy_n(bak.begin(), k + 1, f);
  Add(lmid + 1, min(lr, rl - 1), 1), Add(rmid + 1, rr, 0);
  Divide(ll, lmid, rl, rmid);

  copy_n(bak.begin(), k + 1, f);
  Add(ll, lmid, 0), Add(max(lr + 1, rl), rmid, 1);
  Divide(lmid + 1, lr, rmid + 1, rr);
}
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 >> k >> E;
  for (int i = 1, v, a, b; i <= n; i++)
    cin >> v >> a >> b, p[i] = {v, a, b};
  Divide(1, n, 1, n + 1);
  cout << ans << '\n';
  return 0;
}

Day19A. 排序大师 (2.5)

Date: 2025.10.16

简单题。

一眼一段值域区间 \([l,r]\) 全选,对应序列区间 \([pl,pr]\),然后 \(l-1\)\(pl\) 左侧的可以选上,\(r+1\)\(pr\) 右侧的可以选上,剩下的都需要做操作进行重排。但是这样会遗漏一种情况,即并不存在这样的全选值域区间,而是两个数 \(x,x+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 = 3e5 + 1, kV = 1e6 + 1;

int T, n, a[kN];
vector<int> pos[kV];
vector<int> V;
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], pos[a[i]].push_back(i);
      if (pos[a[i]].size() == 1)
        V.push_back(a[i]);
    }
    sort(V.begin(), V.end());
    int ans = 0;
    for (int l = 0, r = 0, R = 0, cnt = 0; r < V.size(); r++) {
      if (pos[V[r]][0] < R)
        cnt = pos[V[r]].size(), l = r;
      else
        cnt += pos[V[r]].size();
      R = pos[V[r]].back();
      int cur = cnt, w;
      if (l > 0)
        w = V[l - 1], cur += lower_bound(pos[w].begin(), pos[w].end(), pos[V[l]][0]) - pos[w].begin();
      if (r + 1 < V.size())
        w = V[r + 1], cur += pos[w].end() - lower_bound(pos[w].begin(), pos[w].end(), R);
      ans = max(ans, cur);
    } 
    for (int i = 1; i < V.size(); i++)
      for (int j = 0, w = V[i]; j < pos[V[i - 1]].size(); j++) {
        int cur = pos[w].end() - lower_bound(pos[w].begin(), pos[w].end(), pos[V[i - 1]][j]);
        ans = max(ans, j + 1 + cur);
      }
    cout << n - ans << '\n';

    for (auto w : V)
      pos[w].clear();
    V.clear();
  }
  return 0;
}

Day19B. 最大 mex (5)

Date: 2025.10.16

神秘题目漂亮结论!mex 真美丽吧。

求一些区间长对应的最大 mex,由于 mex 对于区间长具有单调性,因此可以求 极短的 mex=? 区间。发现这个个数很少。其实是 \(\mathcal{O}(n)\) 的!

考虑证明。对于一个左端点 \(l\),存在一个 \(r\) 使得 \([l,r]\) 极短。若 \(a_l>a_r\),明显有 \(\text{mex}>a_l>a_r\),由于区间极短,则 \(\text{mex}(l,r-1)<a_r\)。下若还存在一个 \(l<mid<r\) 使 \([l,mid]\) 也是极短区间,那么有 \(\text{mex}>a_l>a_r\),与前式矛盾,故该 \(mid\) 不存在。只能在外面套 \(a_l<a_r\) 的区间。但是对于右端点同样可以如此,因此总的极短区间个数仅有最多 \(2n\) 个。

获取也是简单的,每次拿出所有 \(\text{mex}=i\) 的区间,暴力加上 \(i\),然后 check 它的真实 mex 加入到相应 vector 即可。需要去重,但是我赛时写了个假的去重照样通过了,check 真实 mex 写的暴力校验也过了,跑的还比主席树一个 log 快。没有人类了。

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 + 2;

int n, q, a[kN];
vector<int> pos[kN];
vector<PII> mex[kN];

struct Node { int tim, l, r; } t[24 * kN];
int rt[kN], tot;
int Insert(int x, int w, int p, int l = 0, int r = n) {
  int ret = ++tot;
  if (t[ret] = t[x]; l == r)
    return t[ret].tim = p, ret;
  int mid = (l + r) / 2;
  if (w <= mid)
    t[ret].l = Insert(t[x].l, w, p, l, mid);
  else
    t[ret].r = Insert(t[x].r, w, p, mid + 1, r);
  t[ret].tim = min(t[t[ret].l].tim, t[t[ret].r].tim);
  return ret;
}
int Mex(int x, int L, int l = 0, int r = n) {
  if (l == r)
    return l;
  int mid = (l + r) / 2;
  if (t[t[x].l].tim < L)
    return Mex(t[x].l, L, l, mid);
  return Mex(t[x].r, L, mid + 1, r);
}

inline void Check(int l, int r) { mex[Mex(rt[r], l)].emplace_back(l, r); }

int mx[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 >> a[i], pos[a[i]].push_back(i);
    rt[i] = Insert(rt[i - 1], a[i], i);
  }
  for (auto p : pos[0])
    mex[1].emplace_back(p, p);
  for (int i = 1; i < n; i++) {
    sort(mex[i].begin(), mex[i].end(), greater<PII>());
    int tr = n + 1;
    for (auto [L, R] : mex[i]) {
      if (R >= tr)
        continue;
      tr = R;
      auto it = lower_bound(pos[i].begin(), pos[i].end(), L);
      if (it != pos[i].begin())
        Check(*prev(it), R);
      it = lower_bound(pos[i].begin(), pos[i].end(), R);
      if (it != pos[i].end())
        Check(L, *it);
    }
  }

  for (int i = 1; i <= n; i++) {
    for (auto [L, R] : mex[i])
      mx[R - L + 1] = max(mx[R - L + 1], i);
  }
  for (int i = 1; i <= n; i++)
    mx[i] = max(mx[i], mx[i - 1]);

  cin >> q;
  for (int k; q--;) {
    cin >> k;
    cout << mx[k] << '\n';
  }
  return 0;
}

Day19C. 无限回文 (5.5)

Date: 2025.10.17

比较聪明的题目,但是考场上没有开,做掉 B 之后去想 D 怎么实现了。

分析一下这个题目的性质:无限回文串其实是 原子串+rev 无限循环的成果。无限循环能对上原串意味着这个子串能翻折后对上;且翻折后的 +rev 区间是原串的一个周期。原子串首先如果一个好的区间能够翻折,那么首先翻折后能对的上,且翻折后一定是一个周期。前者发现翻折对的上相当于一个偶回文子串,可以 manacher 预处理每个间隙对应的最大回文长度处理;后者考虑到只要判一下原串是否存在这个长度的周期即可。这个可以 KMP 然后跳 border 获取一个串的所有周期。

然后可以考虑分讨:首先如果该区间能够翻折,则可以如上考虑,很明显如果两边都能翻,那么朝向并不重要;分为可以向左翻,和不能向左翻只能向右。对是周期的区间长度做个前缀和,算一下限制之类的东西即可。但是这个区间并不一定能翻折,不过这样的话 左右端点除不能翻折所对应的区间长度限制以外,是完全独立的。只需要给合法的左端点做个前缀计数,然后枚举右端点即可。总复杂度 \(\mathcal{O}(n)\)

其实这场比赛以前甚至还不会这个 KMP 取出所有周期,本质是不会 KMP,比较唐。

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 = 1e7 + 2;

string s, t;
int T, n;
bool tag[kN];
int sum[kN], nxt[kN], len[2 * kN];
inline LL Solve1() {
  LL ret = 0;
  for (int l = 1; l < n; l++)
    sum[l] += (len[l - 1] >= l - 1);
  for (int i = 1; i <= n; i++)
    sum[i] += sum[i - 1];
  for (int r = (n + 1) / 2; r <= n; r++) {
    if (len[r] >= n - r)
      ret += sum[min((r + 1) / 2, 2 * r - n)];
  }
  fill_n(sum, n + 1, 0);
  return ret;
}
inline LL Solve2() {
  LL ret = 0;
  for (int len = 1; len <= n / 2; len++)
    sum[len] = sum[len - 1] + tag[len];
  for (int l = 2; l <= n; l++) {
    int lim = min({n - l + 1, l - 1, len[l - 1]});
    ret += sum[lim];
  }
  return ret;
}
inline LL Solve3() {
  LL ret = 0;
  for (int r = 1; r < n; r++) {
    int L = r / 2, R = min(n - r, len[r]);
    ret += max(0, sum[R] - sum[L]);
  }
  return ret;
}
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 >> s, n = s.size();
    s = '#' + s;
    for (int i = 2, bd; i <= n; i++) {
      for (bd = nxt[i - 1]; bd != 0 && s[bd + 1] != s[i]; bd = nxt[bd]);
      nxt[i] = bd + (s[bd + 1] == s[i]);
    }
    for (int bd = nxt[n]; bd != 0; bd = nxt[bd])
      tag[(n - bd) / 2] |= (n - bd) % 2 == 0;
    tag[n / 2] |= n % 2 == 0;
    
    t = "(";
    for (int i = 1; i <= n; i++)
      t += s[i], t += "|)"[i == n];
    for (int i = 1, mid = 0, r = 0; i < 2 * n; i++) {
      len[i] = (r < i ? 1 : min(len[2 * mid - i], r - i + 1));
      for (; t[i - len[i]] == t[i + len[i]]; len[i]++);
      if (i + len[i] - 1 > r)
        r = i + len[i] - 1, mid = i;
    }
    for (int i = 1; i < 2 * n; i++)
      len[i] = (i < n ? len[2 * i] / 2 : 0);

    cout << Solve1() + Solve2() + Solve3() << '\n';

    for (int i = 1; i <= n; i++)
      tag[i] = sum[i] = 0;
  }
  return 0;
}

Day19D. 最小步数 (6)

Date: 2025.10.16

考场上并没有动手写这个题,口胡的时候极大低估了这题的实现难度。

首先很明显,可以推出一个 \(x\)\(y\) 的带 popcnt 的答案式,肯定考虑数位 DP,但是这个东西并不是很好转移之类的,再加上它有一个边界,会更加恶心。怎么办呢。

其实做这种数位 DP 的时候从具象的限制转化到抽象的式子并不一定是好的简化。考虑本质,这个式子的推法是:找到 y1x0 的最高位,的下一个 x1y0 位,然后把 \(x\) 后面的全部 \(-2^i\) 减掉,再 -1 使得 \(x\) 后缀全变 1,变为 \(y\) 的超集,再 \(-2^i\) 变成 \(y\)。然后变成 popcnt 之类的事情。但是并不必要如此转化,考虑这个具象的操作过程,其实是一个前缀 \(x\)\(y\) 的超集,然后选择一个 x1y0 开始,后续 xy 全相同,直到再出现一个 x0y1。可以把数位用 x1y0,x0y1 分割成三段。考虑这三段的答案贡献:第一段的贡献是 \(x-y\);第二段全部相同,若是 0 则需要在后缀变 1 之后做操作,若是 1 则需要在之前做操作,均为 \(1\) 的代价;第三段代价则为 \(x-y+1\),即 x1 的时候需要之前做操作,y0 的时候需要之后做操作。然后就可以数位 DP 了。

具体而言,设 \((i,0/1,0/1/2)\) 状态为 当前考虑到第 \(i\) 位,目前还顶在限制上,到了第 1/2/3 个段。然后分 方案数cnt 和 答案sum 一起转移。虽然简化了很多,但是还是写了我很久。因为我一直不愿意下手写一个自己觉得会很不好看很丑的东西,根本写不出来,分讨的思路也不怎么清晰,卡在电脑前写了一万年,十分悲伤。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 = 5e5 + 2, kP = 1e9 + 7;

string s;
struct Info {
  int cnt, sum;
  inline Info operator+(const Info &x) const {
    return {(cnt + x.cnt) % kP, (sum + x.sum) % kP};
  }
  inline Info operator+=(const Info &x) { (*this) = (*this) + x; }
} f[kN][2][3];
inline Info Add(Info x, LL w) { return {x.cnt, (x.sum + w * x.cnt) % 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 >> s;
  int n = s.size();
  s = '#' + s;
  f[0][1][0] = {1, 0};
  for (int i = 1; i <= n; i++) {
    for (int o : {0, 1})
      for (int x : {0, 1}) {
        if (o && x > s[i] - '0')
          continue;
        int t = o && (x == s[i] - '0');
        for (int y : {0, 1}) {
          if (y <= x)
            f[i][t][0] += Add(f[i - 1][o][0], x - y);
          f[i][t][2] += Add(f[i - 1][o][2], x - y + 1);
        }
        f[i][t][1] += Add(f[i - 1][o][1], 1);
        if (x == 1)
          f[i][t][1] += Add(f[i - 1][o][0], 1);
        else
          f[i][t][2] += f[i - 1][o][1];
      }
  }
  int ans = 0;
  for (int o : {0, 1}) {
    for (int t : {0, 2})
      ans = (ans + f[n][o][t].sum) % kP;
  }
  cout << ans << '\n';
  return 0;
}

F. QOJ10545 String Rank (?)

Date: 2025.10.17

翔哥推的。

当时我的思路是,考虑倒着做维护后缀。对于每个子序列长度维护一个集合,每次更新相当于拿一个集合 \(i\),头部拼上一个新字母,然后全部放入集合 \(i+1\)。那么如果对于 \(i\),被某一个字母更新过 \(i+1\),若 \(i\) 未被更新,那么再遇到相同的字母时就不会更新 \(i+1\) 了。所以考虑维护这个“更新”对应的字母。但是这个类似一个 \(nk\),然后也不是很知道对不对,更不知道能不能优化,就没细想了。

其实是比较对的,考虑第 \(i\) 个字母能更新的最小的集合大小,这个可以 DP 转移:设 \(f_i\) 表示...,则可以扩散型,对于每个字母找到上一个出现位置,对这个集合拼上这个字母即可。复杂度 \(\mathcal{O}(nV)\)

看来 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 = 3e6 + 2;

int n, lst[kN][26];
string s;
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);
  cin >> s, n = s.size();
  s = '#' + s;
  for (int i = 1; i <= n; i++) {
    copy_n(lst[i - 1], 26, lst[i]);
    lst[i][s[i] - 'a'] = i;
  }
  fill_n(f, n + 2, 1e9), f[n + 1] = 0;
  int ans = 0;
  for (int i = n + 1; i >= 1; i--) {
    for (int ch = 0, p; ch < 26; ch++)
      p = lst[i - 1][ch], f[p] = min(f[p], f[i] + 1);
    ans = max(ans, f[i]);
  }
  cout << ans << '\n';
  return 0;
}

G. QOJ6354 4 (5)

Date: 2025.10.17

瞎翻看到的,感觉有点意思。

大小为 4 的完全联通子图可以看作 两个共边的三元环拼一起 再加一条边。思考怎么考虑这个共边,它是四边形中新加连边的对边,也是这条新边的两端点在三元环中的对边。可以考虑用 bitset 存下一个点的所有三元环对边,然后枚举新边端点,bitset 求交即可。会算重 6 次,除一下即可。复杂度 \(\mathcal{O}(m\sqrt{m} + \frac{nm}{\omega})\)

需要一个比较优秀的拆分思路才能有效地思考这个题,还挺有趣的。

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <bitset>
#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;

int n, m, deg[kN];
struct Edge { int u, v, w; };
vector<Edge> ed;
vector<PII> e[kN];
bitset<kN> msk[kN];
int vis[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 >> m;
  for (int i = 1, u, v; i <= m; i++) {
    cin >> u >> v, ed.push_back({u, v, i});
    deg[u]++, deg[v]++;
  }
  for (auto [u, v, i] : ed) {
    deg[u] < deg[v] && (swap(u, v), 0);
    e[u].emplace_back(v, i);
  }
  for (int a = 1; a <= n; a++) {
    for (auto [v, id] : e[a])
      vis[v] = id;
    for (auto [b, p] : e[a])
      for (auto [c, q] : e[b])
        if (vis[c] != 0) {
          msk[a].set(q);
          msk[b].set(vis[c]);
          msk[c].set(p);
        }
    for (auto [v, id] : e[a])
      vis[v] = 0;
  }
  LL ans = 0;
  for (auto [u, v, _] : ed) 
    ans += (msk[u] & msk[v]).count();
  cout << ans / 6 << '\n';
  return 0;
}

H. UOJ693 地铁规划 (8.5)

Date: 2025.10.17

超级厉害诡异题。但是并不太知道为什么放分治题单里。

首先我们理想中维护这个答案,其实是需要一个双指针,需要一个队列维护,而不是栈。但是只能顺从,考虑用一些手法维护双栈。首先考虑暴力做:维护一个翻转后的当前边,然后每次弹出就是弹出栈顶,插入就是弹出整个栈,然后插入到栈底,然后再扔回来。后续优化的时候也运用类似思想,把当前边区间的前缀翻转放入栈中,剩下一个未翻转的后缀在栈的某个位置中。未翻转的这个后缀每次弹出来,等需要弹出的翻转前缀的某个点弹出了 就立即塞回去。

这两种类型的边犹如一个双栈,但是互相干扰,带给了我们一些困境:

  1. 插入的时候为了把未翻转后缀扔到栈中某个位置,弹出了很多翻转边,造成巨大无用代价;
  2. 弹出的时候为了把深处的翻转边弹出,随之弹出了很多未翻转边,造成巨大无用代价。

所以我们考虑每次把更多的翻转边拿到栈顶,而不是任由未翻转边在栈顶;一次性让更多的未翻转边塞入翻转边内部,而不是每加入一个就狂往里塞。此时思路类似,每次不把未翻转边立马塞到最底下,而是在中间停住,并且把一些翻转边扔到栈顶,等栈顶的翻转边耗尽后再下移。考虑到取出的这两个集合大小差距太大会比较劣,所以我们期望更深的位置的翻转边块更大。此时可以类似二进制分组,把翻转边分为一块一块的,每次把栈顶的未翻转边集合下沉一个块。

此时的复杂度,大致考虑每个未翻转边只会下沉 \(\mathcal{O}(\log n)\) 次,每个翻转边块被弹意味着上升到了栈顶,每个点的对应块也只会被裂开 \(\mathcal{O}(\log B)\) 次,之后就被弹出。总复杂度 \(\mathcal{O}(n\log n)\)

特别考验脑子的题目,碰巧发现了复杂度劣的本质,比较狗运。

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
#include "subway.h"

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

int n, r, cnt;
vector<int> st;
vector<int> nrev, rev;
bool tag[kN];
void init(int n, int m, int lim) { ::n = m, r = 1; }
int solve(int l) {
  if (l == n)
    return n;
  for (; r <= n && check(r); r++)
    merge(r), st.push_back(r), tag[r] = 0;
  if (cnt == 0) {
    for (; !st.empty(); st.pop_back())
      undo(), rev.push_back(st.back());
    for (auto i : rev)
      merge(i), tag[i] = 1, st.push_back(i);
    cnt = rev.size(), rev.clear();
  }
  if (!tag[st.back()]) {
    for (; !tag[st.back()];)
      nrev.push_back(st.back()), undo(), st.pop_back();
    int sz = (cnt & -cnt);
    for (int i = 1; i <= sz; i++)
      rev.push_back(st.back()), undo(), st.pop_back(); 
    for (; !nrev.empty(); nrev.pop_back()) {
      int i = nrev.back();
      merge(i), st.push_back(i), tag[i] = 0;
    }
    for (; rev.size() > 1; rev.pop_back()) {
      int i = rev.back();
      merge(i), st.push_back(i), tag[i] = 1;
    }
    rev.pop_back();
  } else
    undo(), tag[st.back()] = 0, st.pop_back();
  cnt--;
  return r - 1;
}

I. QOJ8559 k-coloring (7)

Date: 2025.10.18

翔哥集训队作业投的题。狠狠被推荐了。

\(k\leq10\) 看上去很可怕,但是由于这个路径是允许重边的,所以可以简单浪费两步,使其归约到 \(k-2\) 上。所以简单讨论一下 \(k\) 很小的情况。

  • \(k=1\)\(1\) 开始的欧拉路径,随便判。
  • \(k=2\):考虑把一条边的出现时刻分配到一奇一偶。可以考虑拉一个欧拉序出来,因为进入下一个点的时候加入了偶数次,不改变奇偶性,出来的时候刚好和进去的时候奇偶性不同。因此刚好每条边遍历恰好一次。

……做完了?并没有。你考虑到 \(k=3\) 并不能直接归约到 \(k=1\),因为 \(k=1\) 时有一些情况会无法达成,因为它内涵了不能重复走边的限制;但是 \(k=3\) 没有。所以再考虑 \(k=3\)。考虑再沿用 \(k=2\) 的方案。首先不可能只考虑一条边做三次操作解决,因为这和 \(k=1\) 等价;考虑两条边做六次。要与 \(k=1\) 不同,所以走完第一条边后要跟着走第二条边,再回来……操作序列大致 122221。但是这样回到了原点,不能很好的做这个 DFS。不过这给了我们启发(???我也不知道怎么启发的)

考虑在第三下的时候回一步,这样将会走操作序列上的四条边,并且似乎直接归约成了 \(k=2\)!于是就做完了。

首先在 \(k=2\) 的时候想到欧拉序已是一小难点,而深刻感受走边的落点并构造出 \(k=3\) 归约解更是难上加难。感觉这个题想到了就是想到了,没想到也不太能做啊。/ng

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 = 1e5 + 1, kD[] = {1, 1, -1, 1, 1, 1};

int n, m, k;
bool vis[kN];
vector<PII> e[kN];
vector<int> vec;

void Euler(int x) {
  for (auto [v, id] : e[x]) {
    if (!vis[id])
      vis[id] = 1, Euler(v);
  }
  vec.push_back(x);
}
inline void Solve1() {
  int cnt = 0;
  for (int i = 1; i <= n; i++)
    cnt += (e[i].size() % 2 == 1);
  if (cnt > 2 || cnt == 2 && e[1].size() % 2 == 0)
    return cout << "-1\n", void();
  Euler(1);
  reverse(vec.begin(), vec.end());
  cout << vec.size() << '\n';
  for (auto i : vec)
    cout << i << ' ';
}

void Dfs(int x) {
  vec.push_back(x);
  for (auto [v, id] : e[x]) {
    if (vis[id])
      continue;
    vis[id] = 1, Dfs(v);
    vec.push_back(x);
  }
}
int main() {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> m >> k;
  for (int i = 1, u, v; i <= m; i++) {
    cin >> u >> v;
    e[u].emplace_back(v, i), e[v].emplace_back(u, i);
  }
  if (n == 1)
    return cout << "0\n", 0;
  else if (k == 1)
    return Solve1(), 0;
  Dfs(1);
  if (k % 2 == 1) {
    vector<int> tmp;
    for (int i = 0, j = 0; j < vec.size(); j += kD[i % 6], i++)
      tmp.push_back(vec[j]);
    vec.swap(tmp), tmp.clear();
  }
  int b = 2 + k % 2;
  k -= b;
  vector<int> ans = {1};
  for (int i = 0; i < vec.size(); i += b) {
    for (int j = 1; j <= k / 2; j++)
      ans.push_back(e[vec[i]][0].first), ans.push_back(vec[i]);
    for (int j = i + 1; j <= min(int(vec.size()) - 1, i + b); j++)
      ans.push_back(vec[j]);
  }
  for (; ans.size() > 1000001; ans.pop_back());
  cout << ans.size() << '\n';
  for (auto i : ans)
    cout << i << ' ';
  return 0;
}

Day20B. 移除礼物盒 (3)

Date: 2025.10.18

这 T2 真难吧/ll

不难发现礼物编号毫无意义,可以转化为左右括号。思考一下答案贡献,大致是遇到右括号作为第一个就会立即消掉,贡献 1 次操作;第一个是左括号就会再拿一个,如果是右括号就消耗 2 的贡献,如果是左括号就还是 1 贡献;特殊情况是 选出左右括号编号相同,这样只有 1 的贡献。

总结一下,就是右括号必然有 1 代价,左括号若位置为奇数则有 1 代价(与上一个右括号的相对位置),若连续选出两个括号一左一右同编号则扣掉 1 代价。可以简单线段树维护。

那么我为什么没做出来呢!??!?!赛场上脑子不知道在想什么,反正就是没有发现这个相对位置的事情,也觉得现在这个答案计算过于繁琐,感觉上不太好维护,在想一些神秘性质。于是没有在更基础的地方细想,感觉大概是忘记了思考当前的贡献方式是不是已经可以做一做了。比较唐。

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 + 1;

int T, n, q;
int a[2 * kN], L[kN], R[kN];
struct Node { int w[2], od[2]; } t[8 * kN];
void Update(int p, bool o, int x = 1, int l = 1, int r = 2 * n) {
  if (l == r)
    return o ? t[x] = {1, 1, 0, 0} : t[x] = {1, 0, 1, 0}, void();
  int mid = (l + r) / 2;
  if (p <= mid)
    Update(p, o, 2 * x, l, mid);
  else
    Update(p, o, 2 * x + 1, mid + 1, r);
  for (int r : {0, 1}) {
    bool ro = t[2 * x].od[r];
    t[x].od[r] = t[2 * x + 1].od[ro];
    t[x].w[r] = t[2 * x].w[r] + t[2 * x + 1].w[ro] - (ro && a[mid] == a[mid + 1]);
  }
}

inline void Insert(int w, int p) {
  (!L[w] ? L[w] : R[w]) = p;
  if (L[w] && R[w]) {
    L[w] > R[w] && (swap(L[w], R[w]), 0);
    Update(L[w], 0), Update(R[w], 1);
  }
}
inline void Remove(int w, int p) { (L[w] == p ? L[w] : R[w]) = 0; }

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 >> q;
    for (int i = 1; i <= 2 * n; i++)
      cin >> a[i], Insert(a[i], i);
    cout << t[1].w[0] << '\n';
    for (int l, r; q--;) {
      cin >> l >> r;
      Remove(a[l], l), Remove(a[r], r);
      swap(a[l], a[r]);
      Insert(a[l], l), Insert(a[r], r);
      cout << t[1].w[0] << '\n';
    }

    for (int i = 1; i <= n; i++)
      a[L[i]] = a[R[i]] = L[i] = R[i] = 0;
  }
  return 0;
}

Day20C. 共享道路 (7.5)

Date: 2025.10.18

花很多时间在这题上,但也并没有写任何一个字符。

赛时想了一些重链前缀取交的东西,但是不好搞,前途黑暗;想了一些两个子树的路径端点集合交>2 即可将两子树根距离加入答案的东西,但不太会做。

与后者类似,正解考虑维护子树由内到外的路径的外部端点。不难发现首先子树的根可以看作为两路径交集的一个端点;这些外部端点两两之间对应的路径交集另一端点可以直接考虑记入答案。维护外部端点是简单的,做个树上差分 set 启发式合并即可。是不难做的。但是肯定做不到两两之间暴力算,怎么样算复杂度更加低呢?我们渴望一个 size 线性相关的复杂度。

而观察一下,如果弱化一下题目,怎么做。原题转化后变为了 所有路径从树上某个点伸出,求答案。弱化考虑钦定这个点是根节点 1。此时路径交集的另一个端点是路径端点的 LCA。我们希望这个 LCA 深度尽量大。不难联想到一个 LCA 求法:拉出 DFS 序后区间 RMQ 求深度最小值的父亲。很明显,DFS 序区间越大,得到的 LCA 越浅。所以只有 DFS 序相邻的两个点是可能得到有效答案的!

考虑变回原问题,尝试套用该结论:若我的另一个端点不是当前子树根的祖先,那么使用 DFS 序相邻两个一定可以找到所有的 存在的 祖先链上的 兄弟子树内的“另一个端点”。在此基础上,我们希望兄弟子数里这个点越深越好。这和路径一个端点是树根是等价的,结论很明显成立;若 另一个端点是当前子树的根的祖先,那么祖先链上 其他子树内 有外部路径端点的“相邻”两个祖先,一定在这些点中是 DFS 序连续的……吗?不一定,可以在根的两边放两个叶子,这样这个最高可以选到根。

怎么修复呢?考虑到这个 另一个端点 应该是某个外部端点与当前子树根的 LCA,此时有用的点对还可能分在 DFS 序两侧。再判一下 DFS 序最大最小之间产生的贡献即可。但是没写这个也过了,数据比较水。

有点太厉害了,不知道怎么才能像这样思考/hsh

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 + 1;

int n, k;
vector<int> e[kN];
int dfn[kN], st[kN][20], dfc, dep[kN], id[kN];
inline int Get(int x, int y) { return dep[x] > dep[y] ? y : x; }
void Init(int x, int fa) {
  st[dfn[x] = ++dfc][0] = fa;
  dep[x] = dep[fa] + 1, id[dfn[x]] = x;
  for (auto v : e[x])
    Init(v, x);
}
inline int LCA(int x, int y) {
  if (x == y)
    return x;
  x = dfn[x], y = dfn[y];
  x > y && (swap(x, y), 0);
  int k = __lg(y - x);
  return Get(st[x + 1][k], st[y - (1 << k) + 1][k]);
}

struct Node {
  int x, id;   // dfn, road id
  inline bool operator<(const Node &r) const { return x < r.x || x == r.x && id < r.id; }
};
vector<pair<Node, bool>> op[kN];
set<Node> s[kN];
pair<int, PII> ans;
inline int Dis(int x, int a, int b) {
  a = id[a], b = id[b];
  return dep[x] + dep[LCA(a, b)] - dep[LCA(a, x)] - dep[LCA(b, x)];
}
inline void Insert(int x, Node p) {
  auto [y, id] = p;
  auto it = s[x].lower_bound(p);
  if (it != s[x].begin() && prev(it)->id != id)
    ans = max(ans, {Dis(x, y, prev(it)->x), {prev(it)->id, id}});
  if (it != s[x].end() && it->id != id)
    ans = max(ans, {Dis(x, y, it->x), {it->id, id}});
  s[x].emplace_hint(it, p);
}
void Dfs(int x) {
  int ID = 0;
  for (auto v : e[x])
    Dfs(v), (s[v].size() > s[ID].size()) && (ID = v);
  s[x].swap(s[ID]);
  for (auto v : e[x]) {
    if (v == ID)
      continue;
    for (auto p : s[v])
      Insert(x, p);
  }
  for (auto [p, o] : op[x])
    o ? Insert(x, p) : (s[x].erase(p), void());
}

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 >> k;
  for (int i = 2, fa; i <= n; i++)
    cin >> fa, e[fa].push_back(i);
  Init(1, 0);
  ans = {0, {1, 2}};
  for (int d = 1; d < 20; d++) {
    for (int i = 1; i + (1 << d) - 1 <= n; i++)
      st[i][d] = Get(st[i][d - 1], st[i + (1 << d - 1)][d - 1]);
  }
  for (int i = 1, a, b; i <= k; i++) {
    cin >> a >> b;
    int lca = LCA(a, b);
    op[a].emplace_back(Node{dfn[b], i}, 1), op[b].emplace_back(Node{dfn[a], i}, 1);
    for (auto x : {a, b})
      op[lca].emplace_back(Node{dfn[x], i}, 0);
  }
  Dfs(1);
  auto [a, b] = ans.second;
  cout << ans.first << '\n' << a << ' ' << b << '\n';
  return 0;
}

Day20D. 美观的书架 (8.5)

Date: 2025.10.19

超难题,但是似乎以前梦熊搬过,但是我并没有补。

赛场上想到了对左上角矩形进行平移,向下平移得 sum(1, [1, c]) = sum(r + 1, [1, c])。同理有 sum(1, [2, c + 1]) = sum(r + 1, [2, c + 1]),减一下就能发现 (1, 1) + (r + 1, c + 1) = (1, c + 1) + (r + 1, 1)。然后就不太会,但是感觉后续的思路就是枚举一个左上角然后快速地推出后面的位置,形如 (pr + i, qc + j) 的格子可以按 (i, j) 分类,类别之间是比较独立的,或许有一些加和的限制。但是甚至做不到枚举左上角,可能会有一些优化手法。看到 fzx 场切比较惊讶,不过人家确实强啊。

其实我推得很对,也作为正解的一个前缀。但是下一步就比较厉害了:观察到满足这样条件的同一类格子,必须满足同一行的全部相等,或是同一列的全部相等。哇这也太对了吧。后续考虑对类别之间的限制:有左上角的行的和相等,即 sum(1, [1, c]) = sum(r + 1, [1, c])。如果是列相等的类别,那么这个限制自然满足,暂时只考虑行相等。假设行相等的类别个数为 \(k\),那么设同一行的和为 \(s\),有 \(k\choose s\) 的方案满足一行和为 \(s\);有 \(\frac xr\) 行,所以方案数为 \(\sum {k\choose s}^{\frac xr}\)。列相同的限制类似,同样有 sum([1, r], 1) = sum([1, r], c + 1)。设有 \(k\) 个列相同...欸,但是会有类别全相同,会导致算重,所以提前在这里进行去重,即直接式子与行相同一致,但加上一个枚举全相同类别数,简单容斥一下即可。不要忘记全相同可以是 0/1 两种。

此时暴力就考虑枚举左上角状态。但是复杂度很爆炸。一个优化方法是考虑 DP,状态是此时此刻每一行有多少个行相同。两种转移:一种是按列转移,即枚举下一列的行相同位置状态 \(s\),这一列有 r-popcnt(s) 个列相同,然后把状态加一下即可,复杂度 \(\mathcal{O}((2r)^c)\)。另一种是按格转移,考虑类似轮廓线 DP,每次考虑一个点的状态,再在状态里记上当前列的列相同个数,直接转移即可。复杂度像是 \(\mathcal{O}((r+1)^cr^2c^2)\),不知道,可能是我脑子不好使。感觉纯慢,必然跑不过按列的。

写的是按列转移。反思了一下,感觉对一些更加直观好处理的形式、特性不够敏锐,做的太少,或是写的太少?

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 = 8, kP = 998244353, kS = 6e6 + 1;
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 n, m, x, y;
int C[kN][kN], pw[kN], msk[1 << kN];
int f[kS], g[kN], tmp[kS], h[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 >> m >> x >> y;
  if (n < m)
    swap(n, m), swap(x, y);
  x /= n, y /= m;
  C[0][0] = pw[0] = 1;
  for (int i = 1; i <= m; 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];
    pw[i] = (2 * kP - 2 * pw[i - 1]) % kP;
  }
  for (int i = 0; i <= m; i++) {
    for (int j = 0; j <= i; j++)
      g[i] = (g[i] + Pow(C[i][j], x)) % kP;
  }
  for (int i = 0; i <= n; i++) {
    for (int j = 0; j <= i; j++)
      tmp[i] = (tmp[i] + Pow(C[i][j], y)) % kP;
    for (int j = 0; j <= i; j++)
      h[i] = (h[i] + 1ll * tmp[j] * pw[i - j] % kP * C[i][j]) % kP;
  }
  fill_n(tmp, n + 1, 0);
  for (int s = 0; s < 1 << n; s++) {
    for (int d = 0, cur = 1; d < n; d++, cur = (m + 1ll) * cur % kP)
      msk[s] += (s >> d & 1) * cur;
  }
  int A = Pow(m + 1, n);
  f[0] = 1;
  for (int i = 1; i <= m; i++) {
    for (int t = 0; t < A; t++) {
      if (!f[t])
        continue;
      for (int s = 0; s < 1 << n; s++)
        tmp[t + msk[s]] = (tmp[t + msk[s]] + 1ll * f[t] * h[n - __builtin_popcount(s)]) % kP;
    }
    for (int t = 0; t < A; t++)
      f[t] = tmp[t], tmp[t] = 0;
  }
  int ans = 0;
  for (int t = 0; t < A; t++) {
    int cur = f[t];
    for (int i = 0, s = t; i < n; i++)
      cur = 1ll * cur * g[s % (m + 1)] % kP, s /= m + 1;
    ans = (ans + cur) % kP;
  }
  cout << ans << '\n';
  return 0;
}

J. QOJ14575 集你太美 (2.5)

Date: 2025.10.18

This problem is prepared by Made_in_Code .!?!??!!? dyf 的题得做吧。

考虑把所有连通块拿出来,判断只要有一个连通块可行就可行,构造只需要构造一个最优的连通块即可。然后反思一下怎么判断:首先发现一定会选出一个排列,因为连通块里少选点一定会收集出负数,所以必须每个点都选到;每个点全选一遍之后点权会恢复,很符合循环特征;在所有点全部选过之前如果某个点选了两次会很劣,因为会导致一些点无意义的多进行了点权的减。

于是考虑一定会有一个点会被所有连边收集后才被选(排列的最后一个点)。此时需要它的点权大过所有连边的和。而此时可以把这个点及其连边删除,变成一个新的问题。依此,可以维护一下每个点对应的边权和的点券限制,进行一个类似拓扑排序的操作,看能不能删空。于是判断就做完了。

反思一下,发现无论怎么构造一个连通块内的所有边全部会被删一次,即一定会加到某一个点权上,这个排列是啥根本不影响点权和。拿出来一个边权和最小的连通块枚举所有点赋权删边即可。

感觉比今天模拟赛好做啊/hsh

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

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

int o, n, m;
vector<PII> e[kN];
LL a[kN], lim[kN];
queue<int> q;
vector<vector<int>> p;
bool vis[kN];
void Dfs(int x, int o) {
  p[o].push_back(x), vis[x] = 1;
  for (auto [v, _] : e[x])
    (!vis[v]) && (Dfs(v, o), 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 >> o >> n >> m;
  for (int i = 1, u, v, w; i <= m; i++) {
    cin >> u >> v >> w;
    e[u].emplace_back(v, w), e[v].emplace_back(u, w);
    lim[u] += w, lim[v] += w;
  }
  for (int i = 1; i <= n; i++) {
    if (!vis[i])
      p.push_back(vector<int>()), Dfs(i, p.size() - 1);
  }
  if (o == 2) {
    for (int i = 1; i <= n; i++)
      cin >> a[i];
    for (auto &vec : p) {
      for (auto i : vec)
        a[i] >= lim[i] && (q.push(i), lim[i] = 1e18);
      int cnt = 0;
      for (; !q.empty(); q.pop()) {
        int x = q.front();
        cnt++;
        for (auto [v, w] : e[x]) {
          lim[v] -= w;
          (lim[v] <= a[v]) && (q.push(v), lim[v] = 1e18);
        }
      }
      if (cnt == vec.size())
        return cout << "YES\n", 0;
    }
    cout << "NO\n";
  } else {
    LL mn = 1e18;
    vector<int> ans;
    for (auto &vec : p) {
      LL cur = 1e18;
      for (auto i : vec) {
        for (auto [_, w] : e[i])
          cur += w;
      }
      if (cur / 2 < mn)
        mn = cur / 2, ans.swap(vec);
    }
    for (int i : ans) {
      a[i] = lim[i];
      for (auto [v, w] : e[i])
        lim[v] -= w;
    }
    for (int i = 1; i <= n; i++)
      cout << a[i] << ' ';
  }
  return 0;
}
posted @ 2025-10-12 17:58  Lightwhite  阅读(53)  评论(0)    收藏  举报