2023ICPC备战

杭电多校

杭电多校5C String Magic (Easy Version)

给定字符串,计算 i,j 对满足1≤i<j≤n,j−i+1=2k,S[i,i+k−1]=S[i+k,j],S[i,i+k−1]是回文串

S[i,i+k-1]=S[i+k,j]且 S[i,i+k-1]是回文串,则 S[i,j]是回文串,因此先manacher处理一下每个位置为中心的最长奇数长度回文串,最长偶数长度回文串然后对于每个位置i为中心的最长偶数长度回文串[L,R],查找左侧所有满足“左端点大于等于L,右端点大于等于i”的回文串,这就是二维数点

杭电多校5D String Magic (Hard Version)

给定字符串,对于所有字符串所有前缀,计算 i,j 对满足1≤i<j≤n,j−i+1=2k,S[i,i+k−1]=S[i+k,j],S[i,i+k−1]是回文串

题目要求出所有前缀的双回文子串个数,而回PAM建立的过程是在线的,即:我们可以知道原串的所有前缀的信息,因此不难想到做法。
考虑维护增量:PAM上新加入一个结点或者走到一个已有的结点,则这个结点到根的链上的所有结点所代表的子串出现次数都会+1,因为它们都是这个结点所代表的的回文串的border(若 s 的一个子串既是它的前缀又是它的后缀,则这个子串是它的border)。
我们定义一个代表了双回文子串的结点权值是1,其余结点权值是0,我们每走到一个结点对答案带来的增量即为该结点到根的树链的权值和,而当新增一个本质不同回文子串时,PAM的新节点是从叶子结点往下加入的,我们可以很方便的使用树上前缀和维护这个信息。而对于查询一个新增回文串是否合法,分别维护正串和反串哈希即可 O(1)判断。

杭电多校7E Subsequence Not Substring

给定字符串S,找出字典序最小的字符串T满足T是S的子序列但不是S的子串

考虑怎么刻画 S 的一个子串:由 SAM 的定义,SAM 是最小的刻画了 S 所有子串的自动机,所以先建出 SAM。
考虑怎么刻画 S 的一个子序列:每次贪心跳到下一个最近的字符 c,即子序列自动机。
在 SAM 上每个点 u 预处理出 dp[u],表示贪心的起点最多是 dp[u],才存在一个字符串满足从 u 开始走会失配,但是贪心能匹配上。
转移是 dp[u] = max pre[dp[trans(u,c)] − 1][c],其中 pre[i][j] 表示 i 前面最后一个字符 j 的位置。
有了 dp[u] 之后,考虑一个贪心,初始答案字符串为空。
每次尝试加如一个字符 a,贪心会得到一个新的位置,SAM 会得到一个新的节点,如果位置小于等于新节点的 dp 值即可跳过去。如果不存在这个节点,但可以贪心那么就结束循环,否则继续。

杭电多校8F Nested String

定义在T1中任意一个位置插入T2得到的字符串为 T-嵌套串,问S串中有多少个 T-嵌套串。两个嵌套串不同,当且仅当它们在S中的位置不同或T2插入的位置不同

正解做法是求S中以每个位置l起始的后缀与T1的最长公共前缀(LCP)长度 p l p_l pl,以及以每个位置r结尾的前缀与T1的最长公共后缀长度 q r q_r qr。后者相当于反着求 LCP,所以这个东西可以用 Z 函数(即exKMP)预处理出来。假设 r-l+1=|T1|+|T2|,我们求 S l … r S_{l\dots r} Slr对应的合法嵌套串数量。我们考虑T2在 S l … r S_{l\dots r} Slr中的嵌套位置,于是 p l p_l pl将嵌套位置限制在l到r范围的一个前缀内,而 q r q_r qr则将嵌套位置限制在l到r的一个后缀范围内,只需要对其求交集就可以知道T2能够出现在哪些地方。于是我们需要求一个区间内与T2匹配的字符串数量,可以先对S预处理一遍与T2的 KMP 匹配,然后用前缀和统计。

杭电多校3D Chaos Begin

平面上有n个点,还有n个点通过将原来n个点平移一个向量得到,求出所有可能的向量

我们可以枚举第 2~2n 个点与第 1个点的偏移量,首先判断他是否可能成为可行解,对于可能成为可行解的答案,再考虑进一步判断是否合法。为了避免讨论方向问题,对于 ( Δ x , Δ y ) (\Delta x,\Delta y) (Δx,Δy) ( − Δ x , − Δ y ) (-\Delta x,-\Delta y) (Δx,Δy) ,我们不妨只保留 Δ x > 0 \Delta x>0 Δx>0 的偏移量。
对于判断一组偏移量是否可能成为可行解,由于数据随机,不妨直接暴力判断对于每个点 ( x , y ) (x,y) (x,y) 是否存在 ( x + Δ x , y + Δ y ) (x+\Delta x,y+\Delta y) (x+Δx,y+Δy) ( x − Δ x , y − Δ y ) (x-\Delta x,y-\Delta y) (xΔx,yΔy) ,如果都不存在,则直接退出本层循环,如果对每个点都存在,则这个偏移量是潜在的合法解。对于潜在的合法解,直接将每个点尝试和 ( x + Δ x , y + Δ y ) (x+\Delta x,y+\Delta y) (x+Δx,y+Δy) 匹配,且每个点不能重复匹配,看匹配对数是不是 n 即可

杭电多校8B Delivery Robot

给定两个凸包a、b,一个速度向量v。求a以速度v移动一秒的过程内,是否会与b相撞(存在公共点)。若相撞,则求一个新的速度v’,使得一秒内不发生碰撞。且向量v’与向量v的L2范数最小。以分数形式输出两向量的L2范数。

