实践

news/2025/10/14 1:08:49/文章来源:https://www.cnblogs.com/BrillianceZ/p/19139832
  • ans多取合法方案的max/min结果肯定不劣。

  • 对于操作“change x y:把a[x]修改为y”,无论是提前continue掉还是循环末尾,一定要记得令a[x]=y!!!

  • 模数MOD特殊一定要注意!

    1. 遇见小模数MOD,可能复杂度与MOD相关。

      有可能当n的级别很大时,ans%MOD=0。

  • 注意代码中012(有前缀0表示八进制数)的十进制数值是10。

  • 字符数组不可使用==,不会报错,但会警告,运行会得到错误的结果。

  • \(a\in \Z,\lfloor a*b\rfloor≠a*\lfloor b\rfloor\)

  • 错误的代码:n=1;a[++n]=n;;结果:a[2]=1。

    正确的代码:n=1;n++;a[n]=n;

  • 判断a是否为0,若a为负数,则不能用!a判断。

  • spj的题目注意数据范围可能会因为要服务spj而比正解的复杂度宽松。

思维题

分析题目的特性,再思考如何根据题目其特性高效解决题目。

遇到困难时,先分析题目的特性,再从题目的特性入手解决问题。

最优方案:贪心(当前视野,可能全局不优,复杂度一般\(O(N)\)\(O(N\log N)\))、动态规划(全局视野)、网络流(多元条件,n在300附近)。

如果两个属性满足乘法关系:a*b=c,考虑根号分治思想。

如果\(O(N^2)\)做法很难继续优化复杂度,考虑bitset优化常数。

实现细节多:改为进行小范围枚举爆搜。

引例、直觉与证明

  • 实践出真知,没思路的时候研究样例(或者自己举例)。手动模拟方案数比较小时的情况。找字典序最小/大的方案模拟。
  • 打表找规律,要对数据的性质敏感。
  • 逆向思考。
    • “构造一个合法序列a,序列b=calc(a),使得序列b合法”:先构造合法序列b,反过来使得序列a合法。
  • 找反例是个好习惯,往往可以比较精准地直到一个错误算法的缺陷究竟在哪里,然后加以改进。
  • 如果决策是一个排列(或者类似的东西),\(\text{exchange argument}\) 往往可以奏效,并且它的应用范围不止于此,比如我们熟知的 \(\text{Prim/Kruskal}\) 算法。
  • 分类(不重不漏)、规范化是个好东西,有时可以同时简化思路、证明和代码。
  • 多试试抽象题目,剥茧抽丝。
  • 不变量 (\(\text{invariant}\)) 是个重要的工具。
  • 注意题目的特殊条件。\(e.g.\)n为奇数。
  • 别忘了数学归纳法。

一些常见的套路性直觉

  • 括号序列问题常常会将左括号设为 \(1\),右括号设为 \(-1\),因此求一段区间的和为 \(0\) 就说明个数相等,用前缀和解决。
  • 三维几何经常转化为二维几何,包括俯视和立面。
  • 通过多维中每维的独立性可以把它等价位多次一维问题,从而化简。
  • 有些区间[l,r]的问题可以拆成[1,l-1]与[1,r]的子问题。

《基础算法14.构造》

技巧

技巧1.严格不等\(\Leftrightarrow\)非严格不等

给定一个整数序列 a1,a2,⋅⋅⋅,an。

请你求出一个递增序列 b1<b2<⋅⋅⋅<bn,使得|a1−b1|+|a2−b2|+⋅⋅⋅+|an−bn| 最小。

  1. 严格不等→非严格不等
    令$a_i $→ \(a_{i}'\)\(a_i':=a_i-i\)),$b_i $→ \(b_{i}'\)\(b_i':=b_i-i\))。

    此时只需求出b1'≤b2'≤...≤bn',且|a1−b1|+|a2−b2|+⋅⋅⋅+|an−bn| =|a1'−b1'|+|a2'−b2'|+⋅⋅⋅+|an'−bn'| 。

    \(b_i+1==b_{i+1}\Leftrightarrow b'_i==b'_{i+1}\)

  2. 非严格不等→严格不等

    令上面的-i改成+i即可。

技巧2.“变换1次”\(O(N)\)

有些题目是先用状态1从起点走到某个中间点,在中间点变换1次用状态2走到终点,求最值。

  1. 做2个预处理:状态1从起点走到每一个点的贡献\(c_1[N]\)+状态2每一个点走到终点的贡献\(c_2[N]\)
  2. 枚举中间点i,\(ans=\max / \min \{ c_1[i]+c_2[i] \}\)

例题

https://www.luogu.com.cn/problem/CF1706C

https://www.wolai.com/fzoi_wiki/eB6CZxnr34GsbEyhdivF5c

技巧3.模k同余

有些题目要求“最终价值是k的倍数”,可以考虑对状态模k同余这一技巧,降低状态量。

有时环形问题也会用到模k同余。

例题

https://www.luogu.com.cn/problem/P2946

https://www.luogu.com.cn/problem/P1516

https://ac.nowcoder.com/acm/contest/42766/B

同余最短路。

技巧4.括号序列

合法括号序列包括:()(A)AB,其中AB代表合法括号序列,这三种情况互相独立又构成全集。

括号序列的处理方法和合法括号序列的充要条件:

  • (代表1,)代表-1,前缀和代表当前括号的权值。

    合法括号序列的充要条件:任意位置的前缀和非负,并且最后一个位置的前缀和为0。

    结论:n个左括号和n个右括号组成的合法括号序列的数量是Catalan数的第n项。

  • 合法括号序列的充要条件:长度为2n,对于任意的\(i\in[1,n]\),在前2i-1个括号中至少有i个(,并且序列一共有n个(

    构造任意一个合法括号序列S的方法:
    1. 令\(S_1=\)(
    2. 对于i从2到n:
    1. 把2i-2,2i-1放入候选集合。
    2. 从候选集合中拿出一个数j,令\(S_j\)=(
    3. 令序列的剩余位置为)

    该方法生成的任何一个括号序列一定是合法括号序列,任何一个合法括号序列一定能通过该方法生成。但是该方法的不同操作可能生成相同的合法括号序列。当把第2.b步增加“从候选集合中删除小于j的数”的操作时,该方法生成的括号序列与合法括号序列一一对应。
    
  1. 给定一个括号序列,求出有多少个互不相同的子串是合法括号串。(互不相同的子串:在原串中的起始位置或终止位置不同。)

    为避免重复计数,考虑从左到右遍历到当前点的括号作为终止位置与前面括号序列会增加多少新的合法括号。

    对于“(”:不会增加任何新的合法括号,但是可能影响后面。

    对于“)”:

    如果权值x大于等于0:

     首先合法括号一定会增加1个(“()”或“(A)”)。考虑如何计算增加多少个“AB”:手动模拟发现“AB”的增加量等于前面权值为x的“)”个数。但是继续手动模拟会发现权值为<x的“)”前面的括号不能计入贡献。
    

e.g.“((())()”遍历到第7个括号(权值为2的“)”)时,“AB”的增加量不能算上第4个括号(权值为2的“)”),因为第5个括号是权值为1<2的“)”。

  如果权值x小于0:不会增加任何新的合法括号,而且后面的前缀和要从0开始。因此,遍历到当前节点时,首先把当前括号加入前缀和。如果当前括号是权值为x的“)”,备份cnt[x+1],然后把cnt[x+1]清0,回溯时再复原。如果x大于等于0,ans+=1+cnt[x],然后cnt[x]++。如果x小于0,否则递归时前缀和从0开始。例题https://www.luogu.com.cn/problem/P5658

技巧5.“缺一二也可”

形如“n个变量,变量之间有相互限制,求满足所有限制条件的n个变量的具体值”。

看似要枚举n个变量,实则枚举n-1个变量即可,剩下1个变量可以由n-1个变量+限制条件推出。复杂度减小一个指数。

如果只考虑2个变量,也许比较好求。可以从2个变量出发,推及到多个变量;或者说枚举n-2个变量,可能还需要再二分剩下2个变量之和,推出剩下2个变量。

技巧6.01序列/奇偶序列

  1. “00/11”消除

    消除后每个位置的奇偶性仍然不变。

    把偶数位上的0/1取反,转化为“01/10”消除,最终状态为只有0或只有1。最终状态只与初始的偶数位取反后的0和1的数量关系有关,与位置关系无关。

    例题

    https://codeforces.com/gym/105484/problem/B

  2. 只有一个位置是奇数,找到这个位置。

    取前缀和制造可二分性,二分查找。

  3. 手写bitset:可以预处理所有长度为b的2^b个01串的复杂信息。复杂度\(O(2^b+\frac{?}{b})\)

技巧7.转化为01序列

  1. 中位数。

    可以考虑二分中位数,将原序列上小于mid的数值赋为-1,大于等于mid的赋为1。则集合内的中位数≥二分的数\(\Leftrightarrow\)集合的总权值≥0。

  2. 对序列进行多次变换,但是最终只询问一个点上的数值的问题

    可以考虑二分最终答案,将原序列上小于mid的数值赋为0,大于等于mid的赋为1。将序列变换的问题简化为01序列变换的问题。如果最终的位置==1,说明二分的值≤实际的答案;否则就二分的值>实际的答案。可以简化思路或寻找性质。

  3. 寻找复制的、出现次数的关系

    把一个普通序列转化为只含 0,1,-1 的序列。从而把上面的关系转化为数值关系。

技巧8.利用分治转移dp

  1. \(f[i]\):i个……的……,一般f[i]只与个数有关。

    先确定一个点x(例如序列上确定一个点,二叉树上确定根节点),然后可以将问题分成左右两边分治:\(f[i]=calc(x)\oplus f[l]\oplus f[r]\),所以可以利用分治从f[l],f[r](l,r<i)转移到f[i]。

  2. 区间dp中,在[l,r]中存在一个元素可作为[l,r]的分界点。

例题

《举一反三:算法性质:小根堆》。

技巧9.相同数值缩到一个整体

适用条件:\(\sum 数值=N\),且答案只与数值及数值的个数有关。

把相同数值缩到一个整体,然后计算答案。复杂度从\(O(N)\)优化到\(O(\sqrt N)\)

例题

A.幸运国家

https://www.wolai.com/fzoi_wiki/u8FquPD7VF4iz9LceCGB1v

技巧10.置换\(p_{p_i}\)

对于置换\(p_{p_i}\),点i向点\(p_i\)连一条有向边,易证图上形成了多个环(包括自环)。把原问题转化为与环长或环数有关的问题。

例题

思维题17.

https://www.wolai.com/fzoi_wiki/u8FquPD7VF4iz9LceCGB1v

技巧11.维护前驱最大值

适用条件:查询区间[l,r]是否存在满足条件的二元组(i,j)。(条件任意。可以是“重复的两个数”、“两数之和为w”……)

记录每个位置的前驱pre[i]:该位置前面第一个与它构成满足条件的二元组的位置。(下文中后继的定义与前驱对应)若一个位置x会被多个位置设为前驱,为了保证单点修改权值的复杂度,只把x的后继的前驱设为x,剩下后面的位置的前驱均设为0。

