CSU-ACM2025 春季训练赛-第二场 题解

写在前面

DIV1:https://vjudge.net/contest/702171
DIV2:https://vjudge.net/contest/702164

Password:STEAM春季特卖即日起至3月20日上午10时(PT)

大幅降低了难度(可能

涉及到的套路:

  • div2A:二进制拆位;
  • div2D,div1B:考虑下界并证明一定可以取到;
  • div2E:经典树上小结论;
  • div2F,div1C:喜欢我字符串哈希吗;
  • div2G:树形DP套路;
  • div2H,div1E:树状数组的本质,高维前缀和;
  • div1F:考虑上界并证明一定可以取到;
  • div1G:排列轮换模型,转换为图论模型。

div2A

签到,拆位儿 800

我们需要签到!

考虑对 \(a, b, c\) 拆位并依次确定答案的每一位。

容易发现当且仅当 \(a, b, c\) 该位上全为 1 或全为 0 时,答案该位上可以取 0,否则最优情况下只能取 1。

总时间复杂度 \(O(T\log v)\) 级别。

//签到
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
//=============================================================
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  int T; std::cin >> T;
  while (T --) {
    int a, b, c, ans = 0; std::cin >> a >> b >> c;
    for (int i = 0; i <= 30; ++ i) {
      if ((a >> i & 1) && (b >> i & 1) && (c >> i & 1)) continue;
      if (!(a >> i & 1) && !(b >> i & 1) && !(c >> i & 1)) continue;
      ans += (1 << i);
    }
    std::cout << ans << "\n";
  }
  return 0;
}
/*
4
1 1 4
5 1 4
5 2 5
1 7 0
*/

div2B

洛谷 P3865

div2C,div1A

洛谷 P5656

对于 \(ax + by = c\),显然应有 \(\gcd(a, b) \mid c\),若不满足该条件则无解,否则由裴蜀定理,\(ax + by = \gcd(a, b)\) 有无数组整数解,使用 exgcd 即可求得该不定方程的一组整数特解 \((x', y')\),则有:

\[\begin{aligned} ax' + by' = \gcd(a, b)\\ a\dfrac{c\times x'}{\gcd(a, b)} + b\times \dfrac{c\times y'}{\gcd(a, b)} = c \end{aligned}\]

\(\left(\dfrac{c\times x'}{\gcd(a, b)},\dfrac{c\times y'}{\gcd(a, b)}\right)\) 即为原不定方程的一组整数特解。我们记这组特解为 \((x_0, y_0)\),同时记 \(d = \gcd(a, b)\),接下来考虑在这一组特解的基础上,求得题目所需的解。

考虑将 \(x_0\) 变为 \(x_0 + p\),设 \(p\) 为正整数,则存在正整数 \(q\),令 \(x_0\) 变为 \(x_0+p\) 的同时会将 \(y_0\) 缩小到 \(y_0-q\),则有 \(a(x_0 + p) + b(y_0 - q) = c\)。联立 \(ax_0+by_0=c\) 可得 \(p = \frac{b\times q}{a}\)。则有 \(a\mid b\times q\),又有 \(b\mid b\times q\),则 \(b\times q\) 的最小值应为 \(\operatorname{lcm} (a,b)\)。则可得 \((x_0, y_0)\) 变化的最小变化满足:

\[\begin{cases} p_0 = \dfrac{\operatorname{lcm}(a,b)}{a} = \dfrac{b}{d}\\ q_0 = \dfrac{\operatorname{lcm}(a,b)}{a} = \dfrac{a}{d} \end{cases}\]

由于 \(\operatorname{lcm} (a,b) | b\times q\),易证 \((x_0, y_0)\) 的变化一定是 \((p_0, q_0)\) 的整数倍。


然后开始构造答案:

首先将 \(x_0\) 变为最小的正整数值,即找到 \(k\in \N\),使得:\(x_0 + k\times p_0 \ge 1\),则有:\(k\ge \frac{1-x_0}{p}\)\(k\) 应取 \(\left\lceil\frac{1-x_0}{p}\right\rceil\),则令 \(x_1 = x_0 + k\times p\)\(y_1 = y_0 - k\times q\)\((x_1, y_1)\) 即为 \(x\) 最小的正整数解。此时检查 \(y_1\) 的符号,如果 \(y_1>0\),则有正整数解,否则由于 \(x\) 已经取了最小的正整数解,则 \(y\) 不可能也为正整数,原方程无正整数解。

