window.cnblogsConfig = {//可以放多张照片,应该是在每一个博文上面的图片,如果是多张的话,那么就随机换的。 homeTopImg: [ "https://cdn.luogu.com.cn/upload/image_hosting/clcd8ydf.png", "https://cdn.luogu.com.cn/upload/image_hosting/clcd8ydf.png" ], }

2025代码源贵阳集训复盘

Day2 模拟赛 1

A

直接从后往前维护一个单调栈,取后缀最大值,然后直接更新当前 \(f(l,*)\)

B

不难发现性质,最开始一串相同的字符是无法添加的,只有 \(t\)\(s\) 中都包含才满足条件,之后贪心一下,肯定是找到第一个满足条件的子序列,可以通过记录每个数出现位置然后二分寻找,找到之后设最后一个数位置为 \(i\),那么最终右端点在 \([r,n]\) 都是可行的。

C

观察到一个长度的区间是在一段时间内能作为旋律的,所以直接枚举长度,看它在哪个时间上不能作为旋律,通过字符串哈希判断,有结论:\(s(l,r-len)=s(l+len,r)\) 那么就有长度为 \(len\) 的字串作为旋律,二分一下时间,\(check\) 里面直接用字符串哈希判断。

D

\(\sum_{j=1}^ix_j \ge 0\) 和另一个式子大于零,等价于所有前缀和都在 \(s_1 \sim s_n\) 之间,也就是等于最大子段和,所以问题变成了把 \(s_n\) 变成最大子段和需要的操作次数,操作等价于每次删掉一个 \(-1\)。操作一次一定能使 \(s_n\) 变大,并且最大子段和单调不降,现在就考虑如何使最大子段和不变。首先如果是

DAY2 数据结构 1

CF1474C

我们发现只要确定 \(x\) 之后就可以顺着推出所有的数,用一个 set 维护一下剩余数,模拟过程中判断是否可行

CF1237D

单调队列稍微搞一下,如果队头大于队尾乘 \(2\) 就不行

P4587

稍微模拟一下,设当前值域为 \([1,x]\) 能表示出 \([1,s]\) 的数,那么必须存在 \(s+1\),如果不存在,那么神秘数就是 \(s+1\),否则将 \([x+1,s+1]\) 放到值域中,和就变成了 \([1,s + sum_{x+1,s+1}]\),注意到操作次数一定小于 \(\log n\) 次,所以可以对于每个区间直接暴力模拟一遍,对于要维护下标区间上值的信息,直接使用主席树维护。

P5268

通过差分拆询问,然后直接通过莫队维护一个 \(sum\),拆括号之后一个单项式作为一个询问,每移动一次只会改变一个 \(cnt\)

Day3 数据结构 2

CF1208D

直接倒着做,线段树维护区间和。

CF1690G

对于区间类问题直接使用 set 维护起点信息,用二分去查找。

CF1913D

直接 dp,对于子序列的 dp 一般都是设 \(dp_i\) 为以 \(i\) 结尾的维护信息。显然,可以通过单调栈维护前缀最小,考虑 \(dp_i\) 能从哪里转移
:第一个小于 \(a_i\) 的数之后的点作为新区间显然都能被删除,还有单调栈中那些点,可以通过往后删删掉。最终答案一定是单调栈中的点,不然做不到以 \(i\) 为结尾。

CF2000H

相当于查询连续 \(1\) 个数,很类似于最大子段和,维护三个值:当前最大,左侧最大,右侧最大,\(push\_up\) 的时候合并一下,中间、左边和右边

P7706

非常线段树,直接维护区间信息,考虑把 神秘东西拆解一下,变成 \(a_i-b_j\)\(a_k\),或者 \(a_i\)\(-b_j + a_k\)。然后线段树维护一下这几个 tag。维护最大、最小、\(a_i-b_j\)\(-b_j + a_k\) ,再向上合并

CF1638E

颜色段均摊,合并的时候维护一下原来 \(sum\) 和新颜色 \(sum\) 的差,用树状数组区间求和,最后单点查询加上当前颜色的 \(sum\)

P6617

注意到如果 \(w - a_i\) 在前一个 \(a_i\) 之前就是没有意义的,直接设置为 \(0\)。所以只用维护非常少的点:当前点,原来点的后一个相同,原来点后一个 \(w-a_i\),新点后一个相同,新点后一个 \(w-a_i'\)

HDU7436

CF813F

线段树分治,在区间维护整个时间段都有的边,然后在线段树上跑 DFS,用可撤销并查集维护联通,如果 \(l=r\) 就给 \(find(1)\) 打个标记,然后在 merge 和 roll-back 的时候用类似前几题的那个差值方法维护被合并节点的 time。

T-shirt

我们发现如果是对于每一个顾客能买多少是不好维护的,反过来,考虑每个产品能被多少个人买,排给个序,从小到大扫一遍就结束了,问题就变成了快速求解多少个人能买得起 \(c_i\)。考虑平衡树,暴力 \(split\)\(\ge c_i\) 的位置,直接加,但考虑势能分析,平衡树维护的是相对顺序,左边比右边小,所以我们也只用维护一下相对顺序,发现只有 \([c_i,2\times c_i - 1]\) 的人会改变相对顺序,而修改一次会让原来的数变成一半,所以删掉一个数最多只需要 \(\log_{c_i}\) 次,最终修改次数 \(n\log V\)

Day5 模拟赛 2

A

非常简单,直接维护

B

从前往后枚举最大值,值域线段树二分维护后面要扔至少多少钱 。

C

不难发现无人机一定是先上升后下降,并且平飞的长度小于等 \(1\) 格,到某个点的时间就是 \(\max(t_u + 1, a[v])\),容易发现这个差不多就是最短路,并且因为是一直上升,所以 \(t_u\) 也是等于 \(u\) 点的高度,直接用 dijkstsra 求,最终查询答案时需要判断从 \(1\)\(i\) 的高度是否和 \(n\)\(i\) 的高度只相差 \(1\)

D

我们注意到如果一个大区间包含着小区间,要么不管大区间,要么大区间单独成段,所以直接拿出来考虑,然后得到的一堆区间就是不包含的了,所以排序后左右端点都是单调递增的,考虑 dp。\(dp_{i,j}\) 表示考虑前 \(i\) 个区间,分成 \(j\) 段的方案数,所以 \(dp_{i,j} = \max\{dp_{k,j-1}+r_{k+1}-l_i\}(r_k+1 >= l_i)\),因为都是单调递增的,所以用单调队列维护一下可以做到 \(O(n^2)\)。最后再把那些大区间考虑进去,按长度从大到小排个序,由大往小考虑,最终 \(ans\) 就是前 \(i\) 个大区间的长度之和 加上 \(dp_{n,k-i}\)

Day5 数据结构 3

CF459D

非常简单,只需要 维护一下类似逆序对的东西就行。

CF1849E

类似扫描线, 去扫 \(r\),考虑计算 \(f(l,r)\) 表示 \(l \sim r\) 是否满足条件,是如何转移到 \(f(l,r+1)\) 的,分类讨论;发现只有当 \(a_{r+1}\) 是最大值或最小值才会有影响,单调栈维护前缀最大最小,在线段树上直接区间修改每个 \(l\) 对应的 \(f(l,r)\)

CF526F

等价转换成区间连续段,就是一个区间内是连续的,满足 \(\max - \min = r - l\),同前,维护 \(f(l, r)=\max - \min - (r-l)\),对于每一项单独维护,最终查询 \(f(l,r)=0\) 的个数。考虑转化成图论,对于相邻的数连边,令 \(f(l, r)\) 为 点 - 边,没更新一个 \(r+1\) 点加一,然后相邻的两个对应的边加一。

Day6 字符串

CF176B

不难发现如果 \(t\) 能还原成 \(s\),肯定可以通过一次操作做到,所以一切不等于 \(s\) 但可以变化成 \(s\) 的字符串是等价的,于是就有了 \(dp_{i,0/1}\) 表示操作 \(i\) 步后是否是原串的个数,考虑转移,显然 \(dp_{i,0}=dp_{i-1,1}\)\(dp_{i,1}=dp_{i-1,0}\times(n-1) + dp_{i-1,1} \times(n-2)\),第一个式子很好理解,对于第二个,含义就是,在 \(i-1\) 地方是相同的话,

P4824

直接字符串哈希

CF1721E

kmp 自动机板子,其实就是维护 \(aut\) 表示在第 \(i\) 个后面加上字符 \(c\) 最终跑到哪里

CF898F

注意到加数 和 和的长度之差不超过 1。容易发现 \(base\) 等于 \(10\),然后直接字符串哈希做,双模数或者随机模数

P8131

首先直接上 manacher,然后就能知道以 \(i\) 为回文中心的回文串最长是多少,想到贪心,就是左边能删就删、右边能删就删,最终变成一段连续的字符串就是答案,正确性不显然,稍微模拟一下发现是对的。

P4696

发现和 kmp 很像,现在问题就转化成了如何判断子区间相同,因为是个排列,所以有比 \(x\) 大的数等于比 \(y\) 大的数,比 \(x\) 小的数等于比 \(y\) 小的数,这样其实可以用树状数组来维护了,但是进一步,只要 \(x\) 的前驱和 \(y\) 的前驱所在位置相同,\(x\) 的后继和 \(y\) 的后继所在位置相同,那么 \(x\)\(y\) 就是匹配的,但其实我们并不需要维护 \(y\) 的前驱和后继的位置,只需要 \(x\) 的前驱对应的位置在 \(b\) 数组中的值小于等于 \(y\),后继同理,容易发现每个 \(y\) 都没限制在了和 \(a\) 数组中 \(x\) 被限制的一样的位置,所以 \(x\)\(y\) 就是匹配的。

DAY7 模拟赛 3

A

拆解式子发现 \(\lceil \frac{x+y}{x|y} \rceil\) 是在 \(1\sim 2\) 之间的,考虑什么时候等于 \(1\),只有 \(x,y\) 的二进制位没有交集的时候才会等于 \(1\),不然向上取整后会直接等于 \(2\),注意到二进制下 \(1\) 最多只有 \(4\) 位,所以直接暴力容斥一下。

B

大分讨,注意长度不得大于等于 \(m \times 2-1\),不然会 WA 0

C

显然是一个二分,再考虑 \(check\) 怎么写,贪心显然是不可以的,所以肯定是 \(dp\),设 \(dp_{i,j}\) 表示考虑前 \(i\) 个数,使用的字符集为 \(j\),能不能满足要求,易得转移 \(dp_{i,j} = dp_{k,j \oplus x} \and f_{k+1,i, x}\),这里 \(f_{l,r,x}\) 表示 \(l\sim r\) 是否能填下相应个数的 \(x\),可以直接预处理出来。但是我们发现这样复杂度炸了,于是考虑交换 \(dp\) 的键和值,变成 \(dp_{j}\) 表示满足 \(j\) 字符集要求的 \(r\) 最左边在哪里,同样转移,枚举 \(x\)\(dp_{j}=\min(dp_j,pos_{dp_{j \oplus x}, x})\),这里 \(pos_{x,y}\) 表示在 \(x\) 后方第一个满足对应数量 \(y\) 的右端点,同样可以预处理出来,然后最终复杂度为 \(O(nk\log n + 2^kk\log n)\)

D

不容易注意到可以转化成一次函数,先对于一个点考虑,横坐标为到达时间,纵坐标为实际送达时间,所以 \([0,x_i]\) 之间的函数值就是 \(x_i\)\([x_i,y_i]\) 之间的函数值就是 \(x\),然后非常套路的,这个一次函数是可合并的,也就是用线段树来维护,区间信息定义为对应时间从左端点出发,最终到达右端点的实际到达时间。

DAY7 dp 1

CF1849D

直接贪心都能做,只要连续 \(1\) 段里面有 \(2\) 就能照顾到两边的 \(0\),否则只有一边,稍微模拟一下,求出多少个 \(0\) 没被覆盖,再加上连续段的数量。

CF1458B

CF1077F2

直接单调队列优化 dp,注意转移过程。

CF1874C

抽象概率 \(dp\),首先的对于图上的 dp 肯定都是从下往上考虑的,所以节点 \(u\)\(dp\) 值肯定是由 \(v\)\(dp\) 值决定的。不难发现 \(dp_u=\sum dp_v \times P_{u,v}\),也就是从 \(u\) 走到 \(v\) 的概率。然而我们发现 \(p_{u,v}\) 是和 \(u\) 无关的,只和子节点数量和 \(dp_v\) 的排位有关。稍微理解一下题目发现,每一次 Jellyfish 都会选 \(dp_v\) 最大的子节点,所以 \(p_{u,v}\) 的递推变得比较简单,稍微更改一下状态,\(f_{i,j}\) 表示子树大小为 \(i\),当前排位第 \(j\) 位取到的概率,分类讨论。当 \(j=1\),显然 \(f_{i,j} = \frac{1}{i}\),如果不是那么 \(1\) 肯定炸掉了,考虑另一个炸掉了哪条边,设那条边的排位为 \(k\),如果 \(1 < k < j\),那么 \(j\) 会变成剩下的 \(j-2\) 排位的,取到的概率就是 \(f_{i-2,j-2}\),再乘上选 \(k\) 的概率 \(\frac{j-2}{i}\),就是 \(f_{i-2,j-2} \times \frac{j-2}{i}\)。再看 \(j < k\),那么 \(j\) 会变成 \(j-1\) 排位,同理最终概率是 \(f_{i-2,j-1}\times{i-j,i}\)。最终的到转移:

\[f_{i,j}=f_{i-2,j-1}\times{i-j,i} + f_{i-2,j-2} \times \frac{j-2}{i} \]

然后就能去转移 \(dp\) 了,直接一个 dfs 搞定。

DAY9 dp 2

CodeForces - 1324E

非常简单的 dp,\(f_{i,j}\) 表示第 \(i\) 次睡觉在 \(j\) 最多的好的睡眠,注意最开始要 memset\(-inf\)

CodeForces - 2000F

简单 dp,\(f_{i,j}\) 表示到第 \(i\) 个拿了 \(j\) 分最少操作次数,\(f_{i,j} = \min(f_{i-1,j-k} + g_{i,k})\),其中 \(g_{i,k}\) 显然是可以预处理出来的。

CodeForces - 1557D

显然会有很多相同的段数,事实上,只有不超过 \(2\times m\) 个有效区间,给每个区间先编个号。显然有 dp,\(f_{i}\) 表示到第 \(i\) 个的时候最多能保留多少个,显然是直接找前面有重合的区间设为 \(j\),则 \(f_{i}=\max\{f_j\} + 1\)。但是会超时,我们可以把 \(f\) 值放到单个区间上,每次直接查询对应区间上的 \(f\) 值的最大值,就变成了区间修改、区间查询的线段树,非常容易维护,注意还要维护一个 \(pre\),在线段树上体现为当前区间最大值的下标,可以直接使用 pair<int, int>\(\max\) 转移。

CodeForces - 780F

思路很不自然,显然构建的道路都是 \(2^i\) 长度的,设 \(f_{p,l,r,0/1}\) 表示能否构建 \(2^p\) 长度的路径从 \(l\) 走到 \(r\),的正/反路径。转移也比较显然,类似弗洛伊德,注意到 \(2^p\) 长度的只能是由 \(2^{p-1}\) 长度的区间拼出来的,就变成了 \(f_{p,l,r,0}=f_{p-1,l,mid,0} \and f_{p-1,mid,r,1}\)。但是这样复杂度就爆掉了,惊人的注意到 \(f\) 的值只有 \(0/1\),可以用 bitset 维护,然后稍微变一下转移,直接维护 \(f_{p,l,0/1}\) 能到达的点集,如果 \(f_{p-1,l,0/1}\) 能走到 \(mid\),那么同样的能走到 \(f_{p-1,mid,1/0}\),所以 \(f_{p,l,0/1}\) 直接或上 \(f_{p-1,mid,1/0}\)。最后一段路径一定是一堆拼起来的,从 \(1\) 出发,走的尽可能长的 \(2^i\),还是维护点集,慢慢走,直到走不了。

QOJ - 9879

数据范围不大,可以考虑乱搞,先看如何从 \(i\) 走到 \(i+1\),肯定是走先走到 \((\min(x_i,x_{i+1}), \min(y_i,y_{i+1}))\),在分别往两边走,这样我们每次可以合并两组点,就变成了类似树的结构,而这种合并两组点显然可以用区间 dp 来做,直接设 \(f_{l,r}\) 为合并 \(l\sim r\) 的最小代价,每次合并完后我们可以把一整个区间当作 \((\min(\{x_i\}), \min(\{y_i\})\) 继续合并,只需要预处理一下区间 \(\min\) 就行。

CodeForces - 771D

考虑把交换的操作次数加到字符上,方便 dp,我们发现两个相同的字符一定不会交换,所以同一个字符之间的相对顺序是不变的,那么贡献就可以转化为新建的串 \(T\) 中的第 \(i\) 个字符 \(c\),和原串中的第 \(i\) 个字符 \(c\) 之前的元素有什么不一样的,就是其他两个字符出现次数的差值,不过这里我们只取原串中多的字符,这样可以避免算重,然后非常暴力的 dp,设 \(f_{i,j,k,0/1}\) 表示前 \(i + j + k\) 个字符中,有 \(i\)v\(j\)K\(k\) 个其它,当前位是否填的 \(v\) 的最小交换次数。然后可以直接预处理出来第 \(i\) 个字符 \(c\) 前面有多少个其它字符,就能实现 \(O(1)\) 转移。

CodeForces - 1601D

神秘贪心题,考虑按 \(s\) 排序,发现不可以,按 \(a\) 排序也不可以,很难想到可以按 \(\max{(s_i,a_i)}\) 排序。然后就可以分类讨论:

  • \(s_i> a_i\)。显然 \(s_i\) 是可以取的,之后来判断是否更优,当前的山高度肯定是 \(\le s_i\) 的,如果后面有一个 \(s_j < a_j\) 的,那么如果 \(j\) 上山至少得不让 \(i\) 走,并且之后 \(a_j\) 还会变得更大,所以不优。
  • \(s_i < a_i\)。显然,如果 \(s_i \ge d\),那么直接取,证明同上,否则不取,证明和上面类似,不过是反着的,因为让 \(i\) 去必须得让前面至少一个人不去,并且 \(i\) 去了之后 \(d\) 还会变大,不优。

综上,策略就是能去就去。

CodeForces - 311B

在直角坐标系上稍微画一下能发现一个饲养员肯定是管理结束时间减距离在一个区间内的猫,我们考虑给这个 结束时间减距离 排序,所以现在每个饲养员就是管理一个区间的猫,不难设出 \(f_{i,j}\) 表示前 \(i\) 只猫,\(j\) 个饲养员的最少等待时间,转移也很显然,\(f_{i,j} = \min(f_{k,j-1} + cost_{k+1,i})\),很容易发现 \(cost_{l,r}=a_r \times (r-l+1) - s_r + s_{l-1}\)\(j\) 的范围很小,并且每一次都是从上一层转移过来的,可以直接滚动掉,所以现在就变成了一个可斜率优化的形式,\(y=f_{k,j-1} + s_k\)\(k = a_i\)\(x = k\)\(b=f_{i,j} - a_i \times i + s_i\),直接单调队列维护下凸包。

DAY10 模拟赛 4

A

非常简单,只需要枚举一下最低级,再枚举一下哪个元素是最低级的。

B

考虑枚举子集,当前点的能对所有子集造成贡献,给每个子集打上标记,由于 \(i \and j \ge k\) 就行,所以要再跑一次后缀 \(\max\)。注意取最大值的时候不要取模,最终求和的时候再模。

D

首先考虑二维的 dp,设 \(f_{i,j}\) 表示前 \(i\) 个数,选择了 \(j\) 个数的最小 \(X\),但是显然这很不好做,然后考虑倒着做,\(f_{i,j}\) 变成后 \(i\) 个数选择了 \(j\) 个数的最小值。考虑转移,分类讨论,\(a_i > f_{i+1,j}\),显然对应的 \(f_{i+1,j}\) 可以直接越过 \(i\) 不选,所以 \(f_{i,j}=f_{i+1,j}\),再考虑其他情况,肯定得取 \(a_i\),所以 \(f_{i,j}=f_{i+1,j-1}+a_i\)。之后优化的非常神秘,考虑 \(w_x\) 为最开始为 \(x\),能走多少个点,很显然 \(w_x\) 并不是单调递增的,并且 \(w_{x+1} \le w_x+1\),有这个式子之后我们发现,通过一系列神秘的证明,发现 \(w_x\) 越大,\(x\) 一定越大,换句话说,就是 \(f_{i,j}\)\(j\) 越大 \(f_{i,j}\) 越大。再观察转移方程,由于 \(f_{i,j}\) 是单调的了,所以 \(a_i > f_{i+1,j}\) 是一个连续区间假设为 \([1,k]\),这部分 \(f\) 值直接继承,\([k+1,n]\) 部分 \(f\)\(+1\),对于有序的序列找到分界点,区间操作,并且值域非常大的,直接上平衡树,平衡树上维护 \(f\) 值,然后 \(split\) 开来对两个区间操作。注意到第一个 \(a_i \le f_{i+1,j}\) 的位置是从 \(f_{i+1,j-1}\) 转移过来的,所以我们还要复制最后一个 \(a_{i} > f_{i + 1,j}\) 点的 \(f\) 值,把他加到右半边区间,然后一起区间加。

#include <bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define fir first
#define sec second
#define endl '\n'
#define memset(a, b) memset(a, b, sizeof(a))
using namespace std;

const int N = 5e5 + 5;
const int mod = 998244353;
const int inf = 0x3f3f3f3f3f3f3f3f;

int n;
int a[N];

namespace treap{
    int tot, dl, dr, rt, tmp;
    struct node{
        int l, r;
        int val, key;//BST HEAP
        int sz, add;
    }tr[N << 1];

    int gets(int x){
        tr[++tot].val = x;
        tr[tot].key = rand();
        tr[tot].sz = 1;
        return tot;
    }

    void push_up(int p){
        tr[p].sz = tr[tr[p].l].sz + tr[tr[p].r].sz + 1;
    }

    void push_down(int p){
        if (tr[p].add){
            tr[tr[p].l].add += tr[p].add, tr[tr[p].r].add += tr[p].add;
            tr[tr[p].l].val += tr[p].add, tr[tr[p].r].val += tr[p].add;
            tr[p].add = 0;
        }
    }

    void split(int p, int x, int &l, int &r){
        if (!p) return l = r = 0, void();
        push_down(p);
        if (x >= tr[p].val){
            split(tr[p].r, x - tmp, tr[l = p].r, r);
            push_up(l);
        }
        else{
            split(tr[p].l, x, l, tr[r = p].l);
            push_up(r);
        }
    }

    int merge(int l, int r){
        if (!l || !r) return l | r;
        push_down(l); push_down(r);
        if (tr[l].key <= tr[r].key){
            tr[l].r = merge(tr[l].r, r);
            push_up(l);
            return l;
        }
        else{
            tr[r].l = merge(l, tr[r].l);
            push_up(r);
            return r;
        }
    }

    void insert(int x){
        split(rt, x, dl, dr);
        rt = merge(merge(dl, gets(x)), dr);
    }

    int Kth(int p, int x){
        push_down(p);
        int s = tr[tr[p].l].sz + 1;
        if (s == x) return tr[p].val;
        if (s > x) return Kth(tr[p].l, x);
        else return Kth(tr[p].r, x - s);
    }
}
using namespace treap;

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = n; i >= 1; i--){
        split(rt, a[i] - 1, dl, dr);
        int t = (tr[dl].sz ? Kth(dl, tr[dl].sz) : 0);
        dr = merge(gets(t), dr);
        tr[dr].add += a[i], tr[dr].val += a[i];
        rt = merge(dl, dr);
    }
    for (int i = 1; i <= n; i++) cout << Kth(rt, i) << " ";
    return 0;
}

DAY10 dp 3

CodeForces - 1096D

水题,直接 dp,前 \(i\) 个字符不含 hhaharhard 的最小操作次数。

P10156

做法非常显然,肯定要分组求,考虑如何求一个组内选 \(j\) 个人学 OI 的最小不满意度。不难想到设 \(g_{i,j}\) 为前 \(i\) 个人,选了 \(j\) 个人的最小值,但是这样我们不知道第 \(i\) 个人的贡献,考虑给 dp 多加一维,\(g_{i,j,0/1}\) 表示前 \(i\) 个人,选了 \(j\) 个人,最后一个没选的有没有 partner,

把贡献的绝对值拆开,变成 \(a_i+x\times i + a_j - x \times j\),那么转移也很显然了,\(g_{i,j,0}=\min(g_{i-1,j,1}+a_i + x \times i,g_{i-1,j-1,0})\)\(g_{i,j,1}=\min(g_{i-1,j,0} + a_j - x \times j,g_{i-1,j-1,1})\),再稍微统计一个 \(f_{i,j}\) 表示第 \(i\) 各组选 \(j\) 个人最小值其实就等于第 \(i\) 个组的 \(g_{lst,j,0}\)。最后再对每个组来一遍简单 dp 就行。

CodeForces - 1336C

这种从左或者从右加的一般都是区间 dp,很自然的设 \(f_{i,j}\) 表示 \(t\) 中从 \(i\)\(j\) 字符有多少种方式能合成出来,转移也非常显然,如果 \(t_{i-1}=s_{i-j+2}\) 那么区间就能向左边拓展,\(f_{i-1,j}=f_{i-1,j} + f_{i,j}\),右边同理。但是我们只要求新串的前缀为 \(t\),所以我们可以再 \(t\) 后面加上通配符 *,也就是和任何字母配对都可以,但是答案似乎小了一半,事实上,样例解释里面说了,一个字符的时候可以向前或者向后插,也就是 \(f_{i,i}=2\)

Gym - 104128B

显然,如果没有修改,直接单调队列优化 dp,把 \(n + 1\) 当作终点。但是加上修改后会影响 \([p,n+1]\) 的值,暴力更新的话就变成了 \(O(n^2)\),注意到修改不会影响到从 \(i(i>p)\) 走到 \(n\) 的价值,并且给的修改长度很小,所以可以记录一个从 \(i\) 走到 \(n+1\) 的价值,然后对于要修改的区间暴力更新一下,就是 \([p,\min(p+k,n+1)]\),最终答案就是 \(\min_{i=p}\{f_i + g_i - a_i\}\),因为这里 \(a_i\)\(f_i\)\(g_i\) 里面算重了。细节稍微有一点多

#include <bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define fir first
#define sec second
#define endl '\n'
#define memset(a, b) memset(a, b, sizeof(a))
using namespace std;

const int N = 5e5 + 5;
const int mod = 998244353;
const int inf = 0x3f3f3f3f3f3f3f3f;

int T, n, k, m;
int a[N], f[N], g[N], q[N], dp[N];
string s;

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);
    cin >> T;
    while (T--){
        cin >> n >> k;
        for (int i = 0; i <= n + 2; i++) f[i] = g[i] = dp[i] = a[i] = 0;
        for (int i = 1; i <= n; i++) cin >> a[i];
        cin >> s; s = ' ' + s + '1';
        int h = 1, t = 0;
        q[++t] = 0;
        for (int i = 1; i <= n + 1; i++){
            while (h <= t && i - q[h] > k) h++;
            f[i] = f[q[h]] + a[i];
            while (h <= t && f[i] <= f[q[t]]) t--;
            if (s[i] == '1') h = 1, t = 0;
            q[++t] = i, dp[i] = f[i];
        }
        h = 1, t = 0, q[++t] = n + 2;
        for (int i = n + 1; i >= 1; i--){ 
            while (h <= t && q[h] - i > k)  h++;
            g[i] = g[q[h]] + a[i];
            while (h <= t && g[i] <= g[q[t]]) t--;
            if (s[i] == '1') h = 1, t = 0;
            q[++t] = i;
        }
        for (int i = 1; i <= n + 1; i++)  g[i] -= a[i];
        cin >> m;
        for (int i = 1, p, v; i <= m; i++){
            cin >> p >> v;
            swap(a[p], v);
            h = t = 1, q[t] = max(p - k, 0ll);
            int ans = inf;
            for (int j = max(p - k, 0ll); j <= p - 1; j++){
                if (s[j] == '1') h = 1, t = 0;
                while (h <= t && f[j] <= f[q[t]]) t--;
                q[++t] = j;
            }
            for (int j = p; j <= min(p + k, n + 1); j++){
                while (h <= t && j - q[h] > k) h++;
                f[j] = f[q[h]] + a[j];
                while (h <= t && f[j] <= f[q[t]]) t--;
                if (s[j] == '1') h = 1, t = 0;
                q[++t] = j;
                ans = min(ans, f[j] + g[j]);
                // cout << f[j] << " ";
            }
            cout << ans << endl;
            swap(a[p], v);
            for (int j = p; j <= min(p + k, n + 1); j++) f[j] = dp[j];
        }
    }
    return 0;
}