闵可夫斯基和,若线段与凸包没有交点,则整个过程中,a与b不会发生碰撞。当有交点时,我们需要求出一个新的点v’,使得其与 v 距离最小,且与零点连线的线段与凸包无交点。我们把原点加入点集,重新求一下凸包,哪些边消失了,哪些就是我们要求的。求出点到这些线的最短距离,就得到了答案。

杭电多校1J Easy problem I

给定初始序列 a,有两种操作。操作1:在 [l,r] 范围内设置 a i = ∣ a i − x j ∣ a_i=|a_i-x_j| ai=aixj。操作2:询问 [l,r] 区间和。 x j x_j xj 非递减

a i ≥ x j a_i\geq x_j aixj ,就是正常的区间加区间求和。当第一次 a i < x j a_i<x_j ai<xj ,由于 x j x_j xj 非递减,所以之后都是 x j − a i x_j-a_i xjai 。所以区间和用第一类和与第二类和的和来表示。当 a i a_i ai 符号第一次翻转,暴力改成第二类和,以及删去在第一类和的贡献
为了优雅暴力,多维护一个区间最小值,如果区间最小值小于 x j x_j xj ,说明有元素要变成第二类。当元素变成第二类之后,其min设置为正无穷,表示之后不用暴力这块。两类和与区间内第一二类的元素个数有关,因此维护一个区间内第一类元素个数。lazy需要维护3个,lz表示第一类的lz,lz2表示第二类的lz,lz3表示第二类,该区间是否取相反数,每修改一次需要翻转符号

杭电多校6D Tree

给一棵树,每个顶点有颜色a,b,c,计算 i,j 满足1≤i≤j≤n,路径上具有三种不同颜色点数相同

一条路径满足要求的充要条件是 3cnta=3cntb=3cntc=n ,即:3cnta-n=3cntb-n=3cntc-n=0 。我们考虑用一个三元组 (3cnta-n,3cntb-n,3cntc-n) 来表示一条路径,路径合法的充要条件即是代表路径的三元组是 (0,0,0)。对路径新加入一个点时,令 cnta–,cntb–,cntc–,再让对应字母的 cnt+=3,使用 map 维护当前子树中以及之前搜到的所有子树中每个三元组的数量,在加入一棵新子树时注意要先查询再加入。
由于信息在点上,需要一些对于路径端点是分治重心的特判

#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
const int N = 100010;
vector<int> adj[N];
int n, k, sz[N], ms[N];
bool vis[N];
string s;
i64 solve(int u, int tot) {
  int rt = u;
  auto get_rt = [&](auto self, int u, int fa) -> void {//找重心
    sz[u] = 1, ms[u] = 0;
    for (auto v : adj[u]) {
      if (vis[v] || v == fa) continue;
      self(self, v, u);
      sz[u] += sz[v];
      ms[u] = max(ms[u], sz[v]);
    }
    ms[u] = max(ms[u], tot - sz[u]);
    if (ms[u] <= tot / 2) rt = u;
  };
  get_rt(get_rt, u, 0);
  i64 res = 0;
  vis[u = rt] = true;
  map<array<int, 3>, int> all;
  int ua = -1, ub = -1, uc = -1;
  if (s[u] == 'a') ua += 3;
  if (s[u] == 'b') ub += 3;
  if (s[u] == 'c') uc += 3;
  for (auto v : adj[u]) {
    if (vis[v]) continue;
    map<array<int, 3>, int> sub;
    auto get_info = [&](auto self, int u, int fa, int a, int b, int c) -> void {
      a--, b--, c--;
      if (s[u] == 'a') a += 3;
      if (s[u] == 'b') b += 3;
      if (s[u] == 'c') c += 3;
      if (!a && !b && !c) res++; 
      sub[{a, b, c}]++;
      sz[u] = 1;
      for (auto v : adj[u]) {
        if (vis[v] || v == fa) continue;
        self(self, v, u, a, b, c);
        sz[u] += sz[v];
      }
    };
    get_info(get_info, v, u, ua, ub, uc);
    for (auto [arr, cnt] : sub) {
      auto [a, b, c] = arr;
      res += cnt * all[{-a, -b, -c}];
    }
    for (auto [arr, cnt] : sub) {
      auto [a, b, c] = arr;
      all[{a - ua, b - ub, c - uc}] += cnt;
    }
  }
  for (auto v : adj[u]) {
    if (vis[v]) continue;
    res += solve(v, sz[v]);
  }
  return res;
}
int main() {
  cin.tie(nullptr)->sync_with_stdio(false);
  cin >> n >> s;
  s = ' ' + s;
  for (int i = 1; i < n; i++) {
    int u, v;
    cin >> u >> v;
    adj[u].emplace_back(v);
    adj[v].emplace_back(u);
  }
  cout << solve(1, n) << '\n';
  return 0;
}

杭电多校2I Coin

小F这天去参加聚会,现在小F要组织大家一起玩个游戏!一共有N人参加了比赛(小F看了他们玩)。小F准备了N枚相同的硬币。一开始,每个人都只有一枚硬币,每个人都有一个数字 a i a_i ai表示他可以持有的硬币上限。
游戏中有 M 轮。在每一轮中,小F都会选择A和B两个人(当然这两个人是不同的),然后这两个人有三个选择:
A 给 B 一枚硬币
B 给 A 一枚硬币
他们什么都不做
请注意,在任何时候,每个人持有的硬币数量都不能超过其持有限额 a i a_i ai .

在N人中,有K人是小F的朋友,现在小F想知道,经过M轮游戏,在所有可能的情况下,他的K朋友最多持有的硬币总数是多少。
比较明显的最大流,只需通过拆点引入时间概念。
每轮两个人当前时刻结点之间连边,流量为1,并且各分裂出一个新点,表示进入下一时刻,每个人不同时刻结点间有从早到晚单向流动的边,流量为 a i a_i ai 。源点向所有人初始时刻的结点连边,流量为1,只有 f 个朋友的最终时刻结点能向终点连边,流量为 a f i a_{fi} afi