然后考虑每一问:

对于有正整数解的情况:

  • 解的个数。除去当前得到的特解 \((x_1, y_1)\),其他解的个数即在当前解基础上,保证 \(y>0\) 的情况下令 \(y-q_0\) 的次数,答案即 \(\left\lfloor\frac{y-1}{q_0}\right\rfloor + 1\)
  • \(x\) 的最小整数值。即 \(x_1\)
  • \(y\) 的最小正整数值。即在 \(y_1\) 基础上减去最多次 \(q_0\)\(y\) 的值,答案即 \((y_1 - 1)\bmod q + 1\)
  • \(x\) 的最大整数值。即上一问中 \(y\) 对应的 \(x\),答案即 \(x_1 + \left\lfloor\frac{y-1}{q_0}\right\rfloor\times p\)
  • \(y\) 的最大整数值。即 \(y_1\)

对于无正整数解的情况:

  • \(x\) 的最小正整数值。即 \(x_1\)
  • \(y\) 的最小正整数值。同构造 \(x_1\) 时使用的方法,取 \(k = \left\lceil\frac{1-y_0}{p}\right\rceil\),答案即 \(y_0 + k\times q_0\)

总复杂度 \(O(T\log A)\) 级别,\(A\) 为值域。


注意烦的一批的类型转换。

ceil() 的返回值是 double 类型,long long * double = double,此时可能会出现精度损失,需要注意作乘法前先把 ceil() 的返回值转化为 long long 类型。

//By:Luckyblock
/*
*/
#include <cmath>
#include <cstdio>
#include <cctype>
#include <algorithm>
#define LL long long
//=============================================================
//=============================================================
inline LL read() {
  LL f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = - 1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3ll) + (w << 1ll) + ch - '0';
  return f * w;
}
LL exgcd(LL a_, LL b_, LL &x_, LL &y_) {
  if (!b_) {
    x_ = 1, y_ = 0;
    return a_;
  }
  LL d_ = exgcd(b_, a_ % b_, y_, x_);
  y_ -= a_ / b_ * x_;
  return d_;
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  int T = read();
  while (T --) {
    LL a = read(), b = read(), c = read(), x, y;
    LL d = exgcd(a, b, x, y);
    if (c % d != 0) {
      printf("-1\n");
      continue;
    }

    x *= c / d, y *= c / d;
    LL p = b / d, q = a / d, k;
    k = ceil((1.0 - x) / p), x += p * k, y -= q * k;

    if (y > 0) {
      printf("%lld ", (y - 1) / q + 1);
      printf("%lld ", x);
      printf("%lld ", (y - 1) % q + 1);
      printf("%lld ", x + (y - 1) / q * p);
      printf("%lld ", y);
    } else {
      printf("%lld ", x);
      printf("%lld ", y + q * (LL) ceil((1.0 - y) / q));
    }
    printf("\n");
  }
  return 0;
}

div2D,div1B

CF1822E 贪心,构造 1600 2.trick

首先当 \(n\) 为奇数,或者有一种字符出现大于 \(\frac{n}{2}\) 次时无解。

\(\operatorname{num}\) 为相等的对称位置的对数,\(p_i\) 为字母 \(i\) 所有相等的对称位置的对数。

手玩下发现最好的修改方案是把两对字母不同的相等的对称位置交换,这样可以一次消掉两组,则最理想情况下仅进行这样的操作即可;另外对于每种字母的所有相等的对称位置都需要进行至少一次操作,则修改次数的下限为 \(\max\left\{ \left\lceil \frac{\operatorname{num}}{2} \right\rceil , \max\{ p_i \}\right\}\)