CodeForces - 1242C

首先判断不合法的条件,肯定就是 \(sum \bmod k\) 不等于 \(0\) 的情况,那么我们就可以知道每个数要和哪个数交换,可以直接连边,最终如果形成了一个环的话就说明:环上的这几个点是可以通过交换变得合法的,那么最终答案肯定是若干个环拼起来的结果,并且环上经过的箱子不重复,这一步显然可以用 dp 来维护,\(f_i\) 表示能否取 \(i\) 里面为 \(1\) 的箱子,转移直接枚举子集,\(f_i = f_{i \oplus s} \and vis_s\),这里 \(vis_s\) 表示是否存在 \(s\) 中这些箱子的交换方式。由于还要输出路径,所以还要记录从哪里转移过来的。

这时我们再来考虑找环,显然要维护当前环经过了哪些箱子,和环上是怎么走的,具体的,维护每个点上一个点是什么和当前点是哪个箱子的第几个数。

#include <bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define fir first
#define sec second
#define endl '\n'
#define memset(a, b) memset(a, b, sizeof(a))
using namespace std;

const int N = 5e5 + 5, M = 17;
const int mod = 998244353;
const int inf = 0x3f3f3f3f3f3f3f3f;

int n;
int sz[N], a[M][N], s[N], vis[N], f[N], pre[N];
vector<pair<pii, int>> mark[1 << M];
map<int, pii> mp;
pii ans[N];

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);
    cin >> n;
    int sum = 0;
    for (int i = 1; i <= n; i++){
        cin >> sz[i];
        for (int j = 1; j <= sz[i]; j++){
            cin >> a[i][j];
            mp[a[i][j]] = {i, j};
            sum += a[i][j];
            s[i] += a[i][j];
        }   
    }
    if (sum % n != 0) return cout << "No", 0;
    sum /= n;
    for (int i = 1; i <= n; i++){
        for (int j = 1; j <= sz[i]; j++){
            int now = 0, flag = 1;
            int cur = a[i][j];
            vector<pair<pii, int>> p;
            while (1){
                int pos = mp[cur].fir;
                cur = sum - s[pos] + cur;
                if (mp.find(cur) == mp.end()) {flag = 0; break;}
                auto tmp = mp[cur];
                if (now & (1 << (tmp.fir - 1))) {flag = 0; break;}
                now |= (1 << (tmp.fir - 1));
                p.push_back({tmp, pos});
                if (tmp.fir == i){
                    if (tmp.sec != j) flag = 0;
                    break;
                }
            }
            if (flag) vis[now] = 1, mark[now] = p;
        }
    }
    f[0] = 1;
    for (int i = 0; i < (1 << n); i++){
        for (int sub = i; sub; sub = (sub - 1) & i){
            if (vis[sub] && f[i ^ sub]) {f[i] = 1, pre[i] = sub; break;}
        }
    }
    int now = (1 << n) - 1;
    if (f[now] == 0) return cout << "No\n", 0; 
    while (now){
        for (auto it : mark[pre[now]]) ans[it.fir.fir] = {a[it.fir.fir][it.fir.sec], it.sec};
        now ^= pre[now];
    }
    cout << "Yes\n";
    for (int i = 1; i <= n; i++) cout << ans[i].fir << " " << ans[i].sec << endl;
    return 0;
}