对于区间查询[l,r],查询区间[l,r]所有位置的前驱最大值是否≥l。

对于单点修改权值,至多需要修改5个位置的前驱:本位置、本位置原来后面第一个与它的值相同的位置(前驱可能从0变到有,替代本位置的前驱)、本位置原来的后继、本位置现在后面第一个与它的值相同的位置(前驱可能从有变到0,前驱被本位置替代),本位置现在的后继。

使用线段树维护区间前驱最大值。使用set[x]维护数值x出现的位置,进而维护每个位置的前驱。单点修改权值时先借助set求出现在的前驱,再在线段树上单点修改区间前驱最大值。注意一个修改多个位置的前驱的小技巧。

例题

(下面这道题由于二元组的条件是“重复的两个数”,故一个位置至多只会被1个位置设为前驱)

https://www.luogu.com.cn/problem/P5278

https://www.luogu.com.cn/problem/P6617

https://www.luogu.com.cn/problem/P8327

技巧12.移项使得独立

适用条件:遇到权值随距离(\(depth_s-depth_p=w_p\))/时间(\(time_j-time_i>c_j\))增长。

可以通过移项从而使2个变量独立,从而变得容易处理:\(depth_s=depth_p+w_p\)/\(time_j-c_j>time_i\)

例题

https://www.acwing.com/activity/content/problem/content/670/

技巧13.绝对值的处理

  1. 分类讨论去绝对值。

  2. 转化为几何问题。

  3. 绝对值与最值

    \(|f(x)|=\max(f(x),-f(x))\)

    \(\sum\limits_i|f(x_i)-g(x_i)|=\sum\limits_i\max(f(x_i),g(x_i))-\sum\limits_i\min(f(x_i),g(x_i))\)

    \(|f(x)|+|g(y)|=\max\{f(x)+g(y),f(x)-g(y),-f(x)+g(y),-f(x)-g(y)\}\)

    \(\max\limits_i\{|f(x_i)|\}=\max\limits_i\{f(x_i),-f(x_i)\}\)

    \(\max\limits_i\{|f(x_i)|+|g(y_i)|\}=\max\limits_i\{f(x_i)+g(y_i),f(x_i)-g(y_i),-f(x_i)+g(y_i),-f(x_i)-g(y_i)\}\)

    \(\max\limits_i\{|f(x_i)+f(x)|+|g(y_i)+g(y)|\}=\max\{\max\limits_i\{f(x_i)+g(y_i)\}+f(x)+g(y),\max\limits_i\{f(x_i)-g(y_i)\}+f(x)-g(y),\max\limits_i\{-f(x_i)+g(y_i)\}-f(x)+g(y),\max\limits_i\{-f(x_i)-g(y_i)\}-f(x)-g(y)\}\)

  4. 序列上的绝对值求和

    微元贡献法。

    设i<j,序列a已从大到小排好序,则\(|a_j-a_i|=\sum\limits_{k=i}^{j-1}(a_{k+1}-a_k)\)。把原式绝对值求和转化为求每个k的\(a_{k+1}-a_k\)的贡献的和,\(a_{k+1}-a_k\)的贡献=满足i≤k,j≥k+1的(i,j)的个数*\(a_{k+1}-a_k\)

  5. 由$b_i|a_i-x|=
    \begin{cases}
    b_i(a_i-x)&a_i\ge x
    \b_i(x-a_i)&a_i<x
    \end{cases} \(得:\)\sum\limits_i b_i|a_i-x|=(\sum\limits_{a_i\ge x} b_ia_i-\sum\limits_{a_i<x} b_ia_i)+x(\sum\limits_{a_i\ge x} b_i-\sum\limits_{a_i<x} b_i)$。上式的值就是总和-2*<x的前缀和。

技巧14.取模的妙用

  1. 压缩状态。
  2. 若模数p很小且答案含有类似于n!,当n≥p时答案为0。
  3. 当答案要求以分数的形式输出时,若答案的分母至多为2(如扫描线梯形的总面积)且小于p,可以先计算答案的2倍在模p意义下的值,再转化为分数的形式。

技巧15.质数和互质

  1. 在偶数中2是唯一的质数,除2外其他的偶数都是合数(题目复杂度可能就与这点有关)。

  2. [x,x+y-1]最多只有1个≥y的倍数。x和x+y-1不同时为≥y的倍数。

    相邻的正整数一定互质。

    相邻的奇数一定互质。

  3. i与n互质\(\Leftrightarrow\)n-i与n互质\(\Leftrightarrow\)i与i+n互质(更相易减术)。

  4. 《数学1.1.质数》,《数学1.2.4.1.互质》。

  5. 有关质数或者互质的信息是复杂的。该信息一般只能通过上述技巧,或者验证质数或者互质处理。

技巧16.异或

  1. 拆位。

    适用条件:求异或之和。

  2. 01trie树。

    适用条件:求\(a\operatorname{xor}b_i\)的最值。

  3. 数位dp。

    \(x\operatorname{xor}y=z\Leftrightarrow x\operatorname{xor}y\operatorname{xor}z=0\)\(f_{pos,l_x,l_y,l_z}\):第pos位,x,y,z最高位是否有限定,的方案数。

  4. 线性基。

  5. 根据现实意义把原问题转化为方案数\(\bmod 2\)\(\Rightarrow\)方案数的奇偶性\(\Rightarrow\)组合数的奇偶性。

技巧17.区间询问

在线

  1. 直接回答。

  2. 拆成[1,l-1]与[1,r]的子问题。

    信息是否或如何做到满足可逆性(可加减性)/信息如何求逆元(\(f(l,r)=f^{-1}(1,l-1)\oplus f(1,r)\))?如何回答前缀问题[1,r]?

    e.g.主席树求区间第k小数。

  3. 线段树拆成\(O(\log N)\)个区间。

    信息是否或如何做到满足结合律?

    e.g.线段树维护区间有序数列求区间小于x的最大的数。

    补充:

    1. 若询问多,但序列不长:st表。

      信息是否或如何做到可重复性?信息是否或如何做到满足结合律?

    2. 若询问多且序列长:

      对序列线性建立笛卡尔树转为 LCA 问题,然后转为正负 1 RMQ,每log ⁡n分一段打表预处理。时间复杂度O(N),询问复杂度O(1),空间复杂度O(N)。

  4. 猫树。

    是否不修改或者如何快速修改(e.g.维护的是线性基)?信息是否或如何做到满足结合律?(如何维护插入?)信息是否或如何做到大小与区间长度无关?

离线

  1. 拆成[1,l-1]与[1,r]的子问题。

    信息是否或如何做到满足可逆性(可加减性)/信息如何求逆元(\(f(l,r)=f^{-1}(1,l-1)\oplus f(1,r)\))?如何维护插入?

  2. 把问题[l,r]放到右端点r,从前到后插入元素和回答问题。

    如何维护插入?如何在其中回答问题\([l,\infty)\)?(可能要结合树状数组等)

    涉及颜色段、“公共”等:可考虑:后插入的元素覆盖之前的元素。

  3. 莫队。

    如何维护插入和删除?

技巧18.动态查询区间内数值的出现次数/前驱后继

对于每一个数值开一个全局平衡树储存该数值出现的位置。必要时使用map对数值离散化,当且仅当访问root[]/set[]时使用离散化之后的值。

  1. [l,r]内无重复数字\(\Leftrightarrow\)[l,r]前驱(指在它之前和它的权值相同的元素的位置)最大值<l。

    线段树维护前驱最大值(叶子节点储存它的前驱)。修改时先在全局平衡树修改得到此时的前驱,再在线段树叶子节点更新信息。

  2. [l,r]内存在两数之和为w\(\Leftrightarrow\)定义一个位置的前驱为这个位置前面第一个与这个位置加起来等于w的位置,若没有则为0。特别地,当一个数被多个数设为前驱时,为方便修改,除了离该数最近的数,剩余其它数的前驱设为0(显然这样也是正确的)。则此时要求[l,r]前驱最大值≥l。

    • 具体实现

      对于一次修改,只需修改以下5个位置的前驱:该位置,该位置原来后面第一个与它的值相同的位置,该位置原来后面第一个与它相加得w的位置,该位置现在后面第一个与它的值相同的位置,该位置现在后面第一个与它相加得w的位置。

      关于只设某数为距离其最近的与其相加得w的数的前驱,只需判断如果一个数的前驱的位置在它前面第一个与它的值相同的位置的前面,则直接把该数的前驱设为零。

      例题。

技巧19.二维问题

  1. 主席树。

    要求不能修改。

  2. 树套树。

  3. 离线,按照某一维排序,另一维用数据结构维护。

  4. dp中的偏序,一维用时间维护,另一维用线段树维护。

  5. 动态查询区间[l,r]内数值x的出现次数

    开一个平衡树储存数值是x的位置,在数值x的平衡树查询位置小于等于r和小于等于l-1的大小,相减就可以了。

技巧20.划分序列问题

特征:把序列A划分成为若干段。

段数最少

《基础算法5.1.3.2.最小划分问题》。

价值最值

\(f_i=\max\limits_{j=0}^{i-1}/\min\limits_{j=0}^{i-1}(f_j+\bigoplus\limits_{k=j+1}^i a_k)\)

  • dp转移优化。
  • 注意观察有效决策点j的数量的复杂度:《动态规划8.1.剪枝有效状态数》:
    • \(\bigoplus\)是位运算,则可能对于每个i,最多有\(O(\log X)\)种不同的\(\bigoplus\limits_{k=j+1}^i a_k\)

技巧21.复杂度分析

上界分析

  • 一个整数x和其他多个整数依次取gcd,最多有O(log X)个变化值。

势能分析

根据分析需要,自行定义一个势能函数f(s),表示状态为s的某一信息,它可以是元素数量,也可以是容器容量,还可以是容器容量减元素数量等等。势能函数需要满足是非负整数函数并且初始状态为0。

  • 方法一:

    1. 一些操作会使势能不变,因此在该势能定义上这些操作是无效操作,必须剪枝避免。
    2. 一些操作会使势能增大,另一些操作会使势能减小,因此势能的最大值即为复杂度。
  • 方法二:

    设第i次操作的实际开销是c_i,使状态s_{i-1}变成s_i。若c_i+f(s_i)-f(s_{i-1})=O(T(n)),则O(T(n))是均摊复杂度的一个上界。

综合技巧

  1. 对拍
    1. 新建checker.cpp
#include<bits/stdc++.h>
using namespace std;const int T=1e4;int main(void)
{system("g++ rand.cpp -fsanitize=undefined -fsanitize=address -O2 -o rand");system("g++ std.cpp -fsanitize=undefined -fsanitize=address -O2 -o std");system("g++ tmp.cpp -fsanitize=undefined -fsanitize=address -O2 -o tmp");for(int i=1;i<=T;i++){system("./rand > test.in");system("./std < test.in > test.ans");system("./tmp < test.in > test.out");if(system("diff test.ans test.out")){puts("WA");return -1;}else printf("AC on #%d\n",i);}return 0;
}
2. 新建`rand.cpp`。输入数据代码。不需要`freopen`。真随机数:
random_device srd;
srand(srd());
3. 新建`std.cpp`。正确率100%的代码。不需要`freopen`。
4. 新建`tmp.cpp`。要对拍的代码。不需要`freopen`。
5. `g++ checker.cpp`。
6. `./a.out`。
string filePath1="file1.txt";
string filePath2="file2.txt";
if(compareFiles(filePath1,filePath2))
{cout<<"AC"<<endl;
}
else
{cout<<"WA"<<endl;return 1;
}
  1. IDE编译命令