发现总能通过构造达到这个下限,则答案即为 \(\max\left\{ \left\lceil \frac{\operatorname{num}}{2} \right\rceil , \max\{ p_i \}\right\}\)

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
int n;
char s[kN];
int num, cnt[26], pr[26];
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); 
  return f * w;
}
void Solve() {
  if (n % 2 == 1) {
    printf("-1\n");
    return ;
  }
  
  num = 0;
  for (int i = 0; i < 26; ++ i) cnt[i] = pr[i] = 0;
  
  for (int i = 1; i <= n; ++ i) ++ cnt[s[i] - 'a'];
  for (int i = 0; i < 26; ++ i) {
    if (cnt[i] > n / 2) {
      printf("-1\n");
      return ;
    }
  }

  for (int i = 1; i <= n / 2; ++ i) {
    if (s[i] == s[n - i + 1]) {
      ++ num;
      ++ pr[s[i] - 'a'];
    }
  }
  int ans = (num + 1) / 2;
  for (int i = 0; i < 26; ++ i) ans = std::max(ans, pr[i]);
  printf("%d\n", ans);
}
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  int T = read();
  while (T --) {
    n = read();
    scanf("%s", s + 1);
    Solve();
  }
  return 0;
}

div2E

洛谷 P3398 树,lca 绿题 1.板题、2.trick/套路 。

一个经典树上小结论:对于树上的两条路径,它们相交的充要条件是有一条路径的 \(\operatorname{lca}\) 在另一条路径上。通过画图讨论很容易证明该结论的正确性,以下给出一种形式化的证明:

考虑反证法,若树上路径 \(u_1\leftrightarrow v_1\)\(u_2 \leftrightarrow v_2\) 有交,交集为 \(S\),且不满足上述结论。记交集为 \(S\),则可知有 \(\operatorname{lca}(u_1, v_1), \operatorname{lca}(u_2, v_2) \notin S\)

引理1:若树上路径 \(u_1\leftrightarrow v_1\)\(u_2 \leftrightarrow v_2\) 有交,交集为 \(S\),则 \(S\) 一定也是一条路径。

正确性显然,证明略。

记两条路径交集 \(S\) 中深度最浅的点为 \(p\),则由前提以及上述引理可知:

  • 路径 \(\operatorname{root} \leftrightarrow \operatorname{lca}(u_1, v_1)\)\(\operatorname{root} \leftrightarrow \operatorname{lca}(u_2, v_2)\) 不存在交集;
  • 路径 \(\operatorname{lca}(u_1, v_1) \leftrightarrow p\)\(\operatorname{lca}(u_2, v_2) \leftrightarrow p\) 交集为 \(\{p\}\)

则在根节点 \(\operatorname{root}\) 与点 \(p\) 之间,至少存在两条不同的路径:

  • \(\operatorname{root} \rightarrow \operatorname{lca}(u_1, v_1) \rightarrow p\)
  • \(\operatorname{root} \rightarrow \operatorname{lca}(u_1, v_1) \rightarrow p\)

这与树的定义:任意两个结点之间有且仅有一条简单路径的无向图矛盾,于是反证原结论成立。


由上述结论可知,对于本题,仅需分别求出给定两条路径的 \(\operatorname{lca}\),并判断是否在另一条路径上即可。记树上两点 \(u, v\) 的距离为 \(\operatorname{dis}(u, v)\),由树的定义可知,判断树上某个点 \(p\) 是否在路径 \(u\leftrightarrow v\) 上有显然的结论:

\[\operatorname{dis}(u, p) + \operatorname{dis}(p, v) = \operatorname{dis}(u, v) \]

记树上点 \(u\) 的深度为 \(\operatorname{dep}(u)\),由树的定义可知,求树上两点距离 \(\operatorname{dis}\) 同样有显然的结论:

\[\operatorname{dis}(u, v) = \operatorname{dep}(u) + \operatorname{dep}(v) - 2\times \operatorname{dep}(\operatorname{lca}(u, v)) \]

则对于本题,仅需实现求树上 \(\operatorname{lca}\),并根据上述所有结论判断路径是否有交即可。