DAY11 组合数学 1

CodeForces-1436C

直接模拟二分,判断哪个位置一定是要大于 \(x\),的哪个位置一定小于等于 \(x\),然后稍微排列组合一下。

P9306

显然如果把最大值放在第一位,答案肯定是很小的,所以直接分类讨论,如果存在 \((n,n)\) 这样的对,那么答案就是 \(2\),总共方案就是后面的数随便放,就是 \((n-1)!\);否则答案就是 \(3\),那么一定有一个 \(n\) 在后面……

CodeForces-1696E

非常简单,先打个表看看,发现第 \((i,j)\) 的贡献就是 \(C_{i + j}^{j}\),那么一行的贡献就是 \(\sum_{j=0}^{a_i}C_{i+j}^j\),拆开了发现原式等于:

\[C_{i}^0+C_{i+1} ^ 1 +C_{i+2} ^2 + \dots +C_{i+a_i}^{a_i} \]

由于组合数的定义,\(C_{i - 1}^{j} + C_{i-1}^{j} = C_{i}^j\),稍微注意一下 \(C_{i}^0=C_{i+1}^{0}\),所以上面那个式子又可以变成:

\[=C_{i+1}^0+C_{i+1}^1+C_{i+2}^2+\dots + C_{i+a_i}^{a_i}\\ =C_{i+2}^1 + C_{i+2}^2 + \dots + C_{i + a_i}^{a_i}\\ =C_{i+a_i+1}^{a_i} \]