杭电多校5K Cactus Circuit

无向连接图称为仙人掌,当且仅当它的每一条边最多属于一个简单循环时。简单循环是没有重复顶点的循环。请注意,图中没有自环,但可能有重复的边连接相同的节点。
有一个旧的电路网络包含n节点和m无向边,网络保证是仙人掌。每个边缘(ui ,vi )具有耐久性di,表示它可以运输电力的时间长度。现在你的工作是对于每个边(ui​,vi​)选择激活时间si (si​ ≥ 0),则此边只会在时间间隔[si ,si​ +di​]内传输电力。但是,这个脆弱的网络不能与任何节点断开连接。首先,您必须确保,可以在时间0传输电力的边已将所有节点连接在一起。之后,一旦两个节点在某一时刻无法连接,整个电网将立即崩溃。
现在的问题是,你能让这个网络运行多长时间?正式地,如果你的答案是A,那么你应该有办法设置所有si​ 满足对于每个实数t∈[0,A],在时间t所有n节点由可以同时传输电力的边连接.

注意到仙人掌的桥如果断了就没办法用别的代替了,首先答案和所有桥的边权取 min。
剩下的若干个环,任意时刻都要有 len − 1条边是可以传输的,因此将环上所有的边权从小到大排序,答案和 min(w1 + w2, w3) 取 min ,其中 w1, w2, w3 代表前三小。特殊注意两条边的环答案和 w1 + w2 取 min。

牛客多校4H Merge the squares!

题目链接
给定 n × n 个 1 × 1 组成的正方形,每次可以合并相邻不超过 50 个正方形变成一个大正方形。问如何通过合并得到一个大的 n × n 的大正方形

考虑 7 2 ≤ 50 7^2 ≤ 50 7250 ,所以如果边长 x 是 [2,7] 的倍数,可以考虑直接先拆分成 x d × x d \dfrac{x}{d}\times \dfrac{x}{d} dx×dx 个正方形求解。
显然质数不能按照这种乘除法的倍数拆分,因而考虑加减法。注意到完全平方和公式: ( a + b ) 2 = a 2 + 2 a b + b 2 (a+b)^2 =a^2+2ab + b^2 (a+b)2=a2+2ab+b2,即 a × a 和 b × b 的正方形,和两个 a × b 的矩形。正方形可以递归下去构造,考虑矩形如何尽可能少的构造。
不妨令 a > b ,一个贪心的想法是,每次构造一个 b × b 的正方形,然后留下一个 ( a − b , b ) 的矩形递归下去构造,即类似辗转相减法。
回到整体大正方形拆分,可以考虑枚举这样的 a ,求出这样拆分的矩形 ( a , x − a ) 需要包含多少个小正方形,如果不超过 24 个就可以视为一个合法的拆分,这是因为 24 × 2 + 2 = 50。

牛客多校8I Make It Square

题目链接
Luluu 是来自 Eorzea 的“方形”魔术师。“方形”魔术师是使用“方形”咒语的专家。
每个拼写都可以表示为仅包含小写英文字符的非空字符串。“方形”咒语是长度均匀的咒语,前半部分与后半部分相同。例如,abcabc 和 aaaa 是“方形”咒语,而 aaa 和 abcabd 不是。 今天,露露从格里莫尔那里发现了一个强大的“方形”咒语,但不幸的是,书页损坏了,所以咒语的某些部分已经无法阅读了。
更具体地说,原始的“方形”咒语是这样的格式p+s+q+t,其中s和t是两个常量字符串,但p和q是咒语的两个不可读的部分。通过一些调查,Luluu认为p和q长度应相同,但不超过m.
现在,Luluu 请你帮忙计算所有可能的原始“方形”咒语的数量。你能帮助这个可怜的魔术师吗?

分两类讨论:|S|>|T|和|S|<|T|。|S|=|T|可以合并到任意一类里,也可以单独简单讨论,略去不讲。
当|S|>|T|时候,观察到p+S+q+T的中点位置是固定的,即要求 p + S [ 1 , ∣ S ∣ − Δ ] = S [ ∣ S ∣ − Δ + 1 , ∣ S ∣ ] + q + T p+S[1,|S|-\Delta]= S[|S|-\Delta+1,|S|]+q+T p+S[1,SΔ]=S[SΔ+1,S]+q+T,其中 Δ = ∣ S ∣ − ∣ T ∣ 2 \Delta=\frac{|S|-|T|}{2} Δ=2ST 。接着需要进一步讨论 ∣ p ∣ < Δ |p|<\Delta p<Δ ∣ p ∣ ≥ Δ |p|\geq \Delta pΔ
∣ p ∣ < Δ |p|<\Delta p<Δ时,可以发现p串和q串是各自对应等于S串的一部分,取值是存在且唯一的,同时还要求 Δ − ∣ p ∣ \Delta-|p| Δp是S串的border,且 S [ Δ + 1 , ∣ S ∣ − Δ ] = T S[\Delta+1,|S|-\Delta]=T S[Δ+1,SΔ]=T(即S的正中间部分必须完全等于T),此为充要条件。 因此只需要对S串求KMP并枚举border即可。
∣ p ∣ ≥ Δ |p|\geq \Delta pΔ时,观察到p的长度为 ∣ p ∣ − Δ |p|-\Delta pΔ的后缀和q的相同长度的前缀需要相等,而且是自由取值,p和q的其他部分是各自对应等于S串的一部分,取值存在且唯一,当然还必须满足S的正中间部分完全等于T。因此方案数为 2 6 ∣ p ∣ − Δ 26^{|p|-\Delta} 26pΔ(或全0)

牛客多校5B Circle of Mistery

