数据结构乱写
loj6515 贪玩蓝月
容易发现本题中要求的信息不支持快速合并,不支持快速删除,但是支持快速插入。
所以一个简单的离线做法就是线段树分治。
只要按照时间建线段树,把每个操作插入到对应节点上。
最后 \(dfs\) 一遍线段树顺便插入,在叶子节点输出答案即可。
然而这个信息是支持快速合并两个信息的。
即可以将问题分为两半,之后枚举一边大小,另外一边通过单调队列来实现找到最大值。
所以只要取 \(n\) 个元素的中点为分界点。
一个问题是,如果 \(pop\) 的过多导致超出分界点应该如何处理。
方法是继续选择另一边的中点为分界点,对全局进行暴力重构。
直观理解复杂度就是对的,可以通过分析这样一个函数来证明其复杂度:\(\phi=分界点左元素个数-分界点右元素个数\)。
对于每个操作,可能会使其变化 \(+1,-1\),单次操作复杂度为 \(O(p)\)。
另一种可能是使其赋值为 \(0\),因为此时左侧或右侧元素个数为 \(0\),单次操作复杂度就是 \(O(|\phi| p)\)。
因为在操作之后将函数值归零,复杂度是正确的。
CF1129D Isolation
容易想到一个简单的朴素 \(dp\)。
\(dp_i\) 表示当前将 \([1,i]\) 划分为了若干合法段的方案数。
有这样的转移 \(dp_i=\sum \limits_{j=0}^{i-1} dp_j * [f(j+1,i) \leq k]\)。
其中 \(f(l,r)\),表示 \([l,r]\) 这段区间中出现次数恰好为 \(1\) 的颜色数。
然后发现 \(f\) 函数没有什么很好的性质,并不能用数据结构简单的维护。
考虑由 \(f(l,r-1)\) 到 \(f(l,r)\) 的变化。
那么对于 \(a_r\) 出现的最后三个位置 \(x,y,r\)。
\([x+1,y]\) 内不再出现恰好一次,进行区间减法。
\([y+1,r]\) 内恰好出现一次,进行区间加法。
对于其他情况的取值,没有影响。
所以考虑对原序列分块,每个块开一个桶记录每个 \(f\) 值对应 \(dp\) 的和。
对于每次修改操作重构边界块,对内部的整块移动一下指针即可。
loj 3166 魔法树
其实限制的是这样一个东西,每个点的选择时间要小于等于其父节点的选择时间。
若一个点 \(i\) 的选择时间恰好为 \(d_i\),则获得 \(w_i\) 的收益,求最大收益。
可以进行一个 \(dp\),\(dp_{i,j}\) 表示 \(i\) 的选择时间为 \(j\) 的最大收益。
令设 \(g_{i,j}=\max \limits_{k=1}^j \{dp_{i,k}\}=\max \{g_{i,j-1},dp_{i,j}\}\) 辅助转移。
那么 \(dp_{i,j}=[j=d_i]*w_i+\sum \limits_{x\in son_i} g_{x,j}\)。
容易发现一个点有效的 \(dp\) 值只有子树中出现过的选择时间。
根据树上启发式合并的思想可以继承重儿子,暴力累加轻儿子。
得到 \(dp\) 数组,然而仍然无法暴力扫一遍进行取 \(\max\) 操作。
然而其实我们只需要 \(g\) 数组,然后前缀取 \(\max\) 这个操作具有推平的性质。
所以用一个 \(set\) 维护每一个连续段的 \(g\) 值,在合并的过程中跑一跑就可以做。
更简单的做法是对 \(g\) 数组进行一下差分,只维护其中非 \(0\) 的位置。
然后子树合并是简单的,对于单点修改操作,显然并不能直接累加在差分数组上。
执行下面这样的操作来弹掉错的部分即可。
s[x][d[x]]+=w[x]; ll delta=w[x]; auto it=s[x].upper_bound(d[x]),tmp=it;
while(it!=s[x].end())
if(delta>=it->second) delta-=it->second,tmp=it++,s[x].erase(tmp);//dp(x,d(x))更优
else{ it->second-=delta; break; }//这个点更优
loj 2537 Minimax
首先是一个简单的 \(dp\)。
\(dp_{i,j}\) 表示节点 \(i\) 权值为 \(j\) 的概率。
注意到每个叶子节点的权值不同,对于 \(j\) 在左子树中:
\(dp_{i,j}=dp_{lch,j}*(p_i \sum \limits_{k=1}^{j-1} dp_{rch,k}+(1-p_i) \sum \limits_{k=j+1}^{\infty}dp_{rch,k})\)
对于 \(j\) 在右子树中是同理的。
对于一个节点,\(dp_i\) 中不为 \(0\) 的下标一定是子树中出现过的权值。
所以使用线段树合并来优化这个转移。
对于当前合并到的节点 \(lc,rc\),区间为 \([l,r]\)。
若 \(lc,rc\) 均有值,那么递归两个儿子节点。
若只有 \(lc\) 有值,实际上只需要得知 \(\sum \limits_{k=1}^{l-1} dp_{rc,k}\) 和 \(\sum \limits_{k=r+1}^{\infty} dp_{rc,k}\),然后打上区间乘法标记即可。
这两个信息只要维护子树和,就可以在线段树合并的同时维护出来。
CF1340F Nastya and CBS
首先对原序列分块,依次考虑每个块。
若块内的括号无法进行匹配,显然括号序列非法。
否则可能存在一些无法匹配的括号,这些括号一定可以表示为 \())))(((\)。
对于询问操作,从左到右依次考虑每个块(包括散点)。
维护一个栈表示已有的左括号。
然而一个问题是,如果暴力加入每个块中的括号,复杂度显然是错的。
对于栈内元素维护一下元素个数和元素的所属块,首先确定弹出的元素个数,然后通过哈希判断是否可以一次性弹出。
CF1290E Cartesian Tree
根据笛卡尔树的性质,一个子树的 \(size_i\) 实际上就等于 \(r_i-l_i+1\),(这里的 \(l_i,r_i\) 是动态变化的,所以应该是排名)。
这个信息可以拆成 \(l_i,r_i\) 分别维护,考虑后者。
对于 \(r_i\) 这个东西,实际上就是满足 \(a_{r_i+1}>a_i\) 的最小的 \(r\)。
考虑插入一个最大值 \(a_{p}=x\),排名为 \(q\) 对 \(r_i\) 的影响。
对于满足 \(r_i \geq q\) 需要进行 \(+1\) 操作,因为插入了一个数导致排名产生变化。
对于 \(i<p\) 的下标 \(i\),需要使 \(r_i\) 对 \(q-1\) 取 \(\min\)。
注意到前一个操作似乎并不能简单维护,但是将两个操作结合起来却可以。
对于 \(i>p\),有 \(r_i \geq i > p\)。
对于 \(i<p\),显然 \(r_i\) 在进行过第二个操作之后 \(<q\)。
所以问题就是前缀取 \(\min\),后缀 \(+1\),全局求和。
这个东西写个势能分析的线段树,维护 最大值/次大值/最大值出现次数/两个加法标记/区间和/区间合法点数 就好了。
CF1260F Colored Tree
考虑通过枚举颜色/点对来求和。
设 \(g_i=r_i-l_i+1\),这样的话答案可以写为
\(\sum \limits_{c} \sum \limits_{l_i\leq c \leq r_i} \sum \limits_{i<j,l_j\leq c \leq r_j} \frac{dis(i,j)}{g_ig_j} \prod \limits_{k=1}^n g_k\)
将 \(dis(i,j)\) 拆成 \(dep_i+dep_j-2dep_{lca(i,j)}\),这样的话可以将式子中的 \(i,j\) 分裂开求和。
考虑通过类似扫描线的算法,扫一遍颜色 \([1,m]\)。
同时动态加入符合条件的,动态删除不符合条件的 \(i\)。
要求的是 \(\sum \limits_{i} \frac{dep_i}{g_i} \sum \limits_{j>i} \frac{1}{g_j}\)
\(\sum \limits_{i} \frac{1}{g_i} \sum \limits_{j>i} \frac{dep_j}{g_j}\)
\(\sum \limits_{i} \frac{1}{g_i} \sum \limits_{j>i} \frac{dep_{lca(i,j)}}{g_j}\)
前两项都可以简单的维护,最后一项要用到一个特殊的技巧。
把 \(dep_{lca(i,j)}\) 这个东西差分到树上的每个节点。
其实就是把每个 \(i\) 都打到它的每个祖先节点上,贡献为 \(\frac{1}{g_i}\)。
对于每个 \(j\),对它的祖先节点权值求和即可。
区间修改/区间查询,可以用树剖+树状数组/线段树维护,也可以用 \(lct\)。
loj 3277 星座 3
对楼房高度建出笛卡尔树。
这样的话只需要在 \(x\) 两个儿子合并的时候,要求不能同时存在 \(\geq a_i\) 的两个星星。
所以可以写一个 \(dp\) 出来,\(dp_{i,j}\) 表示 \(i\) 子树代表区间中,最大高度为 \(j\) 的最大权值。
同样考虑 \(j\) 的状态数一定是子树中的,同样用线段树合并优化一下转移就行了。
有这样一个更加简单的做法:
将楼房,星星都按照纵坐标排序。
通过并查集维护联通性,即当前情况下每个点向左向右不跨墙能到达的区间。
维护这样一个信息 \(cost_i\),表示在 \(i\) 这个横坐标放星星需要付出的代价。
考虑每个星星,如果 \(w_i \leq cost_x\),显然不放这个星星更优。
否则暂时地选择付出 \(cost_x\) 代价,放下这个星星,并通过并查集找到连通区间,区间加上 \(w_i - cost_x\),表示撤销这次操作。
因为区间是不断扩大的,大概可以理解这样的做法是正确的。
loj 558 我们的 CPU 遭到攻击
用 \(lct\) 来支持这些操作,为了方便进行边化点操作,边权转化为点权。
因为要支持动态的\(link-cut\)操作,对每个节点维护这样的一些信息:
\(lans\) 表示 splay 子树内及其虚子树内所有黑点到达当前根节点(及深度最小点)的答案。
\(rans\) 与 \(lans\) 类似,但表示到达实链深度最大点的答案,其用处是在进行换根操作(下传翻转标记)时直接执行 swap(lans[x],rans[x])
。
\(val\) 表示当前点的点权,\(vals\) 表示 splay 子树内边权。
\(col\) 表示当前点的颜色,\(cnt\) 表示子树内黑点个数的和。
\(ians\) 表示虚子树内对答案的贡献,\(icnt\) 表示虚子树内黑点个数。
在执行 update(x)
的时候,实际上只要考虑跨过 \(x\) 并到达根节点的答案。
而且其中在右儿子部分的点权已经统计过了,只需要统计当前节点以及其左儿子的点权和。
那么有 \(lans[p]=lans[lch]+lans[rch]+ians[x]+(vals[lch]+val[x])*(cnt[rch]+icnt[x]+col[x])\)。
在进行各种修改操作之前,首先将修改的点换成根节点,来保证祖先链处的值是正确的。
loj 6022 重组病毒
对于不含换根的操作,只需要处理一个 \(dfs\) 序。
在实际的 access 的虚实儿子改变的过程中用一个树状数组/线段树执行区间加法操作。
对于询问只需要区间查询。
对于换根操作,可以发现经过分类讨论,操作的 dfs 序仍可以表示为不超过两段区间。
然而有一个很难办的问题,换根过程可能存在影响。
仔细分析一下,实际上只要正常的进行换根,然后设置新根就行了。
因为在换根之后执行了一遍 access 操作,此时二者路径上的虚边均被打通。
也就是说二者路径上不存在能产生贡献的边,而对于非二者路径上的点,其在 dfs 序上其实不受这次换根的影响。