oi thicks
非常综合的一道 dp 好题。
先对 \(x\) 排序。
首先可以想到这题的关键的是最大的 \(S\) ,其次是最小的面积。所以这个长度就是最长上升子序列的长度,所选的 \(S\) 也必须是一个 LIS ,像这样选一个 LIS 的题目可以对数据分层,\(f_i\) 是 \(i\) 为右端点的最长上升子序列,将相同的 \(f\) 放在一层里。对于一层我们只能选择一个数。当然一层里的 \(x\) 递减,\(y\) 递增。令$p_i $ 为 \(i\) 所在的层数。
令 \(dp_i\) 为 \(i\) 这个点为最后一个点与前面结合的最小面积,且这些点必须有 \(f_i\) 个,即每一层选一个。
容易的,可以写出方程 \(dp_i=dp_j+(x_i-x_j)\times(y_i-y_j),j\in p_{i-1},x_j\le x_i,y_i\le y_j\)。
发现在 \(i\) 的转移区间在 \(p_{i-1}\) 这一层的一段连续区间里。这个区间可以用二分来求出。
而通过打表分析可得,\((x_i-x_j)\times(y_i-y_j)\) 符合四边形不等式,所以转移具有决策单调性。
这里可以通过线段树分治将这一段区间都挂上 \(i\) 这个点,这样在线段树上的每一个点都有一定的点需要去转移。这样就可以在线段树的每一个点都进行分治来快速求得答案。时间复杂度 \(O(n\log^2n)\)。
前言
因为有一些算法本人接触的不多,而且用到的比较少,这里用这一个帖子进行总结。
分治技巧
分治题,这样的既有 \(mx\),又有\(mi\) 的题直接可以用一个"三指针"的写法,一个是右边遍历的指针,而左边有两个指针,一个记最大值,一个记最小值,这样左边就有三个区间,分别求贡献即可。
异或的性质
这样的肯定是先建树。而这里给出的运算符是异或,为什么不是加?乘?因为异或非常的特殊,只要一个数出现了两次,这个数就跟没有一样了。所以 \(dis[i][j] =dis[i][rt] \oplus dis[rt][j]\) 根据这个,我们就可以直接搞 \(n\) 个 map 来记一下每一个联通块的dis个数,主要是异或的性质得用起来即可。
给一个长度为 n 由正整数组成的序列,问存在多少 k 区间
k 区间定义:[l,r] 满足任意a[i]在[l,r] 在区间内出现的次数均为 k
这道题其实是一道异或hash 。
异或哈希是个很神奇的算法,
利用了异或操作的特殊性和哈希降低冲突的原理,
可以用于快速找到一个组合是否出现,
序列中的数是否出现了k次。
这题我们把一个数的 hash 值设为rand,到第 \(k\) 个数设为前 \(k-1\) 个数的异或和,那我们这题要找到每一个数都是数量 \(k\) 的区间数量,那么我们先对每一个数的hash值进行前缀异或,找到每一个前缀异或值前面有多少个即可,当然这样的话会把 \(2k,3k,4k...\) 数量的都记进去,所以这里就可以用一个双指针,把这个区间的数固定为每一个数的长度至多为 \(k\) 即可。
for(int r=1,l=1;r<=n;r++){
d[pre[r-1]]++;
cnt[a[r]]++;
while(cnt[a[r]]>k){
d[pre[l-1]]--;
cnt[a[l]]--;
l++;
}
ans+=d[pre[r]];
}
sos dp (高维前缀和)
P6442 [COCI2011-2012#6] KOŠARE
一道高维前缀和的入门题吧,第一次写高维前缀和。
我们可以用一个f数组来记st和它的子集所包含的箱子数。
用一个g数组来记st和它的子集所包含的方案数。
我们这个g就类似一个前缀和了,而我们需要对这个前缀和进行差分,当然可以用容斥做,但是我们这里介绍一个更加简单的算法,因为算前缀和有两种算法,而这个使用第二个更加简单,我们就对每一位进行一个差分即可。
网格图最短路/连通图
[P3350 ZJOI2016] 旅行者 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这是一道网格图最短路的题目,没有修改的,而这样题目可以使用类似分治的方法来解决。
总的来说有点像二维线段树的写法,我们发现这两个点的最短路一定经过以这两个点为矩形的过中心的横线和纵线,每一次都把长的那一条边都折半即可。
那这样的话我们就可以直接对中间这一条纵线或者横线跑最短路,把左右两边的查询进行取一个最小值,当然不是左右两边的查询也可以取一个最小值,反正都是覆盖的,随便怎么写。
然后就是把这整一个分成两个小块,把询问也分为两个小块,一个是左边这个矩阵的查询,一边是右边这个矩形的查询,一直递归下去即可。
时间复杂度 \(O(n\log^2n)\)
P4246 [SHOI2008] 堵塞的交通 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这道题就有意思了,这里是带修改的一个网格图联通图,这个也是可以直接用线段树分治的方法写,把每一个边的作用域给求出来,用一个带撤销的并查集即可。
因为我们的只有 2 行,直接一个线段树即可,我们记一下矩阵的四个点的联通性,进行合并即可。
(反正线段树也算是一种分治的思想吧)。
笛卡尔树
Spinning Round (Hard Version) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这个题是一道笛卡尔树的好题,首先先想到暴力,这里有两个暴力,先固定一个区间。
-
我们先枚举一个点,每次都二分往两边拓展区间,时间复杂度是 \(O(n\log n\log v)\)
-
在这里可以使用一个分治的方法,因为我们发现这个区间里的最大值肯定成立,那我们只要算一下若左边的区间和大于这个最大值,就可以能把左区间都合并掉的数量加到总区间的数量里即可。右边同理。这里的左区间是指 \([l,mxid-1]\) 右区间是 \([mxid+1,r]\) 时间复杂度是 \(O(n)\) 。
最大值的下标用一个
st表即可。
这样就可以直接得到 pts60 了。
那我们先分析这个第二种暴力(因为这个看起来是一个树,可能会比第一种好优化一点?)
这个算法的瓶颈是每一次都需要分治一次,就相当重新建一棵树,这肯定是不可以接受的,我们需要一些性质。
对于下标,左边的下标总是小于中间的下标,而中间的下标总是大于右边的下标,说白了就是一棵二分查找树;而对于权值,我们发现这个数可以看做一个大根堆。那这样的图就可以构造笛卡尔树。
那我们知道了是笛卡尔树有该如何做呢?
我们发现我们只需要递归左链即可,这样的话是时间递减的,我们这样的话就需要对右区间进行处理,对每一个右区间的数,我们需要看它到何时会成为为答案贡献的一部分,那我们就需要找到一个 \(r\ge i\) 我们只对右边合并到 \(r\) 就可以让我们合并到 \(u\) ,只要我们合并过 \(u\) 那后面都比 \(a_u\) 都小。
所以我们找到以 \(i\) 为要二分的点, \(l\) 就是 \(u\) 我们至少需要合并到的点,诶,这不就类似暴力一的方法吗?很好我们就可以直接 \(O(\log n\log v)\) 的解决了。
那就好了,因为每一次都是枚举左链上的右区间,所以每一个点都只会被枚举一次,所以是 \(O(n\log n\log v)\)。
限长判负环
这种题可以有两种解法:以 Floyd 为核心,或以 dfs 为核心。
首先讲以 Floyd 的解法,这个的话我们很容易想到如果 \(dis_{i,i}<0\) 的话就说明有负环,那这里就可以直接用倍增的算法,类似一个矩乘加速:
for(int i=1;i<=8;i++)st[i] =st[i-1]*st[i-1];
for(int i=8;i>=0;i--){
Mat tmp = now*st[i];
if(!tmp.check())ans+=1<<i,now=tmp;
}
再讲一下这个 dfs 的算法,我们首先需要一个小结论,在一个负环中,肯定存在一个点,把这个负环跑一遍前缀和,每一个前缀和都小于 0 。
那如果这样的话就可以直接二分环的大小,把每一个点的可能在负环的情况枚举一下即可。
先把每一个点的点权设为 0 ,每一次松弛都直接看看是不是小于这个点权即可,如果小于,继续往下扫即可。
时间复杂度 \(O(n^3\log n)\) 。
虚树
先考虑链的情况,发现我们可以计算每一条边被算过几次,接着算出总权值。
那树上的也是如此对每一条边,算出下面有多少个点是关键点,乘上上面有多少个关键点。
这样应该是可以拿到 70pts 但是如果我们想拿到 100pts 就需要用一个关键的算法——虚树。
我们发现,在这里的关键点是很少的,有一些边是不必都留着的。我们只需对关键点操作即可。
这时我们就需要建立虚树,把关键点根据 dfs序 排序,把相邻的点的 lca 加入,建树即可。
sort(b + 1, b + k + 1, cmp);
K = k;
b[++k] = 1;
for (int i = 2; i <= K; i++) b[++k] = LCA(b[i], b[i - 1]);
sort(b + 1, b + k + 1, cmp);
k = unique(b + 1, b + k + 1) - b - 1;
for (int i = 1; i < k; i++) {
int lca = LCA(b[i], b[i + 1]);
d[lca].push_back({b[i + 1],dep[b[i + 1]] - dep[lca]});
}
再跑一遍 dfs 即可。
括号匹配
如果一个括号序是一个环,可以转的话,令 ‘(’ 为 1 ‘)’ 为 -1 ,跑一边前缀和,从最小值开始肯定能组成正确的括号序。
前提是总和是 0。
树上哈希的应用
对于两颗树,我们如何快速判断这两个树是否相等,这就必须用到树上哈希了。
常见的hash有:
-
直接用一个
map<vector>来实现,但这样不好修改,只能做静态的匹配,动态的时间复杂度不稳定。 -
也可以通过把每个子节点的哈希值算出来,排个序,乘一个权值即可。通常乘 \(prime_i\) 即第 \(i\) 大的质数,或者 \(pw[i]\),\(base\) 的 \(i\) 次方,这个进行动态的搞不算很难,只需要处理出后缀即可。
-
最后的最后,隆重介绍一个非常好的哈希 \(f_u = \sum_{v\in son_u} F(f(v))\) 这里的
F是一个随便瞎搞的哈希函数,初始化 \(f_{rt}=1\) 如果修改直接在上面加减即可,而且重复的可能性很小,(只要F套的够多)。
基环树dp
对于一般的树上 dp ,我们非常的熟悉,而基环树就有点复杂了,但是其实可以把基环树转化成树来做,这样的话就可以用树上dp 了,但是我们发现,基环树比树会多一条边,那我们就可以分类讨论,这条边两端的选择即可。直接跑两遍即可。
还有一种方法,就是直接通过把基环树的环都取出来,对环上的每一个节点计算若它被选择,它尾巴上的这些节点最多会给它多少贡献,不选同理。
跑一个环上的 dp 即可。
随机游走
这种类型的题目一般不是设 \(f_i\) 为 \(i\sim n\) 的期望步数,而是令 \(f_i\) 为 \(i\sim i+1\) 的期望步数,这样更好转移。
线形动物 这题可以很简单地使用。
一些奇怪的操作
有一些操作如果加上就会对题目的判断起到难度极大的增加。可以考虑这些操作是否有一些平替, Replication G 这题的复制其实就是一个烟雾弹,根本没必要带着复制品走
LIS
如果要在 LIS 上 dp ,就可以对每一个点进行分层,根据以它为结尾的 LIS 的最长长度来分,这样每一层都是单调递减的。可以对题目的解答有很好的帮助。
杂项
这样中间挖掉一个的就直接前缀后缀就可以了。
这题看到 \(n\) 较小,而且时间复杂度是 \(2^n\) 级别的就应该马上想到用折半或者双向宽搜解决。
[旅行者](https://www.luogu.com.cn/problem/P5304)
这题的我们发现 \(n\) 的范围很大,而且只跑两点的最短路,不需要具体的两个点,那我们就可以二进制分组来写,枚举 $k\ in[0,\log k] $ 若 \(c[i]\and 2^k\) 则在第一组,否则在第二组,这里就可以在一次内把每两个点在不同的组内的最小值都算出来。

浙公网安备 33010602011771号