Solution Set #4

P15582. [KTSC 2026] 平衡序列(倍增,找性质)

容易发现好区间的长度一定形如 \(2^k-1\),那么可设 \(f_{i,j}\) 表示 \([j-2^i+1,j+2^i-1]\) 是否合法,单次询问可做到 \(O(n\log n)\)

修改看起来很难做,但是注意到对于每个区间长度 \(2^k-1\),包含某个固定点 \(x\) 的区间至多有两个,因为如果有大于等于三个,则必须会有一个区间的中心在另一个区间里面,由于每个区间的中心都必须是区间最大值,这就出现矛盾了。所以单次修改 \(f_{i,j}\) 的变化量只有 \(O(\log n)\),维护一下每层变化的位置即可。时间复杂度是 \(O((n+q)\log n)\)

对于这类动态修改维护答案的题,如果不知道怎么修改,可以想想答案的变化量会不会不大。

代码
#include <bits/stdc++.h>

#ifdef ORZXKR
#include "grader.cpp"
#endif

const int kMaxN = 1e5 + 5;

int n, ans;
int a[kMaxN];
bool f[18][kMaxN];

bool check(int k, int x) {
  if (x <= (1 << (k - 1)) || x > n - (1 << (k - 1))) return 0;
  int i = x - (1 << (k - 1)), j = x + (1 << (k - 1));
  return a[x] > a[i] && a[x] > a[j] && f[k - 1][i] && f[k - 1][j];
}

long long initialize(int N, std::vector<int> A) {
  n = N;
  for (int i = 1; i <= n; ++i) a[i] = A[i - 1];
  for (int i = 1; i <= n; ++i) f[0][i] = 1, ++ans;
  for (int i = 1; i <= std::__lg(n); ++i) {
    for (int j = 1; j <= n; ++j) {
      f[i][j] = check(i, j);
      ans += f[i][j];
    }
  }
  return ans;
}

long long update_sequence(int p, int v) {
  a[++p] = v;
  std::vector<int> vec = {p};
  for (int k = 1; k <= std::__lg(n); ++k) {
    std::vector<int> vv = {p};
    for (auto x : vec) {
      if (x > (1 << (k - 1))) vv.emplace_back(x - (1 << (k - 1)));
      if (x <= n - (1 << (k - 1))) vv.emplace_back(x + (1 << (k - 1)));
    }
    std::sort(vv.begin(), vv.end());
    vv.erase(std::unique(vv.begin(), vv.end()), vv.end());
    vec.clear();
    for (auto x : vv) {
      if (check(k, x) != f[k][x]) {
        f[k][x] ^= 1;
        if (f[k][x]) ++ans;
        else --ans;
        vec.emplace_back(x);
      } else if (x == p) {
        vec.emplace_back(x);
      }
    }
  }
  return ans;
}

P3571. [POI 2014] SUP-Supercomputer(贪心,猜结论,凸包)

首先把遍历的过程看成一个剥叶子的过程,即每次可以选择至多 \(k\) 个叶子删掉。这样的话有个很好的性质是每次可删的点的数量一定不会增加,而从根往下做就没有这个性质。

不妨设树的高度为 \(d\),容易得到两个答案的下界是 \(d\)\(\left\lceil\frac{n}{k}\right\rceil\),这两个显然都不一定是答案,考虑找一个结合这两种方式的下界。

\(s_i\) 表示树上深度大于等于 \(i\) 的点的数量,那么答案一定不小于 \(\max\limits_{i=0}^{d}{\left(i+\left\lceil\frac{s_{i+1}}{k}\right\rceil\right)}\),这是因为将深度大于 \(i\) 的点删完至少要 \(\max\limits_{i=0}^{d}{\left(i+\left\lceil\frac{s_{i+1}}{k}\right\rceil\right)}\),第一次将所有深度大于 \(i\) 的点删完后至少会有一个深度恰好为 \(i\) 的点,那么之后就需要至少 \(i\) 次操作,总次数也就是 \(i+\left\lceil\frac{s_{i+1}}{k}\right\rceil\)

经过手玩会发现上面的东西就是下界!考虑怎么证明这件事情。注意到当叶子数不超过 \(k\) 时答案一定就是树的高度,所以可以猜测操作的方式是选择深度最大的 \(k\) 个叶子删掉,如果有相同的就随便选。假设删掉的叶子中最浅的点深度为 \(h\),然后讨论 \(\max\limits_{i=0}^{d}{\left(i+\left\lceil\frac{s_{i+1}}{k}\right\rceil\right)}\) 的变化情况:

  1. \(h<d\)
    1. \(i<h\)
      操作完 \(s_{i+1}\) 至少减小 \(k\),所以最大值至少减小 \(1\)
    2. \(i\geq h\)
      此时深度在 \([h+1,d]\) 内的叶子数量一定小于 \(k\),由于每层点的数量一定不超过下一层点的数量加这层叶子的数量,所以 \([h+1,d]\) 每层的总点数一定小于 \(k\)
      因此对于 \(h+1\leq i\leq d\),一定有 \(i+\left\lceil\frac{s_{i+1}}{k}\right\rceil\leq i+1+\left\lceil\frac{s_{i+2}}{k}\right\rceil\),所以 \(\max\limits_{i=h+1}^{d}{\left(i+\left\lceil\frac{s_{i+1}}{k}\right\rceil\right)}=d\)。又因为 \(h<d\),所以第 \(d\) 层的点一定全部被删掉了,所以这部分的最大值也一定会减一。
  2. \(h=d\)
    1. \(d\) 层点数等于 \(k\)
      显然每个 \(i+\left\lceil\frac{s_{i+1}}{k}\right\rceil\) 都会减少恰好 \(1\)
    2. \(d\) 层点数大于 \(k\)
      \(d-1+\left\lceil\frac{s_{d}}{k}\right\rceil>d\),此时的最大值一定在 \([0,d-1]\) 内取到,而这次操作后 \([0,d-1]\) 内的最大值都减小至少 \(1\),所以全局的最大值也会减少至少 \(1\)

得到这个结论后要求 \(\displaystyle\max_{i=0}^{d}{\left(i+\left\lceil\frac{s_{i+1}}{k}\right\rceil\right)}=\left\lceil\frac{\max_{i=0}^{d}{(ki+s_{i+1})}}{k}\right\rceil\),容易用凸包维护。

时间复杂度是 \(O(n)\)

代码
#include <bits/stdc++.h>

// #define int int64_t

using i64 = int64_t;
using pii = std::pair<int, int>;

const int kMaxN = 1e6 + 5;

int n, q, d;
int p[kMaxN], qr[kMaxN], dep[kMaxN], s[kMaxN], res[kMaxN];

inline void chkmax(int &x, int y) { x = (x > y ? x : y); }
inline void chkmin(int &x, int y) { x = (x < y ? x : y); }

pii sub(pii a, pii b) { return {a.first - b.first, a.second - b.second}; }
i64 mul(pii a, pii b) { return (i64)a.first * b.second - (i64)a.second * b.first; }

i64 calc(pii a, int b) { return (i64)a.first * b + a.second; }

void dickdreamer() {
  std::cin >> n >> q;
  for (int i = 1; i <= q; ++i) std::cin >> qr[i], qr[i] = std::min(qr[i], n);
  dep[1] = 1;
  for (int i = 2; i <= n; ++i) {
    std::cin >> p[i];
    dep[i] = dep[p[i]] + 1;
    chkmax(d, dep[i]);
  }
  for (int i = 1; i <= n; ++i) ++s[dep[i]];
  for (int i = n; i; --i) s[i] += s[i + 1];
  std::vector<pii> stk;
  for (int i = 0; i <= d; ++i) {
    pii p = {i, s[i + 1]};
    for (; stk.size() >= 2 && mul(sub(p, stk.back()), sub(stk.back(), stk[stk.size() - 2])) <= 0; stk.pop_back()) {}
    stk.emplace_back(p);
  }
  for (int i = 1, j = 0; i <= n; ++i) {
    for (; j + 1 < stk.size() && calc(stk[j + 1], i) > calc(stk[j], i); ++j) {}
    res[i] = (calc(stk[j], i) + i - 1) / i;
  }
  for (int i = 1; i <= q; ++i) std::cout << res[qr[i]] << " \n"[i == q];
}