然后直接 \(O(n)\) 求和。

CodeForces-571A

正难则反,直接计算总数减去不合法方案。总数很好求 \(\sum_{i=0}^l C_{i+2}^2\),剩下的其实可以直接枚举最大边,假设 \(a\) 是最大边,注意到 \(a\) 长度一定大于等于 \(b+c\) 的长度,设 \(a\) 里面已经添加了 \(l'\),所以 \(b+c\) 部分添加了 \(deta=\min(l-l',a+l'-b-c)\) 这么多,那么依旧是插板法,可以填 \(0\),那么答案就是 \(\sum_{i=0}^{deta}C_{i+1}^1\),其实就是 \(\sum_{i=0}^{deta}i\) 就是等差数列求和,然后结束了。

CodeForces - 1929F

只要想到把二叉搜索树转化成序列就解决了,那么原序列会变成一个一堆 \(-1\) 和正常数的序列,要求构造单调递增的序列,考虑两个最近的非 \(-1\)\(a_i\)\(a_j\),稍微差分一下,发现总共有 \(j-i\) 个空格,现在问题变成了将 \(a_{j}-a_i\) 分成 \(j - i\) 个可以为 \(0\) 的部分插进去,然后直接插板法解决。

CodeForces - 1227F2

先把 F1 做了,设 \(f_{i,j}\) 表示做了 \(i\) 题多对了 \(j\) 题的方案数,显然有 \(f_{i,j} = f_{i,-j}\),所以严格变大的方案数其实就等于 \(\frac{sum-f_{i,0}}{2}\)\(sum\) 就是总方案数,也就是 \(k^n\)\(f_{i,0}\) 其实也不难求,事实上可以直接求 \(f_{n,0}\),肯定是有 \(i\) 个数是从不对变成了对的,\(i\) 个数从对的变成了不对的,剩下没有变化。先考虑从对的变成对的,发现肯定是要 \(a_i=a_{i+1}\),每种只能填对应的,总共有 \(m\) 种,再看从不对变成不对,这时候发现有问题了,\(a_i=a_{i+1}\) 的和不等于的不能放在一起讨论,\(a_{i}=a_{i+1}\) 时有 \(k-1\) 种可能,不等时只有 \(k-2\) 种可能。所以这时可以得到 \(a_i=a_{i+1}\) 时的方案数 \(k^m\)。否则直接枚举 \(i\)\(0 \to 1\) 的位置有 \(C_{n-m}^i\) 个,\(1\to 0\) 的有 \(C_{n-m-i}^i\) 种,每种显然只能填一种数,还要乘上 \(a_i\) 不等于 \(a_{i+1}\)\(0\to 0\) 的方案,就是 \((k-2)^{n-m-2\times i}\) 种,乘起来。

P7118

注意到 \(sz\) 比原来小的时候可以全选,那么就是用 Catalan 数,直接 \(f_{i}=C_{2\times i} ^ i - C_{2\times i}^{i-1}\),然后就是计算点数相同的时候有多少方案,发现可以直接 dfs,记 \(dp_{i}\) 为以点 \(i\) 为子树里面大小相等的小于当前树的个数,对于一个子树内,如果左子树大小小于原 galgame 的对应子树大小,直接用卡特兰数 \(dp_{i}=\sum_{i=0}^{sz_l}f_i \times f_{sz_i - i - 1}\),接下来考虑左子树大小相等的时候,\(dp_{i}=dp_l \times f_{sz_r}\) 就是右子树可以随便选,最后左子树相同的时候,\(dp_i=dp_r\),直接把这三个加起来。但是这样枚举的复杂度是 \(O(n^2)\) 的,但是我们发现可以启发式的计算,也就是每次枚举左右子树中 \(sz\) 较小的那个,那如何枚举右子树呢,我们发现可以通过总数减去不合法的,如果右子树取的点数 \(k< sz_r\) 的一定是大于原来的,如果等于的话不一定,但是对于 \(sz_l \le sz_r\) 的时候也单独计算了 左子树大小相等的情况,所以也是直接减掉就行。

CodeForces - 1237F

c++17 比 c++23 至少慢了 2 秒

我们发现放横着和放竖着是不相关的,比如说你要放 \(i\) 个竖着的,\(j\) 个横着的,在能放下的情况里,放完竖着的之后一定还有空间放横着的, 并且放横着的方案数是不受影响的,所以就可以用乘法原理直接乘起来。然后就可以考虑如何求在原来的时候放 \(i\) 个竖着的方案数,直接考虑 dp,设 \(dp_{i,j}\) 表示前 \(i\) 个格子,放 \(j\) 个的方案数,如果第 \(i\) 个不放就是 \(f_{i-1,j}\),放的话必须要求当前行和前一行都是空的,也就是 \(f_{i-2,j-1}\),直接加起来。最终答案我们可以直接枚举放了 \(i\) 个竖着的,放了 \(j\) 个横着的,总方案数就是:

\[\sum_{i=0}^{\frac{h}{2}}\sum_{j=0}^{\frac{w}{2}}C_{ew-2 \times j}^i \times f_{h,i} \times C_{eh - 2\times i}^j \times g_{w, j} \]

\(ew\) 就是原有的空列的数量,\(eh\) 是原有的空行的数量,在其中任选 \(i\) 个填上原有的方案数,再把行和列乘起来就结束了。

DAY13 模拟赛 5

A

看起来非常简单,实则非常简单,注意到最终答案的大小不会枚举太多,所以直接枚举最终答案,用 set \(O(n\log n)\) 的复杂度 \(check\) 一下。

B

简单题。注意到序列肯定是由一段 ? 和两边的字母组成,那么就可以先把对应的最小值求出来,然后再更新其他值,分类讨论一下,两个 1 时肯定填 1,交换的话段数会加 \(2\),但注意到同一段取反不管多少次都只会增加 \(2\),所以可以差分一下维护;一个 0 一个 1 的时候也是填 1 但是取反是不会增加的;两个 0 的时候肯定填 0,取反一次最终 1 的个数会增加,但是答案也会增加,同时也是一次增加一个区间,所以差分维护。注意,要按长度从大到小排序,这样才能让尽可能多的数更优。

C

DAY13 组合数学 2

CodeForces - 1795D

非常简单。

CodeForces - 1207D

稍微需要容斥一下,显然对于 \(a_i\) 相同的可以随便打乱顺序,\(b_i\) 同理,但是对于 \(a_i=b_i\) 的区间需要减去这部分的贡献,不然会算重。

C - Sum of Goodness

事实上非常简单,考虑计算每一位的贡献,肯定是有 \(a_i-1\) 个数比 \(a_i\) 小,所以排个序,选出 \(C_{i-1}^{a_i-1}\),然后后面的数可以随便选,再乘上 \(2^{n-i}\),这种单个算贡献的题一般不用担心算重,只要限制死每一位的贡献,大概率不会算重的。

CodeForces - 1666F

updating...

CodeForces - 1924D

稍微观察一下不难发现两个结论:最终括号序列一定是左边全是右括号,右边全是左括号;右括号数量等于 \(k-m\),左括号数量等于 \(k-n\);括号序列的前缀和的最小值就是右括号最终的数量。所以就可以从折线的角度考虑,这条折线一定是从 \((0,0)\) 走到纵坐标等于 \(m-k\) 的地方,再走到 \((n+m,n-m)\)。这样就是类似 Catalan 数的样子,将 \((0, 0)\) 关于直线 \(y = k-m\)\(y=k-m-1\) 对称,再分别求这两个点到终点的方案数,相减。但是这里稍微有点不同,每步是向上或者向下走的,以第一个点为例,新起点为 \(2\times k - 2\times m\),要求走 \(n+m\) 步到达 \(n-m\),可以列出方程需要多少个 \(+1\)\(x - (n + m - x) =n-m-(2\times k - m)\),接出来恰好 \(x=k\),所以最终方案数就是 \(C_{n+m}^k\),另外一个同理,最终方案数 \(C_{n+m}^{k-1}\),最终答案 \(C_{n+m}^k - C_{n+m} ^ {k-1}\)。还要判断是否合法。

DAY14 图论 1

CodeForces - 1255C

非常简单,稍微暴力模拟一下。

P9650

dijkstra 的应用条件

不难想到可以从终点反推回一号点,并且每一个点所阻拦的肯定是到出口前 \(d_i\) 小的边,所以转化到“反图”上就等价于把到 \(i\) 点的前 \(d_i\) 短路删掉,实现上我们不能只把 \(dis_v > dis_u + w\) 的入队,我们需要把全部点都入队,但是出队的时候直接判断是否还是前 \(d_i\) 短路,如果不是就 \(vis_i = 1\) 其他跟普通 dijkstra 一样了。

P5839

此题非常简单,显然可以想到使用 Floyd 处理出来把每个字符修改成另一个字符的操作数,然后很容易想到 dp,设 \(f_i\) 表示 \(i\) 之前的数合法最小操作数,非常容易得到转移 \(f_i=\min\{f_j + cost(j + 1,i)\}\),这里 \(cost\) 表示把 \([j+1,i]\) 变相同最小操作数。稍微搞搞前缀和、前缀 \(\min\) 就能搞到 \(O(nm)\)

Gym - 104857J

其实不是很复杂,首先看到最大边最小肯定想到最小生成树,但是这道题要求我们还要求一个次大边,容易发现这样是假的,所以考虑别的方法。这种图论题可以考虑去枚举满足要求的边,比如说这题去枚举最大边,那么什么时候这条边才能产生贡献呢,肯定是 \((1\to u)\)\(v \to n\) 路径上的边权的最大值小于等于这条边才能被取为最大值。所以这下就知道要去找 \(1\to u\) 路径上的最大值的最小值,这种问题是能用 dijkstra 去做的,直接 \(1\) 跑一遍、\(n\) 跑一遍结束。

P2934

最短路树 & 并查集缩点。

最短路树其实就是 \(1\)\(i\) 的最短路径构成的树,一颗树的性质就非常好,断开最后一条边肯定就是 \((fa_i, i)\) 的那条边。不难想到可以用树外边来更新答案,对于一条边 \((u,v)\) 考虑它能更新多少个点,显然由于上面的性质,那么这条边一定是从下往上更新的,也就是可能可以对 \((1,u)\)\((1,v)\) 上的边做贡献,就是 \(u,v\) 到根节点的路径上,但事实上,并不能更新到 \(1\),因为如果 \(i\) 太上面了,那么也走不到 \((u,v)\) 这条边,不难发现肯定是对 \([u,\operatorname{lca}(u,v))\)\(v\) 同理。之后就可以考虑 \((u,v)\) 的贡献肯定就是 \((1\to u) +w + (v \to 1) - (i\to1)\),具体的,新的贡献就是 \(dis_u + dis_v + w - dis_i\)。然而前面三项都是确定的,而最后一项只和当前节点有关,所以非常方便,直接对每条边的这个值排序,然后直接枚举,但是这样的枚举肯定会爆,所以这里就要使用并查集缩点,其实很简单,我们发现第一次更新之后肯定是最优的,之后就不用更新的,所以更新一次之后直接把一个区间缩成一个点,这样即使判断也只会花费 \(O(1)\),具体的,直接把更新了的点 \(merge\)\(lca(u, v)\) 上,然后在跳 \(fa\) 的时候注意是跳到 \(find(fa)\),暴力更新的复杂度就是 \(m\log n\) 的。

P6651

通过标签可以知道,肯定要拓扑排序,也不难想到可以直接在拓扑的时候求出每个点会对出度为 \(0\) 的点贡献多少条链。那么从 \(k=1\) 开始,非常容易的知道直接减去这个点对答案的贡献,但是这个点可能会下放方案到很多个字节点,导致一个点能贡献好多次,事实上这个次数就是出度为 \(0\) 的点到 \(u\) 的方案数,直接建反图,容易得出。那就需要容斥了,考虑会算重什么东西,如果 \(u\) 能走到 \(v\),那么 \(f_u\) 一定会贡献到 \(f_v\),此时就算重了 \(f_u \times d_{u,v}\) 这里指从 \(u\to v\) 的方案数,所以直接令 \(f_v \leftarrow f_v- f_u \times d_{u,v}\),最后在用总答案减去 \(f_u \times cnt_u\) 就结束了。

CodeForces - 875C

如果需要确定整个序列的顺序,其实只需要确定两两之间的顺序,只要保证两两之间都是 \(a_i\le a_{i+1}\) 的,就能确定整个数组的顺序。现在就需要确定如何修改是的相邻的两个是递增的。考虑到每个数只有大写和小写两个状态,很难想到可以使用 2-SAT。具体的,很容易可以判断:

  • 如果存在一位 \(j\) 使得 \(a_{i,j} < a_{i + 1,j}\),那么肯定是大小写相同,一定就是 \(u \to v \and v' \to u'\),因为如果 \(u\) 小写了,\(v\) 一定能小写,但是 \(u\) 大写 \(v\) 不一定大写,而 \(v\) 大写 \(u\) 一定大写,所以只能是从 \(v' \to u'\)
  • 如果存在一位 \(j\) 满足 \(a_{i,j} > a_{i+1,j}\),那么一定是 \(u\) 大写,\(v\) 小写,得出 \(u' \to v\),但是这只有一条边,而 2-SAT 都是 \(\and\) 左右分别连边的,所以稍微改一下 \(u\to u' \and v' \to v\),表示这如果 \(u\) 小写一定要变成大写,\(v\) 大写一定要变成小写。
  • 如果不存在这样的一位,并且 \(a_{i+1}\)\(a_i\) 的前缀,那么直接不可能。

然后直接连边跑一遍 2-SAT,再判断一下拓扑序看 \(u\) 是大是小。

2-SAT 的理解方式有点奇怪,连边的含义应该就是如果 \(u\) 一定能推出来 \(v\),每次固定要连两条边,而且不能连双向边。

P6062

非常类似 wll 的形式,行为左部点、列为右部点然后对于每一个 * 连边,但是发现这样会炸、因为一个木板只能全放在泥土上,所以考虑找出每一条可能的木板,也就是能覆盖最长的横着的或竖着的的所有木板,然后再去看每一个点,现在就是类似最大流了了,横着的木板属于源点,竖着的木板属于汇点,选取尽可能少的木板让全部点覆盖上,此时的最大流一定就是答案。如果有点没有覆盖木板,说明残留网络中一定还有增广路,就不是最大流,也不会算多,每条边的容量都是 \(1\),所以选了较优的边之后不会再来取那些被包含的边。

DAY15 模拟赛 6

A

推式子,非常显然是推式子,首先肯定能想到 \(f_i\) 为到 \(i\) 价值总和,不难想到肯定是 \(\sum_{j=1}^{a_i} C_{a_i}^j \times (f_{i-1} + v_i^j) + f_{i-1}\),把 \(f_{i-1}\) 提出来之后就是 \(2^{a_i} \times f_{i-1} + \sum_{j=1}^{a_i}C_{a_i}^j \times v ^ j\),后面这部分看起来很不好维护,但是注意到二项式定理,后面那项直接变成 \((v_i + 1) ^ {a_i}-1\) 直接快速幂结束。

B

注意到假如一个序列 \(a_1 \times a_2 \times \cdots \times a_n = x\),那么有 \(\frac{n}{a_2} \times \frac{n}{a_2} \times \cdots \times \frac{n}{a_n} = \frac{n ^ n}{x}\),也就是说我们可以得到当 \(x < n^m\) 时和 \(x > n^m\) 时是完全相同的所以 \(\le n^m\) 的答案就变成了 \(\frac{sum + cnt(x = n^m)}{2}\),只需要求解 \(x=n^m\) 的情况。

C

非常不简单。容易发现 \(B\) 一定是在右下角,\(A\) 一定是在左上角,用一条折线隔开的。所以就可以考虑枚举折线的拐点,比如说 \(f_u\) 表示枚举到了横坐标为 \(x_u\) 的地方,并且令 \(u\) 点为拐点,所以 \(u\) 就能从在她左下边的点转移过来,转移的代价就是 \(v\) 右下角横坐标小于 \(X_u\) 的点取 \(b\) 的价值。这里算一个技巧?就是扫到当前点的贡献不要包含 \(X_u\) 上其它点的贡献,这部分之后再来算。这样转移可以做到 \(O(n^2)\),但是这样扫描线的操作很容易想到可以使用线段树维护信息。来看看要维护什么信息,但是看到有两个数 \((a_i,b_i)\) 不是特别好维护,那么就可以假设先全填 \(a_i\),然后再把每个点的权值当作 \(-a_i + b_i\),目标就是取这些点和的最大值。那么线段树就不难想了,首先肯定要更新当前点的答案,设当前点为 \((x,y)\) 那么一定就是从纵坐标在 \([0,y]\) 的地方取最大值转移过来,但是依旧不好维护新点右下角那部分的贡献,所以考虑把点的贡献直接更新到其它点上面,还是 \((x,y)\) ,显然他能对 \(y'\in[y,n] \and x' \in [1,x]\) 的点产生贡献。

稍微理解一下,当前点维护的最大和是不包括当前横坐标上的,所以这个点在更新的时候也要更新自己上面的点,这就引申到了另一个问题,纵坐标相同的时候如何修改,一定是从上往下修改,因为这样能强制加上当前点下面的点的贡献。

然后就做完了,把纵坐标当作线段树上的节点,区间维护最大值、最大和、单点修改最大值、区间修改区间和。线段树上叶子节点的最大值就代表着最后一个拐点的纵坐标为 \(i\) 时最大贡献。

稍微感性理解一下为什么直接更改当前点对其他点的贡献为什么是对的,因为如果另一个点 \(v\) 要选 \(u\),那么他同样会选到 \(u\) 右下方的 \(w\),此时即便 \(w\) 造成的是负贡献,他也必须选上,不然就不合法了,所以可以直接 \(w\) 修改对 \(u\) 的贡献。

还有一点要注意:要在线段树上加一个纵坐标为‘ \(0\) 的点,如果取到代表当前节点是第一个拐点。

注意 pushdown 后清零 tag。

#include <bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define fir first
#define sec second
#define endl '\n'
#define memset(a, b) memset(a, b, sizeof(a))
using namespace std;

const int N = 2e5 + 5;
const int mod = 1e9 + 7;
const int inf = 0x3f3f3f3f3f3f3f3f;

int T, n;
int a[N], b[N], x[N], y[N];
vector<array<int, 3>> v;

int mx[N << 2], add[N << 2];

void upd(int p, int l, int r, int k){
    mx[p] += k;
    add[p] += k;
}

void push_down(int p, int l, int r){
    if (add[p] == 0) return ;
    int mid = l + r >> 1;
    upd(p << 1, l, mid, add[p]);
    upd(p << 1 | 1, mid + 1, r, add[p]);
    add[p] = 0;
}

void push_up(int p){
    mx[p] = max(mx[p << 1], mx[p << 1 | 1]);
}

void update(int p, int l, int r, int x, int y, int k){
    if (l > y || r < x) return ;
    if (x <= l && r <= y) return upd(p, l, r, k), void();
    push_down(p, l, r);
    int mid = l + r >> 1;
    if (x <= mid) update(p << 1, l, mid, x, y, k);
    if (mid < y) update(p << 1 | 1, mid + 1, r, x, y, k);
    push_up(p);
}

void updmax(int p, int l, int r, int x, int k){
    if (l == r) return mx[p] =  k, void();
    push_down(p, l, r);
    int mid = l + r >> 1;
    if (x <= mid) updmax(p << 1, l, mid, x, k);
    else updmax(p << 1 | 1, mid + 1, r, x, k);
    push_up(p);
}

int query(int p, int l, int r, int x, int y){
    if (r < x || y < l) return 0;
    if (x <= l && r <= y) return mx[p];
    push_down(p, l, r);
    int mid = l + r >> 1, res = 0;
    if (x <= mid) res = max(res, query(p << 1, l, mid, x, y));
    if (mid < y) res = max(res, query(p << 1 | 1, mid + 1, r, x, y));
    return res;
}

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);
    cin >> T;
    while (T--){
        cin >> n;
        vector<int> vt;
        for (int i = 1; i <= n; i++){
            cin >> x[i] >> y[i] >> a[i] >> b[i];
            vt.push_back(y[i]);
        }
        sort(vt.begin(), vt.end());
        vt.erase(unique(vt.begin(), vt.end()), vt.end());
        int sum = 0;
        v.clear();
        for (int i = 1; i <= n; i++){
            y[i] = lower_bound(vt.begin(), vt.end(), y[i]) - vt.begin() + 2;
            v.push_back({x[i], y[i], b[i] - a[i]});
            sum += a[i];
        }
        sort(v.begin(), v.end(), [](array<int, 3> x, array<int, 3> y){return (x[0] == y[0] ? x[1] > y[1] : x[0] < y[0]);});
        for (int i = 1; i <= ((n + 1) << 2); i++) mx[i] = 0, add[i] = 0;
        for (auto [tmp, pos, k] : v){
            updmax(1, 1, n + 1, pos, query(1, 1, n + 1, 1, pos));
            update(1, 1, n + 1, pos, n + 1, k);
        }
        cout << sum + query(1, 1, n + 1, 1, n + 1) << endl;
    }
	return 0;
}