以下的 std 使用了树剖实现,总时间复杂度 \(O((n+q)\log n)\) 级别。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int n, q;
int edgenum, head[kN], v[kN << 1], ne[kN << 1];
int fa[kN], sz[kN], dep[kN], son[kN], top[kN];
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); 
  return f * w;
}
void addedge(int u_, int v_) {
  v[++ edgenum] = v_;
  ne[edgenum] = head[u_];
  head[u_] = edgenum;
}
namespace chain {
  void dfs1(int u_, int fa_) {
    sz[u_] = 1;
    fa[u_] = fa_;
    dep[u_] = dep[fa_] + 1;
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      if (v_ == fa_) continue;
      dfs1(v_, u_);
      sz[u_] += sz[v_];
      if (sz[v_] > sz[son[u_]]) son[u_] = v_;
    }
  }
  void dfs2(int u_, int top_) {
    top[u_] = top_;
    if (son[u_]) dfs2(son[u_], top_); 
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      if (v_ == fa[u_] || v_ == son[u_]) continue;
      dfs2(v_, v_);
    }
  }
  int getlca(int u_, int v_) {
    for (; top[u_] != top[v_]; u_ = fa[top[u_]]) {
      if (dep[top[u_]] < dep[top[v_]]) std::swap(u_, v_);
    }
    return dep[u_] < dep[v_] ? u_ : v_;
  }
  int getdis(int u_, int v_) {
    return dep[u_] + dep[v_] - 2 * dep[getlca(u_, v_)];
  }
  bool check(int u_, int v_, int p_) {
    return (getdis(u_, p_) + getdis(p_, v_)) == getdis(u_, v_);
  }
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  n = read(), q = read();
  for (int i = 1; i < n; ++ i) {
    int u_ = read(), v_ = read();
    addedge(u_, v_), addedge(v_, u_);
  }
  chain::dfs1(1, 0), chain::dfs2(1, 1);
  while (q --) {
    int u1 = read(), v1 = read(), u2 = read(), v2 = read();
    int lca1 = chain::getlca(u1, v1), lca2 = chain::getlca(u2, v2);

    std::cout << (chain::check(u1, v1, lca2) || 
                  chain::check(u2, v2, lca1) ?
                  "Y" : "N")
              << "\n";
  }
  return 0;
}

最后再引入一个可能会在未来某天用到的小结论,留给读者自证:距离树上某点最远的点一定是直径的端点

div2F,div1C

二分答案,字符串Hash 1900 2.套路

一个显然的结论是,若某个子串 \(s\) 是两个字符串的公共子串,则 \(s\) 的所有子串同样都是两个字符串的公共子串。

于是考虑二分答案枚举最长公共子串长度 \(mid\),进行 check 时,将一个串所有长度为 \(mid\) 的子串的哈希值扔到哈希表里,再枚举另一个串所有长度为 \(mid\) 的子串,检查在哈希表中是否存在即可。

理论复杂度为 \(O(n\log n)\)

注意写单 hash 或用 set 都可能会被卡,可能需要和 std 一样手写哈希链表。