int32_t main() {
#ifdef ORZXKR
  freopen("in.txt", "r", stdin);
  freopen("out.txt", "w", stdout);
#endif
  std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
  int T = 1;
  // std::cin >> T;
  while (T--) dickdreamer();
  // std::cerr << 1.0 * clock() / CLOCKS_PER_SEC << "s\n";
  return 0;
}

CF2056F1. Xor of Median (Easy Version)(计数,组合数学,卢卡斯定理)

容易发现这题等价于对于每个数 \(x\),求 \(x\) 作为中位数的方案数模 \(2\) 的结果。考虑把所有非零的 \(cnt\) 拿出来,设非零的 \(cnt\)\(t\) 个,那么一组 \(cnt\) 有贡献当且仅当 \(\binom{n}{cnt_1,cnt_2,\ldots,cnt_t}\),根据卢卡斯定理,这就等价于 \(cnt_i\) 两两不交。

注意到如果满足了 \(cnt_i\) 两两不交,就说明中位数一定是最大值,因为最大值的出现次数占了 \(n\) 二进制的最高位,所以它的出现次数一定大于 \(\frac{n}{2}\)。有了这个条件后 dp 出每个 \(t\) 对应 \(cnt\) 数组的方案数模 \(2\) 的结果,再枚举最大值计算答案即可。

时间复杂度是 \(O(k^2+km)\),应该是能够优化到 \(O(\frac{k^2}{w}+k\log m)\) 的。

对于这类对 \(2\) 取模的计数问题,最好第一步就去看哪些方案对答案有贡献,因为可能有贡献的方案会有优美的性质。

代码
#include <bits/stdc++.h>

// #define int int64_t

const int kMaxN = 205;

int k, n, m;
bool f[kMaxN];
std::string str;

void dickdreamer() {
  std::cin >> k >> m >> str;
  n = std::count(str.begin(), str.end(), '1');
  std::fill_n(f, n + 1, 0);
  f[0] = 1;
  for (int i = 1; i <= n; ++i) {
    for (int j = i; j; --j) {
      f[j] = f[j - 1] ^ (f[j] & j);
    }
    f[0] = 0;
  }
  int ans = 0;
  for (int i = 1; i <= n; ++i) {
    if (!f[i]) continue;
    for (int j = 0; j < m; ++j)
      if ((j & (i - 1)) == i - 1)
        ans ^= j;
  }
  std::cout << ans << '\n';
}

int32_t main() {
#ifdef ORZXKR
  freopen("in.txt", "r", stdin);
  freopen("out.txt", "w", stdout);
#endif
  std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
  int T = 1;
  std::cin >> T;
  while (T--) dickdreamer();
  // std::cerr << 1.0 * clock() / CLOCKS_PER_SEC << "s\n";
  return 0;
}

P9151. 计数题(计数dp,adhoc,自动机)

首先一次操作可以看成消掉 \(01/10\) 或者把相邻相同的三个消掉两个。

显然最终状态 \(T\) 一定是 \(S\) 的某个子序列,且这个子序列隔开的每个区间都能被消掉。考虑构建转移的自动机来进行 dp。

\(f_i\) 表示 \(T\) 目前的末尾在 \(S\) 中匹配到的位置是 \(S_i\),假设 \(T\) 的下一位是 \(v\),我们钦定 \(i\) 转移到的位置就是满足 \(S_j=v\)\([i+1,j-1]\) 能消掉的最小的 \(j\)。下面证明这个东西的正确性。

如果实际转移到的位置是 \(k\),那么有 \(j<k\)\(S_j=S_k\)。对于 \([j+1,k-1]\) 区间内的元素,如果有相邻且不同的位置就全部消掉,消到最后区间内剩下的元素一定都相同。由于区间长度是奇数,所以可以再将其消成恰好一个元素。又因为 \(S_j=S_k\),所以剩下的元素无论是什么都能将其和 \(S_k\) 一起消掉。那么对于所有下一位匹配的是 \(k\) 的方案,则将这位调整成 \(j\) 一定合法。

然后考虑对于 \(i\) 和下一位的值 \(v\),怎么找到最近的转移点。不妨假设 \(S_i=0\),分讨 \(S_i\) 和下一位的匹配值 \(S_j=v\) 的关系:

  1. \(S_j=1\)
    对于 \([i+1,j-1]\) 内的元素,将相邻且不同的先删掉,剩下只有一种元素后再将局面消成只剩恰好两个相同元素的形式。由于 \(S_j\neq S_i\),所以剩下的无论是什么都能被消掉。找到最近的满足 \(S_j=1\)\(j\) 即可。

  2. \(S_j=0\)
    我们断言下一步的转移点一定是满足 \(S_j=S_{j-1}=0\) 的最小的 \(j\)

    证明就考虑如果 \(S_j=S_{j-1}=S_i=0\),则将 \([i+1,j-2]\) 消完后无论剩下什么数都能被消掉。

    如果实际的最近转移点 \(k<j\),则 \([i+1,k-1]\) 之内一定没有长度大于等于 \(3\)\(0\) 连续段,且 \(S_{i+1}=S_{k-1}=1\)。先把 \([i+1,k-1]\) 内的 \(1\) 连续段尽量消掉,则此时 \([i+1,k-1]\) 的连续段一定形如 \(0/1/00/11\),且每个满足 \(S_l=S_{l-1}=0\)\(l\) 的奇偶性一定与 \(i\) 相同。下面归纳证明消 \([i+1,k-1]\) 的过程中一定不会有 \(S_l=S_{l-1}=0\)\(l\) 的奇偶性与 \(i\) 不同:

    首先区间内一定不会有 \(00100\) 的子串。

    1. 如果一次操作后合并了两个 \(0\) 段,则其一定形如 \(0\underline{01}01\) 或者 \(10\underline{10}0\),两种都不会产生满足上述条件的 \(l\)
    2. 如果一次操作后合并了两个 \(1\) 段,则其形如 \(1011\),也不会产生新的 \(l\)

    所以我们证明了这个条件。满足了这个条件后容易发现每次操作减少的 \(1\) 段数一定不超过减少的 \(0\) 段数,又因为初始 \(1\) 段比 \(0\) 段多一个,所以最终剩下来的一定是 \(11\),一定消不掉。所以这样的 \(k\) 一定不合法。

将自动机建完之后还要特判边界的情况。对于第一个匹配位置 \(j\),其一定是 \(1\) 和满足 \(S_j\neq S_1\)\(S_j=S_{j-1}\) 最小位置。一个位置 \(j\) 可以作为最后一个匹配位置的条件是 \(S_j=S_n\) 或者 \([j+1,n]\) 里有满足 \(S_l=S_{l-1}=S_j\)\((l-j-1)\bmod 2=0\)。否则可以证明 \([j+1,n]\) 一定消不掉。

时间复杂度是 \(O(n)\)

代码
#include <bits/stdc++.h>

// #define int int64_t

const int kMaxN = 5e6 + 5, kMod = 998244353;

int n;
int f[kMaxN], nxt[kMaxN][2];
std::string str;

int qpow(int bs, int64_t idx = kMod - 2) {
  int ret = 1;
  for (; idx; idx >>= 1, bs = (int64_t)bs * bs % kMod)
    if (idx & 1)
      ret = (int64_t)ret * bs % kMod;
  return ret;
}

inline int add(int x, int y) { return (x + y >= kMod ? x + y - kMod : x + y); }
inline int sub(int x, int y) { return (x >= y ? x - y : x - y + kMod); }
inline void inc(int &x, int y) { (x += y) >= kMod ? x -= kMod : x; }
inline void dec(int &x, int y) { (x -= y) < 0 ? x += kMod : x; }

void dickdreamer() {
  std::cin >> str;
  n = str.size(), str = " " + str;
  for (int i = 1; i <= n; ++i) f[i] = nxt[i][0] = nxt[i][1] = 0;
  int now[2][2] = {0}, lst[2][2] = {0};
  for (int i = n; i; --i) {
    int v = str[i] - '0';
    nxt[i][v ^ 1] = lst[~i & 1][v ^ 1];
    nxt[i][v] = now[~i & 1][v];
    if (str[i] == str[i - 1]) now[i & 1][v] = i;
    lst[i & 1][v] = i;
  }
  f[1] = 1;
  for (int i = 3; i <= n; i += 2) {
    if (str[i] != str[1] && str[i] == str[i - 1]) {
      f[i] = 1;
      break;
    }
  }
  int ans = 0;
  for (int i = 1; i <= n; ++i) {
    for (int v = 0; v < 2; ++v)
      if (nxt[i][v])
        inc(f[nxt[i][v]], f[i]);
    if ((n - i) % 2 == 0 && (str[i] == str[n] || nxt[i][str[i] - '0']))
      inc(ans, f[i]);
  }
  std::cout << ans << '\n';
}