DAY15 图论 2

CodeForces - 1679D

非常简单,显然需要二分,二分之后重新建图,直接跑一遍拓扑排序,如果有的点没有拓扑序的话就说明有环,可以无限走,就是合法的。否则作为一个 DAG 非常容易可以求出来最长路径长度。

CodeForces - 1213F

非常简单,显然直接强连通分量,如果两个点位于一个连通分量内说明字母相同,并且注意到,由于连得边是小于等于的边,所以事实上,一个缩完点之后的 DAG 是可以全部点相同的字母的,所以策略就是对于拓扑序 \(\le k\) 的,直接填对应字母,不然全填 z

CodeForces - 2000G

正着做很难,发现反着做好像简单一点,直接记录 \(dis_u\) 为从 \(u\) 出发到 \(n\) 的最晚出发时间,要最晚出发时间最晚这东西求最大值也是能用 dijkstra 做的。那么多就多在能不能走 \(l1\)。这里需要理解一下:假设到达这个点的时候需要打电话,那么就可能需要走过去或者等待电话打完再坐车,那么会不会有一个点能使得不等待直接坐车吗,并且更优吗?显然不可能,因为上面考虑了的两种情况之际上已经包含了 等待再坐车 这肯定是不劣于 刚好甚至晚到直接坐车的,并且走路可能更快,所以直接用最晚到达这个点的时间更新就行,就是 dijkstra 板子了。只是在更新最大值的时候需要多判断一下。