-Wl,--stack=题目的空间限制大小(单位是Byte) -std=c++14 -O2
ulimit -s 题目的空间限制大小(单位是KB,1MB=1024KB)
g++ first.cpp
./a.out
如果显示段错误之类的东西就是内存超限了。注意这东西调小了调不回来,如果要调回来需要杀死该终端再开一个新的终端。
  1. 测量时间和空间

    • 时间:

      终端

      time ./a.out

      程序

      1.0*clock()/CLOCKS_PER_SEC

    • 空间:

      终端

      • 方法一:在a.out正在运行的时候,新建一个新的终端,在新的终端中输入top。一般第一个进程的VIRT 就是 KB 数。
      • 方法二:在a.out正在运行的时候到系统监视器的进程那一栏去查看a.out的内存。
  2. 快读和快写

    《C++语言.3.快读和快写》

  3. 卡空间

    • 若储存的数值较小,则可用char(-128127)或`short`(-3276832767)类型储存数值。
  4. 随机数

    《C++语言.标准库》

  5. 优化(a+b)%MOD

    前提条件:必须保证a,b<MOD。

inline void add(LL &a,LL b)
{a+=b;if(a>=MOD) a-=MOD;return ;
}
  1. 取整
    • 向下取整

      • p/q
      • ceil(x)
    • 向上取整

      • (p+q-1)/q
      • floor(x)
    • 四舍五入

      • x=round(x);
      • printf("%.0lf\n",x+EPS);//printf中的%.0lf自带四舍五入

      注意:printf中的%.0lf自带四舍五入(可能会有精度问题),而%d则是向下取整。

  2. 内存回收:
int idx;
//建立回收站
int q[M],qidx;
for(int i=1;i<=n;i++) q[++qidx]=i;//新建
int u= qidx==0 ? ++idx : q[qidx--];//删除回收
a[u]=0;
q[++qidx]=u;
  1. 将树变为区间:欧拉序列/括号序列。
  2. 哈希/堆的删除操作:新建一个布尔数组vis[],vis[x]=true;
  3. 判断一个pair是否出现过:显然用不了bool数组,可以用map<PII,bool> existed;if(existed[{a,b}]) continue;existed[{a,b}]=true;set<PII> existed;if(existed.count({a, b})) continue;existed.insert({a, b});
  4. 浮点数卡精度
const double eps = 1e-8;
int sign(double x)  // 符号函数
{if (fabs(x) < eps) return 0;if (x < 0) return -1;return 1;
}
int cmp(double x, double y)  // 比较函数
{if (fabs(x - y) < eps) return 0;if (x < y) return -1;return 1;
}
  1. 统计一个区间内有多少不同的数,若数值的范围≤30,状态压缩。
  2. 依次删除边→倒着思考,反过来依次加边。
  3. “截断”字符串:str[x]=0;注意这里的0是指ascll码值为0——空字符。
  4. 地图类题目数据范围给的是1≤n*m≤100、或矩阵乘法加速二维递推时,均需要把二维压缩到一维:
//压缩
int sets(int x,int y){return (x-1)*m+y;
}//还原
void get(int state,int &x,int &y)
{x=(state-1)/m+1,y=(state-1)%m+1;//注意是state-1return ;
}int state=sets(x,y);//压缩//还原
get(state,x,y);
  1. 常用并查集判断连通性。

    对于一条边u→v:p[find(u)]=find(v);

    判断x能否走到y:if(find(x)==find(y))

  2. vector.clear()的复杂度从O(N)降到O(1):

void cle(vector<pair<int,Tree1> > &qwe)
{vector<pair<int,Tree1> > ewq;swap(qwe,ewq);return ;
}
  1. 阳间题目有时不告诉你一行输入几个数:“接下来的m行,每行是一个实验的有关数据。第一个数赞助商同意支付该实验的费用;接着是该实验需要用到的若干仪器的编号。”
    1. 快读判断getchar()!='\n'
    2. 使用stringstream。
//基本使用
string str="123 456 789";
stringstream a(str);  //边定义边读入int x,y,z;
a>>x;   //x=123
a>>y;   //y=456//清空,二者缺一不可
a.str("");
a.clear();a<<"666";   //读入
a>>z;   //z=666
//实战演练
int res,x[10],idx=0;
string str;
getline(cin,str);//读一整行
stringstream a(str);
while(a>>res) x[++idx]=res;//直至读完。注意不可以直接a>>x[++idx],因为是先读入再判断,这样的话会多读一个空数(随机值)
  1. 场宽:printf("%2d",a);/*从该输出数据的最后一个字符开始往前占2个空格的宽度*/printf("%-2d",a);/*从该输出数据的第一个字符开始往后占2个空格的宽度*/

  2. 对于“删边”类问题,可以从全局图考虑局部图。

  3. 全局平移:用delta维护全局平移量。数据结构中的数值(位置)+delta=实际的数值(位置)。实际的数值(位置)-delta=数据结构中的数值(位置)。

  4. 因调整序列而修改(多个位置)前驱时,应先记录哪些位置的前驱会改变,再调整序列,然后再更新那些位置的前驱。

  5. 结论题:多打表。

  6. 关于dp最值的骗分技巧:

    \(e.g.\)\(dp_{i,j}\):不超过j个的最值。对于每个i,map存\([j,dp_{i,j}]\),即将到i+1时,把\(dp_{i,j}\)劣于前缀最值的状态删除,不转移给i+1。

  7. 与积相关,但是积很大且无模数:取对数。

  8. map判空(x是否访问过):h[x]==0 ?。如果x的哈希值本来就是0,那么当x存入map时给哈希值加上一个偏移量即可:h[x]=0+delta;

  9. 六边形坐标系。

    • 图片

      ![](https://secure2.wostatic.cn/static/ih4UR7sdjgYyzLkBDP2NbN/屏幕截图 2023-02-08 172835.jpg)

    像上图那样建立x,y坐标系,就可以表示任意一个点的坐标。

    有的时候需要像上图那样引入“z=x-y轴”。虽然对表示坐标没有作用,但是有的时候可以帮助思考问题。

    借助z轴,可以推得:六边形坐标系中两个格子的距离\(=\max\{|x_1-x_2|,|y_1-y_2|,|(x_1-y_1)-(x_2-y_2)|\}=\frac{1}{2}(|x_1-x_2|+|y_1-y_2|+|(x_1-y_1)-(x_2-y_2)|)\)

  10. 模数MOD特殊一定要注意!

    1. 遇见小模数MOD,可能复杂度与MOD相关。

      有可能当n的级别很大时,ans%MOD=0。

举一反三

最小生成树

母题\(Prim\) 母题\(Kruskal\)

最大边权最小的生成树

次小生成树

限定根最大度数的最小生成树

最短路径生成树

最优比率生成树


序列问题

  • 例题1:最长上升子序列\(O(N \log N)\)

    二分查找在q中找>=a[i]的数中最小的一个,不存在返回len+1。

    大数往后面填,小数覆盖前面。

int a[N];   //序列
int q[N],len;   //最长上升子序列scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);for(int i=1;i<=n;i++)
{int l=1,r=len+1;    //len+1:给大数留个空while(l<r)  //在q中找>=a[i]的数中最小的一个,不存在返回len+1{int mid=(l+r)>>1;if(q[mid]<a[i]) l=mid+1;else r=mid;}len=max(len,r); //如果a[i]是大数二分返回len+1,更新答案q[r]=a[i];  //大数往后面填,小数覆盖前面
}
printf("%d\n",len);
  • 例题2:最长公共子序列
char a[N],b[N];
int f[N][N];    //f[i][j]:前缀子串a[1~i]与b[1~j]的“最长公共子序列”的长度scanf("%d%d",&n,&m);
scanf("%s%s",a+1,b+1);//边界:f[i,0]=f[0,j]=0
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++){f[i][j]=max(f[i-1][j],f[i][j-1]);if(a[i]==b[j]) f[i][j]=max(f[i][j],f[i-1][j-1]+1);}printf("%d\n",f[n][m]);
  • 例题3:最长先升后降子序列

    最长先升后降子序列=从前往后递推的最长上升子序列+从后往前递推的最长下降子序列-公共交点。

int f[N],g[N];  //从前往后递推的最长上升子序列和从后往前递推的最长下降子序列for(int i=1;i<=n;i++)
{f[i]=1;for(int j=1;j<i;j++) if(a[j]<a[i]) f[i]=max(f[i],f[j]+1);
}for(int i=n;i>=1;i--)
{g[i]=1;for(int j=n;j>i;j--) if(a[i]>a[j]) g[i]=max(g[i],g[j]+1);
}for(int i=1;i<=n;i++) ans=max(ans,f[i]+g[i]-1); //还要减去公共交点i
printf("%d\n",ans);
  • 例题4:最大上升子序列和

    求一个序列所有上升子序列中子序列和的最大值。

    只需把最长上升子序列模型更改一下计算贡献的方式即可。

for(int i=1;i<=n;i++)
{f[i]=a[i];for(int j=1;j<i;j++) if(a[j]<a[i]) f[i]=max(f[i],f[j]+a[i]);
}
  • 例题5:最长公共上升子序列

    对于两个数列 A 和 B,如果它们都包含一段位置不一定连续的数,且数值是严格递增的,那么称这一段数是两个数列的公共上升子序列,而所有的公共上升子序列中最长的就是最长公共上升子序列了。求最长公共上升子序列的长度。

    题解

int n,ans;
int a[N],b[N];
int f[N][N];int main(){scanf("%d",&n);for(int i=1;i<=n;i++) scanf("%d",&a[i]);for(int i=1;i<=n;i++) scanf("%d",&b[i]);for(int i=1;i<=n;i++){int ma=1;for(int j=1;j<=n;j++){f[i][j]=f[i-1][j];if(a[i]==b[j]) f[i][j]=max(f[i][j],ma);if(a[i]>b[j]) ma=max(ma,f[i-1][j]+1);}}for(int i=1;i<=n;i++) ans=max(ans,f[n][i]);printf("%d\n",ans);return 0;
}
  • 例题6:求序列\(A\)长度为\(M\)的严格递增子序列

    求序列\(A\)长度为\(M\)的严格递增子序列。

    \(f[i][j]\):前\(j\)个数以\(A_j\)结尾的数列,长度为\(i\)的严格递增子序列有多少个(\(i\)\(j\)均可作阶段)。

    特殊地,令\(A_0=-INF\)

    状态转移方程

const int INF=1<<30,MOD=1e9+7;memset(f,0,sizeof f);
a[0]=-INF;f[0][0]=1;
for(int i=1;i<=m;i++)for(int j=1;j<=n;j++)for(int k=0;k<j;k++)if(a[k]<a[j]) f[i][j]=(f[i][j]+f[i-1][k])%MOD;int ans=0;
for(int i=1;i<=n;i++) ans=(ans+f[m][i])%MOD;