//知识点:二分答案,Hash
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 3e5 + 10;
const LL kMod1 = 998244353;
const LL kMod2 = 1e9 + 9;
const LL kBase = 1145141;
//=============================================================
int n1, n2, ans, e_num, head[kBase + 10], ne[kMaxn << 1];
char s1[kMaxn], s2[kMaxn];
LL pow1[kMaxn], pow2[kMaxn];
LL has11[kMaxn], has12[kMaxn], has21[kMaxn], has22[kMaxn];
std::pair <LL, LL> val[kMaxn << 1];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void Insert(LL val_, LL v1_, LL v2_) {
  int pos = val_ % kBase + 1;
  for (int i = head[pos]; i; i = ne[i]) {
    if (val[i].first == v1_ && val[i].second == v2_) return ;
  }
  val[++ e_num] = std::make_pair(v1_, v2_);
  ne[e_num] = head[pos];
  head[pos] = e_num;
}
bool Count(LL val_, LL v1_, LL v2_) {
  int pos = val_ % kBase + 1;
  for (int i = head[pos]; i; i = ne[i]) {
    if (val[i].first == v1_ && val[i].second == v2_) return true; 
  }
  return false;
}
void Prepare() {
  scanf("%s", s1 + 1);
  scanf("%s", s2 + 1);
  n1 = strlen(s1 + 1);
  n2 = strlen(s2 + 1);
  pow1[0] = pow2[0] = 1;
  for (int i = 1; i < std::max(n1, n2); ++ i) {
    pow1[i] = pow1[i - 1] * kBase % kMod1;
    pow2[i] = pow2[i - 1] * kBase % kMod2;
  }
  for (int i = 1; i <= n1; ++ i) {
    has11[i] = (has11[i - 1] * kBase + s1[i]) % kMod1;
    has12[i] = (has12[i - 1] * kBase + s1[i]) % kMod2;
  }
  for (int i = 1; i <= n2; ++ i) {
    has21[i] = (has21[i - 1] * kBase + s2[i]) % kMod1;
    has22[i] = (has22[i - 1] * kBase + s2[i]) % kMod2;
  }
}
bool Check(int lth_) {
  e_num = 0;
  memset(head, 0, sizeof (head));
  for (int l = 1, r = lth_; r <= n1; ++ l, ++ r) {
    LL now_has11 = ((has11[r] - has11[l - 1] * pow1[r - l + 1] % kMod1) + kMod1) % kMod1;
    LL now_has12 = ((has12[r] - has12[l - 1] * pow2[r - l + 1] % kMod2) + kMod2) % kMod2;
    Insert(now_has11 * kMod2 + now_has12, now_has11, now_has12);
  }
  for (int l = 1, r = lth_; r <= n2; ++ l, ++ r) {
    LL now_has21 = ((has21[r] - has21[l - 1] * pow1[r - l + 1] % kMod1) + kMod1) % kMod1;
    LL now_has22 = ((has22[r] - has22[l - 1] * pow2[r - l + 1] % kMod2) + kMod2) % kMod2;
    if (Count(now_has21 * kMod2 + now_has22, now_has21, now_has22)) return true;
  }
  return false;
}
//=============================================================
int main() {
  Prepare();
  for (int l = 1, r = std::min(n1, n2); l <= r; ) {
    int mid = (l + r) >> 1;
    if (Check(mid)) {
      ans = mid;
      l = mid + 1;
    } else {
      r = mid - 1;
    }
  }
  printf("%d\n", ans);
  return 0;
}
/*
opawmfawklmiosjcas1145141919810asopdfjawmfwaiofhauifhnawf
opawmdawlmioaszhcsan1145141919810bopdjawmdaw
*/

div1D

CF1528C 树,数据结构 2300 2.trick

发现对第一棵树选点的限制是必须在一条从根到叶的链上,第二棵树选点限制是选的点在构成的虚树上均为叶节点。

考虑每次在第一棵树上多选一个点 \(u\),对第二棵树上选的点的虚树的影响:

  • \(u\) 是已选的点的祖先,显然用它替换它的祖先不会更优,贡献为 0。
  • \(u\) 在已选的点的子树中,显然用它替换它的祖先不会更劣,贡献为 0。
  • 否则可直接将 \(u\) 加入,贡献为 1。

发现我们可以唯一确定选点的策略,于是考虑直接 dfs 枚举所有第一棵树上从根到叶的路径,并在此过程中动态维护路径上的点的选择情况即可。问题转化为如何支持在第二棵树上动态增/删点,查询新增的点是否与已经选择的点有祖先关系。

考虑对第二棵树预处理 dfs 序与每棵子树对应区间,有祖先关系说明新增的区间与之前的区间有交,使用 set 即可维护;当然也可以上线段树维护,仅需支持子树加/子树查询即可。复杂度均为 \(O(\log n)\) 级别。

div2G

CF461B 树形DP 2000 2:套路/trick题

套路树形 DP。

显然可以仅考虑子树内是否有黑色点,以及当前点是否为黑色点自底向上 DP。设 \(f_{u, 0/1}\) 表示以 \(u\) 为根的子树内,钦定根所在的联通块无/有黑色点的选择方案,为了辅助转移再记 \(g_{u, 0/1}\) 表示表示以 \(u\) 为根的子树内,先钦定节点 \(u\) 不选,并钦定根所在的联通块无/有黑色点的选择方案。