P2416

不难想到边双联通分量,考虑求出来之后能干什么,考虑到边双里边有性质:任意两点直接都有两条不重合的路径,所以在边双连通分量中,如果有一条边是有贡献的,就一定能取到,并且一定能顺利的走到下一个边双连通分量中。按边双连通缩点之后还有一个性质。就是会构成一颗树,所以 \(u\to v\) 的路径就是唯一的,直接数上前缀和,最终答案就是判断路径上是否权值和大于 \(0\),这里注意,割边也可能有贡献,所以我们把边的贡献放到深度较深的节点上,这样最终路径和就是 \(s_u + s_v - 2\times s_{lca_{(u,v)}} + a_{lca_{(u,v)}}\),这样就不会算多边,\(a\) 只代表点权。

P11907

看起来非常简单,首先三维的一维的没有区别,直接转化成序列操作,具体的连边就是当前点向上下左右前后六个方向连边,然后就能处理出来每个点到餐厅的距离。但是我们要求最短长度的最大值,一般都是边权,但是这东西是点权,所以得考虑把点权转化成边权,所以边权就变成了连接两点的 \(dis\) 的最小值,因为走了这条边一定会走这两个点,所以是正确的。之后就变成了最大生成树的问题了,实际上就是最大生成树上 \(u \to v\) 的边权最大值。然后就能引出 kruscal 重构树,非常的简单,直接用结论答案就是 \(val_{lca_{(u,v)}}\)

vector 小技巧,使用 vector<vector<vector<int>>> a; 然后对于每一维进行 resize()

DAY17 数上问题 1

CodeForces-1336A

贪心地选,显然肯定会把叶子节点先填满,再去填子节点数较小的点,因为一个点建工业城市造成的贡献为 \(dep_{u} - 1 -cnt_u\)\(cnt_u\) 表示以 \(u\) 为子树有多少个工业城市,但是又有贪心,先填下面的,所以一个点的 \(cnt_u\) 其实就是 \(sz_u-1\),稍微算一下这两个数,然后排个序,选前 \(k\) 大的。

P5536

首先这肯定是贪心,并且要求这一坨点到叶子节点距离最小值最大,所以不难想到树的重心,就是直径的终点,那么直接确定那个点,然后去 bfs 取前 \(k\) 个点,最后再算一下距离。或者直接从叶子节点开始 bfs,知道剩 \(k\) 个节点,再去计算距离。

CodeForces-771C