数据结构优化

树状数组维护前缀和。

在序列A(不包括\(A_0\))中的数值的值域上建立树状数组。

把外层循环i看作定值。当j增加1时,k的取值范围从0≤k<j变为0≤k<j+1,也就是多了一个k=j新决策。

设一个决策\((A_k,f[i-1,k])\)

  1. 插入一个新决策。在j增加1前,把\((A_j,f[i-1,j])\)插入集合:把\(A_k\)上的位置的值增加\(f[i-1,k]\)
  2. 给定一个值\(A_j\),查询满足\(A_k<A_j\)的二元组对应的\(f[i-1,j]\)的和:在树状数组计算\([1,A_j-1]\)的前缀和。
//树状数组维护前缀和
#include<bits/stdc++.h>
using namespace std;const int N=1005,MOD=1e9+7;
int t,n,m,ans;
int a[N],f[N][N];   //f[i][j]:前i个数以Aj结尾的数列,长度为j的严格递增子序列有多少个(i、j均可作阶段)
int nums[N],cnt;    //离散化
int tr[N];  //树状数组维护前缀和inline int lowbit(int x){return x&-x;
}void add(int x,int v){while(x<=cnt){tr[x]=(tr[x]+v)%MOD;x+=lowbit(x);}return ;
}int sum(int x){int res=0;while(x>0){res=(res+tr[x])%MOD;x-=lowbit(x);}return res;
}int main(){scanf("%d",&t);for(int C=1;C<=t;C++){ans=0;scanf("%d%d",&n,&m);cnt=0;for(int i=1;i<=n;i++){scanf("%d",&a[i]);nums[++cnt]=a[i];}//离散化sort(nums+1,nums+cnt+1);cnt=unique(nums+1,nums+cnt+1)-nums-1;   //注意这里要-1for(int i=1;i<=n;i++) a[i]=lower_bound(nums+1,nums+cnt+1,a[i])-nums+1;for(int i=1;i<=n;i++) f[i][1]=1;for(int j=2;j<=m;j++){for(int i=1;i<=cnt;i++) tr[i]=0;for(int i=1;i<=n;i++){f[i][j]=sum(a[i]-1);add(a[i],f[i][j-1]);}}for(int i=1;i<=n;i++) ans=(ans+f[i][m])%MOD;printf("Case #%d: %d\n",C,ans);}return 0;
}
  1. 把一个序列A变成非严格单调递增的,至少需要修改 序列总长度-A的最长不下降子序列长度 个数。
  2. 把一个序列A变成严格单调递增的,至少需要修改 构造序列B[i]=A[i]-i,序列总长度-B的最长不下降子序列长度 个数。
  3. 把一个序列A变成严格单调的,至少需要花费多少偏移量?
  4. 给定一个序列A,可以选择一个区间 [l,r],使下标在这个区间内的数都加一或者都减一,至少要修改 差分后的序列****\(\max(\sum|所有正数|,\sum|所有负数|)\) 次,且可以得到不同的序列 \(|\sum|所有正数|-\sum|所有负数||+1\) 种。
  5. 求序列A的最大连续子段和:\(O(N)\)扫描该数列,不断把新的数加入子段,当子段和变成负数时,把当前的整个子段清空。扫描过程中出现过的最大子段和即为所求。

排列问题

  • 回转排列

    对于一个排列\(p_1,p_2,\dots,p_n\),记\(p_{q_i}=i\),即\(q_i\)为数值i在\(p_1,p_2,\dots,p_n\)中的位置。显然\(p_1,p_2,\dots,p_n\)\(q_1,q_2,\dots,q_n\)一一对应。

    定义\(p_1,p_2,\dots,p_n\)是回转排列\(\Leftrightarrow\)\(\forall i\in[2,n-1],(q_i-q_{i-1})(q_i-q_{i+1})>0\)

    e.g.p=[1,3,4,2],q=[1,4,2,3];p≠[1,4,2,3],q≠[1,3,4,2];p=[1,3,2,4],q=[1,3,2,4];p=[2,4,1,3],q=[3,1,4,2];p=[3,1,4,2],q=[2,4,1,3]。

    有的题目对排列p的两端有限制,此时计算\(p_1,p_2,\dots,p_n\)的合法方案数比较方便;有的题目对排列q的两端有限制,此时计算\(q_1,q_2,\dots,q_n\)的合法方案数比较方便;

    计算\(p_1,p_2,\dots,p_n\)的合法方案数

    位置大小。

    《动态规划7.4..有序序列计数》。

    \(f_{i,j,0/1}\):把前i个数加入排列,数值i位于前i个数的第j位,并且初始方向是 1在2的左边/右边,的合法方案数。

    根据i的奇偶性和 0/1 状态,来确定数值i应该是在i-1的左边还是右边,然后枚举合适的j,进行转移。

    计算\(q_1,q_2,\dots,q_n\)的合法方案数

    数值大小。

    《动态规划2.7.序列·插入·连续段模型》。


区间问题

有些区间[l,r]的问题可以拆成[1,l-1]与[1,r]的子问题。

  • 求前k大区间和:超级钢琴

    这里要知道[l,r]中最大值的位置pos,求出后查询[l,pos-1][pos+1,r]中的最大值(pos已经用过了不能再用)。

#include<bits/stdc++.h>
using namespace std;typedef long long LL;
const int N=5e5+10;
int n,m,l,r;
LL ans=0;
int sum[N];
int f[N][20],log_2[N];
struct Seg
{int st,l,r,pos,s;bool operator < (const Seg &t) const{return s<t.s;}
};
priority_queue<Seg> q;void st_pre()
{for(int i=2;i<=n;i++) log_2[i]=log_2[i>>1]+1;for(int i=1;i<=n;i++) f[i][0]=i;for(int k=1;1+(1<<k)-1<=n;k++)for(int l=1;l+(1<<k)-1<=n;l++){int a=f[l][k-1],b=f[l+(1<<(k-1))][k-1];f[l][k]=sum[a]>sum[b] ? a : b;}return ;
}int st_query(int l,int r)
{int k=log_2[r-l+1];int a=f[l][k],b=f[r-(1<<k)+1][k];return sum[a]>sum[b] ? a : b;
}int main()
{scanf("%d%d%d%d",&n,&m,&l,&r);for(int i=1;i<=n;i++){scanf("%d",&sum[i]);sum[i]+=sum[i-1];}st_pre();for(int i=1;i+l-1<=n;i++){int pos=st_query(i+l-1,min(i+r-1,n));int s=sum[pos]-sum[i-1];q.push((Seg){i,i+l-1,min(i+r-1,n),pos,s});}while(m--){auto t=q.top();q.pop();int st=t.st,pos=t.pos,s=t.s,new_pos,new_s;ans+=s;if(t.l!=pos){new_pos=st_query(t.l,pos-1);new_s=sum[new_pos]-sum[st-1];q.push((Seg){st,t.l,pos-1,new_pos,new_s});}if(t.r!=pos){new_pos=st_query(pos+1,t.r);new_s=sum[new_pos]-sum[st-1];q.push((Seg){st,pos+1,t.r,new_pos,new_s});}}printf("%lld\n",ans);return 0;
}
  • 求前k大区间异或和:异或粽子

    我们求一遍前缀异或和,那么 [l,r]的异或和为 \(sum_{l-1}\ \text{xor}\ sum_r\)

    我们先固定右端点 r,然后在 [0,r−1] 查一个数异或 $sum_r $最大。这个可以用可持久化 01trie 实现。

    我们将 n 个数(显然这n个数就是前n大)放入堆中,每次取出最大的那个状态。最大值的位置是pos,求出后查询[l,pos-1][pos+1,r]中的最大值(pos已经用过了不能再用)放入堆中。

#include<bits/stdc++.h>
using namespace std;typedef long long LL;
const int N=5e5+10,M=N*35;
int n,m;
LL ans,sum[N];
int root[N],idx;
int tr[M][2],max_id[M];
struct Seg
{int st,l,r,pos;LL s;bool operator < (const Seg &t) const{return s<t.s;}
};
priority_queue<Seg> q;void insert(int id,int k,int p,int q)
{if(k<0){max_id[q]=id;return ;}int v=(sum[id]>>k)&1;tr[q][v^1]=tr[p][v^1];tr[q][v]=++idx;insert(id,k-1,tr[p][v],tr[q][v]);max_id[q]=max(max_id[tr[q][0]],max_id[tr[q][1]]);return ;
}int query(int l,int rt,LL num)
{int p=rt;for(int k=31;k>=0;k--){int v=(num>>k)&1;if(max_id[tr[p][v^1]]>=l) p=tr[p][v^1];else p=tr[p][v];}return max_id[p];
}int main()
{scanf("%d%d",&n,&m);max_id[0]=-1;root[0]=++idx;insert(0,31,0,root[0]);for(int i=1;i<=n;i++){scanf("%lld",&sum[i]);sum[i]^=sum[i-1];root[i]=++idx;insert(i,31,root[i-1],root[i]);}for(int i=1;i<=n;i++){int pos=query(i,root[n],sum[i-1]);LL s=sum[pos]^sum[i-1];q.push((Seg){i,i,n,pos,s});}while(m--){auto t=q.top();q.pop();ans+=t.s;int new_pos;LL new_s;if(t.l<t.pos){new_pos=query(t.l,root[t.pos-1],sum[t.st-1]);new_s=sum[new_pos]^sum[t.st-1];q.push((Seg){t.st,t.l,t.pos-1,new_pos,new_s});}if(t.r>t.pos){new_pos=query(t.pos+1,root[t.r],sum[t.st-1]);new_s=sum[new_pos]^sum[t.st-1];q.push((Seg){t.st,t.pos+1,t.r,new_pos,new_s});}}printf("%lld\n",ans);return 0;
}
  • 区间子区间问题

K问题

  • 第k小数
    1. 整体第k小数(静态)

      • 方法一:快速排序求第k小数
int n,k;
int a[N];void quick_sort(int l,int r,int k)
{if(l>=r) return ;//不要忘记这里!!!int i=l,j=r,mid=a[(l+r)>>1];while(i<=j){while(a[i]<mid) i++;while(a[j]>mid) j--;if(i<=j){swap(a[i],a[j]);i++;j--;}}if(j-l+1>=k) quick_sort(l,j,k);//!!!else quick_sort(i,r,k-(j-l+1));//!!!return ;
}int main()
{scanf("%d%d",&n,&k);for(int i=1;i<=n;i++) scanf("%d",&a[i]);quick_sort(1,n,k);printf("%d\n",a[k]);return 0;
}
  - 方法二:二分答案(铺垫整体二分)设当前二分的值是mid,统计序列里cnt个数≤mid。若k≤cnt,则答案一定在左半区间,继续二分;否则在一定在右半区间,令k-=cnt,继续二分。
  1. 整体第k小数(动态修改)

    值域线段树

  2. 区间求不大于S的数的个数(动态修改)(铺垫整体二分)

    • 树状数组

      初始对于任意A[i]≤S,在树状数组中执行add(i,1);,表示在第i个位置上有一个不大于S的数(即第i个位置对答案的贡献+1)。

      询问时答案就是ans=ask(r)-ask(l-1);

      修改A[i]=x时,若A[i]≤S,则执行add(i,-1);,表示删除第i个位置对答案的贡献;若x≤S,则再执行add(i,1);。然后别忘了维护原数组A[i]=x;

  3. 区间第k小数(静态)

    • 方法一:整体二分(必须离线)

      把所有的操作(包括将数组的原值看作一开始就赋值的操作)先按时间排好序(本来就自然地排好序),然后只要进行1次值域上的二分:

      1. merge(lval,rval,st,ed):值域为[L,R]的整数序列a,对操作序列q(含删除、赋值、询问)进行实现。
      2. 利用树状数组,在当前的序列a中,对于q的每个询问,统计不大于mid的数有c个。
      3. 若k≤c则将该询问加入操作序列lq中,否则令k-=c加入rq。
      4. 令整数序列a中≤mid的数构成整数序列la,>mid的数构成整数序列ra,操作序列q中涉及≤mid的数的操作构成操作序列lq,涉及≤mid的数的操作构成操作序列rq。注意保持操作的时间顺序。
      5. 还原树状数组,分治求解merge(lval,mid):la,lqmerge(mid+1,rval):ra,rq。边界:当操作序列为空时直接返回;当整数序列只剩1个数时得到答案。
int n,m;
int ans[N];
int tr[N];int idx;
struct Data
{int op,x,y,z;
}q[N],lq[N],rq[N];int lowbit(int x)
{return x&(-x);
}void add(int x,int val)
{while(x<=n){tr[x]+=val;x+=lowbit(x);}return ;
}int ask(int x)
{int res=0;while(x){res+=tr[x];x-=lowbit(x);}return res;
}void merge(int lval,int rval,int st,int ed)
{if(st>ed) return ;if(lval==rval){for(int i=st;i<=ed;i++) if(q[i].op>0) ans[q[i].op]=lval;return ;}int mid=(lval+rval)>>1;int lqidx=0,rqidx=0;for(int i=st;i<=ed;i++){if(q[i].op==0){if(q[i].y<=mid){add(q[i].x,1);lq[++lqidx]=q[i];}else rq[++rqidx]=q[i];}else{int cnt=ask(q[i].y)-ask(q[i].x-1);if(cnt>=q[i].z) lq[++lqidx]=q[i];else{q[i].z-=cnt;rq[++rqidx]=q[i];}}}for(int i=ed;i>=st;i--){if(q[i].op==-1 && q[i].y<=mid) add(q[i].x,1);else if(q[i].op==0 && q[i].y<=mid) add(q[i].x,-1);}for(int i=1;i<=lqidx;i++) q[st+i-1]=lq[i];for(int i=1;i<=rqidx;i++) q[st+lqidx+i-1]=rq[i];merge(lval,mid,st,st+lqidx-1);merge(mid+1,rval,st+lqidx,ed);return ;
}int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++){int val;scanf("%d",&val);q[++idx]={0,i,val};}for(int i=1;i<=m;i++){int l,r,k;scanf("%d%d%d",&l,&r,&k);q[++idx]={i,l,r,k};}merge(-INF,INF,1,idx);for(int i=1;i<=m;i++) printf("%d\n",ans[i]);return 0;
}
  - 方法二:主席树(可以在线)p:前一个线段树,q:当前新建的线段树。