int32_t main() {
#ifdef ORZXKR
  freopen("in.txt", "r", stdin);
  freopen("out.txt", "w", stdout);
#endif
  std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
  int T = 1;
  std::cin >> T;
  while (T--) dickdreamer();
  // std::cerr << 1.0 * clock() / CLOCKS_PER_SEC << "s\n";
  return 0;
}

agc070c. No Streak(计数,组合数学,反射容斥)

注意到 A 与 B 不能相邻这个条件与前缀 A 数量不少于 B 的数量这两个条件不好同时刻画,考虑设一个只有 A 与 B 不能相邻的状态,再通过反射容斥把 A 数量不少于 B 数量的条件加上。

\(f(a,b,x)\) 表示 A 有 \(a\) 个,B 有 \(b\) 个,平局有 \(x\) 个且 A 与 B 不能相邻的方案数。将 A 的权值看成 \(1\),B 的权值看成 \(-1\),那么原问题等价于从 \((0,0)\) 开始,每次可以向右上/右下走,或者停止,不能连续走两步方向相同,最终走到 \((a+b,a-b)\) 的方案数。

先反射容斥成走到 \((a+b,-a+b-2)\) 的方案,答案乍一看可以看成 \(f(a,b,x)-f(a+1,b-1,x)\),但是这么做是不对的。因为被容斥掉的每个方案第一次碰到 \(x=-1\) 这条线后走的第一步方向会取反,导致第一次碰到 \(x=-1\) 前后这两步会出现问题。由于前一步一定是向右下的,所以考虑原方案分讨下一步是怎么走的:

  1. 不停歇,向右上走:
    对折后下一步变为向右下走,导致这一步变得和前一步相同了。由于对折后走的第二步一定要么停止,要么向右上,否则就和走的第一步方向一致。于是将对折后走的第一步删掉后方案仍然合法,对所有走到 \((a+b-1,-a+b-1)\) 的方案在第一次碰到 \(x=-1\) 的位置后加一步右下即可,方案数为 \(f(a,b-1,x)\)
  2. 停歇:
    先钦定这一步不停歇,最后再在这里加入一个停歇,并分讨停歇后怎么走:
    1. 向右下走:
      对折后所有满足这个的合法方案一定不会出现矛盾,方案数即为 \(f(a+1,b-1,x-1)\)
    2. 向右上走:
      和不停歇向右上走的情况是一样的,将下一步走的变化量先删掉,最后再加回来即可,方案数为 \(f(a,b-1,x-1)\)

综上所述,最终的答案为 \(f(a,b,x)-f(a,b-1,x)-f(a+1,b-1,x-1)-f(a,b-1,x-1)\)

\(f(a,b,x)\) 时,可以先把 \(x\) 次停歇加进方案,再加入 \(a\) 个 A,枚举这些 A 被停歇划分成了多少个连续段即可,式子为:

\[f(a,b,x)=\sum_{i=0}^{a}{\binom{x+1}{i}\binom{a-1}{i-1}\binom{x+i-1}{b-a+i}} \]

总的时间复杂度是 \(O(n)\)

代码
#include <bits/stdc++.h>

// #define int int64_t

const int kMaxN = 4e7 + 5, kMod = 1e9 + 7;

int fac[kMaxN], ifac[kMaxN];

int qpow(int bs, int64_t idx = kMod - 2) {
  int ret = 1;
  for (; idx; idx >>= 1, bs = (int64_t)bs * bs % kMod)
    if (idx & 1)
      ret = (int64_t)ret * bs % kMod;
  return ret;
}

inline int add(int x, int y) { return (x + y >= kMod ? x + y - kMod : x + y); }
inline int sub(int x, int y) { return (x >= y ? x - y : x - y + kMod); }
inline void inc(int &x, int y) { (x += y) >= kMod ? x -= kMod : x; }
inline void dec(int &x, int y) { (x -= y) < 0 ? x += kMod : x; }

void prework(int n = 4e7) {
  fac[0] = 1;
  for (int i = 1; i <= n; ++i) fac[i] = 1ll * fac[i - 1] * i % kMod;
  ifac[n] = qpow(fac[n]);
  for (int i = n; i; --i) ifac[i - 1] = 1ll * ifac[i] * i % kMod;
}

inline int C(int m, int n) {
  if (m == -1 && n == -1) return 1;
  if (m < n || m < 0 || n < 0) return 0;
  return 1ll * fac[m] * ifac[n] % kMod * ifac[m - n] % kMod;
}

int f(int a, int b, int x) {
  if (a < 0 || b < 0 || x < 0) return 0;
  int ret = 0;
  for (int i = 0; i <= std::min(a, x + 1); ++i)
    ret = (ret + 1ll * C(x + 1, i) * C(a - 1, i - 1) % kMod * C(x + i + 1, b - a + i)) % kMod;
  // std::cerr << a << ' ' << b << ' ' << x << ' ' << ret << '\n';
  return ret;
}

void dickdreamer() {
  int n, a, b, x;
  std::cin >> n >> a >> b;
  x = n - a - b;
  std::cout << sub(f(a, b, x), add(f(a, b - 1, x), add(f(a + 1, b - 1, x - 1), f(a, b - 1, x - 1)))) << '\n';
}

int32_t main() {
#ifdef ORZXKR
  freopen("in.txt", "r", stdin);
  freopen("out.txt", "w", stdout);
#endif
  std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
  int T = 1;
  // std::cin >> T;
  prework();
  while (T--) dickdreamer();
  // std::cerr << 1.0 * clock() / CLOCKS_PER_SEC << "s\n";
  return 0;
}

P9293. [ROI 2018] Addition without carry(贪心,数据结构,均摊分析)

下文所有的 \(a_i\) 都是从大到小排好序的,不妨设 \(b_1\geq b_2\geq b_3\geq\ldots \geq b_n\)\(|x|\) 表示 \(x\) 的最高位。

首先一组 \(b_i\) 合法的条件为任意两个 \(b_i,b_j\) 的交为空集。可以得到一个总和 \(s\) 的判定方式:设 \(h=|s|\),并分类讨论:

  1. \(|a_1|<h\):将 \(a_1\) 删掉,再让 \(s\leftarrow s-2^h\)
  2. \(|a_1|=h\):让 \(a_1\leftarrow a_1-2^h,s\leftarrow s-2^h\)
  3. \(|a_1|>h\):一定无解。

考虑从高位到低位依次确定答案的每一位,如果将二进制数的加减法看成 \(O(1)\) 的,则暴力判断可以做到 \(O(n\sum |a_i|)\)

考虑优化找答案的最高位 \(L\) 的过程,注意到对于每个 \(i\) 都有 \(|b_i|>|b_{i+1}|\),所以 \(|a_i|\leq|b_i|\leq L-i+1\Longrightarrow L\geq |a_i|+i-1\),那么可以得到一个答案的下界是 \(h=\max\limits_{i=1}^{n}{\left\{|a_i|+i-1\right\}}\)。由于当 \(L=h+1\) 时,取 \(b_i=2^{L-i+1}\) 一定大于 \(|a_i|\),所以最优的 \(L\) 只有 \(h\)\(h+1\) 两种可能。

下面设计一个函数 \(\text{check}(a,p)\) 表示对于数组 \(a\),判断 \(L\) 是否不超过 \(p\),如果不超过 \(p\) 就构造出方案。取出 \(h=|a_i|+i-1\)\(i\) 中最小的一个,设其为 \(k\)。先把 \(p<h\) 的情况判掉,这种情况一定不合法。如果 \(L=h\),则需要给 \(\forall 1\leq i\leq k\)\(b_i\) 分配 \(2^{h-i+1}\),由于 \(k\) 是取到上界的最小值,所以操作完可以看成把 \(a_1\sim a_{k-1}\) 删掉,让 \(a_k\leftarrow a_k-2^{h-k+1}\)。操作完之后递归调用 \(\text{check}(a,h-k)\) 即可得到 \(h\) 是否能取到 \(l\)