显然就是树形 dp,考虑设 \(f_{u}\) 表示到 \(u\) 节点跳的步数的总和,但是发现合并 \(u,v\) 信息的时候可能 \(u\) 的最后一步跳不满 \(k\) 步,\(v\) 也一样,所以最终跳的步数不是左边加右边,可能还要减 \(1\)。所以考虑 \(f_{u,i}\) 表示到 \(u\) 节点,最后还剩 \(i\) 步的总步数,但是不只是走到根节点的步数,还有从左边走到右边的步数,此时新加进来了节点 \(v\),然就可以分类讨论了,假设走到 \(v\) 的时候还剩 \(j + 1\) 步,那么走到 \(u\) 的时候就只剩 \(j\) 步,注意模 \(k\)

  • 可以发现如果 \(k-i \le j \or k-j \le i\),就能合并,所以总方案数就是左边所有点到右边所有点跳的步数,减去点对个数,稍微推一下,发现就是 \(f_{u, i}\times cnt_{v,j} + f_{v,j} \times cnt_{u,i} - cnt_{u,i} \times cnt_{v,j}\)
  • 如果不满足条件,那么就没有减去的步数,直接就是 \(f_{u,i} \times cnt_{v, j} + f_{v,j} \times cnt_{u,i}\)

这里 \(cnt_{u,i}\) 表示 \(u\) 节点走到 \(u\) 还剩 \(i\) 步的节点数,接下来转移也非常简单了分别对两个转移,\(f_{u,i} \leftarrow f_{u,i} + f_{v, i+1}\)\(cnt_{u,i} \leftarrow cnt_{u,i} + cnt_{v,i+1}\),注意取模,但是注意到,如果 \(i = k-1\) 时,\(i+1 = k\) 也就是一步不剩了,从 \(v\) 跳到 \(u\) 要额外跳一步,总步数就是加上 \(cnt_{v,i+1}\)\(cnt\) 保持不变。但是注意到我们还要加上自己的步数,显然就是 \(cnt_{u,0} \leftarrow cnt_{u,0}+1\),注意这一步要在转移完全部的 \(v\) 再做,不然会算重从 \(u\) 出发的点对。

最终答案就是每个节点的 \(f_{u,i}\) 值加上之前记录的从左边跳到右边的步数和。

CodeForces - 2033G

显然答案就是 \(u\) 向上走小于等于 \(k\) 步后再向下走能到达的最大距离,这样维护最大值显然是能用倍增做的,考虑设 \(f_{u}\) 表示从 \(u\) 节点向下走最多能走多少步,答案就是 \(i + f_{fa_{u,i}}\),所以可以倍增维护这个东西,但是我们发现那个最大值一定不是在当前子树内的,所以我们要记录最大值和次大值,但是我们发现这个非常难维护,于是我们就考虑改一下状态,\(f_{U,0}\) 表示 \(fa_u\) 不包含 \(u\) 最大值,这个就只用在 dfs 的时候维护一下一个点的最大值和次大值就行。考虑如何倍增,\(f_{u,i}\) 表示从 \(u\) 向上 \(2^{i}\) 步的最大距离,于是就可以写成 \(f_{u, i} = f_{fa_{u,i-1},i-1} + 2^{i-1}\) 记得和 \(f_{u,i-1}\)\(\max\),这样维护能保证是正确的,一定是这个区间里面能到达的距离最大值,查询也非常简单,直接往上跳,注意往上跳的时候也要类似的加上 \(lst\),就是之前跳了多少距离,还要和 \(\min(dep_u - 1,k)\) 取个 \(\max\)

洛谷 - P4949

考虑直接拿一条边出来

CodeForces - 1709E

显然我们可以把每个顶点当作 \(LCA\) 来求一遍,如果有两个顶点是不合法的,一定需要替换一个节点,那么我们可以把 \(lca\) 替换成一个非常大的数这样其他子节点也不可能向上贡献了,这样肯定是最优的,然后就可以乱搞了,对于每个节点维护一个 set,里面存所有到 \(lca\) 的异或和,不难发现如果两颗不同的子树内有异或和为 \(a_{lca}\) 的,肯定就是要修改的,不然就合并这几个 set,用启发式合并可以做到 \(O(n\log^2n)\)

CodeForces - 1794E

HASH + 换根 dp

首先如果要匹配一个序列的话不难想到使用 HASH 的方法,把整个序列压缩成一个 hash 值,然后判断 hash 值是否相同,这里就可以使用双模数哈希,避免被卡。Hash 值就直接定义成 \(Base ^ {dep_u}\),直接比对就行,转移稍微思考一下也能出来。但是序列少了一个数,那么我们考虑另一个数随便填的贡献,直接就是 \(Base^x\) 就判断 \(target\) 和当前 HASH 是否就之差 \(Base\) 的次幂。

DAY18 模拟赛 7

A

注意到最长上升子序列在随机数据的情况下长度只有 \(\sqrt n\),可以直接暴力,考虑删点,每次删掉对应点,判断删掉的点是不是 \(\operatorname{LIS}\) 上的点,如果是那就重新跑一遍 \(n\log n\) 的暴力,如果不是那就不用管,因为之前的 \(\operatorname{LIS}\) 肯定是不劣于经过当前点的序列的。总复杂度就是 \(O(n \sqrt n \log n)\)

B

并没有非常困难。考虑最朴素的暴力,直接枚举所有子数组,扫一遍判断合不合法,容易推出一个奇数位和减去偶数位和,或者偶数位和减去奇数位和的式子,但是发现这样十分不好做,既要维护奇数位、偶数位的和、最小值,还要在找答案的时候分类讨论,所以考虑换个角度。还是回到那个固定左端点扫右端点的暴力,发现每次对于最后一位数都是取反之后再加上 \(a_i\),形式化的,假设 \(a_l\) 一直拓展到 \(i-1\) 位时,数字变成了 \(x\),那么再往后一位就变成了 \(-x+a_i\)。我们发现对于全部的数都是这样的,并不用分奇偶数,所以十分方便,那么怎样对所有到 \(r\) 这一位的 \(a_l\) 进行复制呢?因为是一样的,所以可以直接打上懒标记 \(neg\)\(now\),分别表示 \(a_i\) 的正负和加了多少。之后来考虑一下怎样是合法的,只需要当前值为 \(0\) 就行,那就可以直接开一个 map 存一下。那现在就得考虑里面存什么值了,我们知道最终从 map 拿出来后的结果是 \(-1^{neg} \times X + now\),假设我们要把 \(Y\) 存入 map,那就有 \(-1^{neg} \times X + now=Y\),稍微解出来就知道 \(X = (Y -now) \times -1^{neg}\),也就是把这个值插进 map。再考虑判复数情况,也就是 \(-1^{neg} \times X + now < 0\),发现需要分类讨论:如果 \(2|neg\) 也就直接 \(X+now <0\),这样显然是 \(X\) 越小整体越小,从 map.begin() 开始取;反过来,\(-X+now<0\)\(X\) 越大整体越小,从 mp.end() 开始找。等于 \(0\) 的时候也类似取正负数,直接加进答案里。

#include <bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define fir first
#define sec second
#define memset(a, b) memset(a, b, sizeof(a))
#define endl '\n'
using namespace std;

const int N = 3e5 + 5, M = 1e7 + 5;
const int inf = 0x3f3f3f3f3f3f3f3f;

int T, n;
int a[N];
map<int, int> mp;

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin >> T;
	while (T--){
		cin >> n;
		for (int i = 1; i <= n; i++) cin >> a[i];
		mp.clear();
		int neg = 0, now = 0, ans = 0;
		for (int i = 1; i <= n; i++){
			neg ^= 1, now = -now + a[i];
			if (neg) mp[a[i] - now]++;
			else mp[now - a[i]]++;
			while (mp.size()){
				if (neg){
					auto pos = *mp.begin();
					if (pos.fir + now < 0) mp.erase(mp.begin());
					else break;
				}
				else{
					auto pos = *--mp.end();
					if (-pos.fir + now < 0) mp.erase(--mp.end());
					else break;
				}
			}
			if (neg) ans += mp[-now];
			else ans += mp[now];
		}
		cout << ans << endl;
	}
    return 0;
}

D

不难发现有点像最大权闭合子图的样子,就是有 \(m\) 个限制条件 \(n\) 个点,如果要取到这个点的贡献必须要取对应的限制条件,于是就可以建图,把所有树上的点放在源点一侧,连上对应的点权,把限制条件放在另一边,连向汇点,容量为限制条件的 \(c_i\)。然后将每个点与和它对应的限制条件连起来,容量为 \(+\infty\),直接跑最大流,答案就是 \(\sum a_i - maxflow\),可以拿到 50pts。进一步,因为这东西是在树上跑的,所以考虑把这张图重新放到树上面,每个节点连向源点,有限制条件的点连向汇点,能从源点流向汇点的,就是从源点流向 \(x_i\) 子树下距离小于等于 \(k_i\) 的点流向 \(x_i\) 再流向汇点,那么我们想让流量尽可能大,肯定要多流,那么每次肯定要流尽可能深的点,也就是先把深的点流上来,计算贡献。我们发现这之和深度有关,于是就可以拿 map 统计每个深度总共有多少的容量,每次遍历到 \(u\) 点先将 \(mp_u\) 处理出来,这里可以启发式合并,合并复杂度就是 \(O(n\log n)\) 的。合并完之后就可以处理限制条件了,因为有 map 就可以直接二分到最后一个深度小于等于 \(dep_u + k_u\) 并且有剩余容量的深度,然后直接暴力从深处向上更新,每次将 \(c_i\) 减去对应的容量,表示流过了那么多流量,细节有那么一点点多。稍微理解一下,这样均摊下来所有点只会进出 map 一次,复杂度依旧是 \(O(n\log n)\) 的。

#include <bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define fir first
#define sec second
#define memset(a, b) memset(a, b, sizeof(a))
#define endl '\n'
using namespace std;

const int N = 3e5 + 5, M = 1e7 + 5;
const int inf = 0x3f3f3f3f3f3f3f3f;

int n, m, ans;
int a[N], dep[N];
vector<int> g[N];
vector<pii> p[N];
map<int, int> mp[N];