转移时考虑枚举子节点的子树,考虑添加后是否有黑点即可,则有:

\[\begin{cases} g_{u, 0}= \leftarrow g_{u, 0} f_{v, 0}\\ g_{u, 1}\leftarrow g_{u, 1}\times f_{u, 0} + g_{u, 0}\times f_{u, 1} \end{cases}\]

然后考虑到可以将 \(u\) 单独分为一个联通块,当 \(u\) 为黑点时,\(f_{u, 0}\) 无意义,有:\(f_{u, 1} = g_{u, 0}\);否则有: \(f_{u, 1} = g_{u, 1}, f_{u, 0} = g_{u, 0} + g_{u, 1}\)

答案即:\(f_{rt, 1}\)

总时间复杂度 \(O(n)\) 级别。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 10;
const LL p = 1e9 + 7;
//=============================================================
int n;
std::vector<int> edge[kN];
int color[kN];
LL ans, f[kN][2];
//=============================================================
void dfs(int u_) {
  f[u_][0] = 1, f[u_][1] = 0;

  for (auto v_: edge[u_]) {
    dfs(v_);
    f[u_][1] = (f[u_][1] * f[v_][0] % p + f[u_][0] * f[v_][1] % p) % p;
    f[u_][0] *= f[v_][0]; 
    f[u_][0] %= p, f[u_][1] %= p;
  }
  if (color[u_]) f[u_][1] = f[u_][0];
  else f[u_][0] += f[u_][1];
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  std::cin >> n;
  for (int i = 2; i <= n; ++ i) {
    int x; std::cin >> x;
    edge[x + 1].push_back(i);
  }
  for (int i = 1; i <= n; ++ i) std::cin >> color[i];
  dfs(1);
  std::cout << f[1][1] << "\n";
  return 0;
}

div2H,div1E

CF1967C 树状数组,计数 2300 4.人类智慧

非常好玩的题,考察对树状数组本质形态的理解。

对于数列 \(a\),定义数列 \(f(a)\) 为对数列 \(a\) 建立的树状数组。即有:

\[f(a)_k = \sum_{i = k - \operatorname{lowbit}(k) + 1}^k a_i \]

给定 \(T\) 组数据,每组数据给定长度为 \(n\) 的数列 \(b\),参数 \(k\)。请构造长度为 \(n\) 的数列 \(a\),使得数列 \(a\) 在模 998244353 意义下满足 \(b = f^k(a)\)。可以证明对于任意数列 \(b\) 和参数 \(k\) 总存在一种合法的 \(a\) 的构造方案。
\(1\le T\le 10^4\)\(1\le n\le 2\cdot 10^5\)\(1\le k\le 10^9\)\(0\le b_i< 998244353\)
3S,256MB。

树状数组本质上是二进制优化的前缀和,由题目给定的式子可以看出来,每个树状数组的位置 \(s_k\) 实际上是以 \(k\) 为右端点的,长度为 \(\operatorname{lowbit}(k)\) 的区间的和。

而本题对给定数组做了 \(k\) 次建立树状数组的操作,实际上即对所有树状数组区间做了 \(k\) 维二进制优化的前缀和。众所周知做高维前缀和时,每个位置对之后的位置的贡献的系数由下表所示,由组合意义易知位于 \((x, y)\) 的系数的值即 \({{x + y}\choose {y}}\)

\(a\) 1 1 1 1
\(f^1\) 1 2 3 4
\(f^2\) 1 3 6 10
\(f^3\) 1 4 10 20
\(f^4\) 1 5 15 35

记需要还原出来的数列为 \(a\),考虑对于每个位置 \(i\) 会对哪些位置 \(p\) 的高维前缀和 \(f^k(p)\) 有贡献。由建树状数组的过程可知,\(p\) 可以通过树状数组插入操作进行枚举,易知此类位置只有 \(O(\log_2 n)\) 数量级。

根据高维前缀和的式子,手玩下系数矩阵,发现若位置 \(p\) 是在树状数组插入操作时第 \(j(j\ge 0)\) 个被枚举到的位置,则 \(a_i\)\(f^k(p)\) 的贡献的系数即为:

