省选日记 Day-38 - Day-35

省选日记 Day \(-38\) - Day \(-35\)

Day \(-38\) Feb 24, 2022, Thursday

模拟赛, 只做了 T1, 用杜教筛做 \(O(n^{\frac 23})\) 过不了 \(10^{11}\), 正解是转化后枚举 \(O(n^{\frac 23})\) 种情况, 跑得足够快. 补了 T2, 可以认为是前缀和优化建边的最小生成树.

将比赛 T1 补充到亚线性筛法里.

复习原根.

AHOI2013

有长为 \(n\) 的字符串 \(S\), 设以 \(i\) 开始的后缀为 \(T_i\), 求:

\[\sum_{i = 2}^n \frac {3i(i - 1)}2 - 2\sum_{i = 1}^{n - 1} \sum_{j = i + 1}^n LCP(T_i, T_j) \]

也就是求所有后缀两两删掉 LCP 的长度和. 前面的 \(\displaystyle{\sum_{i = 2}^n \frac {3i(i - 1)}2}\) 表示的是对于所有无须数对 \((i \neq j)\), 长度为 \(i\), \(j\) 的后缀长度和, 即 \(\displaystyle{\sum_{i = 2}^n} i \sum_j^{i - 1} j\), 这个式子可以展开然后 \(O(1)\) 求, 但是不影响复杂度就懒得展了.

接下来求 \(\displaystyle{\sum_{i = 1}^{n - 1} \sum_{j = i + 1}^n LCP(T_i, T_j)}\), 我们可以把字符串倒着输入, 这样就可以只求每对前缀的 LCS (Longest Common Suffix).