如果 \(\text{check}(a,h-k)=1\),说明 \(L\) 可以取到 \(h\) 且已经构造好方案了,直接返回。如果 \(\text{check}(a,h-k)=0\),说明 \(L\) 只能取到 \(h+1\)\(p=h\) 时则返回不合法,需要给 \(\forall 1\leq i\leq k\)\(b_i\) 分配 \(2^{h-i+2}\),等价于把 \(a_1\sim a_k\) 删掉。否则操作完再调用 \(\text{check}(a,h-k+1)\) 即可,显然这个递归调用一定合法。这么分配的原因是如果不这么做的话,\(|b_k|\) 一定还是 \(h-k+1\),不优于调用 \(\text{check}(a,h-k)\) 时的情况,显然仍然不合法。

上面的做法乍一看是个指数复杂度的搜索,可是如果将单点修改并排好序看成 \(O(1)\) 的,它的实际时间复杂度却是 \(O(\sum|a_i|)\) 的!

证明则对 \(\text{check}(a,p)\) 的返回值分类讨论:

  1. \(\text{check}(a,p)=0\)

    那么递归过程中一直有 \(p\leq h\),否则一定存在合法的方案。复杂度可分析为 \(T(\text{check}(a,p))=T(\text{check}(a,p-k))+\Theta(k)=O(p)\leq O(|a_k|+k)\)

  2. \(\text{check}(a,p)=1\)

    最劣情况下一定是每次都先调用 \(\text{check}(a,h-k)\) 得到不合法的结果后再调用 \(\text{check}(a,h-k+1)\)。则有

    \[\begin{aligned}T(\text{check}(a,p))&=T(\text{check}(a,h-k))+T(\text{check}(a,h-k+1))\\&=O(h-k)+T(\text{check}(a,h-k+1))\\&=O(|a_k|)+T(\text{check}(a,h-k+1))\end{aligned} \]

    由于每次调用 \(\text{check}(a,h-k+1)\) 前都会把 \(a_1\sim a_k\) 删掉,所以总复杂度就是 \(O(\sum|a_k|)\)

实现 \(\text{check}(a,p)\) 的过程时可以先把所有 \(a_i\) 的后缀从小到大排序完,得到所有后缀的大小关系后用一个线段树维护排序后的编号即可。

\(S=\sum|a_i|\),总的时间复杂度就是 \(O((n+S)\log S)\)

代码
#include <bits/stdc++.h>

// #define int int64_t

using pii = std::pair<int, int>;

const int kMaxN = 6e5 + 5;

int n, cnt;
int len[kMaxN], bel[kMaxN], pre[kMaxN], cur[kMaxN];
std::vector<int> pos[kMaxN], id[kMaxN];
std::string str[kMaxN];
bool res[kMaxN];

struct SGT {
  pii t[kMaxN * 4];
  int cnt[kMaxN * 4];
  void pushup(int x) {
    t[x] = std::max(pii{t[x << 1].first + cnt[x << 1 | 1], t[x << 1].second}, t[x << 1 | 1]);
    cnt[x] = cnt[x << 1] + cnt[x << 1 | 1];
  }
  void build(int x, int l, int r) {
    if (l == r) {
      t[x] = {-1e9, l}, cnt[x] = 0;
      return;
    }
    int mid = (l + r) >> 1;
    build(x << 1, l, mid), build(x << 1 | 1, mid + 1, r);
    pushup(x);
  }
  void update(int x, int l, int r, int ql, int v) {
    if (l == r) {
      cnt[x] += v;
      assert(cnt[x] >= 0);
      if (cnt[x] >= 1) t[x] = {len[l], l};
      else t[x] = {-1e9, l};
      return;
    }
    int mid = (l + r) >> 1;
    if (ql <= mid) update(x << 1, l, mid, ql, v);
    else update(x << 1 | 1, mid + 1, r, ql, v);
    pushup(x);
  }
  void get(int x, int l, int r, int ql, int qr, std::vector<int> &vec) {
    // std::cerr << "heige " << l << ' ' << r << ' ' << ql << ' ' << qr << '\n';
    if (l > qr || r < ql || !cnt[x]) return;
    else if (l == r) return void(vec.emplace_back(l));
    int mid = (l + r) >> 1;
    get(x << 1, l, mid, ql, qr, vec), get(x << 1 | 1, mid + 1, r, ql, qr, vec);
  }
} sgt;

void getid() {
  static std::vector<int> vec[kMaxN];
  int mx = 0;
  for (int i = 1; i <= n; ++i) {
    for (auto j : pos[i]) vec[j].emplace_back(i);
    mx = std::max(mx, pos[i].back());
  }
  for (int b = 1; b <= mx; ++b) {
    std::sort(vec[b].begin(), vec[b].end(), [&] (int i, int j) {
      return (id[i].size() ? id[i].back() : 0) < (id[j].size() ? id[j].back() : 0);
    });
    for (auto i : vec[b]) {
      len[++cnt] = b, bel[cnt] = i, pre[cnt] = (id[i].size() ? id[i].back() : 0);
      id[i].emplace_back(cnt);
    }
  }
}

void upd(int i, int v) {
  if (cur[i] >= 0) sgt.update(1, 1, cnt, id[i][cur[i]], -1);
  cur[i] += v;
  if (cur[i] >= 0) sgt.update(1, 1, cnt, id[i][cur[i]], 1);
}

bool solve(int lim) {
  if (!sgt.cnt[1]) return 1;
  auto [h, p] = sgt.t[1];
  assert(p);
  if (lim < h) return 0;
  std::vector<int> vec;
  sgt.get(1, 1, cnt, p, cnt, vec);
  assert(vec[0] == p);
  // std::cerr << "??? " << len[p] << '\n';
  // std::cerr << "??? " << lim << ' ' << h << ' ' << p << ' ' << bel[p] << ' ' << cnt << ' ' << vec.size() << '\n';
  int rk = h - len[p] + 1;
  assert(rk == vec.size());
  {
    for (int i = 1; i < vec.size(); ++i) sgt.update(1, 1, cnt, vec[i], -1);
    upd(bel[p], -1);
    if (solve(h - rk)) {
      for (int i = h; i > h - rk; --i) res[i] = 1;
      return 1;
    }
    for (int i = 1; i < vec.size(); ++i) sgt.update(1, 1, cnt, vec[i], 1);
    upd(bel[p], 1);
    if (lim == h) return 0;
  }
  for (auto x : vec) sgt.update(1, 1, cnt, x, -1);
  for (int i = h + 1; i > h - rk + 1; --i) res[i] = 1;
  return solve(h - rk + 1);
}

void dickdreamer() {
  std::cin >> n;
  int lim = n;
  for (int i = 1; i <= n; ++i) {
    std::cin >> str[i];
    std::reverse(str[i].begin(), str[i].end());
    for (int j = 0; j < str[i].size(); ++j) {
      if (str[i][j] == '1') {
        pos[i].emplace_back(j + 1);
      }
    }
    lim += str[i].size();
  }
  // std::cerr << "???\n";
  getid();
  // std::cerr << "???1\n";
  for (int i = 1; i <= n; ++i) {
    assert(id[i].size());
    cur[i] = pos[i].size() - 1, sgt.update(1, 1, cnt, id[i].back(), 1);
  }
  // std::cerr << "!!! " << lim << '\n';
  assert(solve(lim));
  bool fl = 0;
  for (int i = lim; i; --i) {
    if (res[i]) fl = 1;
    if (fl) std::cout << res[i];
  }
  if (!fl) std::cout << '0';
  std::cout << '\n';
}

int32_t main() {
#ifdef ORZXKR
  freopen("in.txt", "r", stdin);
  freopen("out.txt", "w", stdout);
#endif
  std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
  int T = 1;
  // std::cin >> T;
  while (T--) dickdreamer();
  // std::cerr << 1.0 * clock() / CLOCKS_PER_SEC << "s\n";
  return 0;
}

P10063. [SNOI2024] 平方数(随机化,二次剩余,哈希)

首先容易想到给每个数先分解质因数再做,但是由于这题的值域是 \(10^{36}\),所以我们无论如何都分解不出质因数。

考虑利用二次剩余的性质来做这道题。二次剩余有一个性质是对于奇质数 \(P\),模 \(P\) 意义下的二次剩余一定恰好有 \(\frac{P-1}{2}\) 个,且如果 \(x\) 是二次剩余,则有 \(x^{\frac{P-1}{2}}\equiv 1\pmod P\);否则 \(x^{\frac{P-1}{2}}\equiv -1\pmod P\)。所以模 \(P\) 意义下二次剩余乘二次剩余是二次剩余,二次剩余乘非二次剩余是非二次剩余,非二次剩余乘非二次剩余是二次剩余。那么判断一个区间 \([l,r]\) 的乘积是模 \(P\) 意义下的二次剩余则判断 \([1,r]\)\([1,l-1]\) 的二次剩余性是否相同即可。