类似于动态开点新建一个节点q,节点q复制节点p的信息,随着p往下走在节点q新建新的信息。

#include<bits/stdc++.h>
using namespace std;const int N=1e5+5,M=1e4+5;
int n,m;
int a[N];
vector<int> nums;
struct Tree //区间左右端点由函数中l和r决定
{int lson,rson;    ////lson:左儿子的编号(注意不是区间左端点);rson:右儿子的编号;int cnt;    //当前区间所包含数的个数
}tr[N*4+N*17];
int root[N],idx;int build(int l,int r)
{int p=++idx;    //类似于动态开点if(l==r) return p;int mid=(l+r)>>1;tr[p].lson=build(l,mid);tr[p].rson=build(mid+1,r);return p;
}void pushup(int p)
{tr[p].cnt=tr[tr[p].lson].cnt+tr[tr[p].rson].cnt;return ;
}//区间左右端点由函数中l和r决定
int insert(int p,int l,int r,int x)
{int q=++idx;tr[q]=tr[p];    //复制if(l==r){tr[q].cnt++;return q;}//新建int mid=(l+r)>>1;if(x<=mid) tr[q].lson=insert(tr[p].lson,l,mid,x);else tr[q].rson=insert(tr[p].rson,mid+1,r,x);pushup(q);return q;
}int query(int q,int p,int l,int r,int k)
{if(l==r) return l;int mid=(l+r)>>1,cnt=tr[tr[q].lson].cnt-tr[tr[p].lson].cnt; //区间具有前缀和的性质,这样区间里的数就一定大于Lif(k<=cnt) return query(tr[q].lson,tr[p].lson,l,mid,k);else return query(tr[q].rson,tr[p].rson,mid+1,r,k-cnt);
}int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++){scanf("%d",&a[i]);nums.push_back(a[i]);}sort(nums.begin(),nums.end());nums.erase(unique(nums.begin(),nums.end()),nums.end());for(int i=1;i<=n;i++) a[i]=lower_bound(nums.begin(),nums.end(),a[i])-nums.begin();root[0]=build(0,nums.size()-1);for(int i=1;i<=n;i++) root[i]=insert(root[i-1],0,nums.size()-1,a[i]);while(m--){int l,r,k;scanf("%d%d%d",&l,&r,&k);printf("%d\n",nums[query(root[r],root[l-1],0,nums.size()-1,k)]);}return 0;
}
  1. 区间第k小数(动态修改)

    • 方法一:整体二分(必须离线)

      结合第3小问,在第4小问增加修改操作:分成删除和增添操作。

int n,m;
int a[N];   //原数组
int ans[N];
int tr[N];  //树状数组维护单个区间静态第k小数//操作序列(按时间排好序)
//当op=-1:删除某个位置上的数的贡献;0:在某个位置增添一个数的贡献;1:询问
int idx;
struct Data
{int op,x,y,z;
}q[N],lq[N],rq[N];  //q:当前操作序列;lq(rq):分配到左(右)儿子的操作序列int lowbit(int x)
{return x&(-x);
}void add(int x,int val)
{while(x<=n){tr[x]+=val;x+=lowbit(x);}return ;
}int ask(int x)
{int res=0;while(x){res+=tr[x];x-=lowbit(x);}return res;
}void merge(int lval,int rval,int st,int ed) //lval、rval:值域;st、ed:操作序列
{if(st>ed) return ;  //空操作序列,返回if(lval==rval)  //找到答案{for(int i=st;i<=ed;i++) if(q[i].op>0) ans[q[i].op]=lval;return ;}//分配到左儿子或右儿子int mid=(lval+rval)>>1;int lqidx=0,rqidx=0;for(int i=st;i<=ed;i++){if(q[i].op==-1){if(q[i].y<=mid) //按值域划分:对答案有贡献的数的值小于mid分到左儿子{add(q[i].x,-1); //删除某个位置上的数的贡献,而不是减这个数的值,因此修改操作分成的两个操作就算分道扬镳也不会影响正确性lq[++lqidx]=q[i];}else rq[++rqidx]=q[i];}else if(q[i].op==0) //类似上面{if(q[i].y<=mid){add(q[i].x,1);lq[++lqidx]=q[i];}else rq[++rqidx]=q[i];}else{int cnt=ask(q[i].y)-ask(q[i].x-1);if(cnt>=q[i].z) lq[++lqidx]=q[i];   //这个区间小于mid的数的个数大于k,因此答案应该在左儿子else{q[i].z-=cnt;    //注意分到右儿子前应减去左儿子的影响rq[++rqidx]=q[i];}}}for(int i=ed;i>=st;i--) //清空树状数组{if(q[i].op==-1 && q[i].y<=mid) add(q[i].x,1);else if(q[i].op==0 && q[i].y<=mid) add(q[i].x,-1);}for(int i=1;i<=lqidx;i++) q[st+i-1]=lq[i];  //节省空间for(int i=1;i<=rqidx;i++) q[st+lqidx+i-1]=rq[i];    //节省空间merge(lval,mid,st,st+lqidx-1);  //往下分治merge(mid+1,rval,st+lqidx,ed);  //往下分治return ;
}int main()
{memset(ans,-1,sizeof ans);  //输出时方便判断是否为查询操作scanf("%d%d",&n,&m);for(int i=1;i<=n;i++){scanf("%d",&a[i]);q[++idx]={0,i,a[i]};}for(int i=1;i<=m;i++){char op[2];scanf("%s",op);if(op[0]=='Q'){int l,r,k;scanf("%d%d%d",&l,&r,&k);q[++idx]={i,l,r,k};}else{int x,y;scanf("%d%d",&x,&y);//修改操作可以分成2个操作。并且因此数组要开3*10^5!q[++idx]={-1,x,a[x]};q[++idx]={0,x,y};a[x]=y; //别忘记修改原数组}}merge(0,INF,1,idx);for(int i=1;i<=m;i++) if(ans[i]!=-1) printf("%d\n",ans[i]);return 0;
}
  - 方法二:主席树(可以在线)
  1. 树上路径第k小数

    • 主席树

      不要学数据结构学傻了,不要看到树上路径就只想到树链剖分,树链剖分+主席树是做不了的。

      该问题很简单,其实就是树上两点间的距离与树上差分+区间第k小数嘛。tr:当前点到根节点的路径上的数的出现次数。

      代码链接。

  • \(\text{A*}\)求第k短路

    第一次出队是最短路,第k次出队是第k短路。

