[专题总结]数据结构 - ploylog/杂项
NOI模拟1 A
首先观察性质,如果我们枚举一个 k,那么一个区间可以代表一个决策(选这个区间内的前k大)。
这个时候我们只需要看哪些区间是合法的就可以了,一个区间合法当且仅当他的左右端点都是前k大。
那么这个时候不难想到一个容斥,全部区间-(左端点不是前k大,右端点随便选)-(右端点不是前k大,左端点是前k大)
求出每个点前后第k个大于他的数,设为 \(l_i, r_i\), 那么第二部分只需要对于每个左端点\(i\) ,在他的 \(r_i\) 后面选择就可以了,有 \(n-r_i+1\) 中选择方式。
第三部分,右端点不是前 \(k\) 大,那么肯定要在 \(l_i\) 左边选择,但是还要求左端点是前k大。
发现左端点是前 \(k\) 大的条件就是左端点的 \(r\) 大于右端点,那么第 \(k\) 个比左端点大的数在右端点右边,自然是前k大。
所以我们把 \(l_i, r_i\) 求出来,树状数组去扫描线就可以了。
求 \(l_i, r_i\) 可以以 \(h_i\) 为历史版本用主席树上二分求出。
这个做法复杂度 \(O(nklogn)\) ,没什么优化的余地。
那么换一个思路,既然左端点右端点都得是前 \(k\) 大,也就意味着 \(max(rk_l, rk_r) \leq k\) 。
并且它还具有单调性,也就是对于 \(k\) 他是合法的区间,对于 \(k+1\) 也是(但是k要小于等于len)。
分类讨论一下 \(\max\) 取到谁,也就是谁更小。
假设现在 \(h_r\) 更小,那么,每个在 \(r\) 左边且大于 \(h_r\) 的数,都可以作为左端点。
且可以作为 \(k\) 的下界是他是从右往左第几个大于 \(h_r\) 的数。
所以只需要查 \(r\) 左边有多少个大于他的数,就可以计算贡献。
\(h_l\) 更小的情况,倒着做一遍就可以了。
code
int n, h[N], Lx, Rx; ll Ans, ans[N];
void sc(){
n = read(), Lx = read(), Rx = read();
for(R i = 1; i <= n; i++) h[i] = read();
}
struct BIT{
int t[N]; inline int lb(int x){ return (x & -x); }
inline void clear(){ memset(t, 0, sizeof(t)); }
inline void add(int x){ for(; x; x -= lb(x)) t[x]++; }
inline int ask(int x){ int ans = 0; for(; x <= n; x += lb(x)) ans += t[x]; return ans; }
} bit; // 后缀和的树状数组
void work(){
for(R i = 1; i <= n; i++) ans[bit.ask(h[i]) + 1]++, bit.add(h[i]);
bit.clear(); for(R i = n; i; i--) ans[bit.ask(h[i]) + 1]++, bit.add(h[i]);
for(R i = n; i >= 2; i--) ans[i] += ans[i + 1]; ans[1] = n;
for(R i = 3; i <= n; i++) ans[i] -= n - i + 2, ans[i] += ans[i - 1];
// ans[i] -= n - i + 2的原因是长度为 i 的区间要在 i + 1 的地方贡献--
for(R i = Lx; i <= Rx; i++) Ans ^= ans[i];
printf("%lld\n", Ans);
}
还有一个做法,枚举作为第 \(k\) 大的数 \(h_i\), 假设把所有大于等于 \(h_i\) 的数都标记一下。
那么,合法的以 \(h_i\) 为第 \(k\) 大的区间的条件就是包含 \(h_i\) 且左右端点都是被标记的数且区间内恰好有 \(k\) 个被标记的数。
假设有 \(x\) 个大于 \(h_i\) 的数, \(h_i\) 是第 \(t\) 个,那么,对 \(k\) 的贡献就是 \(\min\{t, x-t+1,k,x-k+1\}\)
一个一个来, 第一个是因为这个数左边只有 \(t\) 个可行的左端点,第二个是因为他右边只有 \(x-t+1\) 个可行的右端点。
第三个是因为包含 \(h_i\) 且恰好包含 \(k\) 个被标记数的区间最多 \(k\) 个,第四个是因为区间一共有 \(x-k+1\) 个。
所以,发现枚举第 \(k\) 大的数之后,对各个 \(k\) 的贡献是分段函数,差分之后前缀和就可以获得贡献。
P6881 [JOI 2020 Final] 火事
我们考虑把横坐标设置成序列下标,纵坐标设置成时间。
那么肯定最后形成的有用信息是一个三角形,这个三角形上面的东西肯定和三角形的对角线一样。
观察一下,发现一个数能在我们的三角形里面影响的肯定是一个平行四边形,且底边和 \(x\) 轴重合。
这个时候,我们可以把它差分成三个等腰直角三角形,本题提供了一种支持等腰三角形加,线段查询的方法。
当限制比较复杂的时候,重要的思想使用尽量少的变量去表示限制,并且忽略一些不会带来影响的限制。
本题发挥的淋漓尽致,一个等腰直角三角形可以表示成 \((a,b,c) \{ x \leq a, y\leq x +b, val = c\}\)
那么考虑他对一个直线 \((p,t)\{y=t, x\leq p\}\) 的贡献,就是 \(c*(\min(p, a)-\min(p,t-b))\)
发现他和修改,询问都有关系,一个经典的操作是独立他们,\(c*(\min(p, a)-\min(p,t-b))=c*(\min(p,a) -\min(p-t,-b)+t)\)
那么 \(\min\) 两边的东西之和修改询问分别相关,在一个询问讨论 \(\min\) 的取值即可计算。
NOI模拟 6.21 A
先来题解做法,发现我们存在一个暴力:贪心的选择能选择的编号最小的,加入他扩展得到的新的合法的。
发现加入一个点,他仍然不合法,那么他两边的权值至少增大一,增大到 \(\sum s_i\) 就
NOI模拟7.26 B
考场竟然连暴力都不会,思考太少了。
先考虑暴力怎么做,关键是要想到枚举最终的答案, 求最优答案没思路,优先考虑枚举答案如何判定 。
然后我们在枚举一下答案的位置,就是看这个区间内 \(>x\) 的个数 和 \(y\) 的关系。
我们知道 \(2n\) 个区间内大于 \(x\) 的数的个数即可枚举答案,更确切的,我们只需要知道这一层所有区间 \(>x\) 的数的最小值。
考虑顺序枚举 \([1,n]\) ,时刻维护每一个区间的数的个数和这一层个数的最小值,求答案直接枚举层即可。
如果要在线,我们需要把 \([1,n]\) 的每层的信息都记录下来,总共 \(nlogn\) 个信息,题目空间只有 \(\rm 16MB\) ,会爆炸。
时刻记住在 rank 和 kth之间转化,看到一个就可以去向另一个 , 如果我们在每一层记录一下这一层所有区间 \(kth = z\) 的数的最小值,我们只需要看 \(len-y\) 这个 \(kth\) 的最小值是否小于 \(x\) 就行了,于是空间变成了 \(\sum 2^i=O(n)\) 。
NOI模拟7.25 A
一个矩形和他相交必须 \(x\) 轴相交并且 \(y\) 轴相交,\(x\) 轴相交直接交给线段树,因为是单点修改。
\(y\) 轴理论上就该交给扫描线了,发现把所有矩形 \(R_x \leq r\) 都加进去,不相交的条件是 \(L_x < l\) ,直接开两个主席树即可。
\(y\) 轴不是单点,可以 \(\rm KDT\) , \(y\) 轴不相交的矩形需要满足 \(R < L_y\) 或者 \(L>R_y\) ,查询全集减去这两个矩形即可。
NOI模拟7.23 A
赛时太划水了,连个表都没打,这类转移是取 \(\min\) 的 dp , 没有什么优化思路的时候直接打个表看转移点在哪里,有的时候可以找到决策单调性,这题就不一样了,转移点就是最大的 \(\{m|m=2^k \And m < n\}\)。
然后我们可以直接得到一个 \(O(K)\) 计算一个数贡献的柿子,由于题目是给出的若干段区间,所以我们想要得到一个计算前缀和的柿子,那么就可以 \(O(r)\) 计算一个人的贡献, \(r\) 是它这个数里面 \(1\) 的段数。
前缀和的柿子是一个二阶等差套等比的形式,对付这种柿子就是乘公比之后错位相减, 特别的,公比 <0的时候可以计算到正无穷的值。
然后只需要时刻维护连续段即可,发现加减一个 \(2^k\) 位的变化是 \(O(1)\) 的,所以直接用 \(\rm ODT\) 维护连续段。
省选模拟12 B
要想做出来这道题,你需要把这道题拆成两半,然后分别作出这两半。
假设我们可以在常数次数内确定一个下标集合内的元素集合,那么怎么做?
类似汉明码的思想(\(n\) 个大小为 \(2^{n-1}\) 的次方的集合的交集可以为 \([0,2^n-1]\) 的任意数),我们对二进制第 \(i\) 位是 \(1\) 的所有数查一下。
对于一个需要求的数字 \(i\), 如果他这一位在二进制上是 \(1\) ,就和这个集合取交集,如果是 \(0\),就和这个集合关于全集的补集取交集,这样 \(log\) 次查询 “一个下标集合内的元素集合” 就可以确定答案。
怎么在常数次数内确定一个下标集合内的元素集合,有两种做法。
法一:
找到序列的最值(这里假设是最小值,最大值最小值都可以) \(x\)。
之后询问一个集合 \(S\), 再询问一个集合 \(S\cup{x}\) , 两个集合做差,每个数加上最小值就可以了。
找最值的过程
找最值的过程可以先确定一个数,然后查一下所有数和他的差值,有一个性质是和他差值最大的必定是序列的一个最值,(这不禁使我想到了树的直径的一个性质:从一个点走到最远的点肯定是树的直径的一个端点)。
一开始最值一定在 \(0-n\) 这个大集合中,我们可以通过类似线段树二分的过程一步一步缩小这个集合,找到最值和确定值的差值,再通过一操作随意确定一个值,就可以找到具体的最值了。
法二(frome cqbz wangxukang):
先用 qry1 随便问出两个位置 \(x,y\) 的值,不妨设 \(a_x<a_y\)。对于想要确定的下标集合 \(S\),询问 \(S\),\(S\cup{x}\),\(S\cup{y}\),可以得到 \(X={|a_i-a_x|\mid i\in S}\) 和 \(Y={|a_i-a_y|\mid i\in S}\)。取 \(u=\max X\) 和 \(v=\max Y\),考虑一下数轴上这个最大的差值的位置,可以发现若 \(u>v\),那么就必然有一个 \(a_x+u\);若 \(u<v\),就必然有一个 \(a_y-v\);若 \(u=v\),二者都存在(可能就是同一个数)。此后将确定的值在两个集合造成的影响消除。重复这个步骤就可以得到 \(S\) 内所有的值了。
大佬还顺便给出了二进制确定的当 \(n\) 不是二的次方的一种实现方式:并行分治。
solve(l,r,V) 表示处理区间 \([l,r]\),已知值域是 \(V\),不并行的方法就是通过确定 \([l,mid]\) 内的元素递归向 solve(l,mid,V_l) 和 solve(mid+1,r,V_r) 实现一个不如暴力的算法。而可以发现例如你要分别递归 \([2,3,4,5]\),\([6,7,8,9]\),\([9,10,11,12]\),你就一起问一发 \(S=[2,3,6,7,9,10]\),因为三个值域不交,所以你能够确定问出来的值是属于那个区间的。
并行分治的本质就是二进制确定,这也提供了一种思路:分治的两边的信息有可能重叠,可以考虑一起算出或者对值的重复利用把复杂度从 \(nlogn\) 降到 \(logn\)
这也和同样是这场比赛的 C 题中的一个 \(log\) 分治 \(FFT\) 有些相似之处了。
正常的分治 \(FFT\) 是在计算出来 \([l,mid]\) 之后的 \(f\) ,卷上 \(g\) 贡献到右边,再去递归右边,经典的 \(cdq\) 分治。
但是发现右边对右边的贡献和左边对左边的贡献实际上是一个东西。
这个通过考虑答案贡献的路径(答案是一堆 \(g\) 枚举整数划分乘起来在加起来得到的)就可以得到,所以分治的时候只需要把左边的东西先贡献给右边,再把左边的东西卷上 \(g\) 贡献给右边,右边就计算完毕了,用倍增实现会更简单。
A. 【PR #4】赌徒
绝对的耻辱!!! 这都不会! 专门写题解来羞辱自己不会这道题!
直接观察到答案要不是原来的数要不是 \(1\) (用来摆烂), 先离散化下来。
再把答案写成柿子,化一化发现这两个人是独立的!(但凡用脑子想想好像也能知道。。)
贡献是 \(f_i+f_j-4*i*j\), 直接斜率优化。
P7172 [COCI2020-2021#3] Specijacija
发现图/树里面有 1/2 度点,直接考虑缩起来。
然后问题就是求出一个点的所在大点编号和把大点的树建出来就好了。
第二个到是比较简单,第一个我第一时间想到的是求出每个大点包含什么,但是貌似不太好表示。
那不妨就换一个角度,从大到小每一层记下来所有点的标号,变化量是 O(1) 的,使用主席树维护。
查询点的时候类似线段树二分去定位,数据结构问题要学会时常切换考虑角度。
P7164 [COCI2020-2021#1] 3D Histogram
首先可以思考分治,