\[{{{k+j-1}\choose{j}}} = {\frac{(k+j-1)}{j}{{{k+j-2}\choose{j-1}}}} \]

于是考虑顺序枚举所有位置并进行还原,还原第 \(i\) 个位置的值 \(a_i\) 后,考虑枚举它有贡献的位置并减去 \(a_i\) 的贡献即可。

总时间复杂度 \(O(n\log n)\) 级别,代码非常好写!

哎呦我草太优美啦树状数组!

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define lowbit(x) ((x)&(-x))
const int kN = 2e5 + 10;
const LL p = 998244353;
//=============================================================
int n, k;
LL b[kN], a[kN], ans[kN], inv[kN];
//=============================================================
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  inv[1] = 1;
  for (int i = 2; i <= 2e5; ++ i) {
    inv[i] = 1ll * (p - p / i + p) % p * inv[p % i] % p;
  }

  int T; std::cin >> T;
  while (T --) {
    std::cin >> n >> k;
    for (int i = 1; i <= n; ++ i) std::cin >> b[i], a[i] = b[i];
    for (int i = 1; i <= n; ++ i) {
      ans[i] = a[i];
      LL c = 1, d = 0;
      for (int j = i; j <= n; j += lowbit(j)) {
        a[j] = (a[j] - c * ans[i] % p + p) % p;
        c = c * (k + d) % p * inv[d + 1] % p;
        ++ d;
      }
    }
    for (int i = 1; i <= n; ++ i) std::cout << ans[i] << " ";
    std::cout << "\n";
  }

  return 0;
}
/*
打表看对位置 8 有贡献的位置的的系数矩阵的代码:

int a[15][15][15] = {0};
  for (int i = 1; i <= 8; ++ i) a[0][i][i] = 1;
  
  for (int i = 1; i <= 10; ++ i) {
    for (int j = 1; j <= 8; ++ j) {
      if (j % 2 == 1) {
        a[i][j][j] = 1;
        continue;
      }
      if (j == 2 || j == 6) {
        a[i][j][j] = 1, a[i][j][j - 1] = i;
      }
      if (j == 4) {
        for (int k = j - 3; k <= j; ++ k) {
          for (int l = 1; l <= 8; ++ l) {
            a[i][j][l] += a[i - 1][k][l];
          }
        }
      }
      if (j == 8) {
        for (int k = j - 7; k <= j; ++ k) {
          for (int l = 1; l <= 8; ++ l) {
            a[i][j][l] += a[i - 1][k][l];
          }
        }
      }
    }
    std::cout << i << ": ";
    for (int j = 1; j <= 8; ++ j) printf("%5d", a[i][8][j]);
    std::cout << "\n";
  }
*/

div1F

CF1680D 枚举,贪心 2400 2.trick 5.人类智慧

显然有解当且仅当全局之和为 0。

考虑最大化有解时的答案,发现 \(O(n^2)\) 可过,考虑枚举修改后的哈基汪正负位置的极值出现的位置 \(p_1, p_2\),则答案即区间 \([p_1, p_2]\) 之和。问题等价于考虑如何在最大化极差的同时,平衡对区间 \([p_1, p_2]\) 内外的修改,使得区间内外之和恰好为原数列之和。

一个显然的贪心是考虑令区间 \([p_1, p_2]\) 内可修改位置均修改为 \(+k\)\(-k\) 使得极差取到上界,但受外部可修改的数量限制,有可能无法取到该上界,但另一个显然的贪心是在区间内可修改位置均修改为 \(+k\) 时,记此时区间内之和为 \(s_1\) 为了平衡外部可修改位置应尽可能取负值,记外部可修改位置全部取 \(-k\) 时和为 \(s_2\),则此时极差的上界为 \(\min(|s_1|, |s_2|)\),且显然一定可以取到该上界。另一种修改的情况同理可解决。