int h[N],rh[N],e[M],w[M],ne[M],idx;
int d[N],cnt[N];
int n,m,s,t,k;
bool vis[N];void add(int hh[],int a,int b,int c){e[++idx]=b;w[idx]=c;ne[idx]=hh[a];hh[a]=idx;return ;
}//反向边求估价函数
void dij(){priority_queue<PII,vector<PII>,greater<PII> > q;memset(d,0x3f,sizeof d);d[t]=0;q.push({0,t});while(q.size()){auto t=q.top();q.pop();int u=t.second;if(vis[u]) continue;vis[u]=1;for(int i=rh[u];i!=0;i=ne[i]){int j=e[i];if(d[j]>d[u]+w[i]){d[j]=d[u]+w[i];q.push({d[j],j});}}}return ;
}int A_star(){priority_queue<PIII,vector<PIII>,greater<PIII> > q;q.push({d[s],{0,s}});while(q.size()){auto tt=q.top();q.pop();int u=tt.second.second,dis=tt.second.first;cnt[u]++;if(cnt[t]==k) return dis;   //第一次出队是最短路,第k次出队是第k短路for(int i=h[u];i!=0;i=ne[i]){int j=e[i];if(cnt[j]<k) q.push({dis+w[i]+d[j],{dis+w[i],j}});}}return -1;
}int main(){cin>>n>>m;while(m--){int a,b,l;cin>>a>>b>>l;add(h,a,b,l);add(rh,b,a,l);  //反向边求估价函数}cin>>s>>t>>k;if(s==t) k++;   //注意特判dij();  //预处理估价函数cout<<A_star()<<endl;return 0;
}
  • Bellman-Ford求边数不超过k的最短路
int bellford(){memset(d,0x3f,sizeof d);d[1]=0;for(int i=1;i<=k;i++){//用上一次的边权扩展,扩展k次,保证经过不超过k条边memcpy(last,d,sizeof d);for(int j=1;j<=m;j++){int w=q[j].first,u=q[j].second.first,v=q[j].second.second;d[v]=min(d[v],last[u]+w);}}return d[n];
}
  • 求在从 1 到 N 的路径中,路径上第 k 大的值最小是多少

    形如“最大值最小” / “最小值最大”之类的字眼:用二分求解。

    自变量:x。

    因变量:y,表示对于给定的 x ,从 1 到 N 的路径中最少要经过边权大于 x 的边有多少条。

    判定:y≤k-1

    我们设原边权大于 x 的边的新边权为 1 ,小于等于 x 的边新边权为 0,求出从 1 到 N 的最短路即为 y 的值。01边权双端队列BFS求解。

int n,m,k;int h[N],e[M],w[M],ne[M],idx;int dis[N]; //dis[u]:从1到u至少有多少条线路费用大于limit
bool vis[N];
deque<int> q;void add(int a,int b,int c){e[++idx]=b;w[idx]=c;ne[idx]=h[a];h[a]=idx;return ;
}//双向队列BFS
bool check(int limit){memset(dis,0x3f,sizeof dis);memset(vis,false ,sizeof vis);dis[1]=0;q.push_front(1);while(!q.empty()){int u=q.front();q.pop_front();if(vis[u]) continue;    //双段队列BFS是判断u而不是vvis[u]=1;for(int i=h[u];i!=0;i=ne[i]){int j=e[i],d=0;if(w[i]>limit) d=1;if(dis[j]>dis[u]+d){dis[j]=dis[u]+d;if(d==0) q.push_front(j);else q.push_back(j);}}}return dis[n]<=k-1;
}int main(){scanf("%d%d%d",&n,&m,&k);for(int i=1;i<=m;i++){int a,b,c;scanf("%d%d%d",&a,&b,&c);add(a,b,c),add(b,a,c);}//二分答案转化为判定int l=0,r=INF;while(l<r){int mid=(l+r)>>1;if(check(mid)) r=mid;else l=mid+1;}if(l==INF) puts("-1");else printf("%d\n",l);return 0;
}

任意两点距离

  1. 一般图:《图论4.1.1.Floyd求任意两点间最短路》\(O(N^3+Q)\)
  2. 仙人掌和基环树:《图论10.2.求仙人掌上任意两点的最短距离》\(O((N+N\log N)+Q\log N)\)
  3. 树:《图论9.树上差分与树上任意两点的最短距离》\(O(N\log N+Q\log N)\)
  4. 一个简单环:《动态规划5.3.求一个简单环上任意两点的最短距离》\(O(N+Q)\)

算法性质

  • 单调栈

    解题思路

    我们先研究样例,假设询问的区间是\([1,10]\),也就是把所有二元组依次进入这个“单调栈”,观察一下性质:

    我们会发现,当第\(8\)个二元组入栈时,尽管它弹出了栈里很多二元组,为自己成为“成功的”二元组做出了很多努力,但是最终还是没有把第\(4\)个二元组弹出栈,十分可惜。

第8个二元组 (2,7)|v           ->
第7个二元组 (3,4)
第5个二元组 (2,7)         第8个二元组 (2,7)
第4个二元组 (1,9)         第4个二元组 (1,9)

这时我们就会想:如果没有第\(4\)个二元组,也就是询问的区间左端点\(L_{query}\)\(≥4+1=5\),那么第\(8\)个二元组是不是就成为了“成功的”?

我们经过实践验证,发现果真如此。接下来,我们会研究每个二元组入栈时它下面的二元组是什么(如果这个二元组“成功的”入栈后栈内只有它,那我们就假设它下面的二元组是“哨兵”第\(0\)个二元组{\(0,INF\)})?也就是直接使这个入栈的二元组“失败”的是谁:

第1个二元组 第2个二元组 第3个二元组 第4个二元组 第5个二元组 第6个二元组 第7个二元组 第8个二元组 第9个二元组 第10个二元组
0 0 2 0 4 5 5 4 8 8

我们令第二行的数字都\(+1\),得到一个\(success[]\)数组:

第1个二元组 第2个二元组 第3个二元组 第4个二元组 第5个二元组 第6个二元组 第7个二元组 第8个二元组 第9个二元组 第10个二元组
1 1 3 1 5 6 6 5 9 9

于是我们假设一个猜想:当\(L_{query}≥success[i]\)时,第\(i\)个二元组一定是“成功的”。

略证明我们的猜想:

单调栈的一个小性质:单调栈的元素是否被弹出只与后面的元素有关,与前面的元素无关。

略证明:研究单调栈模板的代码,我们会发现:即将进栈的元素只会把它前面的元素弹出,无论它自己有多“差”,它自己最后**一定**会入栈,然后可能会被后面的元素弹出。

\(L_{query}<success[i]\)时,由上面的性质:第\(success[i]-1\)个二元组一定会入栈。既然询问区间是\([1,n]\)时第\(success[i]-1\)个二元组在第\(i\)个二元组到来之前都没有被弹出,现在第\(success[i]-1\)个二元组在第\(i\)个二元组到来之前自然也不会被弹出,仍在栈中的第\(success[i]-1\)个二元组一定会使第\(i\)个二元组“失败”。

\(L_{query}\)\(success[i]\)时,第≤\(L_{query}\)的二元组我们就不用管了,它们都没有入栈。由上面的性质第\(L_{query}\)个二元组是否被弹出与前面的二元组无关,一定是被后面的二元组弹出。既然询问区间是\([1,n]\)时所有第≥\(L_{query}\)的二元组都在第\(i\)个二元组到来之前被弹出,现在它们在第\(i\)个二元组到来之前自然也一定会被弹出,第\(i\)个二元组一定是“成功的”。

同时也注意:当\(L_{query}>i\)越界时,第\(i\)个二元组都没有入栈,自然也不是“成功的”。

因此,我们得到一个结论:当\(success[i]≤L_{query}≤i\)时,第\(i\)个元素一定是“成功的”,对答案的贡献\(+1\):我们建立一个\(ans[]\)数组,以\(L_{query}\)为下标:

L_query==             1   2   3   4   5   6   7   8   9   10
第1个二元组对答案的贡献  +1
第2个二元组对答案的贡献  +1  +1
第3个二元组对答案的贡献          +1
第4个二元组对答案的贡献  +1  +1  +1  +1
第5个二元组对答案的贡献                  +1
第6个二元组对答案的贡献                      +1
第7个二元组对答案的贡献                      +1  +1
第8个二元组对答案的贡献                  +1  +1  +1  +1
第9个二元组对答案的贡献                                  +1
第10个二元组对答案的贡献                                 +1  +1

研究上面的图,我们会发现:当\(L_{query}==1\)时,对答案有贡献的二元组有\(3\)个:第\(1\)\(2\)\(4\)个二元组。可是如果我们询问的区间是\([1,3]\)时,也就是\(R_{query}<4\)时,第\(4\)个二元组都没有入栈,自然对答案没有贡献,这样我们会多算答案,怎么办呢?

我们可以对询问区间按右端点递增进行排序。排序后,对于每个询问,我们把\([R_{Last Query}+1,R_{query}]\)的二元组对答案的贡献加入到\(ans[]\),即\(ans[L]+=1(success[i]≤L≤i)\),然后\(ans[L_{query}]\)就是答案。

由于\(ans[]\)是区间修改,单点查询,我们用非常合适的树状数组\(tr[]\)维护它。

具体步骤

  1. 对于每个二元组:{{\(a\),\(b\)},编号};
  2. 模拟单调栈预处理\(success[]\)数组。注意先把“哨兵”二元组{{\(0\),\(INF\)},\(0\)}入栈。对于每一个即将入栈的二元组\(i\),先按单调栈弹出栈顶的二元组,然后得到success[i]=stack.top().second+1;,最后再将该二元组入栈。时间复杂度是\(O(N)\)
  3. 对于每个询问:{{右端点,左端点},编号}(因为按右端点排序,所以把右端点放在\(query[].first.first\)的位置);
  4. 将询问按右端点排序。时间复杂度是\(O(QlogQ)\)
  5. 排序后,对于每个询问,我们把\([R_{Last Query}+1,R_{query}]\)的二元组对答案的贡献加入到\(ans[]\),即\(ans[L]+=1(success[i]≤L≤i)\),然后\(ans[L_{query}]\)就是答案。
  6. 由于\(ans[]\)是区间修改,单点查询,我们用非常合适的树状数组\(tr[]\)维护它。每个二元组对答案的贡献至多只会加入\(1\)次树状数组\(tr[]\),因此时间复杂度是\(O(NlogN+QlogQ)\)

\(N\)\(Q\)\(≤5 \times 10^5\),因此总的时间复杂度是\(O(N+NlogN)\)

C++代码

#include<bits/stdc++.h>
using namespace std;#define x first
#define y second
typedef pair<int,int> PII;
typedef pair<PII,int> PIII;
const int N=5e5+5;
int n,q;
int success[N],ans[N];
int tr[N];  //树状数组维护
PIII num[N],query[N];
stack<PIII> st;int read()
{int res=0,fff=1;char ch=getchar();while(!isdigit(ch)){if(ch=='-') fff=-1;ch=getchar();}while(isdigit(ch)){res=res*10+ch-'0';ch=getchar();}return res*fff;
}int lowbit(int x)
{return x&(-x);
}void add(int x,int val)
{while(x<=n){tr[x]+=val;x+=lowbit(x);}return ;
}int ask(int x)
{int res=0;while(x){res+=tr[x];x-=lowbit(x);}return res;
}int main()
{// freopen("stack.in","r",stdin);// freopen("stack.out","w",stdout);n=read(),q=read();for(int i=1;i<=n;i++) num[i].x.x=read();for(int i=1;i<=n;i++) num[i].x.y=read();for(int i=1;i<=n;i++) num[i].y=i;//模拟单调栈预处理success[]数组st.push({{0,1e6},0});   //注意先把“哨兵”二元组{{0,INF},0}入栈for(int i=1;i<=n;i++){int ai=num[i].x.x,bi=num[i].x.y;while(ai==st.top().x.x || bi>=st.top().x.y) st.pop();   //按照单调栈弹出栈顶的二元组success[i]=st.top().y+1;    //得到success数组st.push(num[i]);    //最后再将该二元组入栈}for(int i=1;i<=q;i++){query[i].y=i;query[i].x.y=read();query[i].x.x=read();  //因为按右端点排序,所以把右端点放在query[].first.first的位置}sort(query+1,query+q+1);    //将询问按右端点排序int idx=0;for(int i=1;i<=q;i++){int id=query[i].y,l=query[i].x.y,r=query[i].x.x;//对于每个询问,对于[R_{Last Query}+1,R_{query}]的二元组while(idx!=r){idx++;//把该二元组对答案的贡献加入到树状数组tr[]add(success[idx],1);if(idx+1<=n) add(idx+1,-1);}ans[id]=ask(l); //此时ask(L_{query})就是答案}for(int i=1;i<=q;i++) printf("%d\n",ans[i]);return 0;
}
  • AVL树

    • 题目图片

    AVL树:在计算机科学中,AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。

    原题每一个合法排列\(\Leftrightarrow\)n个节点的AVL树的每一个中序遍历

    原题就是求将1~n插入AVL树,也就是n个节点的AVL树的不同中序遍历数。

    1. 状态设计:由于AVL树涉及高度,所以设\(f[i][height]\):i个节点,高度为height的AVL树的不同中序遍历数。

      答案:\(ANS=\sum\limits_{height=1}^{18} f[n][height]\)。节点数为5000的AVL树高度最大是18。

      转移:\(f[i][height]=\sum\limits_{l=1}^{i}(f[l][height-1]*f[i-1-l][height-1]+f[l][height-1]*f[i-1-l][height-2]+f[l][height-2]*f[i-1-l][height-1])*C_{i-1}^l\)

      根据AVL树的定义,高度为height的AVL树的左右子树高度可以为:{height-1,height-1}、{height-1,height-2}和{height-2,height-1}。

      因为要求的是AVL树的不同中序遍历数,所以在求出AVL树的不同形态数后,还要乘上组合数\(C_{i-1}^l\)

       组合数含义:当前有i个数,把最大的数作为根节点(假设此AVL树满足大根堆),还剩i-1个数。从i-1个数中选出l个数去左子树,剩下的数就自然去右子树了
      

      边界:\(f[0][0]=f[1][1]=1\),其余高度为0或1的dp值为0。

    2. 优化。

      1. 只需要求出\(\sum\limits_{height=1}^{+\infty} f[n][height]\),转移过程中不是所有的状态最后都要利用到,因此采用记忆化搜索实现dp。

      2. 预处理\(low/up[height]\):高度为height的AVL树的节点数的下界/上界。这样就能及时排除不合法状态,而且转移时能缩小枚举左子树节点数的范围。

        高度为height的AVL树的节点数的下界/上界递推式:

        下界:

         边界:显然height=1时,节点数下界=1;height=1时,节点数下界=2。高度为height的AVL树的节点数下界=高度为height-1的一子树的节点数下界+高度为height-2的另一子树的节点数下界+根节点。
        

        上界:显然满二叉树就是符合条件且节点数最多的AVL树。

    代码链接。

  • 虚树

    可以证明,点集在树上的斯坦纳树即为其虚树。虚树上的边权和即为正确答案。

    定第一个关键点为根。否则当给定2个不在同一条链上的关键点时,下面的做法会判断错误。

    先考虑边权非0的情况。

    在草稿纸上模拟可易证:给定的错误代码是正确的,当且仅当给定的点集\(V_1\)中的两个点 u,v的LCA也在 \(V_1\)中。发现这等价于虚树的性质:“给定点集\(V_1\)在原图上的虚树点集\(V_1'\)”和“\(V_1\)”相同。

    求出虚树点集:根据虚树的建立,当插入一个关键点u时,设v、w是当前(注意题目是一个一个给关键点并让你分别作出判断,所以后面才给出的关键点不属于当前的关键点)关键点的集合中以dfn序排序的u的前躯、后继,我们会把u、lca(u,v)、lca(u,w)插入虚树(已经插入的就不再插入了),然后我们就会得到此时的虚树点集。

    快速判断“给定点集\(V_1\)在原图上的虚树点集\(V_1'\)”和“\(V_1\)”是否相同:in_tree[u]:点u是否在虚树中;is_key[u]:点u是否是关键点(注意题目是一个一个给关键点并让你分别作出判断)。利用这两个数组可求出\(more=|V_1'|-|V_1|\)。当more=0时两个点集相同,否则就不同。具体见代码

    边权为0的情况:只需要将所有由 0边连接的连通块视为一个点,一个连通块被选中当且仅当其中至少有一个点被选中,用并查集维护连通块,然后应用上述做法即可。

int n;
int key[N];
int h[N],e[M],w[M],ne[M],idx;
int p[N];   //用并查集维护边权为0的连通块
int dfn[N],num;
int dep[N],fa[N][20];
int more;   //more=给定点集V_1在原图上的虚树点集V_1'的大小-V_1的大小
bool in_tree[N],is_key[N];  //in_tree[u]:点u是否在虚树中;is_key[u]:点u是否是关键点(注意题目是一个一个给关键点并让你分别作出判断)
struct Tree
{int x;bool operator < (const Tree &qw) const{return dfn[x]<dfn[qw.x];    //虚树以dfn序排序}
};
set<Tree> tr;int find(int x) {}  //并查集void dfs(int u,int father)
{dfn[u]=++num;for(int i=h[u];i!=0;i=ne[i]){int v=e[i];if(v==father) continue;if(w[i]==0) p[v]=find(u);   //由 0边连接的连通块视为一个点dep[v]=dep[u]+1;fa[v][0]=u;for(int k=1;k<=18;k++) fa[v][k]=fa[fa[v][k-1]][k-1];dfs(v,u);}return ;
}int main()
{scanf("%d",&n);for(int i=1;i<n;i++){int u,v,wor;scanf("%d%d%d",&u,&v,&wor);add(u,v,wor),add(v,u,wor);}for(int i=1;i<=n;i++) scanf("%d",&key[i]);for(int i=1;i<=n;i++) p[i]=i;dep[key[1]]=1;  //定第一个关键点为根dfs(key[1],-1);for(int i=1;i<=n;i++){int x=find(key[i]);if(!is_key[x])  //可能该点所属的连通块已经插入虚树了{is_key[x]=true;tr.insert((Tree){x});if(in_tree[x]) more--;else in_tree[x]=true;auto it=tr.find((Tree){x});if(it!=tr.begin()){it--;int pp=find(lca(it->x,x));if(!in_tree[pp] && !is_key[pp]){in_tree[pp]=true;more++;}it++;}if(it!=(--tr.end())){it++;int pp=find(lca(it->x,x));if(!in_tree[pp] && !is_key[pp]){in_tree[pp]=true;more++;}it--;}}if(more==0) putchar('1');else putchar('0');}puts("");return 0;
}
  • 两次bfs求树的直径

    题目描述

    给定n个点,动态连n-1条边,最后保证连成1棵树。求每次连完1条边后,该边所在树的直径长度。

    解题思路

    由两次bfs求树的直径可知:当每次连完1条边合并两棵树时,新的树的直径的端点一定是原来两棵树的直径的四个端点中之二。

    因此对每个点开一个并查集,储存并查集的根和该连通块(树)的两直径端点。每合并两棵树时,在原来两棵树的直径的四个端点中选出距离最远的两个端点作为新的树的直径,并输出该直径长度。

    怎么求两个端点之间的距离呢?

    • 离线做法

      先把最终的树建出来。由于树上两点之间的距离是确定的,因此最终的树上两个端点之间的距离=此时两个端点之间的距离。

    • 在线做法

      如果把并查集v合并到并查集u,那么整棵树v中的每个点i的dep[i]和fa[i][k]都需要重新计算,因此使用启发式合并

    代码链接。

  • 小根堆

    原题等价于:求1到n的所有排列中,满足小根堆性质的排列的个数。

    \(f[i]\):i个不同的数的所有排列中满足小根堆性质的排列的个数。

    可以利用分治转移dp。首先计算出i个节点的完全二叉树中,根节点的左子树包含的节点数l,右子树包含的节点数r。由于根节点的值必须为最小值,所以根节点确定了,分治到根节点的左右子树。再考虑剩下的i-1个节点,在这i-1个节点中选出l个节点作为左子树,剩下的r个节点作为右子树。所以得出转移:\(f[i]=C_{i-1}^l*f[l]*f[r]\)

  • 线段树

    性质

    1. 线段树递归到叶子节点当且仅当:操作区间左端点是右叶子节点,操作区间右端点是左叶子节点。
    2. 两个叶子结点的 LCA的节点编号其实就是他们编号的最长公共前缀(二进制下)。

错误代码

  • 分块题

    • 题目图片

    调和级数:\(\frac{1}{1}+\frac{1}{2}+\frac{1}{3}+...+\frac{1}{n}=\ln n\)

    所以块长1~N中块的总个数是\(O(N\log N)\)

    一类长度为len的块的答案=分块中一类长度为len的块的贡献+分块中暴力的贡献=分块中一类长度为len的块的贡献+(所有询问的区间长之和-分块中一类长度为len的块的贡献*相应的块长len)。因此我们只要统计出分块中一类长度为len的块的贡献即可算出答案。

    一个长度为len的块[l,r]的贡献=所有满足\(l_q<l,r<r_q\)的询问\([l_q,r_q]\)的个数,因此这是一道二维偏序(sort维护r,树状数组维护l(因为树状数组适于维护\(l_q<l\)的偏序关系))的题目。

    1. 所有块块长信息存储在该块的右端点\(O(N\log N)\)
    2. 将所有询问按右端点排序。
    3. 从n到1依次枚举块的右端点r,先把询问区间右端点大于一类右端点是r的块的询问加入树状数组记入贡献\(O(MlogN)\),再把储存在右端点r的每个块查询所有包含该块(两端点不重合)的询问区间(由于二维偏序维护(所有在树状数组的询问区间的右端点\(r_q\)一定大于r(因为小于等于的还没有加入,sort维护),只需要查询树状数组中的所有左端点\(l_q\)小于l询问区间的个数),此时一定补充不漏)的个数记入长度为len一类块的贡献ans[len]\(O(NlogNlogN)\)
    4. 一类长度为len的块的答案=分块中一类长度为len的块的贡献+(所有询问的区间长之和-分块中一类长度为len的块的贡献*相应的块长len)。
int n,m;
LL ans[N],totlen;//ans[len]:分块中一类长度为len的块的贡献;totlen:所有询问的区间长之和
vector<int> block[N];//将所有块以块长信息存储在该块的右端点
struct Query
{int l,r;bool operator < (const Query &q) const//sort维护r,将所有询问按右端点排序{return r>q.r;}
}que[N];LL tr[N];//树状数组维护lint main()
{n=read(),m=read();for(int len=1;len<=n;len++) for(int r=1+len-1;r<=n;r+=len) block[r].push_back(len);//将所有块以块长信息存储在该块的右端点,O(NlogN)for(int i=1;i<=m;i++){que[i].l=read(),que[i].r=read();totlen+=que[i].r-que[i].l+1;}sort(que+1,que+m+1);//将所有询问按右端点排序for(int r=n,i=1;r>=1;r--)//从n到1依次枚举块的右端点r{while(i<=m && que[i].r>r)//先把询问区间右端点大于一类右端点是r的块的询问加入树状数组记入贡献{add(que[i].l,1);//树状数组,先把询问区间右端点大于一类右端点是r的块的询问加入树状数组记入贡献,O(MlogN),O(MlogN)i++;}for(auto len : block[r]) ans[len]+=ask((r-len+1)-1);//树状数组,查询树状数组中的所有左端点l_q小于l询问区间的个数,再把储存在右端点r的每个块查询所有包含该块(两端点不重合)的询问区间(由于二维偏序维护,此时一定补充不漏)的个数记入长度为len一类块的贡献ans[len],O(NlogNlogN)}for(int len=1;len<=n;len++) printf("%lld ",ans[len]+(totlen-ans[len]*len));//一类长度为len的块的答案=分块中一类长度为len的块的贡献+(所有询问的区间长之和-分块中一类长度为len的块的贡献*相应的块长len)return 0;
}

虚树

  • 我是A题

    把题目转化为给定若干个对顶点为(a,b,c)的直棱柱,求总体积。

    观察给定代码会发现:三元组有\(\frac{2}{9}\)的概率会出现(A,<B,<C),有\(\frac{2}{9}\)的概率会出现(<A,B,<C),有\(\frac{1}{9}\)的概率会出现(A,B,<C),剩下\(\frac{4}{9}\)的概率会出现(<A,<B,C)。

    对于三元组(A,B,<C)的体积是很好算的——大长方体。出现至少一个三元组(A,B,≥C-50)的概率是\(1-(1-\frac{50}{3*10^7})^{3*10^7*\frac{1}{9}}=0.996134098\)。因此在大长方体上面最多只有50层体积。

    这50层体积暴力一层一层算,把三维问题转化为二维问题:将三元组(现在只用关心A和B)按照A排序,从大到小扫,记录长方形的右边界,当当前的三元组的B大于右边界时,体积增加;否则根据单调性体积不增加。

    除此之外,测试点1~2由于数据范围小,出现(A,B,≥C-50)的概率小,但是高度≤100,用上面的做法暴力一层一层算也是没有问题的。

  • 线段树

    构造+线段树的结构分析

    以样例为例,图中每个点标有深度(到最远叶子节点的距离\(+1\)),由于线段树接近完全二叉树,所以最大深度是31(而不是\(30\)\(2^{30}>10^9\))!!!),保证了时间复杂度。

    每次add操作相当于是从根节点到叶子节点选一条链,已经选过的点不能加入贡献(换言之,每次add操作相当于是从最上面到叶子节点选一条链,选择的链不能相交)。

    一个正确的贪心:每次都选择一条对当前贡献最多的链。

    对于样例,第一次选择深度为\(6\)的一条链。第二次选择深度为\(5\)且不包含于第一条链的一条链。第三次由于深度为\(6,5,4\)的链都被包含了,选择深度为\(3\)且不被包含的一条链……

    假设我们求出了\(cnt[depth]\):深度为\(depth\)的点的个数。然后我们从大到小遍历深度\(depth\),若\(cnt[depth]\geqslant 1\),则选择一条深度为\(depth\)的链,并令\(cnt[1\sim depth]\leftarrow cnt[1\sim depth]-1\),因为选择了一条深度为\(depth\)的链就会使得深度为\(1\sim depth\)的链各\(1\)条被包含。

    实际上我们每选择一条深度为\(depth\)的链不需要在线令\(cnt[1\sim depth]\leftarrow cnt[1\sim depth]-1\),可以在求出了\(cnt[depth]\)费用提前计算for(int i=1;i≤max_depth;i++) cnt[i]-=cnt[i+1];,然后从大到小遍历深度\(depth\),若\(cnt[depth]≥1\),则把\(depth\)加入贡献并令\(k\)\(1\),直至\(cnt[1]=0\)\(k=0\)。这一部分的复杂度是\(O(\lceil\log_2 m\rceil)\)

for(int i=1;i<=31;i++) cnt[i]-=cnt[i+1];
int cidx=31;
while(k && cidx>=1)
{ans+=min(k,cnt[cidx])*cidx;k-=min(k,cnt[cidx]);cidx--;
}
printf("%d\n",ans);

所以我们怎么快速求出\(cnt[depth]\)呢?很明显这个只与\(m\)有关。因为\(m\)确定了,线段树的形态就确定了。

线段树是九条可怜很喜欢的一个数据结构,它拥有着简单的结构、优秀的复杂度与强大的功能,因此可怜曾经花了很长时间研究线段树的一些性质。——[ZJOI2017]线段树

于是我们就可以非常开心的打表找规律了。

m= 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 
depth=1节点数: 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 
depth=2节点数: 0  1  1  2  2  2  3  4  4  4  4  4  5  6  7  8  8  8  8  8  8  8  8  8  9 10 11 12 13 14 15 16 16 16 16 16 16 16 16 16 
depth=3节点数: 0  0  1  1  1  2  2  2  2  2  3  4  4  4  4  4  4  4  4  4  5  6  7  8  8  8  8  8  8  8  8  8  8  8  8  8  8  8  8  8 
depth=4节点数: 0  0  0  0  1  1  1  1  1  2  2  2  2  2  2  2  2  2  3  4  4  4  4  4  4  4  4  4  4  4  4  4  4  4  4  4  5  6  7  8 
depth=5节点数: 0  0  0  0  0  0  0  0  1  1  1  1  1  1  1  1  1  2  2  2  2  2  2  2  2  2  2  2  2  2  2  2  2  2  3  4  4  4  4  4 
depth=6节点数: 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  2  2  2  2  2  2  2 

于是我们就可以写出函数get_cnt(int m,int depth):求出值域为\([1,m]\)的线段树深度为\(depth\)的节点数。这一函数的复杂度是\(O(1)\)的。

int get_cnt(int m,int depth)
{if(depth==1) return m;int len=p2[depth-2]+1;if(m<len) return 0;int logm=__lg(m/len);int r=p2[logm+1]*len;return max(p2[logm],p2[logm+1]-(r-m));
}

总的时间复杂度\(O(T\lceil\log m\rceil)=O(31*T)\),常数很小轻松通过。

代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/936556.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构字符串和图

1.字符串的存储 1.1.字符数组和STLstring char s[N]strlen(s+i):\(O(n)\)。返回从 s[0+i] 开始直到 \0 的字符数。 strcmp(s1,s2):\(O(\min(n_1,n_2))\)。若 s1 字典序更小返回负值,两者一样返回 0,s1 字典序更大返…

字典dict

2025.10.14 1.字典的键值必须是不可变的,也就是说元祖,形如下面的初始化是可以的dict1 = {(1, 2): 1} dict1 = {a: 1} dict1 = {}

结婚证识别技术:融合计算机视觉、深度学习与自然语言处理的综合性AI能力的体现

在数字化浪潮席卷各行各业的今天,如何高效、准确地处理海量纸质证件信息,成为提升政务服务与金融业务效率的关键。结婚证作为证明婚姻关系的核心法律文件,因而,结婚证识别技术应运而生。它不仅是光学字符识别技术的…

上下文丢失

2025.10.14 位置编码外推失效是Transformer模型在长文本推理中出现上下文丢失的最常见架构限制,因为训练时使用的固定位置编码(如正弦编码)无法有效外推至超出训练长度的序列位置,导致位置信息丢失。 残差连接梯度…

数据结构序列

不要从数据结构维护信息的角度来思考问题,而是从问题本身思考需要哪些信息,数据结构只是维护信息的工具!!! 可减信息,如区间和、区间异或和 直接用前缀和实现,复杂度 O(n)+O(1)+O(n)。 可重复贡献信息,如区间最…

上下文学习(In-context Learning, ICL)

2025.10.14 上下文学习(In-context Learning, ICL)的核心机制是在推理阶段不更新模型参数,利用提示中的少量示例引导模型生成适应新任务的输出。也就是在不更新参数的情况下,利用提示中的示例让模型在内部条件化地…

混淆矩阵

2025.10.14 混淆矩阵可以显示模型的所有预测结果,包括真正例、假正例、真负例和假负例,从而帮助分析模型的性能 混淆矩阵不仅仅显示准确率,还提供更详细的分类结果 混淆矩阵与训练损失无关 混淆矩阵不涉及超参数设置…

提示词工程实践指南:从调参到对话的范式转变

写在前面 作为一名长期与代码打交道的工程师,我们习惯了编译器的严格和确定性——相同的输入永远产生相同的输出。但当我们开始使用生成式AI时,会发现这是一个完全不同的世界。最近在系统学习Google的AI课程时,我整理…

泛化能力

2025.10.14 在大型语言模型的工程实践中,提高泛化能力的最常见策略是使用更大的预训练数据集,因为更多数据可以帮助模型学习更泛化的表示,例如GPT-3和BERT等模型都强调大规模数据集的应用。

JVM引入

虚拟机与 JVM 虚拟机(Virtual Machine),就是一台虚拟的计算机。它是一款软件,用来执行一系列的虚拟计算机指令。 虚拟机可以分为系统虚拟机和程序虚拟机:Visual Box、VMware 就属于系统虚拟机,它们完全是对物理计…

shiro 架构

一、subject(当前用户信息) 二、SecurityManager(所有用户管理) 三、Realm(数据连接)

[音视频][HLS] HLS_downloader

[音视频][HLS] HLS_downloader$(".postTitle2").removeClass("postTitle2").addClass("singleposttitle");01 简介 1.1 功能: 一个简单的HLS下载器,使用go语言实现 1.2 执行方式 如果…

Python-weakref技术指南

Python weakref 模块是 Python 标准库中用于处理对象弱引用的重要工具。它允许程序员创建对对象的弱引用,这种引用不会增加对象的引用计数,从而不影响对象的垃圾回收过程。本报告将全面介绍 weakref 模块的概念、工作…

从众多知识汲取一星半点也能受益匪浅【day11(2025.10.13)】

Enjoy 基于代码思考问题 先理清楚代码是否用上了文档所定义的api

王爽《汇编语言》第四章 笔记

4.2 源程序 4.2.1 伪指令在汇编语言的源程序中包含两种指令:汇编指令、伪指令。 (1)汇编指令:有对应机器码的指令,可以被编译为机器指令,最终被CPU所执行。 (2)伪指令:没有对应的机器指令,最终不被CPU所执行…

10.13总结

import java.util.*; import java.util.concurrent.TimeUnit; public class ArithmeticPractice { private Set generatedQuestions = new HashSet<>(); private List questions = new ArrayList<>(); pri…

MySql安装中的问题

是一台已经安装过但是失败了的win 1. 2025-10-13T12:42:20.566779Z 0 [ERROR] [MY-010457] [Server] --initialize specified but the data directory has files in it. Aborting. 2025-10-13T12:42:20.566788Z 0 [ERR…

10.14总结

import java.util.*; import java.util.concurrent.TimeUnit; public class ArithmeticPractice { private Set generatedQuestions = new HashSet<>(); private List questions = new ArrayList<>(); pri…

题解:AT_agc050_b [AGC050B] Three Coins

传送门 注:如无特殊说明,本篇题解中所有的序列,均用红色标示已经放置硬币的位置。若本次操作为拿走硬币,用蓝色标示本次操作拿走的硬币的位置,用黑色标示从未放过硬币或放置过硬币且在本次操作之前的操作中被拿走…