key
- 数据结构:并查集,线段树(单点修改,区间查询)
- 树:邻接表建图,树上差分,树上启发式合并
- STL:priority_queue(重载小于号,cmp为真swap,i.e.默认cmp是一个大根堆)、set
- 基本语法:重载运算符,结构体的构造函数写法
- 思路:先猜结论后证明,去分类讨论算贡献(枚举拆绝对值,拆 max / min)
cf1622(div.2)
前 4 题注意特判就好,C 题要注意解的合理性,舍掉小于 0 的解。
E
E 题主要突破口在 n ≤ 10 n \leq 10 n≤10,又因为每个人的贡献有绝对值,有绝对值了就不能把所有人放在一起考虑,不能算每道题的贡献,只能对每个人单独考虑。所以用 O ( 2 n ) O(2^n) O(2n) 枚举拆绝对值,然后就可以算每道题的贡献了。
F
F 题题干非常简洁,一看就是结论题。题目问前
n
n
n 个数中最多可以取出多少个数,使得
∏
i
=
1
k
a
i
\prod_{i=1}^ka_i
∏i=1kai 是一个完全平方数。有用的结论是这个最大子集的大小和 n 相差不大。然后发现
n
n
n 是 4 的倍数或者 2 的倍数的时候可以证明子集大小至少是
n
−
1
n-1
n−1 和
n
−
2
n-2
n−2,然后如果除 4 余 1 或 3 的在前面的基础上再多减掉一个就好了。
然后按 n m o d 4 n\; mod\; 4 nmod4 的数值分类讨论。
判断一个阶乘是否完全平方的方法,把每个数都拆成质数的乘积,判断每个质数的指数是否都是偶数, O ( n l o g n ) O(nlogn) O(nlogn)。btw这里乘上偶数个数对结果完全没有影响,所以可以想到 xor。
就是说要考察乘积可以往质数拆分的方向上想,两个抵消可以往 xor 和二进制数上想。
cf1606(div.2)
D
思路其实挺暴力的,先枚举在哪里切一刀。对于左边的红色和蓝色子矩阵,“蓝色子矩阵的最大值小于红色子矩阵的最小值” 相当于 “任意蓝色矩阵的数都小于任意红色矩阵的数”。所以只要切的那刀位置确定了,红蓝矩阵的分法是 O(n) 的。
E
E 题是个 DP,题意是有 n 个人,你可以给每个人规定一个初始血量 a i a_i ai ,血量有一个上限 k k k 。每回合每个人的血量减少 r e m − 1 rem-1 rem−1 ,其中 rem 是剩下的人数。问在所有的 k n k^n kn 种初始血量排列中,有多少最后会剩下一个人。
考虑暴力的 DP, d p [ i ] [ r e m ] [ d a m ] dp[i][rem][dam] dp[i][rem][dam] 表示经过 i 轮,剩下 rem 个人,已经造成了 dam 点伤害的方案数,那么递推公式就是 d p [ i + 1 ] [ n x t ] [ d a m + r e m − 1 ] + = d p [ i ] [ r e m ] [ d a m ] ∗ C r e m n x t ∗ ( n x t − r e m ) r e m − 1 dp[i+1][nxt][dam+rem-1] += dp[i][rem][dam] * C_{rem}^{nxt} *(nxt-rem) ^ {rem-1} dp[i+1][nxt][dam+rem−1]+=dp[i][rem][dam]∗Cremnxt∗(nxt−rem)rem−1 nxt 需要一个循环去枚举,表示下一轮还剩多少人,这一轮死掉的人的血量必须在这个区间内,所以可以 O ( n k 3 ) O(nk^3) O(nk3) 做。
然后发现伤害了几轮我们并不在意,而且 n x t ≤ r e m , d a m + r e m − 1 ≥ d a m nxt \leq rem,dam+rem-1\geq dam nxt≤rem,dam+rem−1≥dam,所有的更新只会单向进行不会反向,所以我们只需要做一个 O ( n k 2 ) O(nk^2) O(nk2) 的DP就可以了。
F
主要突破口在于问题的转化。
第一种转化基于问题的几个性质。首先对于某一个子树,加上根自己,删除的肯定是一个连通块。其次每删除一个点对答案的贡献是 s o n [ v ] − k − 1 son[v]-k-1 son[v]−k−1 。所以相当于每个点有一个权值,求某个子树中包含根的权值最大的联通块。
离线回答询问,k 从大到小枚举,如果 s o n [ v ] − k − 1 > 0 son[v]-k-1>0 son[v]−k−1>0 就把 v 和他的父亲连起来,维护连通块的大小和 ∑ s o n [ v ] \sum son[v] ∑son[v] 。因为每次把一个点和他的父亲相连的操作,需要修改的点在一条链上,所以可以用树上差分,维护一个线段树,单点修改区间求和。
第二种转化基于对 DP 递推式的化简。 d p ( u , k ) = ∑ m a x { 1 , d p ( v , k ) − k } dp(u,k) = \sum max\{1, dp(v,k)-k\} dp(u,k)=∑max{1,dp(v,k)−k} ,表示要么直接要这个儿子,要么把他删掉。首先可以发现 d p ( u , k ) dp(u,k) dp(u,k) 关于 k 是单调的,所以存在一个临界的 k 0 k_0 k0,使得 k ≤ k 0 k\leq k_0 k≤k0 时删除这个儿子更优, k > k 0 k>k_0 k>k0 时保留这个儿子更优。然后还是考虑像上面那种方法一样,假如这个儿子删掉更优就把他和他的父亲连起来,否则不连边。在这棵新的树上,递推式可以写成 d p ( u , k ) = s o n ( u ) + ∑ ( d p ( v , k ) − k − 1 ) = ∑ s o n ( u ) − ( k + 1 ) ( s z ( u ) − 1 ) dp(u,k) = son(u)+\sum (dp(v,k)-k-1) = \sum son(u) - (k+1)(sz(u)-1) dp(u,k)=son(u)+∑(dp(v,k)−k−1)=∑son(u)−(k+1)(sz(u)−1) 到这一步就跟上面那种转化方法一样了。
对于这种离线询问的题可以把询问和修改都放在同一个 set 或者 priority_queue 里面,依次取出操作。这题注意相同权值的点加入深度深的优先,并且要修改连通块顶端的权值,重新放到队列里。(否则就是一个错误的暴力贪心)
树上启发式合并
一般用于处理:离线询问,询问一般是关于两个不同子树中的点之间的关系。
算是一种优化的工具,跟线段树、树剖差不多,不能算是一种思路。
cf1612(div.2)
D
用“不妨设”规定两数的大小,枚举所有可能的操作(有了大小之后就可以去绝对值),发现这个数能通过一系列操作得到当且仅当这个数在辗转相减过程中能得到。所以做一个辗转相减就行了。
E
突破口是 k ≤ 20 k\leq 20 k≤20 。
考虑对于每一个人,假如这个人的信息被选中了,他能提供多少的贡献, F ( i ) = m i n { k i , p } p , F(i)=\frac{min\{k_i,p\}}{p}, F(i)=pmin{ki,p},其中 p 是选中的总数。可以发现假如 p > 20 p>20 p>20 这个 min 直接可以去掉,对于 p ≤ 20 p\leq 20 p≤20 的暴力判断谁更小,然后 sort 取最优的 p 条信息就好了。复杂度 O ( k n l o g n ) O(knlogn) O(knlogn) ,比赛的时候感觉复杂度有问题不敢写。
这题的思路跟 1622E 很像,都是 min 或者绝对值影响了计算贡献的复杂度,用暴力的方法解决 min 或者绝对值之后快速计算贡献。
F
题解给出的算法是 BFS 加剪枝。把每一个状态 (x, y) 看成一个点,(x, y)->(x, x+y) 和 (x+y,y) 有一条边。剪枝就是 BFS 的每一层都去掉 x’ <= x && y’ <= y 的所有点 (x’, y’)
我本来的想法是贪心,利用的结论是只要 max(x, y) < min(n, m) 就必然是 x + y 赋值给 x, y 里小的那个数。但是有以下两组数据可以把这种方法 hack 掉。
5 6
2
1 1
3 1
9 5
1
2 1
G
有一个序列,你知道每个数字在这个序列中出现了多少次(题目会给出),但你不知道他们之间的顺序。一个序列的权值是其中所有相同数字对之间的距离之和(距离就是,比如ai=aj,距离=绝对值(i-j))。求最大的权值并求出有多少种排列能够达到这个最大权值
猜结论然后证明。
一般会直接去想整个排列以一种什么样的顺序去排最优,然后想不出来。于是可以考虑先部分最优,然后一路贪心把序列生成出来。比如说可以先考虑比较特殊的首尾位置,比如说本题的结论就是:“首尾必须放出现次数最多的数。”证明只要反证法即可,假设中间有一个出现次数比两头多的。
然后推广这个结论,如果有 k 个数出现次数都是最多的,那么只要头上放 k 个,尾部放 k 个(每个数 1 个在头 1 个在尾)即可,这 k 个数之间的排列不会影响答案。
然后考虑递推获得答案,已知最外层两个相同的数的位置,就可以直接计算这两个数的贡献,然后把这两个数去掉对里面剩下的部分再用一样的贪心策略做。
set
- begin(),返回set容器的第一个元素
- end(),返回set容器的最后一个元素
- clear(),删除set容器中的所有的元素
- empty(),判断set容器是否为空
- size(),返回当前set容器中的元素个数
- count() 用来查找set中某个某个键值出现的次数。
- erase(iterator) ,删除定位器iterator指向的值
- erase(first,second),删除定位器first和second之间的值
- erase(key_value),删除键值key_value的值
- find() ,返回给定值值得定位器,如果没找到则返回end()。
- insert(key_value); 将key_value插入到set中 ,返回值是pair<set::iterator,bool>,bool标志着插入是否成功,而iterator代表插入的位置,若key_value已经在set中,则iterator表示的key_value在set中的位置。
- lower_bound(key_value) ,返回第一个大于等于key_value的定位器
- upper_bound(key_value),返回最后一个大于等于key_value的定位器
- 自定义比较函数