题目链接
考虑长度为 n 的每个排列 P = { p 1 , p 2 , ⋯   , p n } P=\{p_1,p_2,\cdots,p_n\} P={p1,p2,,pn},并给定权值数组 { w } i = 1 n \{w\}_{i=1}^n {w}i=1n 和权值阈值 k,若 P 中存在一个任意长度的置换环 { a 1 , a 2 , ⋯   , a l } \{a_1,a_2,\cdots,a_l\} {a1,a2,,al} 满足 ∑ i = 1 l w a i ≥ k \displaystyle \sum_{i=1}^l w_{a_i} \ge k i=1lwaik,则考虑统计该排列 P 的逆序对数。问符合条件的排列中最小逆序对数目是多少

若 k≤0 ,当存在一个位置大于等于 k 时,形成自环即可,逆序数为 0 ;否则所有位置一定小于 0 ,一定无解。接下来考虑 k>0 的情况。
我们先考虑全是正数的特殊情况,显然我们优先选择合法的连续区间 [l,r] 的元素循环左移一次构造大环,其他位置构成自环,这样的逆序数是 r−l ,可以证明是理论最优的,从中去掉某些元素会导致花费增大。这样,我们枚举所有区间构造是 O(n2) 的,用尺取法是 O(n) 的。
现在问题包含了负数,尺取法就失效了,因为区间的和不具备单调性,但我们依旧可以枚举所有区间。不过,此时对于一个区间,有些位置我们是不能选的,否则就不合法了,因此选择的位置不再连续。我们先考虑不连续的位置产生的逆序数的最小值。
若在 [l,r] 这个连续区间中选择了 x 个数,我们可以将选中的位置对应的数循环左移一次,那么对于环上的元素相互产生了 x−1 个逆序对,对于每个不在环上的元素都与环元素产生了 2 个逆序对,即没被选择的点都额外产生了一个逆序对,这样产生的逆序数为 r−l+(r−l+1−x) ,可以证明是理论最优的。结合上面的结论,我们知道对于一个固定区间 [l,r] ,选的数越多越好,这样贪心选择的方式就顺其自然的出现了:正数都选,随后负数选最大的直到不能再选。这里,区间端点原则上是一定要选的,不然求出的答案会比真实答案要大。但对于这种情况的真实答案,其等价于一个更小区间的真实答案,因此即使舍弃导致求出错误答案,也不会使得最终答案错误,所以为了实现方便就允许存在这样的操作。
直接枚举每个区间取数的复杂度是 O(n3) 的。我们发现,对于每一个左端点,右端点从右到左的时候,区间长度一直在增加,那么当没被选择的数严格减小时答案才会更优,因此之前选过的负数是一定会被选上,否则没被选择的数一定比之前更大,答案不会更优。因此,用优先队列维护没被选择的负数即可,选过的就不需要考虑了。

牛客多校6H traffic

题目链接
给定一个 n + 1条边的无向连通图,边权是一个关于 t 的一次函数,要求分别算出 t = 0, 1, 2, 3, . . . , T时图的最小生成树的边权和。

n + 1条边的图,得到生成树只需要删 2 条边,并且删的边一定在环上,然后是分类讨论:如果两个环没有共用的边,那么每个环要断一条边;如果两个环有公用的边,那么实际上这两个环可以看成三条链的并联。这三条链需要断开两条。两种情况都需要对若干个一次函数维护最值,这个可以用李超线段树完成。
问题转化为如何快速找环。这里可以考虑使用并查集,依次枚举边并尝试合并,如果边上两点已经联通,则说明发现了环,将返祖边该两点颜色进行染色。等所有边处理完成后,再进行一次 dfs 将颜色进行传播。不难发现如果点在链上,异或出来的值必然为 0,而返祖边所在的环上的每个点都会染上返祖边的颜色。这时点上颜色就和边上颜色相同。如果颜色使用恰当(如 2 k 2^k 2k ),则可以快速找到每个点和每条边在哪些环上。

void dfs(int u, int fa) {
    for (auto [v, p] : e[u]) {
        if (v == fa) continue;
        dfs(v, u);
        tag[p] = id[v];
        id[u] ^= id[v];
    }
}
void solve() {
    for (int i = 1; i <= m; ++i) {
        int u, v;
        cin >> u >> v;
        if (find(u)!=find(v)) {
            fa[find(u)] = find(v);
            e[u].push_back({ v, i }), e[v].push_back({ u, i });
        }
        else id[u] ^= m, id[v] ^= m, tag[i] = m, m <<= 1;
    }
    dfs(1, -1);
}

2023ICPC网络赛(一)B String

题目链接
给定字符串S1,S2,每次询问给定字符串T,找有多少组合法的 (i,j,k) 满足 S1[i,j]+S2[j+1,k]=T

对于 T 串在中间断开的位置 x , 将串分为 T[1,x],T[x+1,|T|]考虑哪些 j 可以匹配这个断开的位置,需要满足 T[1,x] 是 S1[1,j] 的后缀, T[x+1,|T|] 是 S2[j+1,k] 的前缀。
对于后缀部分的问题,我们需要知道有哪些前缀的后缀是 T[1,x] 。可以考虑建出后缀自动机,在 parent 树上匹配
T[1,x] 对应的顶点,能匹配到的前缀对应整棵子树。对于前缀的问题,建一个反串的自动机即可。
问题转化为子树的交问题,可以用线段树合并或二维数点解决。
对于二维数点的做法,可以把子树看做 dfs 序上的区间,拆成四个关于前缀的询问后,用树状数组进行维护。对于线段树合并的做法,在第一棵后缀树上合并第二棵树上的 dfs 序,在询问时可以直接做区间查询。

百度之星初赛(三)蛋糕划分

题目链接
小度准备切一个蛋糕。这个蛋糕的大小为 N*N,蛋糕每个部分的重量并不均匀。
小度一共可以切 K 刀,每一刀都是垂直或者水平的,现在小度想知道在切了 K 刀之后,最重的一块蛋糕最轻的重量是多少。