void dfs(int u){
	mp[u][dep[u]] = a[u];
	for (auto v : g[u]){
		dep[v] = dep[u] + 1;
		dfs(v);
		if (mp[u].size() < mp[v].size()) swap(mp[u], mp[v]);
		for (auto it : mp[v]) mp[u][it.fir] += it.sec;
	}
	for (auto [k, c] : p[u]){
		auto pos = mp[u].upper_bound(dep[u] + k);
		if (pos == mp[u].begin()) continue;
		pos = prev(pos);
		while (pos != mp[u].begin()){
			if (pos -> sec <= c) c -= pos -> sec, ans += pos -> sec;
			else break;
			auto tmp = pos;
			pos--, mp[u].erase(tmp);
		}
		if (mp[u][pos -> fir] >= c) mp[u][pos -> fir] -= c, ans += c;
		else ans += mp[u][pos -> fir], mp[u].erase(pos);
	}
}

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin >> n >> m;
	for (int i = 2, x; i <= n; i++) cin >> x, g[x].push_back(i);
	int sum = 0;
	for (int i = 1; i <= n; i++) cin >> a[i], sum += a[i];
	for (int i = 1, x, k, c; i <= m; i++){
		cin >> x >> k >> c;
		p[x].push_back({k, c});
	}
	dfs(1);
	cout << sum - ans;
    return 0;
}

DAY18 树上问题 2

洛谷 - P8578

非常简单,注意到直接填 dfs 序就行。

CodeForces - 1244D

注意到一个点最多只能连两条边,所以直接暴力枚举链上最后两个点填什么颜色,一路填下去看最终 \(cost\)

CodeForces - 1790F

注意到填上去一个数需要找的距离肯定不会特别大,所以尝试直接每次从当前节点 bfs 一遍,但是发现 TLE 了,这样做的复杂度其实是不正确的,可以被卡掉,考虑再换一种思路,去求每一个点到黑节点的最大值,这样染到当前 \(u\) 的时候答案就是 \(dis_u\),再去 bfs 一遍更新别的 \(dis\),这样就对了,但是注意剪枝,每次 bfs 到的距离如果大于当前 \(ans\) 就结束。考虑证明复杂度,注意到如果填了 \(x\) 个点,那么最多会跑 \(\frac{n}{x}\) 步,对于前 \(\sqrt n\) 个点,差不多每个点可以跑满 \(n\) 次,但是在 \(\sqrt n\) 个点之后,最多只会跑 \(\sqrt n\) 步,这样复杂度肯定是跑不满 \(n\sqrt n\) 的,肯定可以过。

Gym - 102012G

点-边

显然可以对于每一个点算 \(C_{cnt}^k\) 但是这样会算重,因为有的路径会有很多条重合的边,考虑点减边,在一颗子树中,一定有点数减去边数 \(=1\),这样的话我们对于计算方案数也可以这样做,考虑一条边,如果会经过这条边肯定也会经过两边的顶点,于是这样就算重了这若干条路径的贡献,这就需要减去边上的 \(C_{cnt}^k\) 就结束了,我们直接对于每条路径树上差分,点和边分别考虑,最后从下往上做一遍前缀和,计算答案。

DAY 19 数论

CodeForces-947A

线性筛。

首先我们可以得到式子 \(\lceil\frac{X_1}{b}\rceil \times b = X_2\),这里 \(b|X_2\),所以对于每个质因数 \(b\)\(X_1\) 的取值范围就是 \([X_2-b+1,X_2]\),再去看 \(X_0\) 的取值范围,同理有 \([X_1-a+1,X_1]\)\(a|X_1\),稍微贪心一下,发现 \(X_1\) 尽可能小,并且 \(a\) 尽可能大,而对于 \(X_1\) 也是一样的,注意到 \(X_1\) 的取值范围就是 \(X_2\) 减去最大的质因数到 \(X_2\),所以可以直接枚举 \(X_1\),在筛质数的时候记录一下每一个数对应的最大的质因数,枚举 \(X_1\) 的时候记录 \(X_0\) 的最小值。

CodeForces-1499D

一般我们都将 \(a\) 写做 \(k_1\gcd(a,b)\)\(b\) 变成 \(k_2\gcd(a, b)\)\(\operatorname{lcm}(a,b)=\frac{ab}{\gcd(a,b)}\),于是原式变成了 \(k_1k_2c\gcd(a,b)-d\gcd(a,b)=x\to \gcd(a,b)=\frac{x}{k_1k_2c -d}\),容易得到 \(\gcd(a,b)\)\(x\) 的一个因数,而整数对 \((a,b)\) 对应了唯一的 \(\gcd(a,b),k_1,k_2\),所以计算 \((a,b)\) 就等价于计算这三项满足方程的数量,考虑直接枚举 \(\gcd(a,b)\),这样就能知道 \(k_1k_2\) 的乘积,考虑计算 \(k_1,k_2\) 的数量,由于 \(a=k_1\gcd(a,b),b=k_2\gcd(a,b)\) 所以 \(\gcd(k1,k2)=1\),也就是 \(k_1k_2\) 中的一种质因数只能再其中一个里面出现,也就是每种质因数有两种可能,总可能情况就是 \(2^p\) 这里 \(p\) 指质因数种数。枚举 \(\gcd(a,b)\)\(O(\sqrt x)\) 再乘上 \(T\) 能过,但是注意要预处理出每个数质因数的个数,需要一点常数优化。

洛谷-P12021

不难发现每一条限制最终会形成很多条链,并且互不相关,于是可以对于链的贡献计数还有链的条数。链的贡献非常好做,就是最大独立集, \(f_{i}\) 表示到第 \(i\) 位的方案数,转移就是取或不取,分别是从 \(f_{i-2}\)\(f_{i-1}\) 转移过来。现在考虑条数,也非常难,我们要求的是每条单独的链有多少条,所以去重非常困难,考虑先求大于等于 \(len\) 的熟练,再减去 \(\ge len+1\) 的,就是答案,求法非常抽象,每一次其实都能求出来那个地方至少会被经过一次,画一条数轴不难发现,从 \(\frac{n}{k} \sim n\) 的所有点至多跳 \(0\) 次,但从 \(1\sim \frac{n}{k}\) 这些点跳过来的时候必定会经过一次,那就需要减去前面的贡献,也就是要求至少跳 \(1\) 次有多少种,仿照前面的思路,\(\frac{n}{k^2}\sim\frac{n}{k}\) 这段区间会跳一次,同理前面的也必然会经过它,不过它也会对后面有贡献,但事实上,这从段区间的点出发后一定会跳到下一段区间,所以下一段区间的点只要减去这一段区间就能得到对应跳 \(i\) 步的所有不重合的链,实际上就是 总方案数减去前面来的方案数 就是当前向后跳的方案数。

#include <bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define fir first
#define sec second
#define memset(a, b) memset(a, b, sizeof(a))
#define endl '\n'
using namespace std;

const int N = 2e7 + 5, M = 40;
const int mod = 998244353;
const int inf = 0x3f3f3f3f3f3f3f3f;

int T, n, k;
int f[M];

int ksm(int a, int b){
	int res = 1;
	while (b){
		if (b & 1) res = res * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return res;
}

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin >> T;
	f[1] = 2, f[2] = 3;
	for (int i = 3; i < M; i++) f[i] = (f[i - 1] + f[i - 2]) % mod;
	while (T--){
		cin >> n >> k;
		int tmp = n;
		vector<int> v;
		while (tmp) v.push_back(tmp - tmp / k), tmp /= k;
		for (int i = 0; i < v.size() - 1; i++) v[i] -= v[i + 1];
		int ans = 1;
		for (int i = 0; i < v.size(); i++) (ans *= ksm(f[i + 1], v[i])) %= mod;
		cout << ans << endl;
	}
	return 0;
}

洛谷-P12952

稍微转化一下,然后线性同余方程。

线性同余方程

结论\(ax \equiv b \pmod n \to ax + kn = b\) 通过扩展欧几里得定义解除一组解 \(x_0,k_0\) 对于方程 \(x_0a + k_0n=\gcd(a,n)\),最后原方程为

\[x_0 \frac{b}{\gcd(a,n)} a + k_0\frac{b}{\gcd(a, n)}n=b \]

带入一下即可求出一组解。考虑求通解,若 \(\gcd(a,n)=1\) 那么有

\[x=x_0+nt\\k=k_0-at \]

其中 \(t\) 为任意整数,如果要求特解,如最小整数解,则有

\[x=(x\bmod t+t)\bmod t \]

其中有

\[t=\frac{n}{\gcd(a,n)} \]

一般如果要使 \(|x|\) 尽可能小,先求出最小非负整数解 \(x_1\),再取最大负数解的相反数 \(-(x_1-t)\)

P6583 回首过去

整除分块。

不难得出结论,如果一个分数可以化简为有限循环小数,那么肯定有:分母只有因子 \(2\)\(5\)。考虑直接拆分数,变成 \(\frac{bc}{ac}\)\(\frac{b}{a}\) 就是约分之后的数,可能可以再约不过我们发现对答案没有影响,再看 \(c\),我们假设 \(c\) 中不含因子 \(2\)\(5\),这样就不会算重了,现在可以开始求方案数了。可以直接枚举 \(a\)\(c\) 的取值范围就是 \(1\sim\lfloor\frac{n}{a}\rfloor\)\(b\) 的取值范围就是 \([1,\lfloor\frac{n}{c}\rfloor]\),这样的复杂度只能 \(O(n^2)\),但是注意到,\(a,c\) 确定之后 \(b\) 就能在合法的取值范围内随便选择,贡献就是 \(\lfloor\frac{n}{c}\rfloor\),但是这依旧不好做,考虑换一个数枚举,枚举 \(c\),这样 \(a\) 的取值范围是单调递减的,\(b\) 还是一样直接贡献答案,答案就是 \(\lfloor\frac{n}{c}\rfloor \times f(\lfloor\frac{n}{c}\rfloor)\),这样就有点类似整除分块了,但是怎么求一个块里面 \(c\) 的数量,显然可以容斥,不难得出不含 \(2\)\(5\) 因子的数的个数,乘起来结束。

posted @ 2025-07-28 11:35  CCF_IOI  阅读(18)  评论(0)    收藏  举报