对新的串 \(S'\) 建 SAM, 发现后缀树上的每个节点, 如果它的最长字符串长度为 \(Len\), 公共后缀 (也就是最短字符串) 长度为 \(len\), Endpos 集合大小为 \(|Endpos|\), 那么它的贡献是 \(|Endpos|(|Endpos| - 1) (Len - len + 1)\). 对于以它 Endpos 集合中任意选取一对位置, 这两个位置为结尾的前缀, 都有它作为公共后缀, 不一定最长, 所以我们只记录已知部分的贡献, 也就是长度为 \(Len - len + 1\) 这部分. 这部分左边的贡献在它子树上其它节点处统计, 右边的贡献在它祖先处已经统计了.

好久没有写 SAM 了, 我都快忘了怎么写了, 学习了我 \(10MB\) 代码的 Tabnine 仍记得怎么写 (当然我没有接受它的建议).

unsigned long long Ans(0);
unsigned a[10005], m, n;
unsigned A, B, C, D, t;
unsigned Cnt(0), Tmp(0), STop(0);
char S[500005];
struct Node {
  vector<Node*> Son;
  Node* To[26], * Fa;
  unsigned Len, Siz;
}N[1000005], * Stack[500005], * Last(N), * CntN(N);
inline void Add(char c) {
  Node* Back(Last), * Cur(Last = Back->To[c] = ++CntN);
  Cur->Len = Back->Len + 1, Back = Back->Fa, Stack[++STop] = CntN;
  while (Back) {
    if (Back->To[c]) {
      Node* BcTo(Back->To[c]);
      if (BcTo->Len == Back->Len + 1) { Cur->Fa = BcTo; return; }
      else {
        Node* Copy(++CntN);
        (*Copy) = *(BcTo), Copy->Len = Back->Len + 1, BcTo->Fa = Cur->Fa = Copy;
        while (Back && (Back->To[c] == BcTo)) Back->To[c] = Copy, Back = Back->Fa;
        return;
      }
    }
    Back->To[c] = Cur, Back = Back->Fa;
  }
  if (!(Cur->Fa)) Cur->Fa = N;
}
inline void DFS(Node* x, unsigned len) {
  for (auto i : x->Son) DFS(i, x->Len), x->Siz += i->Siz;
  unsigned long long Ct(x->Siz);
  Ans -= (x->Len - len) * Ct * (Ct - 1);
}
signed main() {
  fread(S + 1, 1, 500002, stdin), n = strlen(S + 1), Ans = 0;
  while (S[n] < 'a') --n;
  for (unsigned long long i(2); i <= n; ++i) Ans += i * (i - 1);
  Ans >>= 1, Ans *= 3;
  for (unsigned i(n); i; --i) Add(S[i] - 'a');
  while (STop) Stack[STop--]->Siz = 1;
  for (Node* i(CntN); i > N; --i) i->Fa->Son.push_back(i);
  DFS(N, 0);
  printf("%llu\n", Ans);
  return Wild_Donkey;
}

发现无论正着还是反着插入, 都能 AC. 分析其原因, 对于两段不重合的极大相同子串 \(A\), \(B\), 长度为 \(Len\), 它们的前缀或后缀作为 LCP 或 LCS 能造成的总贡献都是 \(\displaystyle{\sum_{i = 1}^{Len} i} = \dfrac {i(i + 1)}2\).

法二

对于两个后缀, 我们所统计的 LCS, 属于它们所属节点在后缀树上的 LCA. 如果回归我们一开始的式子, 也就是两个前缀去掉 LCS 后的长度, 那么在后缀树上的体现就是两点间的路径长度. 定义每个点向父亲的边权为 \(Len_{Cur} - Len_{Fa}\), 求路径长度即可.

对每条边计算贡献, 可以知道经过这条边的点对共有 \(Size_{Cur}(n - Size_{Cur})\) 个, 直接计算贡献即可.

法三

用 SA, 求出 \(Height\) 数组. 可以知道对于 \(Height\) 的每个区间 \([l, r]\), 其最小值表示的即为以 \(l\), \(r\) 开头的后缀的 LCP. 用单调栈, 扫描 \(r\), 维护所有 \(l\), 即可求出答案.

本题目开始于 Feb 24, 完成于 Feb 25

Day \(-37\) Feb 25, 2022, Friday

模拟赛爆大零, T1 可持久化线段树被人带跑了, 想分块去了. 结果最后分块写完发现自己做的是问题的弱化 (忘了一开始是经过自己的弱化才开始做的了, 如果一开始往离线上弱化估计就做出来了).

好几个月没写可持久化线段树, \(1\) 个小时才 AC.

unsigned a[200005], Stack[200005], STop(0), m, n;
unsigned A, B, C, D, t;
unsigned Cnt(0), Ans(0), Tmp(0);
struct Node {
  Node* LS, * RS;
  unsigned Fa, Val, Tag;
  inline void PsUp() {
    if (LS) Val = max(Val, LS->Val);
    if (RS) Val = max(Val, RS->Val);
  }
}N[10000005], * Ver[200005], * CntN(N);
inline Node* New() {
  Node* Cur(++CntN);
  Cur->Val = Cur->Tag = 0, Cur->LS = Cur->RS = NULL, Cur->Fa = 1;
  return Cur;
}
inline Node* Copy(Node* x) {
  Node* Cur(++CntN);
  (*Cur) = (*x), Cur->Fa = 1, --(x->Fa);
  if (Cur->LS) ++(Cur->LS->Fa);
  if (Cur->RS) ++(Cur->RS->Fa);
  return Cur;
}
inline void Chg(Node* x, Node* y, unsigned L, unsigned R) {
  if ((A <= L) && (R <= B)) { ++(y->Val), ++(y->Tag); return; }
  unsigned Mid((L + R) >> 1);
  if (A <= Mid) {
    if (!(x->LS)) x->LS = New();
    if (!(y->LS)) y->LS = New();
    else if (y->LS->Fa > 1) y->LS = Copy(y->LS);
    Chg(x->LS, y->LS, L, Mid);
  }
  if (Mid < B) {
    if (!(x->RS)) x->RS = New();
    if (!(y->RS)) y->RS = New();
    else if (y->RS->Fa > 1) y->RS = Copy(y->RS);
    Chg(x->RS, y->RS, Mid + 1, R);
  }
  y->PsUp();
}
inline void Qry(Node* x, unsigned L, unsigned R) {
  if ((A <= L) && (R <= B)) { Ans = max(Ans, x->Val + Tmp); return; }
  unsigned Mid((L + R) >> 1);  Tmp += x->Tag;
  if (A <= Mid) { if (x->LS) Qry(x->LS, L, Mid); else Ans = max(Ans, Tmp); }
  if (Mid < B) { if (x->RS)  Qry(x->RS, Mid + 1, R); else Ans = max(Ans, Tmp); }
  Tmp -= x->Tag;
}
signed main() {
  n = RD(), m = RD(), t = RD(), Ver[0] = N;
  for (unsigned i(1); i <= n; ++i) {
    a[i] = RD();
    while (STop && (a[i] > a[Stack[STop]])) --STop;
    A = Stack[STop] + 1, B = Stack[++STop] = i;
    ++(Ver[i - 1]->Fa), Chg(Ver[i - 1], Ver[i] = Copy(Ver[i - 1]), 1, n);
  }
  for (unsigned i(1); i <= m; ++i) {
    Ans *= t, A = (RD() + Ans + n - 1) % n + 1, B = (RD() + Ans + n - 1) % n + 1;
    if (A > B) swap(A, B);
    Ans = Tmp = 0, Qry(Ver[B], 1, n);
    printf("%u\n", Ans);
  }
  return Wild_Donkey;
}

然后完成了昨天 SAM 的代码.

Day \(-36\) Feb 26, 2022, Saturday

NOI2005

给一个序列, 支持一下操作:

  • 区间插入

  • 区间删除

  • 区间赋值

  • 区间翻转

  • 区间求和

  • 区间最大子段和

一眼平衡树, 首先明确我们要求的东西所需的信息: 区间和, 区间最大子段和, 区间最大前缀和, 区间最大后缀和. 还需要为区间赋值和区间翻转打 Tag.

写了一棵 WBLT, 抢了个最优解榜首.

打 USACO, 铜组用了五十多分钟, 真是太菜了.

银组的 T1 卡了一下午, 因为往费用流和二分图匹配上想了, 最后加上二分答案查询优化到 \(O(n^{3.5}\log n)\), 实在是做不了. 然后突然想到可以强连通分量 \(O(n^2)\) 做. 我们建有向图, 每头牛是节点, 向初始拥有自己更想拥有的礼物的牛连边. 在这个有向图上的每一个环, 我们都可以进行一次交换, 这样可以让环上每一头牛都更加满意.

如何判断一头牛能否得到一个礼物? 它必须存在指向初始拥有这个礼物的牛的边, 并且它们在一个简单环里. 对于所有它拿到后更满意的礼物, 都满足第一个条件, 第二个条件等价于两个点在同一个强连通分量里. 那么我们查询的就是自己所在强连通分量内所有节点中, 初始拥有的自己最想得到的礼物.

T2 给秒了, 爆搜复杂度是 \(O(2^n)\), 但是可惜 \(n \leq 40\), 所以直接双向搜索. 因为 map 的常数极大, 所以实现后会 MLE, 所以尝试把 long long 变成 int, 仍失败. 认为大概率 map 存在平衡树的结构, 所以会有几倍的空间.

所以使用 vector, 记录可达的坐标, 不再记录到达同一个坐标的方案数量, 而是作为相同的元素共存于 vector 中, 不影响复杂度. 最后将所有 vector 排序然后双指针统计答案即可. 碰运气没开 long long 结果也过了.

关于这个题 int 能过的问题: 虽然有 \(40\)\([-10^9, 10^9]\), 总和可以超过 int 的范围, 但是我们的终点也是 \([-10^9, 10^9]\), 所以溢出的点必须再回到合法范围内才能对答案进行贡献. 我们又知道溢出是取模意义下的, 也就是说我加一个数溢出了, 我减同样多的数还能减回来. 所以相当于把坐标生成了一个 int 范围的, 可减的哈希值, 对于 \(40\) 种操作来说, 这个碰撞的概率是及其小的, 但是可以构造数据来卡. 出题人没有刻意卡, 那么不开 long long 出错的可能性是很低的.

T3 是个贪心, 模拟过程比较细节, 但是在一些 stl 的帮助下, 经过几次小重构, 在 \(30\) 分钟左右的时间内过掉了此题.

整场比赛我提前得到了题面, 开题后开始写代码, 遇到了一些问题, 但是仍在 \(100\) 分钟内完成了所有代码.

Day \(-35\) Feb 27, 2022, Sunday

今天为了搞到金组题面, 帮别人调了好几份银组的代码, 当了一上午大好人.

看到在任务清单吃灰的区间众数, 我心想蒲公英也是区间众数, 是紫的, 你凭什么是黑题? 然后把它秒了, 发现 \(62.5MB\) 内存卡掉了空间 \(O(n\sqrt n)\) 的做法, 好不容易在唐爷爷的提醒下想到了 \(O(n)\) 空间的做法, 但 \(O(n\sqrt n \log n)\) 又让我只能望着 \(5 * 10^5\) 的数据兴叹.

下午好不容易搞到了金组题面, 一看 T1 还以为是朋友给我发错了, 这不是银组 T1 吗? 然后发现这个奶牛换礼物虽然规则和背景都一样, 但是求的东西不一样了. 这个题目困住了我, 也让我困倦不堪, 我睡了一觉, 起来后发现自己还是不会. 把 T2 读完想到了 \(O(n)\) 的递推, 又想到可以矩阵快速幂转移, 加上二分干到 \(O(n\log^2 m)\).

晚上终于在朋友的帮助下看懂了 T1, 也意识到 T2 转移是等比数列求和, 所以自己是个傻逼.

觉得自己应该静静, 所以报名了 ARC. 这是我第一次打 ARC, 打得很窝囊, 只做出了 AB 两题, CD 很弱智, 但是我没想出来.

A 是签到题, 把 A 都转化成 BB, 然后贪心地合并连续的 BB 即可.

B 别人都觉得难, 首先判所有数字个数, 不相同直接 No. 然后先考虑所有数字不同的情况, 可以建立映射, 使得第一个序列等价于排列 \(1, 2, 3,...,n\), 然后就可以求出第二个序列对应的排列. 发现每次执行题目中的操作, 会使得排列逆序对变化 \(2\) 的整数倍, 所以判断目标排列的逆序对数量奇偶即可. 然后考虑有相同数字的情况, 如果我们把相同数字的每次出现视为不同情况, 则等价于所有数字不同的情况. 但是不同之处在于, 如果我们一开始就交换两个相同的数字, 那么这个序列逆序对数变化奇数个, 可以使得原本不能得到的序列可以得到. 所以当存在相同数字时, 无论有多少逆序对, 都是 Yes, 否则判断逆序对数量奇偶即可. 这道题求成正序对了, 调了好久.

C 是 NOIP2018 铺设道路在环上的版本, 先看原题的做法, 可以认为是每次差分为正时插入线段, 然后在差分为负时删除线段, 答案即为插入线段的数量. 那么这道题的不同之处在于, 对于不相交的两条线段, 只要它们一条以 \(1\) 为起点, 一条以 \(n\) 为终点, 那么我们就可以把它合并成一条.

接下来的任务就是尽可能地让可合并的线段更多. 因为一条线段起点越靠后, 它相对就更有价值, 所以删除的时候优先删除起点靠前的, 我们可以用小根堆维护这些线段. 由于相同线段较多, 所以我们每次插入时记录数量, 就可以避免多次插入相同线段了, 删除的时候把起点为 \(1\) 的线段插入一个 vector, 最后将所有终点为 \(n\) 的线段插入另一个 vector. 排序后用双指针扫描两个 vector 将可并线段合并. 答案即为 NOIP 原题的答案减去合并线段的对数.

priority_queue <pair<unsigned, unsigned> > Q;
vector<pair<unsigned, unsigned> > Pre, Suf;
unsigned long long Ans(0), Refresh(0);
unsigned a[200005], m, n, t;
unsigned Cnt(0), Tmp(0);
signed main() {
  n = RD();
  for (unsigned i(1); i <= n; ++i) {
    a[i] = RD();
  }
  Ans = a[1], Q.push({ n - 1, a[1] });
  for (unsigned i(2); i <= n; ++i) {
    if (a[i] > a[i - 1]) Q.push({ n - i, a[i] - a[i - 1] }), Ans += a[i] - a[i - 1];
    else {
      unsigned Del(a[i - 1] - a[i]);
      while (Del && (Q.top().second <= Del)) {
        if (Q.top().first == n - 1) Pre.push_back({ i, Q.top().second });
        Del -= Q.top().second, Q.pop();
      }
      if (Del) {
        pair <unsigned, unsigned> Tmpp(Q.top());
        Q.pop(), Tmpp.second -= Del;
        if (Tmpp.first == n - 1) Pre.push_back({ i, Del });
        Q.push(Tmpp);
      }
    }
  }
  while (Q.size()) Suf.push_back({ n - Q.top().first, Q.top().second }), Q.pop();
  for (unsigned i(0), j(0); i < Pre.size() && j < Suf.size();) {
    while (j < Suf.size() && Pre[i].first > Suf[j].first) ++j;
    if (i < Pre.size() && j < Suf.size()) {
      if (Pre[i].second > Suf[j].second) Ans -= Suf[j].second, Pre[i].second -= Suf[j].second, ++j;
      else Ans -= Pre[i].second, Suf[j].second -= Pre[i].second, ++i;
    }
  }
  printf("%llu\n", Ans);
  return Wild_Donkey;
}

D 直接用高位前缀和求出 \(10^6\) 范围内每个数字的答案即可.

unsigned a[1000005], b[1000005], m, n;
unsigned long long Ans(0);
unsigned A, B, C, D, t;
unsigned Cnt(0);
signed main() {
  n = RD();
  for (unsigned i(1); i <= n; ++i) ++a[b[i] = RD()];
  for (unsigned i(1); i <= 999999; ++i) if (i % 10) a[i] += a[i - 1];
  for (unsigned i(1); i <= 999999; ++i) if ((i / 10) % 10) a[i] += a[i - 10];
  for (unsigned i(1); i <= 999999; ++i) if ((i / 100) % 10) a[i] += a[i - 100];
  for (unsigned i(1); i <= 999999; ++i) if ((i / 1000) % 10) a[i] += a[i - 1000];
  for (unsigned i(1); i <= 999999; ++i) if ((i / 10000) % 10) a[i] += a[i - 10000];
  for (unsigned i(100000); i <= 999999; ++i) a[i] += a[i - 100000];
  for (unsigned i(1); i <= n; ++i) {
    unsigned Tmp(b[i]);
    char Flg(0);
    while (Tmp) { if ((Tmp % 10) >= 5) Flg = 1; Tmp /= 10; }
    Ans += a[999999 - b[i]] + Flg - 1;
  }
  printf("%llu\n", Ans >> 1);
  return Wild_Donkey;
}
posted @ 2022-03-05 14:48  Wild_Donkey  阅读(24)  评论(0编辑  收藏  举报