注意到对于一个随机大质数 \(P\),一个不是 \(P\) 的质数的非完全平方数 \(x\) 是模 \(P\) 意义下的二次剩余的概率约等于 \(\frac{1}{2}\),所以我们随机 \(T=60\) 个大质数,维护原数组前缀积是否是模 \(P\) 的二次剩余,将每个前缀的 \(60\) 个状态压进一个数里,那么 \([l,r]\) 是完全平方数的条件就是 \(l-1\)\(r\) 的状态相同。如果 \(a_i\)\(P\) 的倍数,则只需要将 \(a_i\)\(P\) 的最高指数的奇偶性求出来,把这个也压进状态即可。

直接实现是 \(O(nT\log P)\),也可以让 \(P\)\(10^7\) 左右随机,预处理模 \(P\) 的二次剩余做到 \(O((n+P)T)\)

代码
#include <bits/stdc++.h>

// #define int int64_t

namespace FASTIO {
char ibuf[1 << 21], *p1 = ibuf, *p2 = ibuf;
inline char getc() {
  return p1 == p2 && (p2 = (p1 = ibuf) + fread(ibuf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++;
}
template<class T> bool read(T &x) {
  x = 0; int f = 0; char ch = getc();
  while (ch < '0' || ch > '9') f |= ch == '-', ch = getc();
  while (ch >= '0' && ch <= '9') x = (x * 10) + (ch ^ 48), ch = getc();
  x = (f ? -x : x); return 1;
}
template<typename A, typename ...B> bool read(A &x, B &...y) { return read(x) && read(y...); }
 
char obuf[1 << 21], *o1 = obuf, *o2 = obuf + (1 << 21) - 1;
void flush() { fwrite(obuf, 1, o1 - obuf, stdout), o1 = obuf; }
inline void putc(char x) { *o1++ = x; if (o1 == o2) flush(); }
template<class T> void write(T x) {
  if (!x) putc('0');
  if (x < 0) x = -x, putc('-');
  char c[40]; int tot = 0;
  while (x) c[++tot] = x % 10, x /= 10;
  for (int i = tot; i; --i) putc(c[i] + '0');
}
void write(char x) { putc(x); }
void write(char *x) { while (*x) putc(*x++); }
void write(const char *x) { while (*x) putc(*x++); }
template<typename A, typename ...B> void write(A x, B ...y) { write(x), write(y...); }
struct Flusher {
  ~Flusher() { flush(); }
} flusher;
} // namespace FASTIO
using FASTIO::read; using FASTIO::putc; using FASTIO::write;

using i128 = __int128_t;

const int kMaxN = 3e5 + 5, kMaxP = 1.5e7 + 5;

int n, cnt;
int prime[kMaxP / 10];
i128 a[kMaxN], f[kMaxN];
std::bitset<kMaxP> vis;
std::vector<std::pair<int, int>> vec;
std::mt19937 rnd(114514);
std::vector<int> pri;

void prework(int n = 1.5e7) {
  for (int i = 2; i <= n; ++i) {
    if (!vis[i]) {
      prime[++cnt] = i;
      if (i >= 1e7 && pri.size() < 60) pri.emplace_back(i);
    }
    for (int j = 1; j <= cnt && i * prime[j] <= n; ++j) {
      vis[i * prime[j]] = 1;
      if (i % prime[j] == 0) break;
    }
  }
}

void dickdreamer() {
  read(n);
  for (int i = 1; i <= n; ++i) read(a[i]);
  prework();
  for (int c = 1; c <= 60; ++c) {
    static std::bitset<kMaxP> vis;
    // int p = prime[rnd() % cnt + 1];
    int p = pri[c - 1];
    vis.reset();
    for (int i = 0; i < p; ++i) vis[1ll * i * i % p] = 1;
    bool sum = 0;
    for (int i = 1; i <= n; ++i) {
      i128 t = 0, x = a[i];
      for (; x % p == 0; x /= p, ++t) {}
      sum ^= !vis[x % p];
      f[i] ^= (t << (2 * c - 2)) ^ ((i128)sum << (2 * c - 1));
    }
  }
  static int id[kMaxN], l[kMaxN], r[kMaxN];
  std::iota(id, id + 1 + n, 0);
  std::sort(id, id + 1 + n, [&] (int i, int j) { return f[i] != f[j] ? f[i] < f[j] : i < j; });
  i128 tot = 0;
  for (int i = n, lst = n + 1; ~i; --i) {
    tot += lst - i - 1;
    l[id[i] + 1] = i + 1, r[id[i] + 1] = lst - 1;
    if (i && f[id[i]] != f[id[i - 1]]) lst = i;
  }
  for (int i = 1; i <= n; ++i) {
    for (int j = l[i]; j <= r[i]; ++j) {
      if (vec.size() >= 1e5) break;
      vec.emplace_back(i, id[j]);
    }
  }
  write(tot, '\n');
  for (auto [l, r] : vec) write(l, ' ', r, '\n');
}

int32_t main() {
#ifdef ORZXKR
  freopen("in.txt", "r", stdin);
  freopen("out.txt", "w", stdout);
#endif
  int T = 1;
  // std::cin >> T;
  while (T--) dickdreamer();
  // std::cerr << 1.0 * clock() / CLOCKS_PER_SEC << "s\n";
  return 0;
}

P14471. [集训队互测 2025] 火花(最优化dp,树上背包,欧拉序)

不妨定义选择 \(t\) 个不同的根链定义成 A 操作,选择至多 \(k\) 个单个物品的操作定义成 B 操作。

那么如果 \(t=0\),能够将复杂度做到 \(O(nk)\) 的做法只有把原树的 dfs 序拿出来,按照 dfs 序从左往右 dp,设 \(f_{i,j}\) 表示考虑了 dfs 序的前 \(i\) 个点,选了 \(j\) 个物品的最优答案。然后分讨 dfs 序第 \(i\) 个点 \(u\) 的选择情况:

  1. \(u\) 不选进 B:\(f_{i-1,j}\to f_{i+sz_u-1,j}\)
  2. \(u\) 选进 B:\(f_{i-1,j}+x\cdot v_u\to f_{i,j+x}(1\leq x\leq \min(c_u,k-j))\)

得到这个做法后考虑怎么把 A 操作的选择也加进状态。但是会发现一个问题是每个点能够选到 B 中的物品数量与其子树内选 A 的点的数量,而我们如果还按照刚才的做法对着 dfs 序从上往下进行 dp 就很难加上这个限制。

考虑换个方式看这个限制。假设对于一个点 \(u\),钦定它必须选进 B 且子树内有 \(x\) 个点选 A,并设 \(j\) 表示在决策 \(u\) 子树之前选 A 的总点数,也就是说决策 \(u\) 的子树让选 A 的总点数从 \(j\) 变到了 \(j+x\)。我们钦定在刚进入 \(u\) 子树时往 B 的背包内加入一个必选的 \(u\),以及 \(j\) 个可选可不选的 \(u\),出 \(u\) 的子树时再加入 \(c_u-1-(j+x)\) 个可选可不选的 \(u\),则会发现这种加入方式能够钦定 \(u\) 选的数量在 \([1,c_u-x]\),还只依赖于每个时刻选进 A 的总点数。注意到上面出入子树就是欧拉序做的事情,所以将 dfs 序换成欧拉序就能 dp 了!

\(s_u\) 表示 \(u\) 到根链的权值;\(e_i\) 表示欧拉序中第 \(i\) 个元素;\(L_i\) 表示 \(i\) 在欧拉序中第一次出现的位置;\(R_i\) 表示 \(i\) 第二次出现位置;\(f_{i,j,l}\) 表示考虑了欧拉序前 \(i\) 个元素,目前选了 \(j\) 个 A 中的元素,\(l\) 个 B 中的元素的最优值。不妨设 \(u=e_i\),然后分讨 \(i\) 是入子树还是出子树:

  1. \(i=L_u\)

    • \(u\) 不选进 B:则 \(u\) 子树内不会有选进 B 的点,再设一个状态 \(g_{u,x}\) 表示 \(u\) 的子树内最大的 \(x\) 个根链权值和,这个容易 \(O(nt)\) 求,那么转移为:\(f_{i-1,j,l}+g_{u,x}\to f_{i,j+x,l}(0\leq x\leq \min(t-j,k-l))\)
    • \(u\) 选进 B:只加入 B 选择中 \(u\) 的一个必选以及 \(j\) 个可选可不选的部分,不考虑 \(u\) 是否选进 A,转移为:\(f_{i-1,j,l}+x\cdot v_u\to f_{i,j,l+x}(1\leq x\leq\min(j+1,k-l))\)
  2. \(i=R_u\)

    这时加入 B 选择中 \(c_u-1-(j+x)\) 个可选可不选的部分,以及考虑 \(u\) 是否选进 A。

    • \(u\) 不选进 A:转移为 \(f_{i-1,j,l}+x\cdot v_u\to f_{i,j,l+x}(0\leq x\leq\min(c_u-1-j,k-l))\)
    • \(u\) 选进 A:转移为 \(f_{i-1,j,l}+s_u+x\cdot v_u\to f_{i,j+1,l+x}(0\leq x\leq\min(c_u-2-j,k-l))\)

直接实现应该是 \(O(nk^2t)\) 的。考虑优化,第二部分和第一部分的第二个转移都是滑动窗口状物,都可以单调队列优化。对于第一部分的第一个转移,注意到 \(g_{u,*}\) 是凸的,所以这部分转移具有决策单调性,用决策单调性分治优化即可。

最终的时间复杂度是 \(O(nkt\log t)\)

对于这类具有难以加入的限制的背包问题,可以考虑把需要加入的元素拆成很多个部分,分步进行转移。

代码
#include <bits/stdc++.h>

#define int int64_t

const int kMaxN = 1e4 + 5;

int n, p, q, ecnt;
int fa[kMaxN], c[kMaxN], v[kMaxN], s[kMaxN], e[kMaxN * 2], l[kMaxN], r[kMaxN];
std::vector<int> G[kMaxN];
std::vector<std::vector<int>> g, tmp;
std::vector<std::vector<std::vector<int>>> f;

inline void chkmax(int &x, int y) { x = (x > y ? x : y); }
inline void chkmin(int &x, int y) { x = (x < y ? x : y); }

void dfs(int u) {
  e[l[u] = ++ecnt] = u;
  for (auto v : G[u]) dfs(v);
  e[r[u] = ++ecnt] = u;
}

void prework() {
  static int id[kMaxN];
  g.resize(n + 1);
  for (int i = 1; i <= n; ++i) id[i] = i, g[i].emplace_back(0);
  std::sort(id + 1, id + 1 + n, [&] (int i, int j) { return s[i] > s[j]; });
  for (int i = 1; i <= n; ++i) {
    int u = id[i];
    for (int j = u; j; j = fa[j]) {
      if (g[j].size() >= p + 1) break;
      g[j].emplace_back(g[j].back() + s[u]);
    }
  }
}

void dickdreamer() {
  std::cin >> n >> q >> p;
  for (int i = 1; i <= n; ++i) std::cin >> c[i] >> v[i];
  s[1] = v[1];
  for (int i = 2; i <= n; ++i) {
    std::cin >> fa[i];
    G[fa[i]].emplace_back(i);
    s[i] = s[fa[i]] + v[i];
  }
  dfs(1), prework();
  f.resize(ecnt + 1);
  for (int i = 0; i <= ecnt; ++i) {
    f[i].resize(p + 1);
    for (int j = 0; j <= p; ++j) {
      f[i][j].resize(q + 1, -4e18);
    }
  }
  tmp.resize(p + 1);
  for (int i = 0; i <= p; ++i) tmp[i].resize(q + 1, -4e18);
  f[0][0][0] = 0;
  for (int i = 1; i <= ecnt; ++i) {
    int u = e[i];
    // std::cerr << "!!! " << i << ' ' << u << ' ' << l[u] << ' ' << r[u] << '\n';
    if (i == l[u]) {
      for (int j = 0; j <= p; ++j) {
        // int lim = c[u] - p + j;
        int lim = j + 1;
        static int qq[kMaxN];
        int h = 1, t = 0;
        for (int k = 0; k <= q; ++k) {
          // for (int x = 1; x <= std::min(k, lim); ++x)
          //   chkmax(f[i][j][k], f[i - 1][j][k - x] + x * v[u]);
          for (; h <= t && qq[h] < k - lim; ++h) {}
          if (h <= t) chkmax(f[i][j][k], f[i - 1][j][qq[h]] + (k - qq[h]) * v[u]);
          for (; h <= t && f[i - 1][j][qq[t]] - qq[t] * v[u] <= f[i - 1][j][k] - k * v[u]; --t) {}
          qq[++t] = k;
        }
      }
      for (int k = 0; k <= q; ++k) {
        // for (int j = 0; j <= p; ++j) {
        //   // u 不选 B
        //   for (int x = 0; x <= std::min<int>(g[u].size() - 1, j); ++x)
        //     chkmax(f[r[u]][j][k], f[i - 1][j - x][k] + g[u][x]);
        // }
        std::function<void(int, int, int, int)> solve = [&] (int l, int r, int pl, int pr) {
          l = std::max(l, pl), r = std::min<int>(r, pr + g[u].size() - 1);
          pl = std::max<int>(pl, l - (int)g[u].size() + 1), pr = std::min(pr, r);
          if (l > r) return;
          if (pl == pr) {
            for (int j = l; j <= r; ++j) {
              assert(j >= pl && j - pl <= g[u].size() - 1);
              chkmax(f[::r[u]][j][k], f[i - 1][pl][k] + g[u][j - pl]);
            }
            return;
          }
          int mid = (l + r) >> 1, pos = -1, val = -4e18;
          // assert(std::max<int>(pl, mid - (int)g[u].size() + 1) <= std::min<int>(mid, pr));
          for (int j = pl; j <= pr; ++j) {
            if (mid - j < 0 || mid - j > g[u].size() - 1) continue;
            int vv = f[i - 1][j][k] + g[u][mid - j];
            if (pos == -1 || vv > val) chkmax(f[::r[u]][mid][k], vv), pos = j, val = vv;
          }
          assert(pos != -1);
          solve(l, mid - 1, pl, pos), solve(mid + 1, r, pos, pr);
        };
        solve(0, p, 0, p);
      }
    } else {
      // for (int j = 0; j <= p; ++j) {
      //   for (int k = 0; k <= q; ++k) {
      //     if (f[i - 1][j][k] < 0) continue;
      //     // u 不选 A
      //     for (int x = 0; x <= std::min(q - k, p - j); ++x)
      //       chkmax(f[i][j][k + x], f[i - 1][j][k] + x * v[u]);
      //     // u 选 A
      //     for (int x = 0; x <= std::min(q - k, p - j - 1); ++x)
      //       chkmax(f[i][j + 1][k + x], f[i - 1][j][k] + s[u] + x * v[u]);
      //   }
      // }
      for (int j = 0; j <= p; ++j)
        for (int k = 0; k <= q; ++k)
          tmp[j][k] = -4e18;
      for (int j = 0; j <= p; ++j) {
        for (int k = 0; k <= q; ++k) {
          if (f[i - 1][j][k] < 0) continue;
          // u 选 A
          chkmax(tmp[j][k], f[i - 1][j][k]);
          if (j < p) chkmax(tmp[j + 1][k], f[i - 1][j][k] + s[u]);
        }
      }
      for (int j = 0; j <= p; ++j) {
        // int lim = p - j;
        int lim = c[u] - 1 - j;
        static int qq[kMaxN];
        int h = 1, t = 0;
        for (int k = 0; k <= q; ++k) {
          // for (int x = 0; x <= std::min(k, lim); ++x)
          //   chkmax(f[i][j][k], tmp[j][k - x] + x * v[u]);
          for (; h <= t && qq[h] < k - lim; ++h) {}
          for (; h <= t && tmp[j][qq[t]] - qq[t] * v[u] <= tmp[j][k] - k * v[u]; --t) {}
          qq[++t] = k;
          if (h <= t) chkmax(f[i][j][k], tmp[j][qq[h]] + (k - qq[h]) * v[u]);
        }
      }
    }
  }
  int ans = 0;
  for (int i = 0; i <= p; ++i)
    for (int j = 0; j <= q; ++j)
      chkmax(ans, f[ecnt][i][j]);
  std::cout << ans << '\n';
}

int32_t main() {
#ifdef ORZXKR
  freopen("in.txt", "r", stdin);
  freopen("out.txt", "w", stdout);
#endif
  std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
  int T = 1;
  // std::cin >> T;
  while (T--) dickdreamer();
  // std::cerr << 1.0 * clock() / CLOCKS_PER_SEC << "s\n";
  return 0;
}

P5577. [CmdOI2019] 算力训练(FWT,分解张量矩阵,单位根)

考虑 FWT,不妨先假设 \(k=3\)

首先 FWT 的过程本质上可以看成将需要求的 \(k\)\(k\times k\) 的矩阵分解成 \(l\)\(k\times k\) 的张量矩阵线性组合的形式,要求每个张量矩阵形如 \(a_{i,j}=x_iy_j\),即这些张量矩阵秩为 \(1\)。最终的复杂度就是 \(O(\max(l,k)^m\text{poly}(l,k))\)

回到这个题,我们相当于是要将 \(M_0=\begin{bmatrix}1&0&0\\0&0&1\\0&1&0\end{bmatrix},M_1=\begin{bmatrix}0&1&0\\1&0&0\\0&0&1\end{bmatrix},M_2=\begin{bmatrix}0&0&1\\0&1&0\\1&0&0\end{bmatrix}\) 这三个矩阵分解成秩一矩阵,看起来非常难做。注意到单位根天生契合不进位加法的过程,所以这里用单位根来进行矩阵分解。

具体地,分解出来的三个张量矩阵分别为 \(a_0=\begin{bmatrix}1&1&1\\1&1&1\\1&1&1\end{bmatrix},a_1=\begin{bmatrix}1&\omega&\omega^2\\\omega&\omega^2&1\\\omega^2&1&\omega\end{bmatrix},a_2=\begin{bmatrix}1&\omega^2&\omega\\\omega^2&\omega&1\\\omega&1&\omega^2\end{bmatrix}\),其中 \(a_{i,x,y}=\omega^{i(x+y)}\),再推一下式子即可得到 \(M_0=\frac{a_0+a_1+a_2}{3},M_1=\frac{a_0+\omega^2a_1+\omega a_2}{3},M_2=\frac{a_0+\omega a_1+\omega^2a_2}{3}\)。至于怎么推出这个式子,可能是发现了 \(1+\omega+\omega^2=0\) 可以抵消掉很多东西,并且 \(a_{i,x,y}=\omega^{i(x+y)}\) 这东西非常地符合对称性,再试一下发现对了?

由于上面的做法只依赖于 \(1+\omega+\omega^2=0\) 这一个性质,所以很容易推到一般情况。分解出 \(k\) 个张量矩阵,满足 \(a_{i,x,y}=\omega_k^{i(x+y)}\)。最终的 \(M_i=\frac{\sum_{j=0}^{k-1}{\omega_{k}^{-ij}+a_j}}{k}\)

回到这个题,我们需要求 \(\text{FWT}\left[\prod{(1+x^{a_i})}\right]\) 的结果。根据上面的式子,\(\text{FWT}(f)_i=\sum\limits_{j=0}^{k^m-1}f_j\cdot\prod\limits_{l=0}^{m-1}{\omega^{i_lj_l}}\),其中 \(i_l\)\(j_l\) 分别为 \(i\)\(j\)\(k\) 进制下第 \(l\) 位的值。也就是说 \(\text{FWT}(1+x^{v})_i=1+\prod\limits_{l=0}^{m-1}{\omega^{i_lv_l}}\)。由于只关心 \(\sum i_lv_l\)\(k\) 的结果,所以维护一个桶表示每种 \(a_i\) 出现的数量,从低位到高位变换的过程中维护 \(b_{s,r}\) 表示目前状态是 \(s\),当前 \(\sum i_lv_l\bmod k=r\) 的初始 \(a_i\) 的数量。最终 FWT 数组则为 \(f_i=\prod\limits_{j=0}^{k-1}{(1+\omega^j)^{b_{i,j}}}\),这部分需要快速幂处理。

求出 FWT 数组后再通过已知的系数 IFWT 回去即可得到最终的答案。总的时间复杂度为 \(O(k^{m+2}m+k^{m+3}\log n)\)

代码
#include <bits/stdc++.h>

// #define int int64_t

const int kMaxN = 1e6 + 5, kMaxM = 8, kMaxS = 8e4 + 5, kMod = 998244353;

int n, k, m;
int pw[kMaxM], cnt[kMaxS];

int qpow(int bs, int64_t idx = kMod - 2) {
  int ret = 1;
  for (; idx; idx >>= 1, bs = (int64_t)bs * bs % kMod)
    if (idx & 1)
      ret = (int64_t)ret * bs % kMod;
  return ret;
}

inline int add(int x, int y) { return (x + y >= kMod ? x + y - kMod : x + y); }
inline int sub(int x, int y) { return (x >= y ? x - y : x - y + kMod); }
inline void inc(int &x, int y) { (x += y) >= kMod ? x -= kMod : x; }
inline void dec(int &x, int y) { (x -= y) < 0 ? x += kMod : x; }

struct Complex {
  int a[6] = {0};
  void init() { memset(a, 0, sizeof(a)); }
  friend Complex operator +(Complex a, Complex b) {
    static Complex ret;
    ret.init();
    for (int i = 0; i < k; ++i) ret.a[i] = add(a.a[i], b.a[i]);
    return ret;
  }
  friend Complex operator -(Complex a, Complex b) {
    static Complex ret;
    ret.init();
    for (int i = 0; i < k; ++i) ret.a[i] = sub(a.a[i], b.a[i]);
    return ret;
  }
  friend Complex operator *(Complex a, Complex b) {
    static Complex ret;
    ret.init();
    for (int i = 0; i < k; ++i)
      for (int j = 0; j < k; ++j)
        ret.a[(i + j) % k] = (ret.a[(i + j) % k] + 1ll * a.a[i] * b.a[j]) % kMod;
    return ret;
  }
  friend Complex operator *(Complex a, int b) {
    static Complex ret;
    ret.init();
    for (int i = 0; i < k; ++i) ret.a[i] = 1ll * ret.a[i] * b % kMod;
    return ret;
  }
  void fix() {
    int v = *std::min_element(a, a + k);
    for (int i = 0; i < k; ++i) a[i] -= v;
  }
} omg[6], f[kMaxS], pwo[6][20];

Complex omega(int x, int v = 1) {
  static Complex ret;
  ret.init();
  ret.a[(x % k + k) % k] = v;
  return ret;
}

Complex qpow(int o, int idx) {
  Complex ret = omega(0);
  for (int i = 0; i <= std::__lg(n); ++i)
    if (idx >> i & 1)
      ret = ret * pwo[o][i];
  return ret;
}

void fwt(int *cnt, Complex *f) {
  for (int i = 0; i < pw[m]; ++i) f[i] = omega(0, cnt[i]);
  for (int i = 0; i < m; ++i) {
    for (int s = 0; s < pw[m]; ++s) {
      if (s / pw[i] % k != 0) continue;
      static Complex tmp[6];
      for (int r = 0; r < k; ++r) tmp[r] = f[s + r * pw[i]], f[s + r * pw[i]].init();
      for (int x = 0; x < k; ++x) {
        for (int y = 0; y < k; ++y) {
          for (int j = 0, nxt = x * y % k; j < k; ++j, nxt = (nxt == k - 1 ? 0 : nxt + 1))
            f[s + x * pw[i]].a[nxt] += tmp[y].a[j];
        }
      }
    }
  }
  for (int s = 0; s < pw[m]; ++s) {
    static Complex tmp;
    tmp = f[s], f[s] = omega(0);
    // for (int i = 0; i < k; ++i) f[s] = f[s] * pwo[i][tmp.a[i]];
    for (int i = 0; i < k; ++i) f[s] = f[s] * qpow(i, tmp.a[i]);
    // for (int i = 0; i < k; ++i) std::cerr << f[s].a[i] << " \n"[i == k - 1];
    // for (int i = 0; i < k; ++i) std::cerr << tmp.a[i] << " \n"[i == k - 1];
  }
  // std::cerr << "------------\n";
}

void ifwt(Complex *f) {
  int invk = qpow(k);
  for (int i = 0; i < m; ++i) {
    for (int s = 0; s < pw[m]; ++s) {
      if (s / pw[i] % k != 0) continue;
      static Complex tmp[6];
      for (int r = 0; r < k; ++r) tmp[r] = f[s + r * pw[i]], f[s + r * pw[i]].init();
      // for (int x = 0; x < k; ++x)
      //   for (int y = 0; y < k; ++y)
      //     f[s + x * pw[i]] = f[s + x * pw[i]] + tmp[y] * omega(-x * y, invk);
      for (int x = 0; x < k; ++x) {
        for (int y = 0; y < k; ++y) {
          for (int j = 0, nxt = (k - x * y % k) % k; j < k; ++j, nxt = (nxt == k - 1 ? 0 : nxt + 1))
            inc(f[s + x * pw[i]].a[nxt], tmp[y].a[j]);
        }
        for (int j = 0; j < k; ++j) f[s + x * pw[i]].a[j] = 1ll * f[s + x * pw[i]].a[j] * invk % kMod;
      }
    }
  }
}

void dickdreamer() {
  std::cin >> n >> k >> m;
  pw[0] = 1;
  for (int i = 1; i <= m; ++i) pw[i] = pw[i - 1] * k;
  for (int i = 0; i < k; ++i) {
    Complex v = omega(0) + omega(i);
    pwo[i][0] = v;
    for (int j = 1; j <= std::__lg(n); ++j) pwo[i][j] = pwo[i][j - 1] * pwo[i][j - 1];
  }
  for (int i = 0; i < k; ++i) omg[i] = omega(i);
  for (int i = 1; i <= n; ++i) {
    int x = 0;
    std::string str;
    std::cin >> str;
    for (auto c : str) x = k * x + c - '0';
    ++cnt[x];
  }
  fwt(cnt, f), ifwt(f);
  for (int s = 0; s < pw[m]; ++s) {
    // for (int i = 2; i < k; ++i) assert(f[s].a[i] == f[s].a[i - 1]);
    // for (int i = 0; i < k; ++i) std::cerr << f[s].a[i] << ' ';
    // std::cerr << '\n';
    if (k == 5) {
      std::cout << sub(f[s].a[0], f[s].a[1]) << '\n';
    } else {
      for (int i = 0; i < k; i += 2) {
        dec(f[s].a[i], f[s].a[(i + 3) % k]);
        f[s].a[(i + 3) % k] = 0;
      }
      std::cout << sub(f[s].a[0], f[s].a[2]) << '\n';
    }
    // for (int i = 0; i < k; ++i) std::cerr << f[s].a[i] << ' ';
    // std::cerr << '\n';
  }
}

int32_t main() {
#ifdef ORZXKR
  freopen("in.txt", "r", stdin);
  freopen("out.txt", "w", stdout);
#endif
  std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
  int T = 1;
  // std::cin >> T;
  while (T--) dickdreamer();
  // std::cerr << 1.0 * clock() / CLOCKS_PER_SEC << "s\n";
  return 0;
}

qoj3098. Ancient Machine(通信,贪心,压缩状态)

首先考虑给你这个串,怎么简洁地求出答案和最优方案。注意到如果存在一个连续段长度大于等于 \(2\),操作这个段上的字符一定不会对答案有贡献,所以一个连续段至多对答案贡献 \(1\)

然后一个直观的想法是把串中第一个 X 和最后一个 Z 的位置取出来,设它们分别为 \(l\)\(r\)。如果 \(l>r\) 则答案一定是 \(0\),否则答案一定有个下界是 \([l,r]\)Y 连续段的数量。构造就考虑找到 \([l,r]\)X 最靠右的位置 \(p\),那么 \([p+1,r-1]\) 内只有 YZ 两种字符,则 \([p+1,r-1]\) 内每个 Y 连续段后面接着的都是 Z 连续段,所以直接从小到大把 \([p+1,r-1]\) 删掉即可让区间内每个 Y 连续段有贡献。删完后把 \(p\) 再删掉,并重复上面的过程即可,直到第一个 X 被删掉。

观察上述操作过程,可以发现我们只关心串里面第一个 X 和最后一个 Z 的出现位置,以及它们之间 X 的所有出现位置,用一个长度为 \(n\) 的串把这些关键位置传过去即可拿到 \(70\) 分。

考虑继续优化。上面的做法看似已经到了极限,因为不同的关键位置串有 \(2^n\) 种,其至少要用 \(n=10^5\) 位才能传过去,所以需要继续优化关键位置串这一状态。注意到按照上述操作过程,\([l,r]\) 内的所有 X 连续段 \([a,b]\) 只有当局面删到最后一个 X\(a\) 时才会对答案有贡献,所以我们直接只把 \(a\) 看成关键位置,每次把 \([a+1,r-1]\) 依次删掉仍然是对的。把最后一个 Z 的位置往右平移一位之后,会发现最终的关键位置串满足不存在相邻两个都是 \(1\)

\(f_n\) 表示长度为 \(n\) 的关键位置串的数量,那么 \(f_n=f_{n-1}+f_{n-2}\) 就是斐波那契数列,略小于 \(2^n\),但已经足够了。由于直接将关键串的排名传过去需要高精度,不好写。这里退而求其次,每 \(B\) 个位置分一个块,要满足 \(\left\lceil\frac{10^5+1}{B}\right\rceil\left\lceil\log_2f_B\right\rceil\leq 7\times 10^4\),取 \(B=63\) 即可。

总结:对于这类看起来很复杂的交互/通信类型的构造题,一定要先想出给定全部信息后最简洁的构造再去想怎么放到交互/通信上做。

代码
Anna
#include "Anna.h"
#include <bits/stdc++.h>

namespace {
const int kB = 63, kL = 44;

int64_t f[kB + 5];

void prework() {
  f[0] = 1, f[1] = 2;
  for (int i = 2; i <= kB; ++i) f[i] = f[i - 1] + f[i - 2];
}

void work(std::vector<int> a) {
  prework();
  int n = a.size();
  for (; a.size() % kB != 0; a.emplace_back(0)) {}
  for (int l = 0, r = kB - 1; l < n; l = r + 1, r = l + kB - 1) {
    int64_t rk = 0;
    for (int i = r; i >= l; --i) {
      if (a[i] == 1) rk += f[r - i];
    }
    assert(rk < (1ll << kL));
    for (int i = 0; i < kL; ++i) Send(rk >> i & 1);
  }
}
} // namespace

void Anna(int n, std::vector<char> s) {
  int firx = -1, lstz = -1;
  for (int i = 0; i < n; ++i) {
    if (s[i] == 'X' && firx == -1) firx = i;
    if (s[i] == 'Z') lstz = i;
  }
  std::vector<int> a(n + 1);
  for (int i = 0; i < n + 1; ++i) {
    if (i >= firx && i < lstz && s[i] == 'X' && (!i || s[i - 1] != 'X') || i == lstz + 1) a[i] = 1;
    else a[i] = 0;
  }
  work(a);
}
Bruno
#include "Bruno.h"
#include <bits/stdc++.h>

namespace {
const int kB = 63, kL = 44;

int64_t f[kB + 5];

void prework() {
  f[0] = 1, f[1] = 2;
  for (int i = 2; i <= kB; ++i) f[i] = f[i - 1] + f[i - 2];
}

std::vector<int> work(std::vector<int> a) {
  prework();
  std::vector<int> ret;
  assert(a.size() % kL == 0);
  for (int t = 0, l = 0, r = kB - 1; t < a.size() / kL; ++t, l = r + 1, r = l + kB - 1) {
    int64_t rk = 0;
    for (int i = t * kL; i < (t + 1) * kL; ++i) rk += ((int64_t)a[i] << (i - t * kL));
    for (int i = l, lst = 0; i <= r; ++i) {
      if (lst == 0 && rk >= f[r - i]) ret.emplace_back(1), rk -= f[r - i];
      else ret.emplace_back(0);
      lst = ret.back();
    }
    assert(!rk);
  }
  return ret;
}
} // namespace

void Bruno(int n, int m, std::vector<int> a) {
  std::vector<int> pos;
  a = work(a);
  for (int i = 0; i < n + 1; ++i)
    if (a[i])
      pos.emplace_back(i);
  if (pos.size() < 2) {
    for (int i = 0; i < n; ++i) Remove(i);
    return;
  }
  --pos.back();
  for (int i = (int)pos.size() - 2; ~i; --i) {
    for (int j = pos[i] + 1; j <= pos[i + 1] - 1; ++j) Remove(j);
    Remove(pos[i]);
  }
  for (int i = 0; i < pos[0]; ++i) Remove(i);
  for (int i = pos.back(); i < n; ++i) Remove(i);
}
posted @ 2026-03-04 14:57  下蛋爷  阅读(41)  评论(0)    收藏  举报