其实对最大质量的蛋糕块二分答案即可,难在对答案判断:
首先我们对所有的横刀状态模拟,用一个数的(n-1)位的表示所有状态即可,然后根据此时横刀状态对竖刀进行决策,方法是:
从第一列开始,计算每列区间和,若此时横刀区间和加上新的一列区间后值大于答案了,那就要在此列左边切竖刀,然后在此竖刀右面重新计算新一列区间,
不断重复。其实就是每个横刀区间加上新一列区间后都不要大于答案,如果大于我们就要这里要切竖刀了。最后统计竖刀和横刀数,返回结果即可(另外,为了加速处理,还要用上二维前缀和)

2019银川

悬线法dp

F Function!数学根号分治
给出公式 ∑ a = 2 n ( a ∑ b = a n ⌊ f a − 1 ( b ) ⌋ ⌈ f b − 1 ( a ) ⌉ ) \sum\limits_{a=2}^n(a\sum\limits_{b=a}^n⌊f_a^{−1}(b)⌋⌈f_b^{−1}(a)⌉) a=2n(ab=anfa1(b)⌋fb1(a)⌉),其中 f a ( x ) = a x f_a(x)=a^x fa(x)=ax,则 f a − 1 ( x ) = l o g a x f_a^{−1}(x)=log_ax fa1(x)=logax,现在给出n(<=1e12),求出答案对998244353取模后的答案

我们稍微化简一下: ∑ a = 2 n ( a ∑ b = a n ⌊ l o g a b ⌋ ⌈ l o g b a ⌉ ) \sum\limits_{a=2}^n(a\sum\limits_{b=a}^n⌊log_ab⌋⌈log_ba⌉) a=2n(ab=anlogablogba⌉)
再其实仔细观察一下就会发现,因为第二层循环的b是从a开始的,所以 l o g b a log_ba logba一定是大于零并且小于等于1,这样向上取整就变为1了,乘上1就可以直接约分了: ∑ a = 2 n ( a ∑ b = a n ⌊ l o g a b ⌋ ) \sum\limits_{a=2}^n(a\sum\limits_{b=a}^n⌊log_ab⌋) a=2n(ab=anlogab⌋)
这样一来这个公式就比较好想了,因为n给的是1e12,这个数字很容易让人联想到sqrt(n),也就是1e6,所以我们不妨分一下块,对第一层的a简单分为[1,sqrt(n)]+[sqrt(n)+1,n],因为上面公式的b最大为1e12,所以只要a大于1e6,那么 l o g a b log_ab logab向下取整答案就是1了,也就是说对于[sqrt(n)+1,n]这一部分,我们可以稍微化简一下: ∑ a = n n ( a ∑ b = a n ⌊ l o g a b ⌋ ) = ∑ a = n n ( a ∑ b = a n ) = ∑ a = n n ( ( n + 1 ) ∗ a − a 2 ) = ( n + 1 ) ∑ a = n n a − ∑ a = n n a 2 \sum\limits_{a=\sqrt n}^n(a\sum\limits_{b=a}^n⌊log_ab⌋) =\sum\limits_{a=\sqrt n}^n(a\sum\limits_{b=a}^n)=\sum\limits_{a=\sqrt n}^n((n+1)*a-a^2)=(n+1)\sum\limits_{a=\sqrt n}^na-\sum\limits_{a=\sqrt n}^na^2 a=n n(ab=anlogab⌋)=a=n n(ab=an)=a=n n((n+1)aa2)=(n+1)a=n naa=n na2,现在利用等差数列求和公式以及平方和求和公式,就可以O(1)计算出区间[sqrt(n)+1,n]之间的答案了。
而对于区间[1,sqrt(n)]中的答案,我们可以直接O(n)跑了,对于其中第二层循环b,虽然看着是要从a跑到1e12,但其实稍微思考一下就能发现,因为是要求loga(b)的值作为答案,而这个值向下取整后,在a确定的情况下, ⌊ l o g a b ⌋ ⌊log_ab⌋ logab的值在一段连续的区间上肯定是相同的,根据这个性质我们可以将1e12的数据分块,分为一段一段的,每一段上的答案都为一个值,这样分块后我们就能很快的算出答案了,这个分块最大是当a以2为底时, l o g 2 ( 1 e 12 ) log_2(1e12) log2(1e12)也就才40最大了,时间复杂度可以大胆地放缩,因为最大也就才1e6 * 40。
话说回来,我们该怎么求这个区间呢,其实稍微将上面的式子转换一下: x = l o g a b x=log_ab x=logab等价于 b = a x b=a^x b=ax所以我们可以从1开始枚举x,从而计算出b的区间在[ax,ax+1−1]内的 ⌊ l o g a b ⌋ ⌊log_ab⌋ logab都等于x,既然 ⌊ l o g a b ⌋ ⌊log_ab⌋ logab都是一个常数了,那么我们直接根据公式 a ∗ x ∗ ( a x + 1 − 1 − a x + 1 ) a*x*(a^{x+1}−1−a^x+1) ax(ax+11ax+1)就能直接计算贡献了,前面的公式也就是 a ∗ x ∗ a*x* ax区间长度。
最后需要注意一下关于取模时的一个巨坑,也就是遇到n就要模一下,不然1e12*1e9直接就把long long爆掉了,还有就是在计算的过程中可能会出现负数,所以最后需要先模再加模最后再取模,还有就是在计算等差数列求和公式和平方和求和公式时,会遇到除法,这个时候直接用费马小定理求一下逆元就可以解决了。

2021沈阳

M String Problemsam
给一个字符串s, 对于它的每个前缀,求其字典序最大的每个子字符串。

首先可以明确的一点是,“最大的子字符串”一定会延申到该字符串的结尾。那么这道题就有一种很nb的解法,用双指针O(n)地处理字符信息。
考虑后缀自动机 sam。sam已经把每个后缀维护在自动机上了,那我们维护一个pos[],代表该节点在原串中的位置。从起始点开始字典序由大到小 dfs,某个点第一次被访问到的时候对应的路径肯定就是该点最大的后缀。