复杂度 \(O(n^2)\) 级别。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 3010;
//=============================================================
int n, a[kN];
LL k, cnt[kN], sum[kN];
//=============================================================
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  std::cin >> n >> k;
  for (int i = 1; i <= n; ++ i) {
    std::cin >> a[i]; 
    sum[i] = sum[i - 1] + a[i];
    cnt[i] = cnt[i - 1] + (a[i] == 0);
  }
  if (sum[n] < -1ll * k * cnt[n] || 1ll * k * cnt[n] < sum[n]) {
    std::cout << -1 << "\n";
    return 0;
  }

  LL ans = 0;
  for (int i = 1; i <= n; ++ i) {
    for (int j = i; j <= n; ++ j) {
      LL s = sum[j] - sum[i - 1], c = cnt[j] - cnt[i - 1];
      LL s1 = s + k * c, s2 = s - k * c;

      ans = std::max(ans, std::min(abs(s1), abs(sum[n] - s - k * (cnt[n] - c))));
      ans = std::max(ans, std::min(abs(s2), abs(sum[n] - s + k * (cnt[n] - c))));
    }
  }
  std::cout << ans + 1 << "\n";
  return 0;
}

div1G

[AGC016D] XOR Replace 结论,异或,图论 2.trick 4.须魔改某一算法的题 5.人类智慧

好玩题。

发现对 \(a_i\) 进行一次操作相当于令 \(a_i:=\oplus_{1\le i\le n} a_i\),再对 \(j\) 进行一次操作相当于令 \(a_j:= a_i\)

则题意等价于有一个长度为 \(n + 1\) 的数列 \(a\)\(a_{n + 1} = \oplus_{1\le i\le n} a_i\),每次可以交换 \(a_i\)\(a_{n + 1}\),求最小交换次数令 \(\forall 1\le i\le n, a_i = b_i\)

则判断有解仅需判断 \(a_1\sim a_{n + 1}\) 中每种权值出现次数大于 \(b_1\sim b_n\) 中对应权值的出现次数即可。

然后考虑将一种权值的位置缩成一个点,考虑一类排列交换元素问题的套路,若初始时 \(a_i\not= b_i\) 则应在交换过程中用权值 \(b_i\) 替换 \(a_i\),于是考虑转化为图论问题,令 \(b_i\) 对应节点与 \(a_i\) 对应节点连边,

答案即连边数量(每个位置都需要操作)减去大于 1 的连通块的数量(可以节省的操作数量)。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int n, num, ans, edgenum, a[kN], b[kN];
std::map<int, int> cnt;
int fa[kN], sz[kN];
bool vis[kN];
//=============================================================
int find(int x_) {
  return (x_ == fa[x_]) ? x_ : (fa[x_] = find(fa[x_]));
}
void merge(int x_, int y_) {
  int fx = find(x_), fy = find(y_);
  if (fx == fy) return ;
  fa[fx] = fy;
  sz[fy] += sz[fx];
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  std::cin >> n;
  for (int i = 1; i <= n; ++ i) std::cin >> a[i], a[n + 1] ^= a[i], ++ cnt[a[i]];
  ++ cnt[a[n + 1]];
  for (int i = 1; i <= n; ++ i) std::cin >> b[i], -- cnt[b[i]];
  for (auto [x, y]: cnt) {
    if (y < 0) {
      std::cout << -1 << "\n";
      return 0;
    }
  }
  
  for (auto &[x, y]: cnt) y = ++ num;
  for (int i = 1; i <= num; ++ i) fa[i] = i, sz[i] = 1;
  for (int i = 1; i <= n; ++ i) {
    if (a[i] != b[i]) {
      int u_ = cnt[a[i]], v_ = cnt[b[i]];
      merge(u_, v_);
      ++ edgenum;
    }
  }

  for (int i = 1; i <= num; ++ i) {
    if (sz[find(i)] > 1 && !vis[find(i)]) ++ ans, vis[find(i)] = 1;
  }
  if (sz[find(cnt[a[n + 1]])] > 1) -- ans;
  std::cout << edgenum + ans;
  return 0;
}

写在最后

猜猜我手里还能凑几场的题?

image.png
posted @ 2025-03-16 23:45  Luckyblock  阅读(100)  评论(1)    收藏  举报