H Line Graph Matching割边
给一个带边权无向图。构造一个新图:将原图的点变成边、边变成点 , 其中新图的边权为原图中对应的俩条边权值和。求新图中的最大独立边集 — 在新图中选取不相邻的若干条边,让他们的权值和最大。

首先很关键的一点是,新图我们是建不出来的。因为如果原图是菊花图, 那么新图就会是完全图 , 存不下 , 所以只能在原图里找对应关系。
然后不难发现, 其实就是在原图中每次选取两条相邻边,要求最大化权值和,如果一共有偶数条边,那么我们是一定能取完所有边的;如果一共有奇数条边,那我们必须删掉一条边。如果删的边不是桥的话,那原图就变成偶数条边的连通图,显然可以。
考虑桥的情况,桥可能会将图变成下面两种:
1.偶图 - 桥 - 偶图 (第一种桥)
2.奇图 - 桥 - 奇图 (第二种桥)
显然情况2我们是不要的,因为情况2还需要从分出来奇图中再次删边,而很显然,最优解肯定只删一条边(反证一下就行)。所以,奇数条边的答案是 sum - 除去第二种桥外所有的边的最小边权。显然我们只需要判断“桥”两端点的size是奇数还是偶数,这个tarjan+dfs就可以处理。

2022南京

A Stop, Yesterday Please No More二维差分
给定一个m*n的区域,这片区域某个地方存在一个洞,其他地方都有袋鼠,袋鼠会经过一系列上下左右运动,可能出界或者掉入洞内,现在给出运动序列和经过运动变化后剩余袋鼠数量,求洞可能在的位置的可能性

将袋鼠的运动转化成矩形形状的变换,经过一系列的运动后,矩形会相应被裁减成一个小矩形,这个小矩形的大小是如果不存在洞的情况下袋鼠剩余个数。
所以没有出网格的袋鼠数量是x=(D-U+1) * (R-L+1),因为最终剩余k只袋鼠,那么就要有x-k只袋鼠掉到洞中。
我们来统计这个小矩形里活的袋鼠经过在移动过程中经过的格子,也就是统计m * n个格子中每个经过了这个小矩形中多少只袋鼠(注意这个洞可以在m*n的任意位置,而并非只是这个小矩形)然后计算经过数目为(x-k)的格子的数目。
这个可以用二维差分,每次移动得到++的矩形范围,在此范围内差分,由于这个小矩形的大小已经确定,并且肯定不会出界,所以判重可以直接用左上角的坐标来判,这样就保证了每只袋鼠对于每个格子只统计一次。

M Drain the Water Tank计算几何
自来水最近建造了一种多边形水箱,水箱的厚度可以忽略不计,为了启用水箱,工程师们准备在水箱上安装若千出水阀门。一个出水阀门可以被看作水箱上的一个点当阀门打开时,水箱里的水会从阀门流出。
作为总工程师的您需要知道,至少需要安装多少个出水阀门,才能在所有阀门同时打开后,让水箱里的水全部流出。
您可以认为水是一种理想流体且环境中不存在大气压,因此水总有流向更低处的趋势,即使位于水平平面上也是如此。

找的是局部最低点,这样的最低点有两种情况,第一种类似于v型,第二种则是从高到低再保持直线型的,两种情况不一样。
对于v型的,必须是从左到右先下后上,也就是叉积>0。
对于底面平的,没有办法用叉积来计算,判断其x的大小。

2022济南

中位数

2022杭州

A Modulo Ruins the Legend扩欧
给出一个数组a,构造一个等长度的等差数列b,使得 ∑ ( a i + b i ) \sum (a_i+b_i) (ai+bi)对m取模后最小,输出这个取模后的最小值和等差数列的首项s和公差d。

我们要求的是:sum + s * n + n * (n + 1) / 2 * d,(1)
假设结果是ans,因为s和d都是未知数,对于二元一次不定方程而言,根据扩展欧里几德性质知道:s * n + n * (n + 1) / 2 * d = k1 * gcd(n, n * (n + 1) / 2),(2)
其中k1是任意的整数,所以我们要求的原式可以写为:sum + k1 * g = ans - k2 * m(g = gcd(n, n * (n + 1) / 2),减去的k2 *m是取模改为这样的写法);移项可得:k1 * g + k2 * m = ans - sum (3)
因为ans的取值范围是[0, m - 1],所以上式两侧的取值范围都是:[-sum, m - 1 - sum],上式中可设gg = gcd(g, m),设系数为k3,则有k3 * gg = ans-sum,ans-sum即为gg最小倍数,可以得到ans,由exgcd可以求得k1,k2,k1知道了,(2)式由exgcd可以求得结果。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e3 + 5;
ll n, m, sum;
ll gcd(ll a, ll b) {
	return b == 0 ? a : gcd(b, a % b);
}
ll exgcd(ll a, ll b, __int128& x, __int128& 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(){
	ll n, m;
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		ll x;
		cin >> x;
		sum += x;
	}
	sum %= m;
	ll A = n, B = (n + 1) * n / 2;
	ll g = gcd(A, B);
	ll gg = gcd(g, m);
	ll ans = sum % gg;
	__int128 x, y;
	exgcd(g, m, x, y);	//k1*g-k2*m=ans-sum=k3*gcd(g,m)
	x *= (ans - sum) / gg;
	ll k1 = x;
	exgcd(A, B, x, y);	//A*x+B*y=k1g
	x *= k1;
	y *= k1;
	x = (x % m + m) % m;
	y = (y % m + m) % m;
	cout << ans << '\n';
	cout << (ll)x << " " << (ll)y << '\n';
	return 0;
}

字典树

2022沈阳

暴力
F Half Mixed矩阵构造
给出n和m,要求构造一个n×m的01矩阵。定义一个只有0或者只有1的矩阵为纯净矩阵,有0且有1的为混乱矩阵。要求构造的矩阵的所有子矩阵中,纯净矩阵数量和混乱矩阵数量相同。

容易想到n * m的矩阵,一共有((n * n+n)/2) * ((m * m+m)/2)个子矩阵。想到一列0一列1这样交替,那就有((n * n+n)/2) * m个子矩阵是纯净矩阵。
距离总数1/2还需要修正。于是想到再加列,连续两列1,其他交替,那就多了((n * n+n)/2)个子矩阵。连续三列1,就多了((n * n+n)/2) * (1+2)个。最后统一为,连续k列,可以多((n * n+n)/2) * ((k * k+k)/2)个。行同理。
于是,这道题就转变为了,用1,3,6,10…这种数去构造((m * m+m)/4)-m(或者构造((n * n+n)/4)-n)。修正的前提条件是(m * m+m)%4=0 或 (n * n+n)%4=0。

2021上海

J Two Binary Strings Problem01字符串
定义 f(l,r) 为区间内 1 的数量严格大于一半时出 1,否则出 0。现给出 01 串 a, b,问对于所有长度 k,是否能使得,对于 ai 的 f(max(i−k+1,1),i)=bi。

看数据范围再看 01 串,一眼 bitset 递推。求数量关系的经典做法是把 0 看成 −1,求一下前缀和 pre,若 pre[i]−pre[j]>0,则代表区间 1 的数量大于 0。从这里出发,我们可以想到对于每个 i 来说,前面所有前缀和比当前小的地方最后都是 1,每次重新找肯定不合算。
设当前点为 i,上一个前缀和为 pre[i]的点为 j。对于所有位置来说,我们肯定要找前面最近的和当前点前缀和相同的点,继承过来。接下来位置分成了两种情况,上升和下降。对于上升的函数段,由于 pre[i]>pre[i−1],所以前缀和小于 i−1 的所有点也都小于 i,那么我直接继承过来就行了;对于前缀和正好等于 pre[i−1]的点,由于中间可能是凹凸不平的,我们不能很好的直接找到一个点继承过来,所以我们需要遍历[i,j]之间所有 pre[u]=pre[i−1]的点 u,并将对应位置标 1。对于下降的函数段,我们继承 j 就行。
最后注意不要忘记了 pre[i]−pre[0] 的情况。
我们再遍历一下所有点,将 bitset 位移到对应的 k,处理一下前面超出的情况,将结果和答案与一下,最后输出即可。

重构树

校赛E 小伍的王国

题目链接
维护一个树上数据结构使得能够完成

  1. 子树所有节点权值加减
  2. 任意路径所有节点权值加减
  3. 查询子树所有节点权值在区间 [l,r] 的数量
  4. 查询路径上所有节点权值在区间 [l,r] 的数量

第一行输入两个整数 n(1≤n≤50000) 和 q(1≤q≤50000) ,表示城市的个数和操作数
第二行输入一行 n 个整数 a i(1≤a i≤10e9 )
接下来输入 n−1 行,每行包含两个整数 u,v(1≤u,v≤n) ,表示点 u 和点 v 之间连有一条边,保证所有边能组成一棵树
保证输入的 u,v 满足(1≤u,v≤n) , x(1≤x≤10e9) , l,r(1≤l≤r≤10e18 )

对于像对子树与链上的修改来说,我们容易想到用树链剖分来得到我们要修改的区间,因此我们还要一个能够完成区间修改与区间求在范围内节点数量的数据结构。此时利用分块来处理区间,在块内数据有序,因此就可用二分来找出区间内元素的数量

根据树链剖分的知识,在一次查询路径中,我们最多得到log2n 的区间,每次的区间修改、查询,最多会处理到 n \sqrt n n 个块,再加上二分的复杂度 log2 n,综合复杂度应为 O ( q n ( l o g 2 n ) 2 ) O(q\sqrt n(log_2n)^2) O(qn (log2n)2)。该值大概再 2e9 到 3e9 之间,照理来说应该会超时,但是我们考虑的最坏的树链剖分的复杂度与最坏的分块复杂度冲突,因此要在此复杂度基础上除以 10 左右的常数,足以通过此题

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int maxn = 50010;
ll aa[maxn], tag[maxn], a[maxn], w[maxn];
int block[maxn];
int fa[maxn], dep[maxn], siz[maxn], top[maxn], dfn[maxn], son[maxn];
int tim;
vector<ll> ve[maxn];
int n, blo;
void init(){
    blo = sqrt(n);
    for (int i = 1; i <= n; i++){
        tag[i] = 0;
        aa[i] = w[i];
    }
    for (int i = 1; i <= n; i++){
        block[i] = (i - 1) / blo + 1;
        ve[block[i]].emplace_back(aa[i]);
    }
    for (int i = 1; i <= block[n]; i++) sort(ve[i].begin(), ve[i].end());
}
void reset(int x){
    ve[x].clear();
    for (int i = (x - 1) * blo + 1; i <= min(x * blo, n); i++) ve[x].emplace_back(aa[i]);
    sort(ve[x].begin(), ve[x].end());
}
void update(int l, int r, int x)
{
    for (int i = l; i <= min(1ll * block[l] * blo, (ll)r); i++) aa[i] += x;
    reset(block[l]);
    if (block[l] != block[r]){
        for (int i = (block[r] - 1) * blo + 1; i <= r; i++) aa[i] += x;
        reset(block[r]);
    }
    for (int i = block[l] + 1; i <= block[r] - 1; i++) tag[i] += x;
}
int query(int l, int r, ll lo, ll hi){
    int ans = 0;
    for (int i = l; i <= min(1ll * block[l] * blo, (ll)r); i++){
        ll now = aa[i] + tag[block[l]];
        if (now <= hi && now >= lo) ans++;
    }
    if (block[l] != block[r]){
        for (int i = (block[r] - 1) * blo + 1; i <= r; i++){
            ll now = aa[i] + tag[block[r]];
            if (now <= hi && now >= lo) ans++;
        }
    }
    for (int i = block[l] + 1; i <= block[r] - 1; i++){
        ll loo = lo - tag[i];
        ll hii = hi - tag[i];
        ans += upper_bound(ve[i].begin(), ve[i].end(), hii) - lower_bound(ve[i].begin(), ve[i].end(), loo);
    }
    return ans;
}
vector<int> g[maxn];
void add(int uu, int vv){
    g[uu].push_back(vv);
    g[vv].push_back(uu);
}
void dfs1(int now, int fr){
    fa[now] = fr;
    dep[now] = dep[fr] + 1;
    siz[now] = 1;
    int max_size = -1;
    for (int x : g[now]){
        if (x == fr) continue;
        dfs1(x, now);
        siz[now] += siz[x];
        if (siz[x] > max_size){
            max_size = siz[x];
            son[now] = x;
        }
    }
}
void dfs2(int now, int tp){
    dfn[now] = ++tim;
    top[now] = tp;
    w[tim] = a[now];
    if (!son[now]) return;
    dfs2(son[now], tp);
    for (int x : g[now]){
        if (x == fa[now] || x == son[now]) continue;
        dfs2(x, x);
    }
}
void make_tree(int root){
    dfs1(root, root);
    dfs2(root, root);
    init();
}
void update_son(int x, int z){
    update(dfn[x], dfn[x] + siz[x] - 1, z);
}
int query_son(int x, ll l, ll r){
    return query(dfn[x], dfn[x] + siz[x] - 1, l, r);
}
void update_chain(int x, int y, int z){//路径修改
    while (top[x] != top[y]){
        if (dep[top[x]] < dep[top[y]]) swap(x, y);
        update(dfn[top[x]], dfn[x], z);
        x = fa[top[x]];
    }
    if (dep[x] > dep[y]) swap(x, y);
    update(dfn[x], dfn[y], z);
}
int query_chain(int x, int y, ll l, ll r){//路径查询
    int res = 0;
    while (top[x] != top[y]){
        if (dep[top[x]] < dep[top[y]]) swap(x, y);
        res += query(dfn[top[x]], dfn[x], l, r);
        x = fa[top[x]];
    }
    if (dep[x] > dep[y]) swap(x, y);
    res += query(dfn[x], dfn[y], l, r);
    return res;
}
void solve(){
    int q;
    cin >> n >> q;
    for (int i = 1; i <= n; i++) cin >> a[i];
    tim = 0;
    for (int i = 0; i < n - 1; i++){
        int u, v;
        cin >> u >> v;
        add(u, v);
    }
    make_tree(1);
    while (q--){
        int op;
        cin >> op;
        int u, v, x;
        ll l, r;
        if (op == 1){
            cin >> u >> x;
            update_son(u, x);
        }
        else if (op == 2){
            cin >> u >> v >> x;
            update_chain(u, v, x);
        }
        else if (op == 3){
            cin >> u >> l >> r;
            cout << query_son(u, l, r) << "\n";
        }
        else{
            cin >> u >> v >> l >> r;
            cout << query_chain(u, v, l, r) << "\n";
        }
    }
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    solve();
    return 0;
}

技巧:数组修改某一个数后查询区间

对于不包含该数的区间,满足题意的存下个数;对于包含该数的区间再统计

2022icpc南京B

技巧:记录去掉x后区间和为sum的个数,“双向奔赴”,“折半搜索”

hdu7341 Perfect square number
给你长为 n 的数组,每个数的大小均在 [-300,300] 之间。你尽可以操作一次,可以选择数组中任意的一个数,将其变为 [-300,300] 之间的一个数。问你最大的区间个数满足这些区间和均为完全平方数。

将a[x]修改后区间和为平方数的个数=改之前区间和为平方数的个数-改之前含有x的区间和为平方数的区间个数+改之后含有x的区间和为平方数的区间个数

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 310;
int n, a[N], s[N], cnt[N * N], f[N];//f[i]记录将a[x]修改为i之后含有x的区间和为平方数的区间个数
bool check(int x) {//判断x是否是平方数
    if (pow((int)sqrt(x), 2) == x) return 1;
    return 0;
}
void solve() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        s[i] = s[i - 1] + a[i];//前缀和
    }
    int res = 0, ans = 0;
    for (int i = 1; i <= n; i++) {//枚举区间
        for (int j = 1; j <= i; j++) {
            if (check(s[i] - s[j - 1])) ans++;//ans记录修改前的个数
        }
    }
    res = ans;
    for (int x = 1; x <= n; x++) {
        memset(cnt, 0, sizeof(cnt));
        memset(f, 0, sizeof(f));
        int suml = 0;
        int sum = 0;//记录含有x的区间和为平方数的区间个数
        for (int i = x; i >= 1; i--) {
            suml += a[i];
            int sumr = 0;
            for (int j = x; j <= n; j++) {
                sumr += a[j];
                int sumn = suml + sumr - 2 * a[x];//把x删掉后含有x的区间的区间和为sumn
                if (check(sumn + a[x])) sum++;//如果含有x的区间和为平方数,那么sum++
                cnt[sumn]++;//记录去掉x后区间和为sumn的个数
            }
        }
        for (int i = 1; i <= 300; i++) {//枚举所有的平方数
            int m = i * i;
            for (int j = m; j >= 0 && m - j <= 300; j--) f[m - j] += cnt[j];//将a[x]修改为m-j  
        }
        for (int i = 1; i <= 300; i++) res = max(res, ans - sum + f[i]);
    }
    cout << res << '\n';
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        solve();
    }
    return 0;
}

互质ab-a-b

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值